about summary refs log tree commit diff
path: root/third_party/nix/perl/lib/Nix
diff options
context:
space:
mode:
authorVincent Ambo <tazjin@google.com>2020-05-17T14·52+0100
committerVincent Ambo <tazjin@google.com>2020-05-17T14·52+0100
commit7994fd1d545cc5c876d6f21db7ddf9185d23dad6 (patch)
tree32dd695785378c5b9c8be97fc583e9dfc62cb105 /third_party/nix/perl/lib/Nix
parentcf8cd640c1adf74a3706efbcb0ea4625da106fb2 (diff)
parent90b3b31dc27f31e9b11653a636025d29ddb087a3 (diff)
Add 'third_party/nix/' from commit 'be66c7a6b24e3c3c6157fd37b86c7203d14acf10' r/724
git-subtree-dir: third_party/nix
git-subtree-mainline: cf8cd640c1adf74a3706efbcb0ea4625da106fb2
git-subtree-split: be66c7a6b24e3c3c6157fd37b86c7203d14acf10
Diffstat (limited to 'third_party/nix/perl/lib/Nix')
-rw-r--r--third_party/nix/perl/lib/Nix/Config.pm.in34
-rw-r--r--third_party/nix/perl/lib/Nix/CopyClosure.pm61
-rw-r--r--third_party/nix/perl/lib/Nix/Manifest.pm325
-rw-r--r--third_party/nix/perl/lib/Nix/SSH.pm110
-rw-r--r--third_party/nix/perl/lib/Nix/Store.pm95
-rw-r--r--third_party/nix/perl/lib/Nix/Store.xs346
-rw-r--r--third_party/nix/perl/lib/Nix/Utils.pm47
7 files changed, 1018 insertions, 0 deletions
diff --git a/third_party/nix/perl/lib/Nix/Config.pm.in b/third_party/nix/perl/lib/Nix/Config.pm.in
new file mode 100644
index 000000000000..67a20c3f41d7
--- /dev/null
+++ b/third_party/nix/perl/lib/Nix/Config.pm.in
@@ -0,0 +1,34 @@
+package Nix::Config;
+
+use MIME::Base64;
+
+$version = "@PACKAGE_VERSION@";
+
+$binDir = $ENV{"NIX_BIN_DIR"} || "@nixbindir@";
+$libexecDir = $ENV{"NIX_LIBEXEC_DIR"} || "@nixlibexecdir@";
+$stateDir = $ENV{"NIX_STATE_DIR"} || "@nixlocalstatedir@/nix";
+$logDir = $ENV{"NIX_LOG_DIR"} || "@nixlocalstatedir@/log/nix";
+$confDir = $ENV{"NIX_CONF_DIR"} || "@nixsysconfdir@/nix";
+$storeDir = $ENV{"NIX_STORE_DIR"} || "@nixstoredir@";
+
+$bzip2 = "@bzip2@";
+$xz = "@xz@";
+$curl = "@curl@";
+
+$useBindings = 1;
+
+%config = ();
+
+sub readConfig {
+    my $config = "$confDir/nix.conf";
+    return unless -f $config;
+
+    open CONFIG, "<$config" or die "cannot open '$config'";
+    while (<CONFIG>) {
+        /^\s*([\w\-\.]+)\s*=\s*(.*)$/ or next;
+        $config{$1} = $2;
+    }
+    close CONFIG;
+}
+
+return 1;
diff --git a/third_party/nix/perl/lib/Nix/CopyClosure.pm b/third_party/nix/perl/lib/Nix/CopyClosure.pm
new file mode 100644
index 000000000000..902ee1a1bc9f
--- /dev/null
+++ b/third_party/nix/perl/lib/Nix/CopyClosure.pm
@@ -0,0 +1,61 @@
+package Nix::CopyClosure;
+
+use utf8;
+use strict;
+use Nix::Config;
+use Nix::Store;
+use Nix::SSH;
+use List::Util qw(sum);
+use IPC::Open2;
+
+
+sub copyToOpen {
+    my ($from, $to, $sshHost, $storePaths, $includeOutputs, $dryRun, $useSubstitutes) = @_;
+
+    $useSubstitutes = 0 if $dryRun || !defined $useSubstitutes;
+
+    # Get the closure of this path.
+    my @closure = reverse(topoSortPaths(computeFSClosure(0, $includeOutputs,
+        map { followLinksToStorePath $_ } @{$storePaths})));
+
+    # Send the "query valid paths" command with the "lock" option
+    # enabled. This prevents a race where the remote host
+    # garbage-collect paths that are already there. Optionally, ask
+    # the remote host to substitute missing paths.
+    syswrite($to, pack("L<x4L<x4L<x4", 1, 1, $useSubstitutes)) or die;
+    writeStrings(\@closure, $to);
+
+    # Get back the set of paths that are already valid on the remote host.
+    my %present;
+    $present{$_} = 1 foreach readStrings($from);
+
+    my @missing = grep { !$present{$_} } @closure;
+    return if !@missing;
+
+    my $missingSize = 0;
+    $missingSize += (queryPathInfo($_, 1))[3] foreach @missing;
+
+    printf STDERR "copying %d missing paths (%.2f MiB) to '$sshHost'...\n",
+        scalar(@missing), $missingSize / (1024**2);
+    return if $dryRun;
+
+    # Send the "import paths" command.
+    syswrite($to, pack("L<x4", 4)) or die;
+    exportPaths(fileno($to), @missing);
+    readInt($from) == 1 or die "remote machine '$sshHost' failed to import closure\n";
+}
+
+
+sub copyTo {
+    my ($sshHost, $storePaths, $includeOutputs, $dryRun, $useSubstitutes) = @_;
+
+    # Connect to the remote host.
+    my ($from, $to) = connectToRemoteNix($sshHost, []);
+
+    copyToOpen($from, $to, $sshHost, $storePaths, $includeOutputs, $dryRun, $useSubstitutes);
+
+    close $to;
+}
+
+
+1;
diff --git a/third_party/nix/perl/lib/Nix/Manifest.pm b/third_party/nix/perl/lib/Nix/Manifest.pm
new file mode 100644
index 000000000000..6438398e1766
--- /dev/null
+++ b/third_party/nix/perl/lib/Nix/Manifest.pm
@@ -0,0 +1,325 @@
+package Nix::Manifest;
+
+use utf8;
+use strict;
+use DBI;
+use DBD::SQLite;
+use Cwd;
+use File::stat;
+use File::Path;
+use Fcntl ':flock';
+use MIME::Base64;
+use Nix::Config;
+use Nix::Store;
+
+our @ISA = qw(Exporter);
+our @EXPORT = qw(readManifest writeManifest addPatch parseNARInfo fingerprintPath);
+
+
+sub addNAR {
+    my ($narFiles, $storePath, $info) = @_;
+
+    $$narFiles{$storePath} = []
+        unless defined $$narFiles{$storePath};
+
+    my $narFileList = $$narFiles{$storePath};
+
+    my $found = 0;
+    foreach my $narFile (@{$narFileList}) {
+        $found = 1 if $narFile->{url} eq $info->{url};
+    }
+
+    push @{$narFileList}, $info if !$found;
+}
+
+
+sub addPatch {
+    my ($patches, $storePath, $patch) = @_;
+
+    $$patches{$storePath} = []
+        unless defined $$patches{$storePath};
+
+    my $patchList = $$patches{$storePath};
+
+    my $found = 0;
+    foreach my $patch2 (@{$patchList}) {
+        $found = 1 if
+            $patch2->{url} eq $patch->{url} &&
+            $patch2->{basePath} eq $patch->{basePath};
+    }
+
+    push @{$patchList}, $patch if !$found;
+
+    return !$found;
+}
+
+
+sub readManifest_ {
+    my ($manifest, $addNAR, $addPatch) = @_;
+
+    # Decompress the manifest if necessary.
+    if ($manifest =~ /\.bz2$/) {
+        open MANIFEST, "$Nix::Config::bzip2 -d < $manifest |"
+            or die "cannot decompress '$manifest': $!";
+    } else {
+        open MANIFEST, "<$manifest"
+            or die "cannot open '$manifest': $!";
+    }
+
+    my $inside = 0;
+    my $type;
+
+    my $manifestVersion = 2;
+
+    my ($storePath, $url, $hash, $size, $basePath, $baseHash, $patchType);
+    my ($narHash, $narSize, $references, $deriver, $copyFrom, $system, $compressionType);
+
+    while (<MANIFEST>) {
+        chomp;
+        s/\#.*$//g;
+        next if (/^$/);
+
+        if (!$inside) {
+
+            if (/^\s*(\w*)\s*\{$/) {
+                $type = $1;
+                $type = "narfile" if $type eq "";
+                $inside = 1;
+                undef $storePath;
+                undef $url;
+                undef $hash;
+                undef $size;
+                undef $narHash;
+                undef $narSize;
+                undef $basePath;
+                undef $baseHash;
+                undef $patchType;
+                undef $system;
+                $references = "";
+                $deriver = "";
+                $compressionType = "bzip2";
+            }
+
+        } else {
+
+            if (/^\}$/) {
+                $inside = 0;
+
+                if ($type eq "narfile") {
+                    &$addNAR($storePath,
+                        { url => $url, hash => $hash, size => $size
+                        , narHash => $narHash, narSize => $narSize
+                        , references => $references
+                        , deriver => $deriver
+                        , system => $system
+                        , compressionType => $compressionType
+                        });
+                }
+
+                elsif ($type eq "patch") {
+                    &$addPatch($storePath,
+                        { url => $url, hash => $hash, size => $size
+                        , basePath => $basePath, baseHash => $baseHash
+                        , narHash => $narHash, narSize => $narSize
+                        , patchType => $patchType
+                        });
+                }
+
+            }
+
+            elsif (/^\s*StorePath:\s*(\/\S+)\s*$/) { $storePath = $1; }
+            elsif (/^\s*CopyFrom:\s*(\/\S+)\s*$/) { $copyFrom = $1; }
+            elsif (/^\s*Hash:\s*(\S+)\s*$/) { $hash = $1; }
+            elsif (/^\s*URL:\s*(\S+)\s*$/) { $url = $1; }
+            elsif (/^\s*Compression:\s*(\S+)\s*$/) { $compressionType = $1; }
+            elsif (/^\s*Size:\s*(\d+)\s*$/) { $size = $1; }
+            elsif (/^\s*BasePath:\s*(\/\S+)\s*$/) { $basePath = $1; }
+            elsif (/^\s*BaseHash:\s*(\S+)\s*$/) { $baseHash = $1; }
+            elsif (/^\s*Type:\s*(\S+)\s*$/) { $patchType = $1; }
+            elsif (/^\s*NarHash:\s*(\S+)\s*$/) { $narHash = $1; }
+            elsif (/^\s*NarSize:\s*(\d+)\s*$/) { $narSize = $1; }
+            elsif (/^\s*References:\s*(.*)\s*$/) { $references = $1; }
+            elsif (/^\s*Deriver:\s*(\S+)\s*$/) { $deriver = $1; }
+            elsif (/^\s*ManifestVersion:\s*(\d+)\s*$/) { $manifestVersion = $1; }
+            elsif (/^\s*System:\s*(\S+)\s*$/) { $system = $1; }
+
+            # Compatibility;
+            elsif (/^\s*NarURL:\s*(\S+)\s*$/) { $url = $1; }
+            elsif (/^\s*MD5:\s*(\S+)\s*$/) { $hash = "md5:$1"; }
+
+        }
+    }
+
+    close MANIFEST;
+
+    return $manifestVersion;
+}
+
+
+sub readManifest {
+    my ($manifest, $narFiles, $patches) = @_;
+    readManifest_($manifest,
+        sub { addNAR($narFiles, @_); },
+        sub { addPatch($patches, @_); } );
+}
+
+
+sub writeManifest {
+    my ($manifest, $narFiles, $patches, $noCompress) = @_;
+
+    open MANIFEST, ">$manifest.tmp"; # !!! check exclusive
+
+    print MANIFEST "version {\n";
+    print MANIFEST "  ManifestVersion: 3\n";
+    print MANIFEST "}\n";
+
+    foreach my $storePath (sort (keys %{$narFiles})) {
+        my $narFileList = $$narFiles{$storePath};
+        foreach my $narFile (@{$narFileList}) {
+            print MANIFEST "{\n";
+            print MANIFEST "  StorePath: $storePath\n";
+            print MANIFEST "  NarURL: $narFile->{url}\n";
+            print MANIFEST "  Compression: $narFile->{compressionType}\n";
+            print MANIFEST "  Hash: $narFile->{hash}\n" if defined $narFile->{hash};
+            print MANIFEST "  Size: $narFile->{size}\n" if defined $narFile->{size};
+            print MANIFEST "  NarHash: $narFile->{narHash}\n";
+            print MANIFEST "  NarSize: $narFile->{narSize}\n" if $narFile->{narSize};
+            print MANIFEST "  References: $narFile->{references}\n"
+                if defined $narFile->{references} && $narFile->{references} ne "";
+            print MANIFEST "  Deriver: $narFile->{deriver}\n"
+                if defined $narFile->{deriver} && $narFile->{deriver} ne "";
+            print MANIFEST "  System: $narFile->{system}\n" if defined $narFile->{system};
+            print MANIFEST "}\n";
+        }
+    }
+
+    foreach my $storePath (sort (keys %{$patches})) {
+        my $patchList = $$patches{$storePath};
+        foreach my $patch (@{$patchList}) {
+            print MANIFEST "patch {\n";
+            print MANIFEST "  StorePath: $storePath\n";
+            print MANIFEST "  NarURL: $patch->{url}\n";
+            print MANIFEST "  Hash: $patch->{hash}\n";
+            print MANIFEST "  Size: $patch->{size}\n";
+            print MANIFEST "  NarHash: $patch->{narHash}\n";
+            print MANIFEST "  NarSize: $patch->{narSize}\n" if $patch->{narSize};
+            print MANIFEST "  BasePath: $patch->{basePath}\n";
+            print MANIFEST "  BaseHash: $patch->{baseHash}\n";
+            print MANIFEST "  Type: $patch->{patchType}\n";
+            print MANIFEST "}\n";
+        }
+    }
+
+
+    close MANIFEST;
+
+    rename("$manifest.tmp", $manifest)
+        or die "cannot rename $manifest.tmp: $!";
+
+
+    # Create a bzipped manifest.
+    unless (defined $noCompress) {
+        system("$Nix::Config::bzip2 < $manifest > $manifest.bz2.tmp") == 0
+            or die "cannot compress manifest";
+
+        rename("$manifest.bz2.tmp", "$manifest.bz2")
+            or die "cannot rename $manifest.bz2.tmp: $!";
+    }
+}
+
+
+# Return a fingerprint of a store path to be used in binary cache
+# signatures. It contains the store path, the base-32 SHA-256 hash of
+# the contents of the path, and the references.
+sub fingerprintPath {
+    my ($storePath, $narHash, $narSize, $references) = @_;
+    die if substr($storePath, 0, length($Nix::Config::storeDir)) ne $Nix::Config::storeDir;
+    die if substr($narHash, 0, 7) ne "sha256:";
+    # Convert hash from base-16 to base-32, if necessary.
+    $narHash = "sha256:" . convertHash("sha256", substr($narHash, 7), 1)
+        if length($narHash) == 71;
+    die if length($narHash) != 59;
+    foreach my $ref (@{$references}) {
+        die if substr($ref, 0, length($Nix::Config::storeDir)) ne $Nix::Config::storeDir;
+    }
+    return "1;" . $storePath . ";" . $narHash . ";" . $narSize . ";" . join(",", @{$references});
+}
+
+
+# Parse a NAR info file.
+sub parseNARInfo {
+    my ($storePath, $content, $requireValidSig, $location) = @_;
+
+    my ($storePath2, $url, $fileHash, $fileSize, $narHash, $narSize, $deriver, $system, $sig);
+    my $compression = "bzip2";
+    my @refs;
+
+    foreach my $line (split "\n", $content) {
+        return undef unless $line =~ /^(.*): (.*)$/;
+        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; }
+        elsif ($1 eq "System") { $system = $2; }
+        elsif ($1 eq "Sig") { $sig = $2; }
+    }
+
+    return undef if $storePath ne $storePath2 || !defined $url || !defined $narHash;
+
+    my $res =
+        { url => $url
+        , compression => $compression
+        , fileHash => $fileHash
+        , fileSize => $fileSize
+        , narHash => $narHash
+        , narSize => $narSize
+        , refs => [ @refs ]
+        , deriver => $deriver
+        , system => $system
+        };
+
+    if ($requireValidSig) {
+        # FIXME: might be useful to support multiple signatures per .narinfo.
+
+        if (!defined $sig) {
+            warn "NAR info file '$location' lacks a signature; ignoring\n";
+            return undef;
+        }
+        my ($keyName, $sig64) = split ":", $sig;
+        return undef unless defined $keyName && defined $sig64;
+
+        my $publicKey = $Nix::Config::binaryCachePublicKeys{$keyName};
+        if (!defined $publicKey) {
+            warn "NAR info file '$location' is signed by unknown key '$keyName'; ignoring\n";
+            return undef;
+        }
+
+        my $fingerprint;
+        eval {
+            $fingerprint = fingerprintPath(
+                $storePath, $narHash, $narSize,
+                [ map { "$Nix::Config::storeDir/$_" } @refs ]);
+        };
+        if ($@) {
+            warn "cannot compute fingerprint of '$location'; ignoring\n";
+            return undef;
+        }
+
+        if (!checkSignature($publicKey, decode_base64($sig64), $fingerprint)) {
+            warn "NAR info file '$location' has an incorrect signature; ignoring\n";
+            return undef;
+        }
+
+        $res->{signedBy} = $keyName;
+    }
+
+    return $res;
+}
+
+
+return 1;
diff --git a/third_party/nix/perl/lib/Nix/SSH.pm b/third_party/nix/perl/lib/Nix/SSH.pm
new file mode 100644
index 000000000000..490ba0ea991e
--- /dev/null
+++ b/third_party/nix/perl/lib/Nix/SSH.pm
@@ -0,0 +1,110 @@
+package Nix::SSH;
+
+use utf8;
+use strict;
+use File::Temp qw(tempdir);
+use IPC::Open2;
+
+our @ISA = qw(Exporter);
+our @EXPORT = qw(
+  @globalSshOpts
+  readN readInt readString readStrings
+  writeInt writeString writeStrings
+  connectToRemoteNix
+);
+
+
+our @globalSshOpts = split ' ', ($ENV{"NIX_SSHOPTS"} or "");
+
+
+sub readN {
+    my ($bytes, $from) = @_;
+    my $res = "";
+    while ($bytes > 0) {
+        my $s;
+        my $n = sysread($from, $s, $bytes);
+        die "I/O error reading from remote side\n" if !defined $n;
+        die "got EOF while expecting $bytes bytes from remote side\n" if !$n;
+        $bytes -= $n;
+        $res .= $s;
+    }
+    return $res;
+}
+
+
+sub readInt {
+    my ($from) = @_;
+    return unpack("L<x4", readN(8, $from));
+}
+
+
+sub readString {
+    my ($from) = @_;
+    my $len = readInt($from);
+    my $s = readN($len, $from);
+    readN(8 - $len % 8, $from) if $len % 8; # skip padding
+    return $s;
+}
+
+
+sub readStrings {
+    my ($from) = @_;
+    my $n = readInt($from);
+    my @res;
+    push @res, readString($from) while $n--;
+    return @res;
+}
+
+
+sub writeInt {
+    my ($n, $to) = @_;
+    syswrite($to, pack("L<x4", $n)) or die;
+}
+
+
+sub writeString {
+    my ($s, $to) = @_;
+    my $len = length $s;
+    my $req .= pack("L<x4", $len);
+    $req .= $s;
+    $req .= "\000" x (8 - $len % 8) if $len % 8;
+    syswrite($to, $req) or die;
+}
+
+
+sub writeStrings {
+    my ($ss, $to) = @_;
+    writeInt(scalar(@{$ss}), $to);
+    writeString($_, $to) foreach @{$ss};
+}
+
+
+sub connectToRemoteNix {
+    my ($sshHost, $sshOpts, $extraFlags) = @_;
+
+    $extraFlags ||= "";
+
+    # Start ‘nix-store --serve’ on the remote host.
+    my ($from, $to);
+    # FIXME: don't start a shell, start ssh directly.
+    my $pid = open2($from, $to, "exec ssh -x -a $sshHost @globalSshOpts @{$sshOpts} nix-store --serve --write $extraFlags");
+
+    # Do the handshake.
+    my $magic;
+    eval {
+        my $SERVE_MAGIC_1 = 0x390c9deb; # FIXME
+        my $clientVersion = 0x200;
+        syswrite($to, pack("L<x4L<x4", $SERVE_MAGIC_1, $clientVersion)) or die;
+        $magic = readInt($from);
+    };
+    die "unable to connect to '$sshHost'\n" if $@;
+    die "did not get valid handshake from remote host\n" if $magic  != 0x5452eecb;
+
+    my $serverVersion = readInt($from);
+    die "unsupported server version\n" if $serverVersion < 0x200 || $serverVersion >= 0x300;
+
+    return ($from, $to, $pid);
+}
+
+
+1;
diff --git a/third_party/nix/perl/lib/Nix/Store.pm b/third_party/nix/perl/lib/Nix/Store.pm
new file mode 100644
index 000000000000..d226264d4df3
--- /dev/null
+++ b/third_party/nix/perl/lib/Nix/Store.pm
@@ -0,0 +1,95 @@
+package Nix::Store;
+
+use strict;
+use warnings;
+use Nix::Config;
+
+require Exporter;
+
+our @ISA = qw(Exporter);
+
+our %EXPORT_TAGS = ( 'all' => [ qw( ) ] );
+
+our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
+
+our @EXPORT = qw(
+    setVerbosity
+    isValidPath queryReferences queryPathInfo queryDeriver queryPathHash
+    queryPathFromHashPart
+    topoSortPaths computeFSClosure followLinksToStorePath exportPaths importPaths
+    hashPath hashFile hashString convertHash
+    signString checkSignature
+    addToStore makeFixedOutputPath
+    derivationFromPath
+    addTempRoot
+);
+
+our $VERSION = '0.15';
+
+sub backtick {
+    open(RES, "-|", @_) or die;
+    local $/;
+    my $res = <RES> || "";
+    close RES or die;
+    return $res;
+}
+
+if ($Nix::Config::useBindings) {
+    require XSLoader;
+    XSLoader::load('Nix::Store', $VERSION);
+} else {
+
+    # Provide slow fallbacks of some functions on platforms that don't
+    # support the Perl bindings.
+
+    use File::Temp;
+    use Fcntl qw/F_SETFD/;
+
+    *hashFile = sub {
+        my ($algo, $base32, $path) = @_;
+        my $res = backtick("$Nix::Config::binDir/nix-hash", "--flat", $path, "--type", $algo, $base32 ? "--base32" : ());
+        chomp $res;
+        return $res;
+    };
+
+    *hashPath = sub {
+        my ($algo, $base32, $path) = @_;
+        my $res = backtick("$Nix::Config::binDir/nix-hash", $path, "--type", $algo, $base32 ? "--base32" : ());
+        chomp $res;
+        return $res;
+    };
+
+    *hashString = sub {
+        my ($algo, $base32, $s) = @_;
+        my $fh = File::Temp->new();
+        print $fh $s;
+        my $res = backtick("$Nix::Config::binDir/nix-hash", $fh->filename, "--type", $algo, $base32 ? "--base32" : ());
+        chomp $res;
+        return $res;
+    };
+
+    *addToStore = sub {
+        my ($srcPath, $recursive, $algo) = @_;
+        die "not implemented" if $recursive || $algo ne "sha256";
+        my $res = backtick("$Nix::Config::binDir/nix-store", "--add", $srcPath);
+        chomp $res;
+        return $res;
+    };
+
+    *isValidPath = sub {
+        my ($path) = @_;
+        my $res = backtick("$Nix::Config::binDir/nix-store", "--check-validity", "--print-invalid", $path);
+        chomp $res;
+        return $res ne $path;
+    };
+
+    *queryPathHash = sub {
+        my ($path) = @_;
+        my $res = backtick("$Nix::Config::binDir/nix-store", "--query", "--hash", $path);
+        chomp $res;
+        return $res;
+    };
+}
+
+1;
+__END__
diff --git a/third_party/nix/perl/lib/Nix/Store.xs b/third_party/nix/perl/lib/Nix/Store.xs
new file mode 100644
index 000000000000..ce553bb53ebc
--- /dev/null
+++ b/third_party/nix/perl/lib/Nix/Store.xs
@@ -0,0 +1,346 @@
+#include "config.h"
+
+#include "EXTERN.h"
+#include "perl.h"
+#include "XSUB.h"
+
+/* Prevent a clash between some Perl and libstdc++ macros. */
+#undef do_open
+#undef do_close
+
+#include "derivations.hh"
+#include "globals.hh"
+#include "store-api.hh"
+#include "util.hh"
+#include "crypto.hh"
+
+#if HAVE_SODIUM
+#include <sodium.h>
+#endif
+
+
+using namespace nix;
+
+
+static ref<Store> store()
+{
+    static std::shared_ptr<Store> _store;
+    if (!_store) {
+        try {
+            loadConfFile();
+            settings.lockCPU = false;
+            _store = openStore();
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+    }
+    return ref<Store>(_store);
+}
+
+
+MODULE = Nix::Store PACKAGE = Nix::Store
+PROTOTYPES: ENABLE
+
+
+#undef dNOOP // Hack to work around "error: declaration of 'Perl___notused' has a different language linkage" error message on clang.
+#define dNOOP
+
+
+void init()
+    CODE:
+        store();
+
+
+void setVerbosity(int level)
+    CODE:
+        verbosity = (Verbosity) level;
+
+
+int isValidPath(char * path)
+    CODE:
+        try {
+            RETVAL = store()->isValidPath(path);
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+    OUTPUT:
+        RETVAL
+
+
+SV * queryReferences(char * path)
+    PPCODE:
+        try {
+            PathSet paths = store()->queryPathInfo(path)->references;
+            for (PathSet::iterator i = paths.begin(); i != paths.end(); ++i)
+                XPUSHs(sv_2mortal(newSVpv(i->c_str(), 0)));
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+
+
+SV * queryPathHash(char * path)
+    PPCODE:
+        try {
+            auto s = store()->queryPathInfo(path)->narHash.to_string();
+            XPUSHs(sv_2mortal(newSVpv(s.c_str(), 0)));
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+
+
+SV * queryDeriver(char * path)
+    PPCODE:
+        try {
+            auto deriver = store()->queryPathInfo(path)->deriver;
+            if (deriver == "") XSRETURN_UNDEF;
+            XPUSHs(sv_2mortal(newSVpv(deriver.c_str(), 0)));
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+
+
+SV * queryPathInfo(char * path, int base32)
+    PPCODE:
+        try {
+            auto info = store()->queryPathInfo(path);
+            if (info->deriver == "")
+                XPUSHs(&PL_sv_undef);
+            else
+                XPUSHs(sv_2mortal(newSVpv(info->deriver.c_str(), 0)));
+            auto s = info->narHash.to_string(base32 ? Base32 : Base16);
+            XPUSHs(sv_2mortal(newSVpv(s.c_str(), 0)));
+            mXPUSHi(info->registrationTime);
+            mXPUSHi(info->narSize);
+            AV * arr = newAV();
+            for (PathSet::iterator i = info->references.begin(); i != info->references.end(); ++i)
+                av_push(arr, newSVpv(i->c_str(), 0));
+            XPUSHs(sv_2mortal(newRV((SV *) arr)));
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+
+
+SV * queryPathFromHashPart(char * hashPart)
+    PPCODE:
+        try {
+            Path path = store()->queryPathFromHashPart(hashPart);
+            XPUSHs(sv_2mortal(newSVpv(path.c_str(), 0)));
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+
+
+SV * computeFSClosure(int flipDirection, int includeOutputs, ...)
+    PPCODE:
+        try {
+            PathSet paths;
+            for (int n = 2; n < items; ++n)
+                store()->computeFSClosure(SvPV_nolen(ST(n)), paths, flipDirection, includeOutputs);
+            for (PathSet::iterator i = paths.begin(); i != paths.end(); ++i)
+                XPUSHs(sv_2mortal(newSVpv(i->c_str(), 0)));
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+
+
+SV * topoSortPaths(...)
+    PPCODE:
+        try {
+            PathSet paths;
+            for (int n = 0; n < items; ++n) paths.insert(SvPV_nolen(ST(n)));
+            Paths sorted = store()->topoSortPaths(paths);
+            for (Paths::iterator i = sorted.begin(); i != sorted.end(); ++i)
+                XPUSHs(sv_2mortal(newSVpv(i->c_str(), 0)));
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+
+
+SV * followLinksToStorePath(char * path)
+    CODE:
+        try {
+            RETVAL = newSVpv(store()->followLinksToStorePath(path).c_str(), 0);
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+    OUTPUT:
+        RETVAL
+
+
+void exportPaths(int fd, ...)
+    PPCODE:
+        try {
+            Paths paths;
+            for (int n = 1; n < items; ++n) paths.push_back(SvPV_nolen(ST(n)));
+            FdSink sink(fd);
+            store()->exportPaths(paths, sink);
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+
+
+void importPaths(int fd, int dontCheckSigs)
+    PPCODE:
+        try {
+            FdSource source(fd);
+            store()->importPaths(source, nullptr, dontCheckSigs ? NoCheckSigs : CheckSigs);
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+
+
+SV * hashPath(char * algo, int base32, char * path)
+    PPCODE:
+        try {
+            Hash h = hashPath(parseHashType(algo), path).first;
+            auto s = h.to_string(base32 ? Base32 : Base16, false);
+            XPUSHs(sv_2mortal(newSVpv(s.c_str(), 0)));
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+
+
+SV * hashFile(char * algo, int base32, char * path)
+    PPCODE:
+        try {
+            Hash h = hashFile(parseHashType(algo), path);
+            auto s = h.to_string(base32 ? Base32 : Base16, false);
+            XPUSHs(sv_2mortal(newSVpv(s.c_str(), 0)));
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+
+
+SV * hashString(char * algo, int base32, char * s)
+    PPCODE:
+        try {
+            Hash h = hashString(parseHashType(algo), s);
+            auto s = h.to_string(base32 ? Base32 : Base16, false);
+            XPUSHs(sv_2mortal(newSVpv(s.c_str(), 0)));
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+
+
+SV * convertHash(char * algo, char * s, int toBase32)
+    PPCODE:
+        try {
+            Hash h(s, parseHashType(algo));
+            string s = h.to_string(toBase32 ? Base32 : Base16, false);
+            XPUSHs(sv_2mortal(newSVpv(s.c_str(), 0)));
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+
+
+SV * signString(char * secretKey_, char * msg)
+    PPCODE:
+        try {
+#if HAVE_SODIUM
+            auto sig = SecretKey(secretKey_).signDetached(msg);
+            XPUSHs(sv_2mortal(newSVpv(sig.c_str(), sig.size())));
+#else
+            throw Error("Nix was not compiled with libsodium, required for signed binary cache support");
+#endif
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+
+
+int checkSignature(SV * publicKey_, SV * sig_, char * msg)
+    CODE:
+        try {
+#if HAVE_SODIUM
+            STRLEN publicKeyLen;
+            unsigned char * publicKey = (unsigned char *) SvPV(publicKey_, publicKeyLen);
+            if (publicKeyLen != crypto_sign_PUBLICKEYBYTES)
+                throw Error("public key is not valid");
+
+            STRLEN sigLen;
+            unsigned char * sig = (unsigned char *) SvPV(sig_, sigLen);
+            if (sigLen != crypto_sign_BYTES)
+                throw Error("signature is not valid");
+
+            RETVAL = crypto_sign_verify_detached(sig, (unsigned char *) msg, strlen(msg), publicKey) == 0;
+#else
+            throw Error("Nix was not compiled with libsodium, required for signed binary cache support");
+#endif
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+    OUTPUT:
+        RETVAL
+
+
+SV * addToStore(char * srcPath, int recursive, char * algo)
+    PPCODE:
+        try {
+            Path path = store()->addToStore(baseNameOf(srcPath), srcPath, recursive, parseHashType(algo));
+            XPUSHs(sv_2mortal(newSVpv(path.c_str(), 0)));
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+
+
+SV * makeFixedOutputPath(int recursive, char * algo, char * hash, char * name)
+    PPCODE:
+        try {
+            Hash h(hash, parseHashType(algo));
+            Path path = store()->makeFixedOutputPath(recursive, h, name);
+            XPUSHs(sv_2mortal(newSVpv(path.c_str(), 0)));
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+
+
+SV * derivationFromPath(char * drvPath)
+    PREINIT:
+        HV *hash;
+    CODE:
+        try {
+            Derivation drv = store()->derivationFromPath(drvPath);
+            hash = newHV();
+
+            HV * outputs = newHV();
+            for (DerivationOutputs::iterator i = drv.outputs.begin(); i != drv.outputs.end(); ++i)
+                hv_store(outputs, i->first.c_str(), i->first.size(), newSVpv(i->second.path.c_str(), 0), 0);
+            hv_stores(hash, "outputs", newRV((SV *) outputs));
+
+            AV * inputDrvs = newAV();
+            for (DerivationInputs::iterator i = drv.inputDrvs.begin(); i != drv.inputDrvs.end(); ++i)
+                av_push(inputDrvs, newSVpv(i->first.c_str(), 0)); // !!! ignores i->second
+            hv_stores(hash, "inputDrvs", newRV((SV *) inputDrvs));
+
+            AV * inputSrcs = newAV();
+            for (PathSet::iterator i = drv.inputSrcs.begin(); i != drv.inputSrcs.end(); ++i)
+                av_push(inputSrcs, newSVpv(i->c_str(), 0));
+            hv_stores(hash, "inputSrcs", newRV((SV *) inputSrcs));
+
+            hv_stores(hash, "platform", newSVpv(drv.platform.c_str(), 0));
+            hv_stores(hash, "builder", newSVpv(drv.builder.c_str(), 0));
+
+            AV * args = newAV();
+            for (Strings::iterator i = drv.args.begin(); i != drv.args.end(); ++i)
+                av_push(args, newSVpv(i->c_str(), 0));
+            hv_stores(hash, "args", newRV((SV *) args));
+
+            HV * env = newHV();
+            for (StringPairs::iterator i = drv.env.begin(); i != drv.env.end(); ++i)
+                hv_store(env, i->first.c_str(), i->first.size(), newSVpv(i->second.c_str(), 0), 0);
+            hv_stores(hash, "env", newRV((SV *) env));
+
+            RETVAL = newRV_noinc((SV *)hash);
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
+    OUTPUT:
+        RETVAL
+
+
+void addTempRoot(char * storePath)
+    PPCODE:
+        try {
+            store()->addTempRoot(storePath);
+        } catch (Error & e) {
+            croak("%s", e.what());
+        }
diff --git a/third_party/nix/perl/lib/Nix/Utils.pm b/third_party/nix/perl/lib/Nix/Utils.pm
new file mode 100644
index 000000000000..44955a70698c
--- /dev/null
+++ b/third_party/nix/perl/lib/Nix/Utils.pm
@@ -0,0 +1,47 @@
+package Nix::Utils;
+
+use utf8;
+use File::Temp qw(tempdir);
+
+our @ISA = qw(Exporter);
+our @EXPORT = qw(checkURL uniq writeFile readFile mkTempDir);
+
+$urlRE = "(?: [a-zA-Z][a-zA-Z0-9\+\-\.]*\:[a-zA-Z0-9\%\/\?\:\@\&\=\+\$\,\-\_\.\!\~\*]+ )";
+
+sub checkURL {
+    my ($url) = @_;
+    die "invalid URL '$url'\n" unless $url =~ /^ $urlRE $ /x;
+}
+
+sub uniq {
+    my %seen;
+    my @res;
+    foreach my $name (@_) {
+        next if $seen{$name};
+        $seen{$name} = 1;
+        push @res, $name;
+    }
+    return @res;
+}
+
+sub writeFile {
+    my ($fn, $s) = @_;
+    open TMP, ">$fn" or die "cannot create file '$fn': $!";
+    print TMP "$s" or die;
+    close TMP or die;
+}
+
+sub readFile {
+    local $/ = undef;
+    my ($fn) = @_;
+    open TMP, "<$fn" or die "cannot open file '$fn': $!";
+    my $s = <TMP>;
+    close TMP or die;
+    return $s;
+}
+
+sub mkTempDir {
+    my ($name) = @_;
+    return tempdir("$name.XXXXXX", CLEANUP => 1, DIR => $ENV{"TMPDIR"} // $ENV{"XDG_RUNTIME_DIR"} // "/tmp")
+        || die "cannot create a temporary directory";
+}