Content-Addressable Paths
Content-Addressable Paths
Most Nix store paths you meet are input-addressed: the hash is a digest of the derivation (builder, env, input paths), not of the bytes that came out. Content-addressed (CA) paths hash the output itself. The boring default is: understand the difference, keep producing ordinary input-addressed builds, and let fixed-output derivations (fetchers) be the CA paths you actually use every day.
Mental model
| Kind | Hash comes from | Same output, different builder? |
|---|---|---|
| Input-addressed | The .drv (inputs, builder, env) |
Two different store paths |
| Content-addressed / FOD | The output bytes (and hash mode) | Same store path |
Input-addressed is why a comment in a build script rebuilds everything downstream: the .drv changed, so the output path changed, even if gcc emitted identical bytes.
Fixed-output derivations (the fetchurl / fetchFromGitHub family) are the everyday CA case. You declare hash = "sha256-…"; Nix may use the network; it then checks the bytes against that hash. The store path is a function of the hash, not of the URL. Change the URL to a mirror, keep the hash, keep the path.
Experimental CA derivations for ordinary builds exist (contentAddressed in Nix). They are not the boring default for a desk workstation. The platform-engineering internals chapter covers them later.
Input-addressed:
.drv ──hash──► /nix/store/AAAA-desk-api
(even if bytes == yesterday's BBBB-desk-api)
Fixed-output:
declared hash ──► /nix/store/HHHH-source.tar.gz
(URL is just how we get the bytes; the hash is identity)
Worked examples
Case 1: Input hash changes when the builder changes
Save as banner-a.nix:
# banner-a.nix
let
pkgs = import <nixpkgs> {};
in
derivation {
name = "desk-banner";
system = builtins.currentSystem;
builder = "${pkgs.bash}/bin/bash";
args = [ "-c" "echo 'Desk System Online' > $out" ];
}Save as banner-b.nix — same bytes out, extra comment in the script:
# banner-b.nix
let
pkgs = import <nixpkgs> {};
in
derivation {
name = "desk-banner";
system = builtins.currentSystem;
builder = "${pkgs.bash}/bin/bash";
args = [ "-c" "echo 'Desk System Online' > $out # lab note" ];
}nix-instantiate banner-a.nix
nix-instantiate banner-b.nixOutput:
/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-desk-banner.drv
/nix/store/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-desk-banner.drv
Two .drv files. Two future output paths. The echo is the same; the input is not.
Case 2: Store paths are immutable, not “updated in place”
touch $(readlink -f $(which nix)) 2>&1 || trueOutput:
touch: cannot touch '/nix/store/…-nix-2.35.2/bin/nix': Permission denied
Nix never overwrites AAAA-desk-banner with new bytes. It builds CCCC-desk-banner and swings a profile symlink. That is input-addressing plus immutability working together.
Case 3: Fixed-output identity is the hash
Save as fod.nix:
# fod.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.fetchurl {
url = "https://cache.nixos.org/nix-cache-info";
hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
}nix-build fod.nixThe build fails (unless you lucked into that hash). The error is the point:
error: hash mismatch in fixed-output derivation '/nix/store/…-nix-cache-info':
specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
got: sha256-<the real hash>
Paste the got value into hash, rebuild, and the path is now determined by those bytes. Point url at a different mirror of the same file and the store path stays put.
Case 4: Why CA helps a cache
If two CI jobs fetch the same tarball with the same hash, they share one store path. If two CI jobs compile the same C file with two different comment-only builder changes, input-addressed Nix stores two outputs even when cmp would say the binaries match.
That waste is real. It is also cheaper than debugging “why did the binary change when the source didn’t” because a timestamp leaked into an input. Boring teams live with input-addressed builds and pin fetch hashes.
Case 5: Inspect which kind a path is
nix path-info --json $(nix-build '<nixpkgs>' -A hello --no-out-link) | headLook for ca / narHash fields when present. Ordinary hello from nixpkgs is input-addressed. A fetchurl output is fixed-output. You do not need to memorise JSON; you need to know which family you are looking at when a cache misses.
The trap
The trap is turning off the hash (hash = ""; or sha256 = lib.fakeSha256 left in production) so the fetch “just works.” The next time GitHub retags, or a mirror is replaced, you silently take new bytes at the same URL. The whole point of a FOD is the mismatch error.
The other trap is enabling experimental CA derivations on a laptop because a blog post promised automatic sharing. Keep CA-for-builds in the lab until the whole team agrees. Fetchers already gave you the CA that matters.
The boring rule
- Everyday builds are input-addressed. A
.drvchange is a new path, even if the bytes look the same. - Fetchers are fixed-output. Identity is the hash you wrote.
- Never leave a dummy hash in a committed file.
- Do not chmod the store to “fix” a path. Build a new one.
- Treat experimental content-addressed builds as optional, not as the desk default.
Try this
- Change only the
nameinbanner-a.nixfromdesk-bannertodesk-banner-v2, instantiate, and confirm the.drvhash moved. - Run Case 3, copy the
gothash, rebuild, then runnix path-info -Sh resultand note the size. - Flip one character of the working hash and read the mismatch error. That error is success: Nix refused the bytes.
- Explain in one sentence, to a colleague, why
hellofrom nixpkgs and afetchurltarball are hashed differently.