# This script mirrors a remote Nix channel in the local filesystem. # It downloads the remote manifest, then any NAR files that are not # already available in the target directory. If $ENABLE_PATCHES is # set, it also generates patches between the NAR files in the old # version of the manifest and the new version. Because this script # can take a long time to finish, it uses a lock to guard against # concurrent updates, allowing it to be run periodically from a cron # job. use strict; use Nix::Manifest; use Nix::GeneratePatches; use File::Basename; use File::stat; use File::Temp qw/tempfile tempdir/; use Fcntl ':flock'; use POSIX qw(strftime); if (scalar @ARGV != 6 && scalar @ARGV != 7) { print STDERR "Syntax: perl mirror-channel.pl []\n"; exit 1; } my $curl = "curl --location --silent --show-error --fail"; my $srcChannelURL = $ARGV[0]; my $dstChannelPath = $ARGV[1]; my $narPath = $ARGV[2]; my $narURL = $ARGV[3]; my $patchesPath = $ARGV[4]; my $patchesURL = $ARGV[5]; my $nixexprsURL = $ARGV[6] || "$srcChannelURL/nixexprs.tar.bz2"; my $enablePatches = defined $ENV{'ENABLE_PATCHES'} && -e "$dstChannelPath/MANIFEST"; die "$dstChannelPath doesn't exist\n" unless -d $dstChannelPath; die "$narPath doesn't exist\n" unless -d $narPath; die "$patchesPath doesn't exist\n" unless -d $patchesPath; my $manifestPath = "$dstChannelPath/MANIFEST"; my $tmpDir = tempdir("nix-mirror-XXXXXXX", TMPDIR => 1, CLEANUP => 1); open LOCK, ">$dstChannelPath/.lock" or die; flock LOCK, LOCK_EX; print STDERR "started mirroring at ", strftime("%a %b %e %H:%M:%S %Y", localtime), "\n"; # Backup the old manifest once per day. my $backupPath = strftime("$dstChannelPath/MANIFEST.backup-%Y%m%d", gmtime); if (-f $manifestPath && ! -f $backupPath) { system "cp $manifestPath $backupPath"; } # Read the old manifest, if available. my %narFilesOld; my %patchesOld; readManifest($manifestPath, \%narFilesOld, \%patchesOld) if -f $manifestPath; my %knownURLs; while (my ($storePath, $files) = each %narFilesOld) { $knownURLs{$_->{url}} = $_ foreach @{$files}; } # Fetch the new manifest. my $srcManifest = "$tmpDir/MANIFEST.src"; system("$curl '$srcChannelURL/MANIFEST' > $srcManifest") == 0 or die; # Read the manifest. my (%narFiles, %patches); readManifest($srcManifest, \%narFiles, \%patches); %patches = (); # not supported yet my $size = scalar (keys %narFiles); print "$size store paths in manifest\n"; # Protect against Hydra problems that leave the channel empty. die "cowardly refusing to mirror an empty channel" if $size == 0; # Download every file that we don't already have, and update every URL # to point to the mirror. Also fill in the size and hash fields in # the manifest in order to be compatible with Nix < 0.13. while (my ($storePath, $files) = each %narFiles) { foreach my $file (@{$files}) { my $narHash = $file->{narHash}; my $srcURL = $file->{url}; my $dstName = $narHash; $dstName =~ s/:/_/; # `:' in filenames might cause problems my $dstFile = "$narPath/$dstName"; my $dstURL = "$narURL/$dstName"; $file->{url} = $dstURL; if (! -e $dstFile) { print "downloading $srcURL\n"; my $dstFileTmp = "$narPath/.tmp.$$.nar.$dstName"; system("$curl '$srcURL' > $dstFileTmp") == 0 or die "failed to download `$srcURL'"; # Verify whether the downloaded file is a bzipped NAR file # that matches the NAR hash given in the manifest. system("bunzip2 < $dstFileTmp > $tmpDir/out") == 0 or die "downloaded file is not a bzip2 file!"; my $hash = `nix-hash --type sha256 --flat $tmpDir/out`; chomp $hash; die "hash mismatch in downloaded file `$srcURL'" if "sha256:$hash" ne $file->{narHash}; rename($dstFileTmp, $dstFile) or die "cannot rename $dstFileTmp"; } my $old = $knownURLs{$dstURL}; if (defined $old) { $file->{size} = $old->{size}; $file->{hash} = $old->{hash}; } else { $file->{size} = stat($dstFile)->size or die "cannot get size of $dstFile"; my $hashFile = "$narPath/.hash.$dstName"; my $hash; if (-e $hashFile) { open HASH, "<$hashFile" or die; $hash = ; close HASH; } else { $hash = `nix-hash --flat --type sha256 --base32 '$dstFile'` or die; chomp $hash; open HASH, ">$hashFile" or die; print HASH $hash; close HASH; } $file->{hash} = "sha256:$hash"; } } } # Read all the old patches and propagate the useful ones. We use the # file "all-patches" to keep track of all patches that have been # generated in the past, so that patches are not lost if (for # instance) a package temporarily disappears from the source channel, # or if multiple instances of this script are running concurrently. my (%dummy1, %dummy2, %allPatches); sub readAllPatches { readManifest("$patchesPath/all-patches", \%dummy1, \%dummy2, \%allPatches) if -f "$patchesPath/all-patches"; } readAllPatches; propagatePatches \%allPatches, \%narFiles, \%patches; propagatePatches \%patchesOld, \%narFiles, \%patches; # not really needed # Make the temporary manifest available. writeManifest("$dstChannelPath/MANIFEST.tmp", \%narFiles, \%patches); rename("$dstChannelPath/MANIFEST.tmp", "$manifestPath") or die; rename("$dstChannelPath/MANIFEST.tmp.bz2", "$manifestPath.bz2") or die; # Mirror nixexprs.tar.bz2. This should really be done atomically with updating the manifest. my $tmpFile = "$dstChannelPath/.tmp.$$.nixexprs.tar.bz2"; system("$curl '$nixexprsURL' > $tmpFile") == 0 or die "cannot download `$nixexprsURL'"; rename($tmpFile, "$dstChannelPath/nixexprs.tar.bz2") or die "cannot rename $tmpFile"; # Release the lock on the manifest to allow the manifest to be updated # by other runs of this script while we're generating patches. flock LOCK, LOCK_UN; if ($enablePatches) { # Generate patches asynchronously. This can take a long time. generatePatches(\%narFilesOld, \%narFiles, \%allPatches, \%patches, $narPath, $patchesPath, $patchesURL, $tmpDir); # Lock all-patches. open PLOCK, ">$patchesPath/all-patches.lock" or die; flock PLOCK, LOCK_EX; # Update the list of all patches. We need to reread all-patches # and merge in our new patches because the file may have changed # in the meantime. readAllPatches; copyPatches \%patches, \%allPatches; writeManifest("$patchesPath/all-patches", {}, \%allPatches, 0); # Reacquire the manifest lock. flock LOCK, LOCK_EX; # Rewrite the manifest. We have to reread it and propagate all # patches because it may have changed in the meantime. readManifest($manifestPath, \%narFiles, \%patches); propagatePatches \%allPatches, \%narFiles, \%patches; writeManifest($manifestPath, \%narFiles, \%patches); }