Day 46 — Fetchers & FODs

Updated

July 30, 2026

Day 46 — Fetchers & FODs

Stage V · ~4h (lab-heavy)
Goal: Use fixed-output derivations (FODs) correctly—fetchurl, fetchFromGitHub, vendor hashes—and debug hash mismatches without turning off purity.

Why this day exists

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.


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=";
};

Modern nixpkgs prefers SRI hash = "sha256-…" over separate sha256 = "…" legacy attrs (both still appear in the wild).


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 Days 47–48

fetchFromGitHub pattern

{ stdenv, fetchFromGitHub, lib }:
stdenv.mkDerivation rec {
  pname = "ripgrep-demo";
  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
}

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


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 / empty once to force a fail).
  4. Rebuild; lock the known-good hash in git.
  5. Never leave hash = ""; or lib.fakeSha256 in committed production flakes.

Prefetch helpers

# URL
nix-prefetch-url https://example.com/foo-1.2.3.tar.gz
nix store prefetch-file https://example.com/foo-1.2.3.tar.gz

# GitHub-style (nix-prefetch-github if available in shell)
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"

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

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


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

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.


Worked examples

Example A — tarball package skeleton

# pkgs/cowsay-mini.nix  (illustrative; prefer real meta from nixpkgs)
{ stdenv, fetchurl, perl, lib }:
stdenv.mkDerivation rec {
  pname = "cowsay";
  version = "3.8.4";
  src = fetchurl {
    url = "https://github.com/cowsay-org/cowsay/archive/refs/tags/v${version}.tar.gz";
    hash = "sha256-…";
  };
  buildInputs = [ perl ];
  meta = {
    description = "Configurable talking cow";
    license = lib.licenses.gpl3Only;
    mainProgram = "cowsay";
  };
}

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=";
    })
  ];
}

Example C — intentional mismatch lab snippet

src = fetchurl {
  url = "https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.1.tar.xz";
  hash = lib.fakeHash;
};

First build prints the real hash; you paste it back.


Lab — multi-step

Suggested workspace: ~/lab/90daysofx/02-nixos/day46

Step 1 — Prefetch a known tarball

cd ~/lab/90daysofx/02-nixos/day46
mkdir -p pkgs
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

Add an empty or no-op local patch first:

# create patches/no-op or real fix
patches = [ ./patches/fix-typo.patch ];

Local patches are not FODs (they’re path inputs). 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 (optional)

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.”


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 sha256 = "000…" empty Prefer lib.fakeHash for clarity; never commit fakes
Network error inside normal derivation Move download to fetcher
Submodules missing fetchFromGitHub fetchSubmodules = true (+ hash changes)
Huge monorepo as src Sparse checkout / release tarball / sourceRoot
Private repo fetch SSH/builtins.fetchGit + credentials patterns; prefer CI tokens 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

Commit

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

Write three personal gotchas before continuing.

Tomorrow

Day 47 — Language ecosystem A: buildGoModule and friends for your primary language.