nixpkgs/pkgs/build-support/vm/deb/deb-closure.pl
Damien Diederen 3363377530 vmTools.debClosureGenerator: Fix non-determinism in dependency graph
By default, Perl versions since 5.8.1 use randomization to make hashes
resistant to complexity attacks.

That randomization makes building VM images such as ubuntu1804x86_64
non-deterministic because the (imported) derivations built by
deb/deb-closure.pl are not stable.

This can easily be observed by repeating the following sequence of
commands and noting the path of the image's .drv:

    nix-instantiate -E '(import <nixpkgs> {}).vmTools.diskImageFuns.ubuntu1804x86_64 {}'
    nix-store --delete /nix/store/*ubuntu-18.04-bionic-amd64.nix

One source of non-determinism is the handling of Provides/Replaces,
which depends on the order of iteration over %packages.  Here is a
diff showing the corresponding change in output:

     >>> awk
    -virtual awk: using original-awk
    -    original-awk: libc6 (>= 2.14)
    +virtual awk: using mawk
    +    mawk: libc6 (>= 2.14)

    -    mawk: libc6 (>= 2.14)
    ->>> libc6

This patch sorts packages by name for Provides/Replaces processing,
which seems to result in stable output.

(If the above turns out not to be sufficient, one could also set the
PERL_HASH_SEED and PERL_PERTURB_KEYS environment variables, documented
in 'perlrun', to disable Perl's built-in randomization.  Complexity
attacks are not an issue as we control and trust all inputs.)
2020-12-30 11:37:37 +01:00

181 lines
4.8 KiB
Perl

use strict;
use Dpkg::Control;
use Dpkg::Deps;
use File::Basename;
my $packagesFile = shift @ARGV;
my $urlPrefix = shift @ARGV;
my @toplevelPkgs = @ARGV;
my %packages;
# Parse the Packages file.
open PACKAGES, "<$packagesFile" or die;
while (1) {
my $cdata = Dpkg::Control->new(type => CTRL_INFO_PKG);
last if not $cdata->parse(\*PACKAGES, $packagesFile);
die unless defined $cdata->{Package};
#print STDERR $cdata->{Package}, "\n";
$packages{$cdata->{Package}} = $cdata;
}
close PACKAGES;
# Flatten a Dpkg::Deps dependency value into a list of package names.
sub getDeps {
my $deps = shift;
#print "$deps\n";
if ($deps->isa('Dpkg::Deps::AND')) {
my @res = ();
foreach my $dep ($deps->get_deps()) {
push @res, getDeps($dep);
}
return @res;
} elsif ($deps->isa('Dpkg::Deps::OR')) {
# Arbitrarily pick the first alternative.
return getDeps(($deps->get_deps())[0]);
} elsif ($deps->isa('Dpkg::Deps::Simple')) {
return ($deps->{package});
} else {
die "unknown dep type";
}
}
# Process the "Provides" and "Replaces" fields to be able to resolve
# virtual dependencies.
my %provides;
foreach my $cdata (sort {$a->{Package} cmp $b->{Package}} (values %packages)) {
if (defined $cdata->{Provides}) {
my @provides = getDeps(Dpkg::Deps::deps_parse($cdata->{Provides}));
foreach my $name (@provides) {
#die "conflicting provide: $name\n" if defined $provides{$name};
#warn "provide by $cdata->{Package} conflicts with package with the same name: $name\n";
next if defined $packages{$name};
$provides{$name} = $cdata->{Package};
}
}
# Treat "Replaces" like "Provides".
if (defined $cdata->{Replaces}) {
my @replaces = getDeps(Dpkg::Deps::deps_parse($cdata->{Replaces}));
foreach my $name (@replaces) {
next if defined $packages{$name};
$provides{$name} = $cdata->{Package};
}
}
}
# Determine the closure of a package.
my %donePkgs;
my %depsUsed;
my @order = ();
sub closePackage {
my $pkgName = shift;
print STDERR ">>> $pkgName\n";
my $cdata = $packages{$pkgName};
if (!defined $cdata) {
die "unknown (virtual) package $pkgName"
unless defined $provides{$pkgName};
print STDERR "virtual $pkgName: using $provides{$pkgName}\n";
$pkgName = $provides{$pkgName};
$cdata = $packages{$pkgName};
}
die "unknown package $pkgName" unless defined $cdata;
return if defined $donePkgs{$pkgName};
$donePkgs{$pkgName} = 1;
if (defined $cdata->{Provides}) {
foreach my $name (getDeps(Dpkg::Deps::deps_parse($cdata->{Provides}))) {
$provides{$name} = $cdata->{Package};
}
}
my @depNames = ();
if (defined $cdata->{Depends}) {
print STDERR " $pkgName: $cdata->{Depends}\n";
my $deps = Dpkg::Deps::deps_parse($cdata->{Depends});
die unless defined $deps;
push @depNames, getDeps($deps);
}
if (defined $cdata->{'Pre-Depends'}) {
print STDERR " $pkgName: $cdata->{'Pre-Depends'}\n";
my $deps = Dpkg::Deps::deps_parse($cdata->{'Pre-Depends'});
die unless defined $deps;
push @depNames, getDeps($deps);
}
foreach my $depName (@depNames) {
closePackage($depName);
}
push @order, $pkgName;
$depsUsed{$pkgName} = \@depNames;
}
foreach my $pkgName (@toplevelPkgs) {
closePackage $pkgName;
}
# Generate the output Nix expression.
print "# This is a generated file. Do not modify!\n";
print "# Following are the Debian packages constituting the closure of: @toplevelPkgs\n\n";
print "{fetchurl}:\n\n";
print "[\n\n";
# Output the packages in strongly connected components.
my %done;
my %forward;
my $newComponent = 1;
foreach my $pkgName (@order) {
$done{$pkgName} = 1;
my $cdata = $packages{$pkgName};
my @deps = @{$depsUsed{$pkgName}};
foreach my $dep (@deps) {
$dep = $provides{$dep} if defined $provides{$dep};
$forward{$dep} = 1 unless defined $done{$dep};
}
delete $forward{$pkgName};
print " [\n\n" if $newComponent;
$newComponent = 0;
my $origName = basename $cdata->{Filename};
my $cleanedName = $origName;
$cleanedName =~ s/~//g;
print " (fetchurl {\n";
print " url = $urlPrefix/$cdata->{Filename};\n";
print " sha256 = \"$cdata->{SHA256}\";\n";
print " name = \"$cleanedName\";\n" if $cleanedName ne $origName;
print " })\n";
print "\n";
if (keys %forward == 0) {
print " ]\n\n";
$newComponent = 1;
}
}
foreach my $pkgName (@order) {
my $cdata = $packages{$pkgName};
}
print "]\n";
if ($newComponent != 1) {
print STDERR "argh: ", keys %forward, "\n";
exit 1;
}