#! @perl@ -w @perlFlags@ use strict; use File::Basename; use Nix::Config; use Nix::Store; use DBI; my @binaryCacheUrls = split / /, ($ENV{"NIX_BINARY_CACHES"} || ""); my ($dbh, $insertNAR, $queryNAR); my %cacheIds; sub initCache { my $dbPath = "$Nix::Config::stateDir/binary-cache-v1.sqlite"; # Open/create the database. $dbh = DBI->connect("dbi:SQLite:dbname=$dbPath", "", "") or die "cannot open database `$dbPath'"; $dbh->{RaiseError} = 1; $dbh->{PrintError} = 0; $dbh->do("pragma synchronous = off"); # we can always reproduce the cache $dbh->do("pragma journal_mode = truncate"); # Initialise the database schema, if necessary. $dbh->do(<do(<prepare( "insert or replace into NARs(cache, storePath, url, compression, fileHash, fileSize, narHash, " . "narSize, refs, deriver, system, timestamp) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") or die; $queryNAR = $dbh->prepare("select * from NARs where cache = ? and storePath = ?") or die; } sub getInfoFrom { my ($storePath, $pathHash, $binaryCacheUrl) = @_; my $infoUrl = "$binaryCacheUrl/$pathHash.narinfo"; print STDERR "checking $infoUrl...\n"; my $s = `$Nix::Config::curl --fail --silent --location $infoUrl`; if ($? != 0) { my $status = $? >> 8; print STDERR "could not download ‘$infoUrl’ (curl returned status ", $? >> 8, ")\n" if $status != 22 && $status != 37; return undef; } my ($storePath2, $url, $compression, $fileHash, $fileSize, $narHash, $narSize, $deriver); my @refs; foreach my $line (split "\n", $s) { $line =~ /^(.*): (.*)$/ or return undef; if ($1 eq "StorePath") { $storePath2 = $2; } elsif ($1 eq "URL") { $url = $2; } elsif ($1 eq "Compression") { $compression = $2; } elsif ($1 eq "FileHash") { $fileHash = $2; } elsif ($1 eq "FileSize") { $fileSize = int($2); } elsif ($1 eq "NarHash") { $narHash = $2; } elsif ($1 eq "NarSize") { $narSize = int($2); } elsif ($1 eq "References") { @refs = split / /, $2; } elsif ($1 eq "Deriver") { $deriver = $2; } } return undef if $storePath ne $storePath2; if ($storePath ne $storePath2 || !defined $url || !defined $narHash) { print STDERR "bad NAR info file ‘$infoUrl’\n"; return undef; } return { url => $url , compression => ($compression || "bzip2") , fileHash => $fileHash , fileSize => $fileSize , narHash => $narHash , narSize => $narSize , refs => [ @refs ] , deriver => $deriver }; } sub getCacheId { my ($binaryCacheUrl) = @_; my $cacheId = $cacheIds{$binaryCacheUrl}; return $cacheId if defined $cacheId; # FIXME: not atomic. my @res = @{$dbh->selectcol_arrayref("select id from BinaryCaches where url = ?", {}, $binaryCacheUrl)}; if (scalar @res == 1) { $cacheId = $res[0]; } else { $dbh->do("insert into BinaryCaches(url) values (?)", {}, $binaryCacheUrl); $cacheId = $dbh->last_insert_id("", "", "", ""); } $cacheIds{$binaryCacheUrl} = $cacheId; return $cacheId; } sub cachedGetInfoFrom { my ($storePath, $pathHash, $binaryCacheUrl) = @_; my $cacheId = getCacheId($binaryCacheUrl); # Look up $storePath in the SQLite cache. $queryNAR->execute($cacheId, basename($storePath)); my $res = $queryNAR->fetchrow_hashref(); return { url => $res->{url} , compression => $res->{compression} , fileHash => $res->{fileHash} , fileSize => $res->{fileSize} , narHash => $res->{narHash} , narSize => $res->{narSize} , refs => [ split " ", $res->{refs} ] , deriver => $res->{deriver} } if defined $res; # Not found, so do an HTTP request to get the info. my $info = getInfoFrom($storePath, $pathHash, $binaryCacheUrl); # Cache the result. $insertNAR->execute( $cacheId, basename($storePath), $info->{url}, $info->{compression}, $info->{fileHash}, $info->{fileSize}, $info->{narHash}, $info->{narSize}, join(" ", @{$info->{refs}}), $info->{deriver}, $info->{system}, time()) if defined $info; return $info; } sub getInfo { my ($storePath) = @_; my $pathHash = substr(basename($storePath), 0, 32); cache: foreach my $binaryCacheUrl (@binaryCacheUrls) { my $info = cachedGetInfoFrom($storePath, $pathHash, $binaryCacheUrl); return $info if defined $info; } return undef; } sub downloadBinary { my ($storePath) = @_; my $pathHash = substr(basename($storePath), 0, 32); cache: foreach my $binaryCacheUrl (@binaryCacheUrls) { my $info = cachedGetInfoFrom($storePath, $pathHash, $binaryCacheUrl); if (defined $info) { my $decompressor; if ($info->{compression} eq "bzip2") { $decompressor = "$Nix::Config::bzip2 -d"; } elsif ($info->{compression} eq "xz") { $decompressor = "$Nix::Config::xz -d"; } else { print STDERR "unknown compression method ‘$info->{compression}’\n"; next; } if (system("$Nix::Config::curl --fail --location $binaryCacheUrl/$info->{url} | $decompressor | $Nix::Config::binDir/nix-store --restore $storePath") != 0) { die "download of `$info->{url}' failed" . ($! ? ": $!" : "") . "\n" unless $? == 0; next; } # The hash in the manifest can be either in base-16 or # base-32. Handle both. $info->{narHash} =~ /^sha256:(.*)$/ or die "invalid hash"; my $hash = $1; my $hash2 = hashPath("sha256", 1, $storePath); die "hash mismatch in downloaded path ‘$storePath’; expected $hash, got $hash2\n" if $hash ne $hash2; return 1; } } return 0; } initCache(); if ($ARGV[0] eq "--query") { while () { my $cmd = $_; chomp $cmd; if ($cmd eq "have") { my $storePath = ; chomp $storePath; # FIXME: want to give correct info here, but it's too slow. print "0\n"; #my $info = getInfo($storePath); #if (defined $info) { print "1\n"; } else { print "0\n"; } } elsif ($cmd eq "info") { my $storePath = ; chomp $storePath; my $info = getInfo($storePath); if (defined $info) { print "1\n"; print $info->{deriver} ? "$Nix::Config::storeDir/$info->{deriver}" : "", "\n"; print scalar @{$info->{refs}}, "\n"; print "$Nix::Config::storeDir/$_\n" foreach @{$info->{refs}}; print $info->{fileSize} || 0, "\n"; print $info->{narSize} || 0, "\n"; } else { print "0\n"; } } else { die "unknown command `$cmd'"; } flush STDOUT; } } elsif ($ARGV[0] eq "--substitute") { my $storePath = $ARGV[1] or die; if (!downloadBinary($storePath)) { print STDERR "could not download ‘$storePath’ from any binary cache\n"; } } else { die; }