fixed-output derivations

how fetchers work and why network access requires a content hash.

every derivation you have seen so far is input-addressed. nix hashes the inputs to compute the output path, and the sandbox blocks undeclared input like network access.

fetchers are the exception. you cannot hash the inputs to a download because the interesting input is a URL, and URLs are not content-addressed. the same URL can serve different content tomorrow. a git repository at the same URL grows new commits.

fixed-output derivations solve this by flipping the guarantee: instead of "same inputs produce the same output", they say "the output must match this hash, or the build fails".

input-addressedfixed-output
hash based onderivation inputs (builder, deps, env, args)declared content hash of the output
network accessblockedallowed
sandboxfull isolationrelaxed (network open)
reproducibilityguaranteed by input isolationguaranteed by output hash check
when output changesimpossible (same inputs = same output)build fails (hash mismatch)
typical usecompiling, linking, installingdownloading source tarballs, git clones

how they work

src = fetchurl {
  url = "https://curl.se/download/curl-8.18.0.tar.xz";
  sha256 = "sha256-QHkMV0L6dSQaf3sVGGeGIBJDQ7uftFaFMJfnMbMxNiE=";
};

fetchurl creates a derivation that:

  1. gets network access (the sandbox is relaxed for this derivation only)
  2. downloads the file
  3. hashes the output
  4. compares it to the declared sha256
  5. if they match, the output is registered in the store
  6. if they do not match, the build fails immediately

the store path is computed from the output hash, not the input hash. the fingerprint is:

"fixed:out:" <mode> <algo> ":" <hash> ":"

where mode is "" for flat or "r:" for recursive (see below). the URL, the builder script, the version of curl used to download; none of that appears in the fingerprint. two different URLs producing the same file get the same store path.

hash modes

fixed-output derivations require three special attributes: outputHash, outputHashAlgo, and outputHashMode.

outputHashMode controls how the output is digested:

flat (default): hash the output file directly. fetchurl uses this. the output must be a single file.

recursive (aliased as nar since nix 2.21): hash the NAR serialization of the output. fetchFromGitHub and fetchgit use this because their output is a directory.

the trust model

you declare the hash upfront. nix trusts that declaration at evaluation time. it computes the output path from the hash, wires it into downstream derivations, and only verifies at build time.

if the hash is wrong, the build fails:

hash mismatch in fixed-output derivation '/nix/store/...-curl-8.18.0.tar.xz':
  specified: sha256-QHkMV0L6dSQaf3sVGGeGIBJDQ7uftFaFMJfnMbMxNiE=
  got:       sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=

no fallback. no warning. hard failure.

this is the one place where you make a claim about content and nix verifies it. normal derivations get their repeatability from declared inputs and sandboxing instead.

one more relaxation: fixed-output derivations can use impureEnvVars to pass through environment variables from the host. fetchurl uses this for http_proxy, https_proxy, and related variables. this is safe because the output hash check catches any difference. if your proxy corrupts the download, the hash mismatches and the build fails.

getting the hash

you do not compute the hash yourself. nix does it.

the standard workflow: leave the hash empty (or use a dummy), run the build, nix tells you the correct hash in the error message, paste it in.

src = fetchurl {
  url = "https://curl.se/download/curl-8.18.0.tar.xz";
  sha256 = "";
};
hash mismatch in fixed-output derivation:
  specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
  got:       sha256-QHkMV0L6dSQaf3sVGGeGIBJDQ7uftFaFMJfnMbMxNiE=

copy the got: line. done. nix-prefetch-url and nix hash can also compute it without attempting a full build.

common fetchers

fetcher what it fetches
fetchurl a single file by URL
fetchzip a zip/tarball, auto-extracted
fetchFromGitHub a GitHub repository at a specific rev
fetchgit a git repository at a specific rev
fetchpatch a patch file by URL

all of them are fixed-output derivations. all of them require a hash. all of them get network access in the sandbox.

fetchFromGitHub

src = fetchFromGitHub {
  owner = "curl";
  repo = "curl";
  rev = "curl-8_18_0";
  sha256 = "sha256-...";
};

this resolves to a URL like https://github.com/curl/curl/archive/curl-8_18_0.tar.gz. the rev pins the exact commit or tag. without it, the hash would change on every new commit and your build would break.

the important part is the pin. a fixed-output derivation with a pinned rev and a verified hash is as reproducible as an input-addressed one. the network access is a one-time fetch, not an ongoing dependency.

why not hash everything by output?

if content-addressing is so clean, why not use it for all derivations?

because you would have to build first and hash second. you could not compute output paths before building, so nix could not wire dependency paths into downstream derivations or check binary caches before doing local work.

input-addressing gives you output paths at evaluation time. that is what makes binary caches work: you compute the output path from the .drv hash, check if the cache has it, and skip the build entirely. with content-addressing, you would have to build locally to discover the output path, defeating the purpose.

nix does support experimental content-addressed derivations (__contentAddressed = true). they hash the output after building, enabling deduplication of identical outputs from different inputs. but the standard model is input-addressed, and nearly everything in nixpkgs uses it.

the leaf nodes

in the .drv file , the hash chain bottoms out at fixed-output derivations. every non-trivial package eventually depends on source code fetched from the internet. those fetches are the leaves of the derivation tree.

curl.drv
  → openssl.drv
    → openssl-3.6.1.tar.gz.drv (fixed-output)
  → curl-8.18.0.tar.xz.drv (fixed-output)

the leaves are fixed-output, verified by content hash. everything above is input-addressed. the chain is unbroken from source to binary.

nixpkgs is where all of this comes together. 100,000 packages, the callPackage pattern, overlays, and overrides.