check-abi-version.pl: add ABI version checker
authorJo-Philipp Wich <jo@mein.io>
Fri, 31 Jan 2020 21:08:13 +0000 (22:08 +0100)
committerJo-Philipp Wich <jo@mein.io>
Fri, 6 Mar 2020 11:28:41 +0000 (12:28 +0100)
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
check-abi-version.pl [new file with mode: 0755]

diff --git a/check-abi-version.pl b/check-abi-version.pl
new file mode 100755 (executable)
index 0000000..6214c52
--- /dev/null
@@ -0,0 +1,195 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+use File::Temp 'tempfile';
+
+$ENV{'LC_ALL'} = 'C';
+
+sub version_cmp($$) {
+       my ($a, $b) = @_;
+
+       my $x = join '', map { sprintf "%04s", $_ } split /\./, $a;
+       my $y = join '', map { sprintf "%04s", $_ } split /\./, $b;
+
+       return ($x cmp $y);
+}
+
+sub print_diag($$) {
+       my ($source, $pkgs) = @_;
+       my $issues = 0;
+       my @messages;
+
+       foreach my $pkg (@$pkgs) {
+               my (@pkgissues, %abi_versions);
+
+               next if !defined($pkg->{'libs'}) || @{$pkg->{'libs'}} == 0;
+
+               foreach my $lib (@{$pkg->{'libs'}}) {
+                       next unless defined $lib->{'soname'};
+
+                       if ($lib->{'soname'} =~ m!^.+\.so\.(.+?)$!) {
+                               $abi_versions{$1}++;
+                       }
+               }
+
+               if (keys(%abi_versions) > 1) {
+                       push @pkgissues, "bundles multiple libraries with different SONAME versions,\n".
+                                        "      consider splitting into multiple packages:";
+
+                       foreach my $lib (@{$pkg->{'libs'}}) {
+                               next unless defined $lib->{'soname'};
+
+                               $pkgissues[-1] .= sprintf "\n       - define Package/lib%s (%s)",
+                                       $lib->{'name'}, $lib->{'soname'};
+                       }
+               }
+
+               my ($highest_version) = sort version_cmp keys %abi_versions;
+
+               if (defined($highest_version) && !defined($pkg->{'abiversion'})) {
+                       push @pkgissues, sprintf "should specify ABI_VERSION:=%s", $highest_version;
+               }
+               elsif (defined($highest_version) && defined($pkg->{'abiversion'}) &&
+                      !exists($abi_versions{$pkg->{'abiversion'}})) {
+                       push @pkgissues,
+                               sprintf "specifies ABI_VERSION:=%s but none of the libary sonames matches, " .
+                                       "consider changing to ABI_VERSION:=%s",
+                                       $pkg->{'abiversion'}, $highest_version;
+               }
+
+               foreach my $lib (@{$pkg->{'libs'}}) {
+                       next unless defined $lib->{'soname'};
+
+                       if ($lib->{'soname'} =~ m!\.so(?:\.[0-9a-zA-Z]+)+$! && $lib->{'unversioned_symlink'}) {
+                               push @pkgissues,
+                                       sprintf "should not package unversioned %s symlink",
+                                               $lib->{'unversioned_symlink'};
+                       }
+               }
+
+               if (@pkgissues > 0) {
+                       push @messages,
+                               sprintf " Package %s (define Package/%s)\n",
+                                       $pkg->{'name'}, $pkg->{'name'};
+
+                       foreach my $issue (@pkgissues) {
+                               push @messages,
+                                       sprintf "  [-] %s\n", $issue;
+                       }
+
+                       $issues += @pkgissues;
+               }
+       }
+
+       if ($issues) {
+               printf "Source %s/Makefile\n", $source;
+               print @messages;
+       }
+}
+
+sub analyze_ipk($) {
+       my $ipk = shift;
+       my (%info, $lib);
+
+       $ipk =~ s/'/'"'"'/g;
+
+       if (open my $control, '-|', "tar -Ozxf '$ipk' ./control.tar.gz | tar -Ozx ./control") {
+               while (defined(my $line = readline $control)) {
+                       chomp $line;
+
+                       if ($line =~ m!^Package: *(\S+)$!) {
+                               $info{'name'} = $1;
+                       }
+                       elsif ($line =~ m!^Source: *(\S+)$!) {
+                               $info{'source'} = $1;
+                       }
+                       elsif ($line =~ m!^SourceName: *(\S+)$!) {
+                               my $abiv = substr $info{'name'}, length $1;
+
+                               $info{'name'} = $1;
+                               $abiv =~ s/^-//;
+                               $info{'abiversion'} = $abiv if length $abiv;
+                       }
+               }
+
+               close $control;
+       }
+
+       if (open my $listing, '-|', "tar -Ozxf '$ipk' ./data.tar.gz | tar -tz | sort") {
+               while (defined(my $entry = readline $listing)) {
+                       chomp $entry; $entry =~ s/'/'"'"'/g;
+
+                       if ($entry =~ m!.+/lib/lib(\S+)\.so((?:\.[0-9a-zA-Z]+)+)?$!) {
+                               my ($fd, $fname) = tempfile('/tmp/libfile.so.XXXXXXX', 'UNLINK' => 1);
+                               my ($libname, $libversion) = ($1, $2);
+
+                               if (!$lib || $lib->{'name'} ne $libname) {
+                                       $lib = { 'name' => $libname };
+                                       push @{$info{'libs'}}, $lib;
+                               }
+
+                               if (open my $extract, '-|', "tar -Ozxf '$ipk' ./data.tar.gz | tar -Ozx '$entry'") {
+                                       while (read $extract, my $buf, 1024) {
+                                               print $fd $buf;
+                                       }
+
+                                       close $extract;
+                               }
+
+                               if (tell($fd) > 0) {
+                                       if (open my $readelf, '-|', 'readelf', '-d', $fname) {
+                                               while (defined(my $line = readline $readelf)) {
+                                                       chomp $line;
+
+                                                       if ($line =~ m!^ 0x[0-9a-f]{8,16} \(SONAME\) +Library soname: \[(.+)\]$!) {
+                                                               $lib->{'soname'} = $1;
+                                                               last;
+                                                       }
+                                               }
+
+                                               close $readelf;
+                                       }
+                                       else {
+                                               warn "Failed to execute readelf: $!\n";
+                                       }
+                               }
+                               elsif ($libversion) {
+                                       $lib->{'versioned_symlink'} = $entry;
+                               }
+                               else {
+                                       $lib->{'unversioned_symlink'} = $entry;
+                               }
+
+                               unlink $fname;
+                               close $fd;
+                       }
+               }
+
+               close $listing;
+       }
+
+       return \%info;
+}
+
+@ARGV >= 1 || die "Usage: $0 <.ipk directory> [<.ipk directory>...]\n";
+
+my %sources;
+
+foreach my $dir (@ARGV) {
+       if (open my $find, '-|', 'find', $dir, '-type', 'f', '-name', 'lib*.ipk') {
+               while (defined(my $ipk = readline $find)) {
+                       chomp $ipk;
+                       my $pkg = analyze_ipk($ipk);
+                       if (defined($pkg) && defined($pkg->{'source'})) {
+                               push @{$sources{$pkg->{'source'}}}, $pkg;
+                       }
+               }
+
+               close $find;
+       }
+}
+
+foreach my $source (sort keys %sources) {
+       print_diag($source, $sources{$source});
+}