2011-05-13 14:00:56 +04:00
#!/usr/bin/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" ;
Introduce an LXC specific public API & library
This patch introduces support for LXC specific public APIs. In
common with what was done for QEMU, this creates a libvirt_lxc.so
library and libvirt/libvirt-lxc.h header file.
The actual APIs are
int virDomainLxcOpenNamespace(virDomainPtr domain,
int **fdlist,
unsigned int flags);
int virDomainLxcEnterNamespace(virDomainPtr domain,
unsigned int nfdlist,
int *fdlist,
unsigned int *noldfdlist,
int **oldfdlist,
unsigned int flags);
which provide a way to use the setns() system call to move the
calling process into the container's namespace. It is not
practical to write in a generically applicable manner. The
nearest that we could get to such an API would be an API which
allows to pass a command + argv to be executed inside a
container. Even if we had such a generic API, this LXC specific
API is still useful, because it allows the caller to maintain
the current process context, in particular any I/O streams they
have open.
NB the virDomainLxcEnterNamespace() API is special in that it
runs client side, so does not involve the internal driver API.
Signed-off-by: Daniel P. Berrange <berrange@redhat.com>
2012-12-21 17:15:19 +04:00
my $ symslxc = "$srcdir/libvirt_lxc.syms" ;
2014-11-11 19:28:26 +03:00
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" ,
) ;
2011-05-13 14:00:56 +04:00
my % groupheaders = (
2014-10-16 13:25:59 +04:00
"virHypervisorDriver" = > "Hypervisor APIs" ,
2011-05-13 14:00:56 +04:00
"virNetworkDriver" = > "Virtual Network APIs" ,
"virInterfaceDriver" = > "Host Interface APIs" ,
2013-04-23 14:15:48 +04:00
"virNodeDeviceDriver" = > "Host Device APIs" ,
2011-05-13 14:00:56 +04:00
"virStorageDriver" = > "Storage Pool APIs" ,
"virSecretDriver" = > "Secret APIs" ,
"virNWFilterDriver" = > "Network Filter APIs" ,
) ;
my @ srcs ;
find ( {
wanted = > sub {
2014-08-20 20:17:07 +04:00
if ( m !$srcdir/.*/\w+_(driver|common|tmpl|monitor|hal|udev)\.c$! ) {
2012-07-27 16:51:28 +04:00
push @ srcs , $ _ if $ _ !~ /vbox_driver\.c/ ;
}
2011-05-13 14:00:56 +04:00
} , no_chdir = > 1 } , $ srcdir ) ;
2016-06-28 14:28:48 +03:00
# 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 ;
}
2016-06-28 13:37:50 +03:00
sub parseSymsFile {
my $ apisref = shift ;
my $ prefix = shift ;
my $ filename = shift ;
my $ xmlfilename = shift ;
2011-05-13 14:00:56 +04:00
2016-06-28 13:37:50 +03:00
my $ line ;
my $ vers ;
my $ prevvers ;
2011-05-13 14:00:56 +04:00
2016-06-28 14:28:48 +03:00
my $ filenames = getAPIFilenames ( $ xmlfilename ) ;
2011-05-13 14:00:56 +04:00
2016-06-28 13:37:50 +03:00
open FILE , "<$filename"
or die "cannot read $filename: $!" ;
2011-05-13 14:00:56 +04:00
2016-06-28 13:37:50 +03:00
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 ;
2016-06-28 14:28:48 +03:00
$$ apisref { $ 1 } - > { file } = $$ filenames { $ 1 } ;
2016-06-28 13:37:50 +03:00
} else {
die "unexpected data $line\n" ;
2012-07-27 16:51:28 +04:00
}
Introduce an LXC specific public API & library
This patch introduces support for LXC specific public APIs. In
common with what was done for QEMU, this creates a libvirt_lxc.so
library and libvirt/libvirt-lxc.h header file.
The actual APIs are
int virDomainLxcOpenNamespace(virDomainPtr domain,
int **fdlist,
unsigned int flags);
int virDomainLxcEnterNamespace(virDomainPtr domain,
unsigned int nfdlist,
int *fdlist,
unsigned int *noldfdlist,
int **oldfdlist,
unsigned int flags);
which provide a way to use the setns() system call to move the
calling process into the container's namespace. It is not
practical to write in a generically applicable manner. The
nearest that we could get to such an API would be an API which
allows to pass a command + argv to be executed inside a
container. Even if we had such a generic API, this LXC specific
API is still useful, because it allows the caller to maintain
the current process context, in particular any I/O streams they
have open.
NB the virDomainLxcEnterNamespace() API is special in that it
runs client side, so does not involve the internal driver API.
Signed-off-by: Daniel P. Berrange <berrange@redhat.com>
2012-12-21 17:15:19 +04:00
}
2016-06-28 13:37:50 +03:00
close FILE ;
Introduce an LXC specific public API & library
This patch introduces support for LXC specific public APIs. In
common with what was done for QEMU, this creates a libvirt_lxc.so
library and libvirt/libvirt-lxc.h header file.
The actual APIs are
int virDomainLxcOpenNamespace(virDomainPtr domain,
int **fdlist,
unsigned int flags);
int virDomainLxcEnterNamespace(virDomainPtr domain,
unsigned int nfdlist,
int *fdlist,
unsigned int *noldfdlist,
int **oldfdlist,
unsigned int flags);
which provide a way to use the setns() system call to move the
calling process into the container's namespace. It is not
practical to write in a generically applicable manner. The
nearest that we could get to such an API would be an API which
allows to pass a command + argv to be executed inside a
container. Even if we had such a generic API, this LXC specific
API is still useful, because it allows the caller to maintain
the current process context, in particular any I/O streams they
have open.
NB the virDomainLxcEnterNamespace() API is special in that it
runs client side, so does not involve the internal driver API.
Signed-off-by: Daniel P. Berrange <berrange@redhat.com>
2012-12-21 17:15:19 +04:00
}
2016-06-28 13:37:50 +03:00
my % apis ;
# Get the list of all public APIs and their corresponding version
parseSymsFile ( \ % apis , "LIBVIRT" , $ symslibvirt , "$srcdir/../docs/libvirt-api.xml" ) ;
Introduce an LXC specific public API & library
This patch introduces support for LXC specific public APIs. In
common with what was done for QEMU, this creates a libvirt_lxc.so
library and libvirt/libvirt-lxc.h header file.
The actual APIs are
int virDomainLxcOpenNamespace(virDomainPtr domain,
int **fdlist,
unsigned int flags);
int virDomainLxcEnterNamespace(virDomainPtr domain,
unsigned int nfdlist,
int *fdlist,
unsigned int *noldfdlist,
int **oldfdlist,
unsigned int flags);
which provide a way to use the setns() system call to move the
calling process into the container's namespace. It is not
practical to write in a generically applicable manner. The
nearest that we could get to such an API would be an API which
allows to pass a command + argv to be executed inside a
container. Even if we had such a generic API, this LXC specific
API is still useful, because it allows the caller to maintain
the current process context, in particular any I/O streams they
have open.
NB the virDomainLxcEnterNamespace() API is special in that it
runs client side, so does not involve the internal driver API.
Signed-off-by: Daniel P. Berrange <berrange@redhat.com>
2012-12-21 17:15:19 +04:00
2016-06-28 13:37:50 +03:00
# And the same for the QEMU specific APIs
parseSymsFile ( \ % apis , "LIBVIRT_QEMU" , $ symsqemu , "$srcdir/../docs/libvirt-qemu-api.xml" ) ;
Introduce an LXC specific public API & library
This patch introduces support for LXC specific public APIs. In
common with what was done for QEMU, this creates a libvirt_lxc.so
library and libvirt/libvirt-lxc.h header file.
The actual APIs are
int virDomainLxcOpenNamespace(virDomainPtr domain,
int **fdlist,
unsigned int flags);
int virDomainLxcEnterNamespace(virDomainPtr domain,
unsigned int nfdlist,
int *fdlist,
unsigned int *noldfdlist,
int **oldfdlist,
unsigned int flags);
which provide a way to use the setns() system call to move the
calling process into the container's namespace. It is not
practical to write in a generically applicable manner. The
nearest that we could get to such an API would be an API which
allows to pass a command + argv to be executed inside a
container. Even if we had such a generic API, this LXC specific
API is still useful, because it allows the caller to maintain
the current process context, in particular any I/O streams they
have open.
NB the virDomainLxcEnterNamespace() API is special in that it
runs client side, so does not involve the internal driver API.
Signed-off-by: Daniel P. Berrange <berrange@redhat.com>
2012-12-21 17:15:19 +04:00
# And the same for the LXC specific APIs
2016-06-28 13:37:50 +03:00
parseSymsFile ( \ % apis , "LIBVIRT_LXC" , $ symslxc , "$srcdir/../docs/libvirt-lxc-api.xml" ) ;
2011-05-13 14:00:56 +04:00
# Some special things which aren't public APIs,
# but we want to report
2014-12-01 16:36:13 +03:00
$ 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" ;
2013-05-20 18:59:08 +04:00
2011-05-13 14:00:56 +04:00
# 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.
2016-06-28 13:37:50 +03:00
my $ line ;
2011-05-13 14:00:56 +04:00
# Group name -> hash of APIs { fields -> api name }
my % groups ;
my $ ingrp ;
2014-11-11 19:28:26 +03:00
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)/ ) {
next ;
} else {
die "driver $name does not have a public API" ;
}
$ groups { $ ingrp } - > { apis } - > { $ field } = $ api ;
} elsif ( $ line =~ /};/ ) {
$ ingrp = undef ;
2012-07-27 16:51:28 +04:00
}
}
2011-05-13 14:00:56 +04:00
}
2014-11-11 19:28:26 +03:00
close FILE ;
}
2011-05-13 14:00:56 +04:00
# 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
2012-07-27 16:51:28 +04:00
die "cannot read $src: $!" ;
2011-05-13 14:00:56 +04:00
2016-06-28 16:16:01 +03:00
my $ groups_regex = join ( "|" , keys % groups ) ;
2011-05-13 14:00:56 +04:00
$ ingrp = undef ;
my $ impl ;
while ( defined ( $ line = <FILE> ) ) {
2012-07-27 16:51:28 +04:00
if ( ! $ ingrp ) {
2016-06-28 16:23:58 +03:00
# skip non-matching lines early to save time
next if not $ line =~ /$groups_regex/ ;
2016-06-28 16:16:01 +03:00
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 , ;
2012-07-27 16:51:28 +04:00
}
2016-06-28 16:16:01 +03:00
if ( $ groups { $ ingrp } - > { drivers } - > { $ impl } ) {
die "Group $ingrp already contains $impl" ;
}
$ groups { $ ingrp } - > { drivers } - > { $ impl } = { } ;
2012-07-27 16:51:28 +04:00
}
} else {
if ( $ line =~ m !\s*\.(\w+)\s*=\s*(\w+)\s*,?\s*(?:/\*\s*(\d+\.\d+\.\d+)\s*\*/\s*)?$! ) {
my $ api = $ 1 ;
my $ meth = $ 2 ;
my $ vers = $ 3 ;
next if $ api eq "no" || $ api eq "name" ;
die "Method $meth in $src is missing version" unless defined $ vers ;
die "Driver method for $api is NULL in $src" if $ meth eq "NULL" ;
if ( ! exists ( $ groups { $ ingrp } - > { apis } - > { $ api } ) ) {
2013-04-23 16:49:21 +04:00
next if $ api =~ /\w(Open|Close)/ ;
2012-07-27 16:51:28 +04:00
die "Found unexpected method $api in $ingrp\n" ;
}
$ groups { $ ingrp } - > { drivers } - > { $ impl } - > { $ api } = $ vers ;
if ( $ api eq "domainMigratePrepare" ||
$ api eq "domainMigratePrepare2" ||
$ api eq "domainMigratePrepare3" ) {
$ groups { $ ingrp } - > { drivers } - > { $ impl } - > { "domainMigrate" } = $ vers
unless $ groups { $ ingrp } - > { drivers } - > { $ impl } - > { "domainMigrate" } ;
}
} elsif ( $ line =~ /}/ ) {
$ ingrp = undef ;
}
}
2011-05-13 14:00:56 +04:00
}
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
2014-11-11 19:28:26 +03:00
$ groups { virHypervisorDriver } - > { apis } - > { "openAuth" } = "virConnectOpenAuth" ;
$ groups { virHypervisorDriver } - > { apis } - > { "openReadOnly" } = "virConnectOpenReadOnly" ;
$ groups { virHypervisorDriver } - > { apis } - > { "domainMigrate" } = "virDomainMigrate" ;
2011-05-13 14:00:56 +04:00
my $ openAuthVers = ( 0 * 1000 * 1000 ) + ( 4 * 1000 ) + 0 ;
2014-11-11 19:28:26 +03:00
foreach my $ drv ( keys % { $ groups { "virHypervisorDriver" } - > { drivers } } ) {
my $ openVersStr = $ groups { "virHypervisorDriver" } - > { drivers } - > { $ drv } - > { "connectOpen" } ;
2011-05-13 14:00:56 +04:00
my $ openVers ;
if ( $ openVersStr =~ /(\d+)\.(\d+)\.(\d+)/ ) {
2012-07-27 16:51:28 +04:00
$ openVers = ( $ 1 * 1000 * 1000 ) + ( $ 2 * 1000 ) + $ 3 ;
2011-05-13 14:00:56 +04:00
}
# virConnectOpenReadOnly always matches virConnectOpen version
2014-11-11 19:28:26 +03:00
$ groups { "virHypervisorDriver" } - > { drivers } - > { $ drv } - > { "connectOpenReadOnly" } =
$ groups { "virHypervisorDriver" } - > { drivers } - > { $ drv } - > { "connectOpen" } ;
2011-05-13 14:00:56 +04:00
# 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" ||
2012-07-27 16:51:28 +04:00
$ openVers >= $ openAuthVers ) {
2014-11-11 19:28:26 +03:00
$ groups { "virHypervisorDriver" } - > { drivers } - > { $ drv } - > { "connectOpenAuth" } = $ openVersStr ;
2011-05-13 14:00:56 +04:00
} else {
2014-11-11 19:28:26 +03:00
$ groups { "virHypervisorDriver" } - > { drivers } - > { $ drv } - > { "connectOpenAuth" } = "0.4.0" ;
2011-05-13 14:00:56 +04:00
}
}
# Another special case for the virDomainCreateLinux which was replaced
# with virDomainCreateXML
2014-11-11 19:28:26 +03:00
$ groups { virHypervisorDriver } - > { apis } - > { "domainCreateLinux" } = "virDomainCreateLinux" ;
2011-05-13 14:00:56 +04:00
my $ createAPIVers = ( 0 * 1000 * 1000 ) + ( 0 * 1000 ) + 3 ;
2014-11-11 19:28:26 +03:00
foreach my $ drv ( keys % { $ groups { "virHypervisorDriver" } - > { drivers } } ) {
my $ createVersStr = $ groups { "virHypervisorDriver" } - > { drivers } - > { $ drv } - > { "domainCreateXML" } ;
2011-05-13 14:00:56 +04:00
next unless defined $ createVersStr ;
my $ createVers ;
if ( $ createVersStr =~ /(\d+)\.(\d+)\.(\d+)/ ) {
2012-07-27 16:51:28 +04:00
$ createVers = ( $ 1 * 1000 * 1000 ) + ( $ 2 * 1000 ) + $ 3 ;
2011-05-13 14:00:56 +04:00
}
# 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" ||
2012-07-27 16:51:28 +04:00
$ createVers >= $ createAPIVers ) {
2014-11-11 19:28:26 +03:00
$ groups { "virHypervisorDriver" } - > { drivers } - > { $ drv } - > { "domainCreateLinux" } = $ createVersStr ;
2011-05-13 14:00:56 +04:00
} else {
2014-11-11 19:28:26 +03:00
$ groups { "virHypervisorDriver" } - > { drivers } - > { $ drv } - > { "domainCreateLinux" } = "0.0.3" ;
2011-05-13 14:00:56 +04:00
}
}
# Finally we generate the HTML file with the tables
print << EOF ;
2013-05-07 14:18:10 +04:00
< ? xml version = "1.0" encoding = "UTF-8" ? >
2017-07-26 20:01:25 +03:00
< ! DOCTYPE html >
2013-05-07 14:18:10 +04:00
< html xmlns = "http://www.w3.org/1999/xhtml" >
2016-10-31 14:25:26 +03:00
< body class = "hvsupport" >
2011-05-13 14:00:56 +04:00
<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 .
</p>
EOF
2014-11-11 19:28:26 +03:00
foreach my $ grp ( sort { $ a cmp $ b } keys % groups ) {
2011-05-13 14:00:56 +04:00
print "<h2><a name=\"$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 } } ) {
2012-07-27 16:51:28 +04:00
print " <th>$drv</th>\n" ;
2011-05-13 14:00:56 +04:00
}
print << EOF ;
</tr>
</thead>
<tbody>
EOF
my $ row = 0 ;
foreach my $ field ( sort {
2012-07-27 16:51:28 +04:00
$ groups { $ grp } - > { apis } - > { $ a }
cmp
$ groups { $ grp } - > { apis } - > { $ b }
} keys % { $ groups { $ grp } - > { apis } } ) {
my $ api = $ groups { $ grp } - > { apis } - > { $ field } ;
2014-12-01 16:36:13 +03:00
my $ vers = $ apis { $ api } - > { vers } ;
my $ htmlgrp = $ apis { $ api } - > { file } ;
2012-07-27 16:51:28 +04:00
print << EOF ;
2011-05-13 14:00:56 +04:00
<tr>
2014-12-01 16:36:13 +03:00
<td>
EOF
if ( defined $ htmlgrp ) {
print << EOF ;
< a href = \ " html /libvirt-$htmlgrp.html#$api\">$api</ a >
EOF
} else {
print $ api ;
}
print << EOF ;
</td>
2011-05-13 14:00:56 +04:00
<td> $ vers </td>
EOF
foreach my $ drv ( sort { $ a cmp $ b } keys % { $ groups { $ grp } - > { drivers } } ) {
2012-07-27 16:51:28 +04:00
if ( exists $ groups { $ grp } - > { drivers } - > { $ drv } - > { $ field } ) {
print "<td>" , $ groups { $ grp } - > { drivers } - > { $ drv } - > { $ field } , "</td>\n" ;
} else {
print "<td></td>\n" ;
}
2011-05-13 14:00:56 +04:00
}
2012-07-27 16:51:28 +04:00
print << EOF ;
2011-05-13 14:00:56 +04:00
</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 } } ) {
2012-07-27 16:51:28 +04:00
print " <th>$drv</th>\n" ;
2011-05-13 14:00:56 +04:00
}
print << EOF ;
</tr>
EOF
2012-07-27 16:51:28 +04:00
}
2011-05-13 14:00:56 +04:00
}
print << EOF ;
</tbody>
</table>
EOF
}
print << EOF ;
</body>
</html>
EOF