Fetchers and fixed-output derivations

Updated

July 30, 2026

Fetchers and fixed-output derivations

Goal: Use fixed-output derivations (FODs) correctly—fetchurl, fetchFromGitHub, vendor hashes—and debug hash mismatches without turning off purity.

Note

Baseline: Nix 2.34 / nixpkgs 26.05. Prefer SRI hash = "sha256-…" over legacy sha256 = "…" attrs (both still appear in the wild).

Why it matters

Every package starts as bytes from somewhere. If those bytes are not fixed-output and hashed, your “reproducible” package is a network role-play. FODs are the contract: given this hash, the store path is determined by content, and the build may touch the network only to produce those exact bytes.

Without FOD discipline you cannot trust CI, caches, or offline rebuilds. With it, hash mismatches become a clear signal: either you meant to bump content, or something untrustworthy changed.


Theory 1 — Fixed-output vs normal derivations

Kind Network in sandbox? Output path depends on Failure mode
Normal build No (default) Inputs (input-addressed) Compile errors, missing tools
Fixed-output (FOD) Yes, limited Declared output hash Hash mismatch if bytes change

FODs power almost every src = fetch… you see in nixpkgs.

src = fetchurl {
  url = "https://example.com/foo-1.2.3.tar.gz";
  hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
};

Why the store path is special

For FODs, the output path is a function of the hash (and related fixed-output metadata), not of the builder script details the same way normal derivations are. That is why two packages fetching the same bytes can share a store path, and why a wrong hash never “quietly” accepts different content.

Normal:  out = f(name, builder, input paths, env, system, …)
FOD:     out = f(outputHash, outputHashMode, …)  # content-addressed flavor

(Exact hashing rules are an internals topic; operationally: trust the hash field.)


Theory 2 — Common fetchers (2026 mental map)

Fetcher Use when Notes
fetchurl Direct tarball/file URL Mirrors, curlOpts, name
fetchTarball Eval-time tarball (flakes/inputs style) Different API; not always for package src
fetchFromGitHub GitHub repo + rev/tag Sets owner, repo, rev, hash
fetchFromGitLab GitLab Similar shape
fetchgit Arbitrary git Needs hash; leaveDotGit carefully
fetchzip Zip that should unpack Unpacks in fetcher
fetchpatch / fetchpatch2 Single patch files Great for patches = [ … ]
fetchPypi Python sdist/wheel helpers Via python infra
Language-specific vendor hashes Go modules, Cargo, npm See language ecosystem chapters

fetchFromGitHub pattern

{ stdenv, fetchFromGitHub, lib }:
stdenv.mkDerivation rec {
  pname = "example-src-only";
  version = "14.1.1";
  src = fetchFromGitHub {
    owner = "BurntSushi";
    repo = "ripgrep";
    rev = version;           # tag or full commit
    hash = "sha256-…";      # FOD for the archive Nix downloads
  };
  # real packaging would use rustPlatform; this is fetcher-focused
  meta = {
    description = "Fetcher demo only";
    license = lib.licenses.unfreeRedistributable; # demo—set real license for real pkgs
  };
}

Prefer tags or full commit SHAs over branch names (main). Branches move; FODs must not.

Submodules and deep clones

src = fetchFromGitHub {
  owner = "org";
  repo = "repo";
  rev = "v1.0.0";
  hash = "sha256-…";
  fetchSubmodules = true; # changes the hash vs without
};

Sparse / partial checkouts are advanced—prefer release tarballs when upstream publishes them.


Theory 3 — The hash mismatch workflow

You will see this forever:

error: hash mismatch in fixed-output derivation '…':
         specified: sha256-AAAA…
            got:    sha256-BBBB…

Correct response

  1. Trust “got” only after you trust the URL/rev. If the project was compromised, blindly updating the hash pins malware.
  2. Confirm you intended that rev / version.
  3. Replace hash with the got value (or use lib.fakeHash once to force a fail).
  4. Rebuild; lock the known-good hash in git.
  5. Never leave hash = ""; or lib.fakeHash in committed production flakes.

Prefetch helpers

# URL
nix-prefetch-url https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz
nix store prefetch-file https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz

# Flake-style prefetch (metadata + hash)
nix flake prefetch github:BurntSushi/ripgrep/14.1.1

# Generic: let the build fail once
# hash = lib.fakeHash;
# Temporary during packaging
hash = lib.fakeHash; # forces mismatch with a clear "got"

SRI vs base32

Modern nixpkgs prints and prefers SRI (sha256-…). Older docs show base32. Both can refer to the same bytes; do not mix formats carelessly when comparing strings by eye—use the got: line Nix prints.


Theory 4 — Impure traps and purity

Trap Why it hurts Fix
builtins.fetchGit without flake lock discipline Eval-time drift Prefer flake inputs or FOD fetchers with hash
Branch rev = "main" Hash churn Pin commit
curl in a normal buildPhase Sandbox network blocked Use FOD or pkgs.fetchurl as input
Post-download patching inside FOD Changes bytes → hash mismatch Patch in normal derivation patches
Leaving .git without care Non-determinism Default archives omit .git; leaveDotGit is advanced
Mirror that rewrites tarballs Same URL, new bytes Pin mirrors; notice mismatch as signal
LFS pointers instead of files Tiny “source”, broken build Ensure LFS materialization in fetch

Rule: Download and fix bytes in the FOD or patch after unpack in the normal build—do not mix casually.

[FOD: pure download → exact bytes]
        │
        ▼
[Normal derivation: patch / compile / install]

Theory 5 — Flake inputs vs package fetchers

Mechanism Pinned by Good for
inputs.foo.url = "github:…" flake.lock Nix code, whole repos as flakes
fetchFromGitHub in a package hash on the fetcher Upstream source of a package
pkgs.fetchgit of a monorepo path hash + sparse options Subdir projects
path: / ./src Your git tree Lab-local code

Do not confuse “my flake input updated” with “my package src hash updated.” Both can change store paths; only one is your app’s source tarball.

# flake input — for Nix modules / another flake
inputs.tooling.url = "github:acme/tooling/v1.2.3";

# package src — for building C/Go/… sources
src = fetchFromGitHub {
  owner = "acme";
  repo = "tooling";
  rev = "v1.2.3";
  hash = "sha256-…";
};

Sometimes you use both: flake input for Nix files, FOD for the language project’s release tarball. That is normal—document which is which.


Theory 6 — outputHashMode (awareness)

FODs declare how content is hashed:

Mode Typical use
flat Single file
recursive Directory tree (default for many fetchers)

You rarely set this by hand when using fetchurl / fetchFromGitHub. When writing custom FODs (stdenv.mkDerivation with outputHash), wrong mode causes confusing mismatches. Prefer high-level fetchers.

# Conceptual custom FOD — prefer fetchurl in real packages
stdenv.mkDerivation {
  name = "my-fod";
  builder =;
  outputHashMode = "flat";
  outputHashAlgo = "sha256";
  outputHash = "sha256-…";
}

Theory 7 — Private sources and credentials

Approach Notes
CI token in netrc / access tokens Keep out of world-readable store; prefer FODs that do not embed secrets in drv env
SSH fetchgit Agent/daemon SSH complexity; document who can build
Vendor a release tarball internally Mirror to your cache/object store + fetchurl
builtins.fetchGit with local credentials Eval-time; flake lock discipline still required

Never put passwords in hash or URL query strings committed to git. Prefer: internal mirror + FOD hash of the mirrored artifact.


Worked examples

Example A — tarball package skeleton

# pkgs/hello-fod.nix
{ stdenv, fetchurl, lib }:
stdenv.mkDerivation rec {
  pname = "hello";
  version = "2.12.1";
  src = fetchurl {
    url = "https://ftp.gnu.org/gnu/hello/hello-${version}.tar.gz";
    hash = "sha256-jZkUKv2SV28wsM18tCqNxoCZmLxdYH2Idh9RLibH2yA=";
  };
  doCheck = true;
  meta = {
    description = "GNU Hello via explicit FOD";
    license = lib.licenses.gpl3Plus;
    mainProgram = "hello";
  };
}

Example B — GitHub + patch FOD

{ stdenv, fetchFromGitHub, fetchpatch, lib }:
stdenv.mkDerivation {
  pname = "example";
  version = "1.0.0";
  src = fetchFromGitHub {
    owner = "example-org";
    repo = "example";
    rev = "v1.0.0";
    hash = "sha256-SRCSOURCESOURCESOURCESOURCESOURCESOURCESO=";
  };
  patches = [
    (fetchpatch {
      url = "https://github.com/example-org/example/commit/abcdef.patch";
      hash = "sha256-PATCHPATCHPATCHPATCHPATCHPATCHPATCHPATCHPA=";
    })
  ];
  meta = {
    license = lib.licenses.mit;
  };
}

Local patches (./patches/foo.patch) are path inputs, not FODs. Remote fetchpatch is a FOD—second hash to maintain.

Example C — intentional mismatch

src = fetchurl {
  url = "https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz";
  hash = lib.fakeHash;
};

First build prints the real hash; paste it back.

Example D — flake wiring

{
  description = "FOD lab";
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
  outputs = { self, nixpkgs }:
    let
      system = "x86_64-linux";
      pkgs = import nixpkgs { inherit system; };
    in {
      packages.${system}.hello-fod = pkgs.callPackage ./pkgs/hello-fod.nix { };
      packages.${system}.default = self.packages.${system}.hello-fod;
    };
}

Lab — multi-step

Suggested workspace: ~/lab/nixos/packaging/fod

Step 1 — Prefetch a known tarball

mkdir -p ~/lab/nixos/packaging/fod/pkgs
cd ~/lab/nixos/packaging/fod
nix store prefetch-file \
  https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz

Record the SRI hash.

Step 2 — Package from that tarball

Write pkgs/hello-fod.nix with fetchurl + your hash. Export from a flake:

nix build .#hello-fod -L
./result/bin/hello

Step 3 — Force a mismatch

Change one character of the hash; rebuild; capture the error. Restore the correct hash.

Step 4 — GitHub fetcher

Pick a tiny tagged GitHub project (or a single-file tool). Package with fetchFromGitHub:

src = fetchFromGitHub {
  owner = "…";
  repo = "…";
  rev = "vX.Y.Z";
  hash = lib.fakeHash;
};

Iterate until the hash stabilizes. Prefer a project with a clear install (copy binary/script).

Step 5 — Patch as FOD and as path

Add a local patch first:

patches = [ ./patches/fix-typo.patch ];

Then replace with fetchpatch for one upstream commit if available—note the second hash.

Step 6 — Document the trust boundary

In NOTES.md:

  1. Where did bytes come from?
  2. Who could change them?
  3. What does updating hash mean operationally?
  4. Difference between updating flake.lock and updating src.hash.

Step 7 — Offline check

After populate:

nix build .#hello-fod --offline

If it works, your FOD is already in the store; purity is about definition, not “never used network once.”

Step 8 — Submodules experiment (optional)

Find a tiny repo with a submodule or skip with a written note on why fetchSubmodules changes hashes.


Exercises

  1. Prefetch the same URL twice; confirm hash identity.
  2. Change only the name of a fetchurl (if supported) and observe whether the store path changes—document findings.
  3. Write a one-paragraph runbook: “upstream re-tagged v1.2.3 with different bytes.”
  4. Compare nix flake prefetch output to fetchFromGitHub hash for the same rev—are they interchangeable? (Often related but not always drop-in; note what you observe.)
  5. List every FOD in one of your language packages (src + vendor/cargo/npm).

Common gotchas

Symptom / mistake What to do
Hash mismatch after “nothing changed” Upstream re-released same tag; pin commit SHA
fetchFromGitHub hash differs on Darwin vs Linux Unusual for pure archives; check name, filters, or LFS
Using empty sha256 Prefer lib.fakeHash for clarity; never commit fakes
Network error inside normal derivation Move download to fetcher
Submodules missing fetchSubmodules = true (+ hash changes)
Huge monorepo as src Sparse checkout / release tarball / sourceRoot
Private repo fetch Mirror + FOD; careful with credentials
Patched inside FOD builder Move patches to normal derivation
Confused flake lock with src hash Update the pin that actually feeds the package
leaveDotGit = true nondeterminism Avoid unless required; pin carefully

Checkpoint

  • Explain FOD vs normal derivation in one paragraph
  • Packaged something with fetchurl and correct SRI hash
  • Packaged something with fetchFromGitHub
  • Demonstrated and fixed a hash mismatch deliberately
  • Notes on trust when updating hashes
  • Offline rebuild succeeded for a cached FOD
  • Distinguishes flake input pins from package src hashes

Commit

git add .
git commit -m "packaging: fetchers and FOD hash workflow"

Write three personal gotchas before continuing to language ecosystems.