[libvirt] [PATCH v2 23/24] docs: rewrite hvsupport.html page generator in python

Daniel P. Berrangé berrange at redhat.com
Wed Sep 11 16:23:32 UTC 2019


As part of an goal to eliminate Perl from libvirt build tools,
rewrite the hvsupport.pl tool in Python.

This was a straight conversion, manually going line-by-line to
change the syntax from Perl to Python. Thus the overall structure
of the file and approach is the same.

The new impl generates byte-for-byte identical output to the
old impl.

Signed-off-by: Daniel P. Berrangé <berrange at redhat.com>
---
 docs/Makefile.am  |   8 +-
 docs/hvsupport.pl | 458 --------------------------------------------
 docs/hvsupport.py | 479 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 483 insertions(+), 462 deletions(-)
 delete mode 100755 docs/hvsupport.pl
 create mode 100755 docs/hvsupport.py

diff --git a/docs/Makefile.am b/docs/Makefile.am
index 14f3faffd4..e4aa8b4ad9 100644
--- a/docs/Makefile.am
+++ b/docs/Makefile.am
@@ -194,7 +194,7 @@ EXTRA_DIST= \
   $(internals_html_in) $(internals_html) $(fonts) \
   $(kbase_html_in) $(kbase_html) \
   aclperms.htmlinc \
-  hvsupport.pl \
+  hvsupport.py \
   $(schema_DATA)
 
 acl_generated = aclperms.htmlinc
@@ -230,12 +230,12 @@ web: $(dot_html) $(internals_html) $(kbase_html) \
 
 hvsupport.html: $(srcdir)/hvsupport.html.in
 
-$(srcdir)/hvsupport.html.in: $(srcdir)/hvsupport.pl $(api_DATA) \
+$(srcdir)/hvsupport.html.in: $(srcdir)/hvsupport.py $(api_DATA) \
 		$(top_srcdir)/src/libvirt_public.syms \
 	$(top_srcdir)/src/libvirt_qemu.syms $(top_srcdir)/src/libvirt_lxc.syms \
 	$(top_srcdir)/src/driver.h
-	$(AM_V_GEN)$(PERL) $(srcdir)/hvsupport.pl $(top_srcdir)/src > $@ \
-		|| { rm $@ && exit 1; }
+	$(AM_V_GEN)$(RUNUTF8) $(PYTHON) $(srcdir)/hvsupport.py \
+		$(top_srcdir)/src > $@ || { rm $@ && exit 1; }
 
 news.html.in: \
 	  $(srcdir)/news.xml \
diff --git a/docs/hvsupport.pl b/docs/hvsupport.pl
deleted file mode 100755
index 494b8a27ec..0000000000
--- a/docs/hvsupport.pl
+++ /dev/null
@@ -1,458 +0,0 @@
-#!/usr/bin/env perl
-
-use strict;
-use warnings;
-
-use File::Find;
-
-die "syntax: $0 SRCDIR\n" unless int(@ARGV) == 1;
-
-my $srcdir = shift @ARGV;
-
-my $symslibvirt = "$srcdir/libvirt_public.syms";
-my $symsqemu = "$srcdir/libvirt_qemu.syms";
-my $symslxc = "$srcdir/libvirt_lxc.syms";
-my @drivertable = (
-    "$srcdir/driver-hypervisor.h",
-    "$srcdir/driver-interface.h",
-    "$srcdir/driver-network.h",
-    "$srcdir/driver-nodedev.h",
-    "$srcdir/driver-nwfilter.h",
-    "$srcdir/driver-secret.h",
-    "$srcdir/driver-state.h",
-    "$srcdir/driver-storage.h",
-    "$srcdir/driver-stream.h",
-    );
-
-my %groupheaders = (
-    "virHypervisorDriver" => "Hypervisor APIs",
-    "virNetworkDriver" => "Virtual Network APIs",
-    "virInterfaceDriver" => "Host Interface APIs",
-    "virNodeDeviceDriver" => "Host Device APIs",
-    "virStorageDriver" => "Storage Pool APIs",
-    "virSecretDriver" => "Secret APIs",
-    "virNWFilterDriver" => "Network Filter APIs",
-    );
-
-
-my @srcs;
-find({
-    wanted => sub {
-        if (m!$srcdir/.*/\w+_(driver|common|tmpl|monitor|hal|udev)\.c$!) {
-            push @srcs, $_ if $_ !~ /vbox_driver\.c/;
-        }
-    }, no_chdir => 1}, $srcdir);
-
-# Map API functions to the header and documentation files they're in
-# so that we can generate proper hyperlinks to their documentation.
-#
-# The function names are grep'd from the XML output of apibuild.py.
-sub getAPIFilenames {
-    my $filename = shift;
-
-    my %files;
-    my $line;
-
-    open FILE, "<", $filename or die "cannot read $filename: $!";
-
-    while (defined($line = <FILE>)) {
-        if ($line =~ /function name='([^']+)' file='([^']+)'/) {
-            $files{$1} = $2;
-        }
-    }
-
-    close FILE;
-
-    if (keys %files == 0) {
-        die "No functions found in $filename. Has the apibuild.py output changed?";
-    }
-    return \%files;
-}
-
-sub parseSymsFile {
-    my $apisref = shift;
-    my $prefix = shift;
-    my $filename = shift;
-    my $xmlfilename = shift;
-
-    my $line;
-    my $vers;
-    my $prevvers;
-
-    my $filenames = getAPIFilenames($xmlfilename);
-
-    open FILE, "<$filename"
-        or die "cannot read $filename: $!";
-
-    while (defined($line = <FILE>)) {
-        chomp $line;
-        next if $line =~ /^\s*#/;
-        next if $line =~ /^\s*$/;
-        next if $line =~ /^\s*(global|local):/;
-        if ($line =~ /^\s*${prefix}_(\d+\.\d+\.\d+)\s*{\s*$/) {
-            if (defined $vers) {
-                die "malformed syms file";
-            }
-            $vers = $1;
-        } elsif ($line =~ /\s*}\s*;\s*$/) {
-            if (defined $prevvers) {
-                die "malformed syms file";
-            }
-            $prevvers = $vers;
-            $vers = undef;
-        } elsif ($line =~ /\s*}\s*${prefix}_(\d+\.\d+\.\d+)\s*;\s*$/) {
-            if ($1 ne $prevvers) {
-                die "malformed syms file $1 != $vers";
-            }
-            $prevvers = $vers;
-            $vers = undef;
-        } elsif ($line =~ /\s*(\w+)\s*;\s*$/) {
-            $$apisref{$1} = {};
-            $$apisref{$1}->{vers} = $vers;
-            $$apisref{$1}->{file} = $$filenames{$1};
-        } else {
-            die "unexpected data $line\n";
-        }
-    }
-
-    close FILE;
-}
-
-my %apis;
-# Get the list of all public APIs and their corresponding version
-parseSymsFile(\%apis, "LIBVIRT", $symslibvirt, "$srcdir/../docs/libvirt-api.xml");
-
-# And the same for the QEMU specific APIs
-parseSymsFile(\%apis, "LIBVIRT_QEMU", $symsqemu, "$srcdir/../docs/libvirt-qemu-api.xml");
-
-# And the same for the LXC specific APIs
-parseSymsFile(\%apis, "LIBVIRT_LXC", $symslxc, "$srcdir/../docs/libvirt-lxc-api.xml");
-
-
-# Some special things which aren't public APIs,
-# but we want to report
-$apis{virConnectSupportsFeature}->{vers} = "0.3.2";
-$apis{virDomainMigratePrepare}->{vers} = "0.3.2";
-$apis{virDomainMigratePerform}->{vers} = "0.3.2";
-$apis{virDomainMigrateFinish}->{vers} = "0.3.2";
-$apis{virDomainMigratePrepare2}->{vers} = "0.5.0";
-$apis{virDomainMigrateFinish2}->{vers} = "0.5.0";
-$apis{virDomainMigratePrepareTunnel}->{vers} = "0.7.2";
-
-$apis{virDomainMigrateBegin3}->{vers} = "0.9.2";
-$apis{virDomainMigratePrepare3}->{vers} = "0.9.2";
-$apis{virDomainMigratePrepareTunnel3}->{vers} = "0.9.2";
-$apis{virDomainMigratePerform3}->{vers} = "0.9.2";
-$apis{virDomainMigrateFinish3}->{vers} = "0.9.2";
-$apis{virDomainMigrateConfirm3}->{vers} = "0.9.2";
-
-$apis{virDomainMigrateBegin3Params}->{vers} = "1.1.0";
-$apis{virDomainMigratePrepare3Params}->{vers} = "1.1.0";
-$apis{virDomainMigratePrepareTunnel3Params}->{vers} = "1.1.0";
-$apis{virDomainMigratePerform3Params}->{vers} = "1.1.0";
-$apis{virDomainMigrateFinish3Params}->{vers} = "1.1.0";
-$apis{virDomainMigrateConfirm3Params}->{vers} = "1.1.0";
-
-
-
-# Now we want to get the mapping between public APIs
-# and driver struct fields. This lets us later match
-# update the driver impls with the public APis.
-
-my $line;
-
-# Group name -> hash of APIs { fields -> api name }
-my %groups;
-my $ingrp;
-foreach my $drivertable (@drivertable) {
-    open FILE, "<$drivertable"
-        or die "cannot read $drivertable: $!";
-
-    while (defined($line = <FILE>)) {
-        if ($line =~ /struct _(vir\w*Driver)/) {
-            my $grp = $1;
-            if ($grp ne "virStateDriver" &&
-                $grp ne "virStreamDriver") {
-                $ingrp = $grp;
-                $groups{$ingrp} = { apis => {}, drivers => {} };
-            }
-        } elsif ($ingrp) {
-            if ($line =~ /^\s*vir(?:Drv)(\w+)\s+(\w+);\s*$/) {
-                my $field = $2;
-                my $name = $1;
-
-                my $api;
-                if (exists $apis{"vir$name"}) {
-                    $api = "vir$name";
-                } elsif ($name =~ /\w+(Open|Close|URIProbe)/) {
-                    next;
-                } else {
-                    die "driver $name does not have a public API";
-                }
-                $groups{$ingrp}->{apis}->{$field} = $api;
-            } elsif ($line =~ /};/) {
-                $ingrp = undef;
-            }
-        }
-    }
-
-    close FILE;
-}
-
-
-# Finally, we read all the primary driver files and extract
-# the driver API tables from each one.
-
-foreach my $src (@srcs) {
-    open FILE, "<$src" or
-        die "cannot read $src: $!";
-
-    my $groups_regex = join("|", keys %groups);
-    $ingrp = undef;
-    my $impl;
-    while (defined($line = <FILE>)) {
-        if (!$ingrp) {
-            # skip non-matching lines early to save time
-            next if not $line =~ /$groups_regex/;
-
-            if ($line =~ /^\s*(?:static\s+)?($groups_regex)\s+(\w+)\s*=\s*{/ ||
-                $line =~ /^\s*(?:static\s+)?($groups_regex)\s+NAME\(\w+\)\s*=\s*{/) {
-                $ingrp = $1;
-                $impl = $src;
-
-                if ($impl =~ m,.*/node_device_(\w+)\.c,) {
-                    $impl = $1;
-                } else {
-                    $impl =~ s,.*/(\w+?)_((\w+)_)?(\w+)\.c,$1,;
-                }
-
-                if ($groups{$ingrp}->{drivers}->{$impl}) {
-                    die "Group $ingrp already contains $impl";
-                }
-
-                $groups{$ingrp}->{drivers}->{$impl} = {};
-            }
-
-        } else {
-            if ($line =~ m!\s*\.(\w+)\s*=\s*(\w+)\s*,?\s*(?:/\*\s*(\d+\.\d+\.\d+)\s*(?:-\s*(\d+\.\d+\.\d+))?\s*\*/\s*)?$!) {
-                my $api = $1;
-                my $meth = $2;
-                my $vers = $3;
-                my $deleted = $4;
-
-                next if $api eq "no" || $api eq "name";
-
-                if ($meth eq "NULL" && !defined $deleted) {
-                    die "Method impl for $api is NULL, but no deleted version is provided";
-                }
-                if ($meth ne "NULL" && defined $deleted) {
-                    die "Method impl for $api is non-NULL, but deleted version is provided";
-                }
-
-                die "Method $meth in $src is missing version" unless defined $vers || $api eq "connectURIProbe";
-
-                if (!exists($groups{$ingrp}->{apis}->{$api})) {
-                    next if $api =~ /\w(Open|Close|URIProbe)/;
-
-                    die "Found unexpected method $api in $ingrp\n";
-                }
-
-                $groups{$ingrp}->{drivers}->{$impl}->{$api} = {};
-                $groups{$ingrp}->{drivers}->{$impl}->{$api}->{vers} = $vers;
-                $groups{$ingrp}->{drivers}->{$impl}->{$api}->{deleted}  = $deleted;
-                if ($api eq "domainMigratePrepare" ||
-                    $api eq "domainMigratePrepare2" ||
-                    $api eq "domainMigratePrepare3") {
-                    if (!$groups{$ingrp}->{drivers}->{$impl}->{"domainMigrate"}) {
-                        $groups{$ingrp}->{drivers}->{$impl}->{"domainMigrate"} = {};
-                        $groups{$ingrp}->{drivers}->{$impl}->{"domainMigrate"}->{vers} = $vers;
-                    }
-                }
-
-            } elsif ($line =~ /}/) {
-                $ingrp = undef;
-            }
-        }
-    }
-
-    close FILE;
-}
-
-
-# The '.open' driver method is used for 3 public APIs, so we
-# have a bit of manual fixup todo with the per-driver versioning
-# and support matrix
-
-$groups{virHypervisorDriver}->{apis}->{"openAuth"} = "virConnectOpenAuth";
-$groups{virHypervisorDriver}->{apis}->{"openReadOnly"} = "virConnectOpenReadOnly";
-$groups{virHypervisorDriver}->{apis}->{"domainMigrate"} = "virDomainMigrate";
-
-my $openAuthVers = (0 * 1000 * 1000) + (4 * 1000) + 0;
-
-foreach my $drv (keys %{$groups{"virHypervisorDriver"}->{drivers}}) {
-    my $openVersStr = $groups{"virHypervisorDriver"}->{drivers}->{$drv}->{"connectOpen"}->{vers};
-    my $openVers;
-    if ($openVersStr =~ /(\d+)\.(\d+)\.(\d+)/) {
-        $openVers = ($1 * 1000 * 1000) + ($2 * 1000) + $3;
-    }
-
-    # virConnectOpenReadOnly always matches virConnectOpen version
-    $groups{"virHypervisorDriver"}->{drivers}->{$drv}->{"connectOpenReadOnly"} =
-        $groups{"virHypervisorDriver"}->{drivers}->{$drv}->{"connectOpen"};
-
-    $groups{"virHypervisorDriver"}->{drivers}->{$drv}->{"connectOpenAuth"} = {};
-
-    # virConnectOpenAuth is always 0.4.0 if the driver existed
-    # before this time, otherwise it matches the version of
-    # the driver's virConnectOpen entry
-    if ($openVersStr eq "Y" ||
-        $openVers >= $openAuthVers) {
-        $groups{"virHypervisorDriver"}->{drivers}->{$drv}->{"connectOpenAuth"}->{vers} = $openVersStr;
-    } else {
-        $groups{"virHypervisorDriver"}->{drivers}->{$drv}->{"connectOpenAuth"}->{vers} = "0.4.0";
-    }
-}
-
-
-# Another special case for the virDomainCreateLinux which was replaced
-# with virDomainCreateXML
-$groups{virHypervisorDriver}->{apis}->{"domainCreateLinux"} = "virDomainCreateLinux";
-
-my $createAPIVers = (0 * 1000 * 1000) + (0 * 1000) + 3;
-
-foreach my $drv (keys %{$groups{"virHypervisorDriver"}->{drivers}}) {
-    my $createVersStr = $groups{"virHypervisorDriver"}->{drivers}->{$drv}->{"domainCreateXML"}->{vers};
-    next unless defined $createVersStr;
-    my $createVers;
-    if ($createVersStr =~ /(\d+)\.(\d+)\.(\d+)/) {
-        $createVers = ($1 * 1000 * 1000) + ($2 * 1000) + $3;
-    }
-
-    $groups{"virHypervisorDriver"}->{drivers}->{$drv}->{"domainCreateLinux"} = {};
-
-    # virCreateLinux is always 0.0.3 if the driver existed
-    # before this time, otherwise it matches the version of
-    # the driver's virCreateXML entry
-    if ($createVersStr eq "Y" ||
-        $createVers >= $createAPIVers) {
-        $groups{"virHypervisorDriver"}->{drivers}->{$drv}->{"domainCreateLinux"}->{vers} = $createVersStr;
-    } else {
-        $groups{"virHypervisorDriver"}->{drivers}->{$drv}->{"domainCreateLinux"}->{vers} = "0.0.3";
-    }
-}
-
-
-# Finally we generate the HTML file with the tables
-
-print <<EOF;
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html>
-<html xmlns="http://www.w3.org/1999/xhtml">
-<body class="hvsupport">
-<h1>libvirt API support matrix</h1>
-
-<ul id="toc"></ul>
-
-<p>
-This page documents which <a href="html/">libvirt calls</a> work on
-which libvirt drivers / hypervisors, and which version the API appeared
-in. If a hypervisor driver later dropped support for the API, the version
-when it was removed is also mentioned (highlighted in
-<span class="removedhv">dark red</span>).
-</p>
-
-EOF
-
-    foreach my $grp (sort { $a cmp $b } keys %groups) {
-    print "<h2><a id=\"$grp\">", $groupheaders{$grp}, "</a></h2>\n";
-    print <<EOF;
-<table class="top_table">
-<thead>
-<tr>
-<th>API</th>
-<th>Version</th>
-EOF
-
-    foreach my $drv (sort { $a cmp $b } keys %{$groups{$grp}->{drivers}}) {
-        print "  <th>$drv</th>\n";
-    }
-
-    print <<EOF;
-</tr>
-</thead>
-<tbody>
-EOF
-
-    my $row = 0;
-    foreach my $field (sort {
-        $groups{$grp}->{apis}->{$a}
-        cmp
-        $groups{$grp}->{apis}->{$b}
-        } keys %{$groups{$grp}->{apis}}) {
-        my $api = $groups{$grp}->{apis}->{$field};
-        my $vers = $apis{$api}->{vers};
-        my $htmlgrp = $apis{$api}->{file};
-        print <<EOF;
-<tr>
-<td>
-EOF
-
-        if (defined $htmlgrp) {
-            print <<EOF;
-<a href=\"html/libvirt-$htmlgrp.html#$api\">$api</a>
-EOF
-
-        } else {
-            print $api;
-        }
-        print <<EOF;
-</td>
-<td>$vers</td>
-EOF
-
-        foreach my $drv (sort {$a cmp $b } keys %{$groups{$grp}->{drivers}}) {
-            print "<td>";
-            if (exists $groups{$grp}->{drivers}->{$drv}->{$field}) {
-                if ($groups{$grp}->{drivers}->{$drv}->{$field}->{vers}) {
-                    print $groups{$grp}->{drivers}->{$drv}->{$field}->{vers};
-                }
-                if ($groups{$grp}->{drivers}->{$drv}->{$field}->{deleted}) {
-                    print " - <span class=\"removedhv\">", $groups{$grp}->{drivers}->{$drv}->{$field}->{deleted}, "</span>";
-                }
-            }
-            print "</td>\n";
-        }
-
-        print <<EOF;
-</tr>
-EOF
-
-        $row++;
-        if (($row % 15) == 0) {
-            print <<EOF;
-<tr>
-<th>API</th>
-<th>Version</th>
-EOF
-
-            foreach my $drv (sort { $a cmp $b } keys %{$groups{$grp}->{drivers}}) {
-                print "  <th>$drv</th>\n";
-            }
-
-        print <<EOF;
-</tr>
-EOF
-        }
-
-    }
-
-    print <<EOF;
-</tbody>
-</table>
-EOF
-}
-
-print <<EOF;
-</body>
-</html>
-EOF
diff --git a/docs/hvsupport.py b/docs/hvsupport.py
new file mode 100755
index 0000000000..cfbecd82ae
--- /dev/null
+++ b/docs/hvsupport.py
@@ -0,0 +1,479 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2011-2019 Red Hat, Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.  If not, see
+# <http://www.gnu.org/licenses/>.
+
+from __future__ import print_function
+
+import sys
+import os.path
+import re
+
+if len(sys.argv) != 2:
+    print("syntax: %s SRCDIR\n" % sys.argv[0], file=sys.stderr)
+
+srcdir = sys.argv[1]
+
+symslibvirt = os.path.join(srcdir, "libvirt_public.syms")
+symsqemu = os.path.join(srcdir, "libvirt_qemu.syms")
+symslxc = os.path.join(srcdir, "libvirt_lxc.syms")
+drivertablefiles = [
+    os.path.join(srcdir, "driver-hypervisor.h"),
+    os.path.join(srcdir, "driver-interface.h"),
+    os.path.join(srcdir, "driver-network.h"),
+    os.path.join(srcdir, "driver-nodedev.h"),
+    os.path.join(srcdir, "driver-nwfilter.h"),
+    os.path.join(srcdir, "driver-secret.h"),
+    os.path.join(srcdir, "driver-state.h"),
+    os.path.join(srcdir, "driver-storage.h"),
+    os.path.join(srcdir, "driver-stream.h"),
+]
+
+groupheaders = {
+    "virHypervisorDriver": "Hypervisor APIs",
+    "virNetworkDriver": "Virtual Network APIs",
+    "virInterfaceDriver": "Host Interface APIs",
+    "virNodeDeviceDriver": "Host Device APIs",
+    "virStorageDriver": "Storage Pool APIs",
+    "virSecretDriver": "Secret APIs",
+    "virNWFilterDriver": "Network Filter APIs",
+}
+
+
+srcs = []
+for root, dirs, files in os.walk(srcdir):
+    for file in files:
+        if ((file.endswith("driver.c") and
+             not file.endswith("vbox_driver.c")) or
+            file.endswith("common.c") or
+            file.endswith("tmpl.c") or
+            file.endswith("monitor.c") or
+            file.endswith("hal.c") or
+            file.endswith("udev.c")):
+            srcs.append(os.path.join(root, file))
+
+
+# Map API functions to the header and documentation files they're in
+# so that we can generate proper hyperlinks to their documentation.
+#
+# The function names are grep'd from the XML output of apibuild.py.
+def getAPIFilenames(filename):
+    files = {}
+
+    with open(filename) as fh:
+        prog = re.compile(r"\s*<function name='([^']+)' file='([^']+)'.*")
+        for line in fh:
+            res = prog.match(line)
+            if res is not None:
+                files[res.group(1)] = res.group(2)
+
+    if len(files) == 0:
+        raise Exception("No functions found in %s. Has the apibuild.py output changed?" % filename)
+
+    return files
+
+
+def parseSymsFile(apisref, prefix, filename, xmlfilename):
+    vers = None
+    prevvers = None
+
+    filenames = getAPIFilenames(xmlfilename)
+
+    with open(filename) as fh:
+        groupstartprog = re.compile(r"^\s*%s_(\d+\.\d+\.\d+)\s*{\s*$" % prefix)
+        groupendprog1 = re.compile(r"^\s*}\s*;\s*$")
+        groupendprog2 = re.compile(r"^\s*}\s*%s_(\d+\.\d+\.\d+)\s*;\s*$" % prefix)
+        symbolprog = re.compile(r"^\s*(\w+)\s*;\s*$")
+        for line in fh:
+            line = line.strip()
+
+            if line == "":
+                continue
+            if line[0] == '#':
+                continue
+            if line.startswith("global:"):
+                continue
+            if line.startswith("local:"):
+                continue
+
+            groupstartmatch = groupstartprog.match(line)
+            groupendmatch1 = groupendprog1.match(line)
+            groupendmatch2 = groupendprog2.match(line)
+            symbolmatch = symbolprog.match(line)
+            if groupstartmatch is not None:
+                if vers is not None:
+                    raise Exception("malformed syms file when starting group")
+
+                vers = groupstartmatch.group(1)
+            elif groupendmatch1 is not None:
+                if prevvers is not None:
+                    raise Exception("malformed syms file when ending group")
+
+                prevvers = vers
+                vers = None
+            elif groupendmatch2 is not None:
+                if groupendmatch2.group(1) != prevvers:
+                    raise Exception("malformed syms file %s != %s when ending group" % (
+                        groupendmatch2.group(1), prevvers))
+
+                prevvers = vers
+                vers = None
+            elif symbolmatch is not None:
+                name = symbolmatch.group(1)
+                apisref[name] = {
+                    "vers": vers,
+                    "file": filenames.get(name),
+                }
+            else:
+                raise Exception("unexpected data %s" % line)
+
+
+apis = {}
+# Get the list of all public APIs and their corresponding version
+parseSymsFile(apis, "LIBVIRT", symslibvirt, os.path.join(srcdir, "../docs/libvirt-api.xml"))
+
+# And the same for the QEMU specific APIs
+parseSymsFile(apis, "LIBVIRT_QEMU", symsqemu, os.path.join(srcdir, "../docs/libvirt-qemu-api.xml"))
+
+# And the same for the LXC specific APIs
+parseSymsFile(apis, "LIBVIRT_LXC", symslxc, os.path.join(srcdir, "../docs/libvirt-lxc-api.xml"))
+
+
+# Some special things which aren't public APIs,
+# but we want to report
+apis["virConnectSupportsFeature"] = {
+    "vers": "0.3.2"
+}
+apis["virDomainMigratePrepare"] = {
+    "vers": "0.3.2"
+}
+apis["virDomainMigratePerform"] = {
+    "vers": "0.3.2"
+}
+apis["virDomainMigrateFinish"] = {
+    "vers": "0.3.2"
+}
+apis["virDomainMigratePrepare2"] = {
+    "vers": "0.5.0"
+}
+apis["virDomainMigrateFinish2"] = {
+    "vers": "0.5.0"
+}
+apis["virDomainMigratePrepareTunnel"] = {
+    "vers": "0.7.2"
+}
+
+apis["virDomainMigrateBegin3"] = {
+    "vers": "0.9.2"
+}
+apis["virDomainMigratePrepare3"] = {
+    "vers": "0.9.2"
+}
+apis["virDomainMigratePrepareTunnel3"] = {
+    "vers": "0.9.2"
+}
+apis["virDomainMigratePerform3"] = {
+    "vers": "0.9.2"
+}
+apis["virDomainMigrateFinish3"] = {
+    "vers": "0.9.2"
+}
+apis["virDomainMigrateConfirm3"] = {
+    "vers": "0.9.2"
+}
+
+apis["virDomainMigrateBegin3Params"] = {
+    "vers": "1.1.0"
+}
+apis["virDomainMigratePrepare3Params"] = {
+    "vers": "1.1.0"
+}
+apis["virDomainMigratePrepareTunnel3Params"] = {
+    "vers": "1.1.0"
+}
+apis["virDomainMigratePerform3Params"] = {
+    "vers": "1.1.0"
+}
+apis["virDomainMigrateFinish3Params"] = {
+    "vers": "1.1.0"
+}
+apis["virDomainMigrateConfirm3Params"] = {
+    "vers": "1.1.0"
+}
+
+
+# Now we want to get the mapping between public APIs
+# and driver struct fields. This lets us later match
+# update the driver impls with the public APis.
+
+# Group name -> hash of APIs { fields -> api name }
+groups = {}
+ingrp = None
+for drivertablefile in drivertablefiles:
+    with open(drivertablefile) as fh:
+        starttableprog = re.compile(r"struct _(vir\w*Driver)")
+        endtableprog = re.compile(r"};")
+        callbackprog = re.compile(r"^\s*vir(?:Drv)(\w+)\s+(\w+);\s*$")
+        ignoreapiprog = re.compile(r"\w+(Open|Close|URIProbe)")
+        for line in fh:
+            starttablematch = starttableprog.match(line)
+            if starttablematch is not None:
+                grp = starttablematch.group(1)
+                if grp != "virStateDriver" and grp != "virStreamDriver":
+                    ingrp = grp
+                    groups[ingrp] = {
+                        "apis": {},
+                        "drivers": {}
+                    }
+            elif ingrp != None:
+                callbackmatch = callbackprog.match(line)
+                if callbackmatch is not None:
+                    name = callbackmatch.group(1)
+                    field = callbackmatch.group(2)
+
+                    api = "vir" + name
+                    if api in apis:
+                        groups[ingrp]["apis"][field] = api
+                    elif ignoreapiprog.match(api) != None:
+                        continue
+                    else:
+                        raise Exception("driver %s does not have a public API" % name)
+                elif endtableprog.match(line):
+                    ingrp = None
+
+
+# Finally, we read all the primary driver files and extract
+# the driver API tables from each one.
+
+for src in srcs:
+    with open(src) as fh:
+        groupsre = "|".join(groups.keys())
+        groupsprog2 = re.compile(r"^\s*(static\s+)?(" + groupsre + r")\s+(\w+)\s*=\s*{")
+        groupsprog3 = re.compile(r"^\s*(static\s+)?(" + groupsre + r")\s+NAME\(\w+\)\s*=\s*{")
+        nodedevimplprog = re.compile(r".*/node_device_(\w+)\.c")
+        miscimplprog = re.compile(r".*/(\w+?)_((\w+)_)?(\w+)\.c")
+        callbackprog = re.compile(r"\s*\.(\w+)\s*=\s*(\w+)\s*,?\s*(?:/\*\s*(\d+\.\d+\.\d+)\s*(?:-\s*(\d+\.\d+\.\d+))?\s*\*/\s*)?$")
+        skipapiprog = re.compile(r"\w+(Open|Close|URIProbe)")
+
+        ingrp = None
+        impl = None
+        for line in fh:
+            if ingrp is None:
+                m = groupsprog2.match(line)
+                if m is None:
+                    m = groupsprog3.match(line)
+                if m is not None:
+                    ingrp = m.group(2)
+                    impl = src
+
+                    implmatch = nodedevimplprog.match(impl)
+                    if implmatch is None:
+                        implmatch = miscimplprog.match(impl)
+                    if implmatch is None:
+                        raise Exception("Unexpected impl format '%s'" % impl)
+                    impl = implmatch.group(1)
+
+                    if impl in groups[ingrp]["drivers"]:
+                        raise Exception("Group %s already contains %s" % (ingrp, impl))
+
+                    groups[ingrp]["drivers"][impl] = {}
+            else:
+                callbackmatch = callbackprog.match(line)
+                if callbackmatch is not None:
+                    api = callbackmatch.group(1)
+                    meth = callbackmatch.group(2)
+                    vers = callbackmatch.group(3)
+                    deleted = callbackmatch.group(4)
+
+                    if api == "no" or api == "name":
+                        continue
+
+                    if meth == "NULL" and deleted is None:
+                        raise Exception("Method impl for %s is NULL, but no deleted version is provided" % api)
+
+                    if meth != "NULL" and deleted is not None:
+                        raise Exception("Method impl for %s is non-NULL, but deleted version is provided" % api)
+
+                    if vers is None and api != "connectURIProbe":
+                        raise Exception("Method %s in %s is missing version" % (meth, src))
+
+                    if api not in groups[ingrp]["apis"]:
+                        if skipapiprog.match(api):
+                            continue
+
+                        raise Exception("Found unexpected method %s in %s" % (api, ingrp))
+
+                    groups[ingrp]["drivers"][impl][api] = {
+                        "vers": vers,
+                        "deleted": deleted,
+                    }
+
+                    if (api == "domainMigratePrepare" or
+                        api == "domainMigratePrepare2" or
+                        api == "domainMigratePrepare3"):
+                        if "domainMigrate" not in groups[ingrp]["drivers"][impl]:
+                            groups[ingrp]["drivers"][impl]["domainMigrate"] = {
+                                "vers": vers,
+                            }
+                elif line.find("}") != -1:
+                    ingrp = None
+
+
+# The '.open' driver method is used for 3 public APIs, so we
+# have a bit of manual fixup todo with the per-driver versioning
+# and support matrix
+
+groups["virHypervisorDriver"]["apis"]["openAuth"] = "virConnectOpenAuth"
+groups["virHypervisorDriver"]["apis"]["openReadOnly"] = "virConnectOpenReadOnly"
+groups["virHypervisorDriver"]["apis"]["domainMigrate"] = "virDomainMigrate"
+
+openAuthVers = (0 * 1000 * 1000) + (4 * 1000) + 0
+
+for drv in groups["virHypervisorDriver"]["drivers"].keys():
+    openVersStr = groups["virHypervisorDriver"]["drivers"][drv]["connectOpen"]["vers"]
+    openVers = 0
+    if openVersStr != "Y":
+        openVersBits = openVersStr.split(".")
+        if len(openVersBits) != 3:
+            raise Exception("Expected 3 digit version for %s" % openVersStr)
+        openVers = (int(openVersBits[0]) * 1000 * 1000) + (int(openVersBits[1]) * 1000) + int(openVersBits[2])
+
+    # virConnectOpenReadOnly always matches virConnectOpen version
+    groups["virHypervisorDriver"]["drivers"][drv]["connectOpenReadOnly"] = \
+        groups["virHypervisorDriver"]["drivers"][drv]["connectOpen"]
+
+    # virConnectOpenAuth is always 0.4.0 if the driver existed
+    # before this time, otherwise it matches the version of
+    # the driver's virConnectOpen entry
+    if openVersStr == "Y" or openVers >= openAuthVers:
+        vers = openVersStr
+    else:
+        vers = "0.4.0"
+    groups["virHypervisorDriver"]["drivers"][drv]["connectOpenAuth"] = {
+        "vers": vers,
+    }
+
+
+# Another special case for the virDomainCreateLinux which was replaced
+# with virDomainCreateXML
+groups["virHypervisorDriver"]["apis"]["domainCreateLinux"] = "virDomainCreateLinux"
+
+createAPIVers = (0 * 1000 * 1000) + (0 * 1000) + 3
+
+for drv in groups["virHypervisorDriver"]["drivers"].keys():
+    if "domainCreateXML" not in groups["virHypervisorDriver"]["drivers"][drv]:
+        continue
+    createVersStr = groups["virHypervisorDriver"]["drivers"][drv]["domainCreateXML"]["vers"]
+    createVers = 0
+    if createVersStr != "Y":
+        createVersBits = createVersStr.split(".")
+        if len(createVersBits) != 3:
+            raise Exception("Expected 3 digit version for %s" % createVersStr)
+        createVers = (int(createVersBits[0]) * 1000 * 1000) + (int(createVersBits[1]) * 1000) + int(createVersBits[2])
+
+    # virCreateLinux is always 0.0.3 if the driver existed
+    # before this time, otherwise it matches the version of
+    # the driver's virCreateXML entry
+    if createVersStr == "Y" or createVers >= createAPIVers:
+        vers = createVersStr
+    else:
+        vers = "0.0.3"
+
+    groups["virHypervisorDriver"]["drivers"][drv]["domainCreateLinux"] = {
+        "vers": vers,
+    }
+
+
+# Finally we generate the HTML file with the tables
+
+print('''<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<body class="hvsupport">
+<h1>libvirt API support matrix</h1>
+
+<ul id="toc"></ul>
+
+<p>
+This page documents which <a href="html/">libvirt calls</a> work on
+which libvirt drivers / hypervisors, and which version the API appeared
+in. If a hypervisor driver later dropped support for the API, the version
+when it was removed is also mentioned (highlighted in
+<span class="removedhv">dark red</span>).
+</p>
+''')
+
+for grp in sorted(groups.keys()):
+    print("<h2><a id=\"%s\">%s</a></h2>" % (grp, groupheaders[grp]))
+    print('''<table class="top_table">
+<thead>
+<tr>
+<th>API</th>
+<th>Version</th>''')
+
+    for drv in sorted(groups[grp]["drivers"].keys()):
+        print("  <th>%s</th>" % drv)
+
+
+    print('''</tr>
+</thead>
+<tbody>''')
+
+    row = 0
+    def sortkey(field):
+        return groups[grp]["apis"][field]
+    for field in sorted(groups[grp]["apis"].keys(), key=sortkey):
+        api = groups[grp]["apis"][field]
+        vers = apis[api]["vers"]
+        htmlgrp = apis[api].get("file")
+        print("<tr>")
+
+        if htmlgrp is not None:
+            print('''<td>\n<a href=\"html/libvirt-%s.html#%s\">%s</a>\n</td>''' % (htmlgrp, api, api))
+        else:
+            print("<td>\n%s</td>" % api)
+
+        print("<td>%s</td>" % vers)
+
+        for drv in sorted(groups[grp]["drivers"].keys()):
+            info = ""
+            if field in groups[grp]["drivers"][drv]:
+                vers = groups[grp]["drivers"][drv][field]["vers"]
+                if vers is not None:
+                    info = info + vers
+
+                deleted = groups[grp]["drivers"][drv][field].get("deleted")
+                if deleted is not None:
+                    info = info + (''' - <span class="removedhv">%s</span>''' % deleted)
+
+            print("<td>%s</td>" % info)
+
+        print("</tr>")
+
+        row = row + 1
+        if (row % 15) == 0:
+            print('''<tr>
+<th>API</th>
+<th>Version</th>''')
+
+            for drv in sorted(groups[grp]["drivers"].keys()):
+                print("  <th>%s</th>" % drv)
+
+            print("</tr>")
+
+    print("</tbody>\n</table>")
+
+print("</body>\n</html>")
-- 
2.21.0




More information about the libvir-list mailing list