Cryptographic Hashes and Inputs
Cryptographic Hashes and Inputs
Two machines produce the same store path only if they evaluate the same derivation. The boring default is: every input — source, compiler, flags, patches, build script — is part of the hash, and a one-bit change is a new path, not an in-place upgrade.
Mental model
Traditional package managers treat ripgrep-14.1.0 as identity. It is a label. A rg built against glibc 2.38 is not the same program as a rg built against glibc 2.39.
Nix hashes the .drv file: a Nix-language serialisation of the builder, environment, and input store paths. The output path is /nix/store/<hash-of-that-drv>-<name>.
sources + toolchain + env + builder
│
▼
.drv file ──SHA──► output path
│
▼
sandbox build ──► bytes at that path
If the build is allowed to see the network, the time of day, or /usr/bin/cc, the hash is a lie. The sandbox exists so the hash means something.
Worked examples
Case 1: Instantiate before you build
Save as sample.nix:
# sample.nix
let
pkgs = import <nixpkgs> {};
in
derivation {
name = "desk-sample";
system = builtins.currentSystem;
builder = "${pkgs.bash}/bin/bash";
args = [ "-c" "echo ok > $out" ];
}nix-instantiate sample.nixOutput:
/nix/store/7vqw…-desk-sample.drv
No compiler ran. The hash is already decided. cat that .drv (it is text) and you will see the builder path, name, and system.
Case 2: One flag, new path
Save as opt.nix:
# opt.nix
{ cflags ? "-O2" }:
let
pkgs = import <nixpkgs> {};
in
derivation {
name = "desk-opt";
system = builtins.currentSystem;
builder = "${pkgs.bash}/bin/bash";
inherit cflags;
args = [ "-c" "echo $cflags > $out" ];
}nix-instantiate opt.nix
nix-instantiate --argstr cflags -O3 opt.nixOutput:
/nix/store/aaaa…-desk-opt.drv
/nix/store/bbbb…-desk-opt.drv
-O2 versus -O3 is two derivations. Both can live on disk. Nothing was upgraded in place.
Case 3: The compiler is an input
nix-instantiate '<nixpkgs>' -A hello
nix-store -q --references $(nix-instantiate '<nixpkgs>' -A hello)Output (abbreviated):
/nix/store/…-hello-2.12.1.drv
/nix/store/…-stdenv-linux.drv
/nix/store/…-source
…
hello does not hash “a C compiler.” It hashes this stdenv, which hashes this gcc, which hashes this source tarball. That is why a nixpkgs bump rebuilds the world: gcc moved, so everything that listed gcc as an input moved.
Case 4: Purity — the sandbox cannot see /usr
Save as impure.nix:
# impure.nix
let
pkgs = import <nixpkgs> {};
in
derivation {
name = "desk-impure";
system = builtins.currentSystem;
builder = "${pkgs.bash}/bin/bash";
args = [ "-c" "/usr/bin/id > $out" ];
}nix-build impure.nixOutput:
building '/nix/store/…-desk-impure.drv'...
/bin/bash: line 1: /usr/bin/id: No such file or directory
error: builder for '/nix/store/…-desk-impure.drv' failed
The sandbox does not contain /usr. That failure is the hash doing its job: if /usr/bin/id had been used, two machines with different id binaries would still have produced the “same” store path.
Case 5: Pin the input set with a lockfile
nix flake new ./desk-pin
cd desk-pin
nix flake lock
nix hash path flake.lockflake.lock records the git revision and NAR hash of nixpkgs. Two laptops with the same lock file instantiate the same .drv hashes. Without the lock, nixpkgs is a moving label.
The trap
The trap is nix-build --impure (or builtins.currentTime, or filterSource that includes .git) so a “quick test” leaks host state into the hash. The path then differs between CI and a laptop for reasons that do not show up in git diff.
A second trap is trusting version numbers in pname / version as identity. They are labels inside the path. The hash is identity. Rename freely; never assume 1.0.0 means the same bytes as last week’s 1.0.0.
The boring rule
- The
.drvis hashed before the build. Instantiate to see identity. - Toolchains, flags, and sources are inputs. Changing any of them is a new path.
- Keep the sandbox on. A build that needs
/usris not a Nix build. - Pin nixpkgs (flake lock or a pinned fetch). Do not build against a floating channel on a desk that ships artifacts.
- Version strings are labels. Hashes are names.
Try this
- Add an environment variable
desk = "window-a";tosample.nix, instantiate, and confirm the.drvhash changed even thoughecho okdid not. - Run
nix-store --query --binding builder $(nix-instantiate sample.nix)and confirm it points at bash in/nix/store, not/bin/bash. - Build
opt.nixtwice (-O2and-O3), thendiff -u $(nix-build opt.nix) $(nix-build --argstr cflags -O3 opt.nix). - Search a
.drvwithgrep gcc $(nix-instantiate '<nixpkgs>' -A hello)and note that gcc appears as a store path, not as the word “gcc”.