Nix caches don’t serve store paths as regular .tar or .zip archives, but as .nar (Nix ARchive) files. There are a few reasons for that, but the main ones are that Nix requires symbolic links and execute bits to be preserved, while it doesn’t care about other info like timestamps.
TL;DR
- Build standalone
nix-nar
binary on a machine that has nix installed:nix build gitlab:abstract-binary/nix-nar-rs#static-x86_64-linux
- Put the resulting
target/x86_64-unknown-linux-musl/release/nix-nar
binary somewhere you can retrieve it (git repo, ftp server)
- Download it to your target machine (and
chmod +x nix-nar
)
- Download the static nix NAR file on the target:
wget -O nix.nar.xz https://cache.nixos.org/nar/00iic1w1zq5amx1iz75k4k8qihh4jz40635xnnqsghkiwadpypnr.nar.xz
- Decompress it:
xz -d nix.nar.xz
- Extract the
nix
binary from the NAR:./nix-nar cat nix.nar /bin/nix > nix
chmod +x nix
- Profit
$ ./nix --version
nix (Nix) 2.13.3
Anyway, let’s dive into a rabbit hole, shall we?
Generally, interacting with NARs should only be done by nix commands themselves, and downloading something directly from a cache without nix is not fun.
But for academic interest, let’s see how far we can go!
First off, to understand what’s going on, I took a look at the nix-serve
source code, which is extremely short, less than 100 lines. I don’t know if this is the script that runs cache.nixos.org, but you can use it to create your own cache locally, so it needs to be somewhat compliant.
Additionally, I locally exported a downloaded derivation to a temporary file cache and took a look at that:
$ nix copy /nix/store/m5c34ml3wbna241jgdf9vlwc1xzp0d1w-nix-static-x86_64-unknown-linux-musl-2.13.5 --to file:///tmp/cache
$ tree /tmp/cache
/tmp/cache
├── 4vn9iyljlnj0kn6gqlads4mfhl547dv6.narinfo
├── a4bk0fhq67ih6y6kzm0si7a8qzyh21vf.narinfo
├── g0hmn34bf2q0w4f6c8521sx4y7w8fd6j.narinfo
├── log
├── m2dgyjz36c6yjcxnhhbr3scm698vas0c.narinfo
├── m5c34ml3wbna241jgdf9vlwc1xzp0d1w.narinfo
├── nar
│ ├── 00ndpgx7l0p93h01ladririr9s4banp0ccwjqllni6lsazwvhsk9.nar.xz
│ ├── 03l8my59a6ybgxmqghsrm8zgs2wfv4vsfhizkh35bax9lwvq429k.nar.xz
│ ├── 0g5aad578bw9115v80naj3yyj5v3hhklsvgsmxmh9cisy1ms8rjm.nar.xz
│ ├── 0qm7zfi163sw36880gfbfdpk9d6j48ms0g8i1cwz6db8lqary96a.nar.xz
│ └── 1vp2cpa9w1n08an22c60wsy42692ypgazxrks1fvrbzdvmz6v60p.nar.xz
├── nix-cache-info
└── realisations
4 directories, 11 files
This is basically the structure of cache.nixos.org.
And sure enough, when I take the hash from the output we care about, and curl it:
$ curl https://cache.nixos.org/m5c34ml3wbna241jgdf9vlwc1xzp0d1w.narinfo ✔
StorePath: /nix/store/m5c34ml3wbna241jgdf9vlwc1xzp0d1w-nix-static-x86_64-unknown-linux-musl-2.13.5
URL: nar/0qm7zfi163sw36880gfbfdpk9d6j48ms0g8i1cwz6db8lqary96a.nar.xz
Compression: xz
FileHash: sha256:0qm7zfi163sw36880gfbfdpk9d6j48ms0g8i1cwz6db8lqary96a
FileSize: 7337388
NarHash: sha256:0g6g4qmn9wfhq59jp3al7mx8q0v1h6hbf5z5p3c69mahlcpkk6kf
NarSize: 23630272
References: 4vn9iyljlnj0kn6gqlads4mfhl547dv6-openssl-static-x86_64-unknown-linux-musl-3.0.11-etc g0hmn34bf2q0w4f6c8521sx4y7w8fd6j-libxml2-static-x86_64-unknown-linux-musl-2.10.4 m2dgyjz36c6yjcxnhhbr3scm698vas0c-nlohmann_json-static-x86_64-unknown-linux-musl-3.11.2 m5c34ml3wbna241jgdf9vlwc1xzp0d1w-nix-static-x86_64-unknown-linux-musl-2.13.5
Deriver: z4s9qy45scy8w7z75qyaqf44y2a3zb0l-nix-static-x86_64-unknown-linux-musl-2.13.5.drv
Sig: cache.nixos.org-1:J7iAPhJoZ6dL0iASny3u3Yr1JY/9BWNnxqTnxwYJ/riygRvN+sHPZY7gS3spz2WLI02C0wYsNIDuPx2A2/7oAg==
Cool! And it even tells us the URL we need to download:
$ wget https://cache.nixos.org/nar/0qm7zfi163sw36880gfbfdpk9d6j48ms0g8i1cwz6db8lqary96a.nar.xz
--2023-10-17 20:53:22-- https://cache.nixos.org/nar/0qm7zfi163sw36880gfbfdpk9d6j48ms0g8i1cwz6db8lqary96a.nar.xz
Loaded CA certificate '/etc/ssl/certs/ca-certificates.crt'
Resolving cache.nixos.org (cache.nixos.org)... 2a04:4e42:6f::729, 199.232.190.217
Connecting to cache.nixos.org (cache.nixos.org)|2a04:4e42:6f::729|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 7337388 (7.0M) [application/x-nix-nar]
Saving to: ‘0qm7zfi163sw36880gfbfdpk9d6j48ms0g8i1cwz6db8lqary96a.nar.xz’
0qm7zfi163sw36880gfbfdpk9d6j48ms0g8i1cwz6db8lqary96a.nar.xz 100%[===============>] 7.00M 9.67MB/s in 0.7s
2023-10-17 20:53:23 (9.67 MB/s) - ‘0qm7zfi163sw36880gfbfdpk9d6j48ms0g8i1cwz6db8lqary96a.nar.xz’ saved [7337388/7337388]
Now we can decompress this file and look at the result with nix nar
:
$ xz -d 0qm7zfi163sw36880gfbfdpk9d6j48ms0g8i1cwz6db8lqary96a.nar.xz
$ nix nar ls 0qm7zfi163sw36880gfbfdpk9d6j48ms0g8i1cwz6db8lqary96a.nar /
./bin
./etc
./lib
./libexec
./share
$ nix nar ls 0qm7zfi163sw36880gfbfdpk9d6j48ms0g8i1cwz6db8lqary96a.nar /bin
./nix
./nix-build
./nix-channel
./nix-collect-garbage
./nix-copy-closure
./nix-daemon
./nix-env
./nix-hash
./nix-instantiate
./nix-prefetch-url
./nix-shell
./nix-store
And if you were to have nix installed, you could now also extract files from it, like the nix
binary:
$ nix nar cat 0qm7zfi163sw36880gfbfdpk9d6j48ms0g8i1cwz6db8lqary96a.nar /bin/nix > nix
$ chmod +x nix
$ ./nix
So it feels as if we’re ever so close, and the NAR format is very simple and very stable, but you need something to read it. Luckily, there’s a dedicated rust utility called nix-nar-cli that you can use. You have to build it once, of course, but you can do that on a machine with Nix (or cargo):
nix build gitlab:abstract-binary/nix-nar-rs
The result/bin/nix-nar
has the same interface as nix nar
, so you can run
nix-nar cat $file /bin/nix > nix
to unpack it. I tried it out with the NAR above, and the result was binary-identical to nix nar
, but I didn’t audit the code itself.
It is only 1.3MB big, so you could probably embedd it in a git repo or wherever else to bootstrap the download of nix. HOWEVER, this binary is not built statically by default either, so if you just try to copy it like that, it will fail to run with the very confusing error message:
bash: no such file or directory: ./nix-nar
So, let’s try to do that as well (assuming you already have rust):
$ rustup target add x86_64-unknown-linux-musl
info: downloading component 'rust-std' for 'x86_64-unknown-linux-musl'
info: installing component 'rust-std' for 'x86_64-unknown-linux-musl'
$ clone https://gitlab.com/abstract-binary/nix-nar-rs.git
Cloning into 'nix-nar-rs'...
Resolving deltas: 100% (187/187), done.
$ cd nix-nar-rs
# If you have direnv installed and get an error, do NOT run `direnv allow`!
$ cargo build --target=x86_64-unknown-linux-musl --release --workspace
$ target/x86_64-unknown-linux-musl/release/nix-nar --version
nix-nar-cli 0.3.0
The resulting output target/release/nix-nar
is now a statically linked binary that you can easily copy to your target machine.
The nix-nar-rs
repo has a flake that can be used for building as well, but it builds against glibc and links it dynamically, which will most likely cause the binary to not work if you just copy it to another machine.
I’m sure there’s some way to add an additional output to the flake, I might revisit that option at another point in time. For now, I added a Feature Request to the repo.
EDIT: I submitted a PR that was accepted, so now building a static binary is as simple as:
nix build gitlab:abstract-binary/nix-nar-rs#static-x86_64-linux
I updated the instructions accordingly.