the .drv file

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.

looking at one

$ 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 ATerm format , a compact serialized representation.

the raw format

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.

anatomy

.drv structure (ATerm)
type tag
Derive(
every .drv starts with this. it marks the file as a derivation in ATerm format.
outputs
[("out","/nix/store/...-hello","","")]
list of (name, path, hashAlgo, hash) tuples. the last two are empty strings for input-addressed derivations; for fixed-output derivations they contain the expected hash. most packages have one output called "out".
inputDrvs
[("/nix/store/...-bash-5.3p3.drv",["out"]),("/nix/store/...-stdenv.drv",["out"])]
derivations this build depends on. each entry names which outputs to use. changing any of these changes this derivation's hash.
inputSrcs
["/nix/store/...-default-builder.sh"]
store paths used directly as sources. not built from derivations. usually scripts or tarballs.
system
"x86_64-linux"
target platform. the derivation is only buildable on this architecture.
builder
"/nix/store/...-bash-5.3p3/bin/bash"
the executable nix runs to perform the build. almost always bash.
args
["-e","/nix/store/...-default-builder.sh"]
arguments passed to the builder. stdenv passes -e and the setup script.
env
[("name","hello"),("out","/nix/store/...-hello"),("system","x86_64-linux"),...]
environment variables the builder sees. every attribute you pass to mkDerivation ends up here as a string.

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.

how the output path is computed

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.

the hash chain

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.

outputs

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.

evaluation vs instantiation vs building

three distinct steps:

  1. evaluation: nix evaluates the expression. attribute sets, functions, lazy thunks. the result is a derivation value in memory. nix eval stops here
  2. instantiation: nix writes the .drv file to the store. nix-instantiate does evaluation and instantiation, then stops
  3. building: nix reads the .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.