the .drv file
what a derivation actually looks like on disk and what each field means.
what a derivation actually looks like on disk and what each field means.
every derivation becomes a .drv file in the store. the .drv is the build plan: what to build, how to build it, what it depends on. nix reads it, builds it, and registers the output.
$ nix derivation show nixpkgs#hello
{
"/nix/store/6s37a0flbzr5xqy67clqwvr3g8pzafvq-hello-2.12.2.drv": {
"outputs": {
"out": { "path": "/nix/store/...-hello-2.12.2" }
},
"inputDrvs": {
"/nix/store/...-bash-5.3p3.drv": { "outputs": ["out"] },
"/nix/store/...-hello-2.12.2.tar.gz.drv": { "outputs": ["out"] },
"/nix/store/...-stdenv-linux.drv": { "outputs": ["out"] }
},
"inputSrcs": [
"/nix/store/...-default-builder.sh"
],
"system": "x86_64-linux",
"builder": "/nix/store/...-bash-5.3p3/bin/bash",
"args": ["-e", "/nix/store/...-default-builder.sh"],
"env": {
"name": "hello-2.12.2",
"out": "/nix/store/...-hello-2.12.2",
"src": "/nix/store/...-hello-2.12.2.tar.gz",
"system": "x86_64-linux",
"buildInputs": "",
"nativeBuildInputs": ""
}
}
}
nix derivation show gives you a JSON view. the actual file on disk is in , a compact serialized representation.
you can read the .drv file directly:
$ cat /nix/store/6s37a0flbzr5xqy67clqwvr3g8pzafvq-hello-2.12.2.drv
Derive([("out","/nix/store/...-hello","","")],[("/nix/store/...-bash.drv",["out"]),
("/nix/store/...-stdenv.drv",["out"])],["/nix/store/...-default-builder.sh"],
"x86_64-linux","/nix/store/...-bash/bin/bash",["-e","/nix/store/...-default-builder.sh"],
[("name","hello"),("out","/nix/store/...-hello"),("system","x86_64-linux")])
one line. no whitespace. this is what nix actually hashes.
Derive([("out","/nix/store/...-hello","","")][("/nix/store/...-bash-5.3p3.drv",["out"]),("/nix/store/...-stdenv.drv",["out"])]["/nix/store/...-default-builder.sh"]"x86_64-linux""/nix/store/...-bash-5.3p3/bin/bash"["-e","/nix/store/...-default-builder.sh"][("name","hello"),("out","/nix/store/...-hello"),("system","x86_64-linux"),...]the .drv is a function of its fields. change any field and you get a different hash, a different .drv path, and a different output path.
the .drv file itself is content-addressed: its store path is SHA-256 of its contents. but the output path is not a hash of the .drv file directly. nix computes it with a fingerprint:
fingerprint = "output:out:sha256:" <inner-digest> ":/nix/store:" <name>
inner-digest is the SHA-256 of the .drv serialized in ATerm, but with two modifications before hashing. first, output paths and output-named env vars are replaced with empty strings (maskOutputs in derivations.cc); this breaks the circular dependency between "output path depends on .drv contents" and ".drv contents include the output path". second, every fixed-output input derivation is replaced with the hash of its declared output (hashDerivationModulo). the output path depends on the entire transitive build closure, but through a chain of hashes, not literal paths.
the fingerprint is then hashed with SHA-256, compressed to 160 bits, and encoded in nix's base-32 alphabet. that becomes the 32-character prefix of the output store path.
inputDrvs contains paths to other .drv files. each of those .drv files has its own inputDrvs. the chain goes all the way down to fixed-output derivations (source fetchers) and bootstrap binaries.
hello.drv
→ stdenv-linux.drv
→ gcc-wrapper.drv
→ gcc.drv
→ gcc-14.3.0.tar.xz.drv (fixed-output)
→ glibc.drv
→ glibc-2.40.tar.xz.drv (fixed-output)
→ hello-2.12.2.tar.gz.drv (fixed-output)
every non-leaf is input-addressed: its hash comes from its inputs. every leaf is a fixed-output derivation: its hash comes from the declared content hash. the tree is deterministic from leaves to root.
most derivations have a single output called out. some split into multiple:
outputs = [ "out" "lib" "dev" "man" ];
each output gets its own store path. lib contains shared libraries, dev contains headers and pkg-config files, man contains manpages. splitting lets you install only what you need. a runtime dependency on curl pulls in curl and curl-lib, not curl-dev with all the headers.
nix derivation show nixpkgs#curl shows four outputs. each is a separate store path with its own hash.
three distinct steps:
nix eval stops here.drv file to the store. nix-instantiate does evaluation and instantiation, then stops.drv, sets up the sandbox, runs the builder. nix-build and nix build do all three$ nix-instantiate '<nixpkgs>' -A hello
/nix/store/6s37a0flbzr5xqy67clqwvr3g8pzafvq-hello-2.12.2.drv
the .drv now exists in the store. nothing has been built yet. the output path is known (it is computed from the .drv hash), but the output directory does not exist.
$ nix-store --realise /nix/store/6s37a0flbzr5xqy67clqwvr3g8pzafvq-hello-2.12.2.drv
/nix/store/...-hello-2.12.2
now it is built. the output directory exists and is populated.
this separation matters. binary caches work because the .drv hash determines the output path. if someone else already built the same .drv, you download the result instead of building it locally. no building needed. just check if the output path exists on the cache.
stdenv is what turns mkDerivation { pname = "hello"; ... } into the .drv file you just saw. it provides the builder, the compiler, and the phased build script.