Fetchers and Fixed-Output Derivations
Fetchers and Fixed-Output Derivations
Every package starts as bytes from somewhere. The boring default is: fetchurl / fetchFromGitHub with an SRI hash = "sha256-…", and a hash mismatch is a stop, not a prompt to disable the check.
Mental model
A fixed-output derivation (FOD) may use the network. It must produce bytes whose hash you declared. The store path is a function of that hash. Mirrors can change; the hash must not.
| Fetcher | When |
|---|---|
fetchurl |
A tarball or file at a URL |
fetchFromGitHub |
owner + repo + rev (tag or commit) |
fetchgit |
Arbitrary git remote |
fetchzip |
A zip you want unpacked |
fetchpatch / fetchpatch2 |
One patch file (fetchpatch2 on 26.05) |
Language vendor hashes (vendorHash, cargoHash) are the same idea: FODs for Go modules / crates.io.
url + rev hash you wrote
│ │
▼ ▼
download ──cmp──► sha256 ──ok──► /nix/store/<fod-hash>-src
│
fail → error: hash mismatch (got …)
Empty hash sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= is a probe. You run the build once, paste got, commit. Leaving the probe in git is how supply chain theatre happens.
Worked examples
Case 1: fetchurl with a probe hash
Save as fod.nix:
# fod.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.fetchurl {
url = "https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz";
hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
}nix-build fod.nixOutput (shape):
error: hash mismatch in fixed-output derivation '/nix/store/…-hello-2.12.1.tar.gz':
specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
got: sha256-0P4s2aQwHzpwC3w3ys8ls6A6/JbuiJvTYdbBr9pQMnQ=
Paste got into hash, rebuild:
nix-build fod.nix
tar -tzf result | headThe path is now stable. Changing only the URL to a mirror of the same bytes keeps the path.
Case 2: fetchFromGitHub
Save as gh.nix:
# gh.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.fetchFromGitHub {
owner = "NixOS";
repo = "nix";
rev = "2.35.2";
hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
}Use a tag or full commit. rev = "master" is a moving label; the hash will mismatch the moment master moves, or worse, you use lib.fakeHash and skip the mismatch.
Probe, paste, pin. Prefer rev = "abc1234…" (full git sha) for anything the desk ships.
fetchFromGitHub is fetchzip of the GitHub archive URL. fetchgit talks git and can set leaveDotGit (almost always off for src). hash is SRI; sha256 = "0…" still works and is the old base32 — new listings use hash. Do not set both.
Case 3: nix-prefetch so you do not fake-hash in a loop
nix-prefetch-url https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz
nix hash convert --hash-algo sha256 --to sri "$(nix-prefetch-url https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz)"or:
nix-prefetch-url --unpack https://github.com/NixOS/nix/archive/2.35.2.tar.gzfetchFromGitHub unpacks; the hash is over the unpacked tree (outputHashMode = "recursive"), not the .tar.gz bytes. That is why a fetchurl hash and a fetchFromGitHub hash for “the same release” differ. nix-prefetch-url (packed) vs nix-prefetch-url --unpack / nix-prefetch-github (unpacked) — match the fetcher.
nix-prefetch-github NixOS nix --rev 2.35.2fetchpatch2 hashes the applied patch more reliably than fetchpatch (line endings, GitHub PR URLs). Prefer it on 26.05. leaveDotGit = true on fetchgit makes the hash include .git — usually wrong for src; use it only when the build runs git describe.
fetchurl { name = "hello-2.12.1.tar.gz"; … } sets the store basename; without name, Nix uses a hash of the URL and debugging nix-store -q --references is noisier.
Case 4: A package src
Save as pkg.nix:
# pkg.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.stdenv.mkDerivation rec {
pname = "hello";
version = "2.12.1";
src = pkgs.fetchurl {
url = "https://ftp.gnu.org/gnu/hello/hello-${version}.tar.gz";
hash = "sha256-0P4s2aQwHzpwC3w3ys8ls6A6/JbuiJvTYdbBr9pQMnQ=";
};
}The hash in this listing is illustrative. Always use the got from your fetch. If GNU republishes the tarball, the mismatch is the news.
Case 5: lib.fakeHash is a development aid
hash = pkgs.lib.fakeHash;It expands to a dummy SRI. Fine in a dirty worktree. git grep fakeHash must be empty on main.
The trap
The trap is sha256 = ""; or hash = lib.fakeHash in production so CI “stays green” when upstream retags. You are no longer pinning bytes. You are pinning a URL, which is not a pin.
The other trap is hashing the wrong thing: fetchurl hash on a GitHub archive URL while the package uses fetchFromGitHub (unpacked). Always mismatch. Prefetch with the same fetcher you will use.
A third: rev = "v1.2.3" on a repo that moves tags. Pin the git sha, put the tag in version.
The boring rule
- Every
srcis a FOD with a committed SRIhash. - Probe once (
fakeHash/AAAA…), pastegot, commit. - Pin
revto a git sha (tag inversiononly), nevermaster. git grep fakeHashis empty on the default branch.- Prefetch with the same fetcher (packed vs unpacked) you ship.
fetchpatch2for remote patches. NoleaveDotGitunlessgit describeis the build.
Try this
- Run Case 1, paste the real hash, rebuild, then flip one character of the hash and read the mismatch.
nix hash convert --helpand convert a hex sha256 to SRI.- Fetch the same hello tarball with
fetchurland, unpacked, withfetchzip/fetchFromGitHubif applicable. Confirm the hashes differ. git grep -n fakeHashon a flake repo you maintain. Delete any hit on the main branch.