Day 16 — stdenv Phases, Fetchers & Go Packaging

Updated

July 30, 2025

Day 16 — stdenv Phases, Fetchers & Go Packaging

Day 45 — stdenv phases

Stage V · ~4h (lab-heavy)
Goal: Package a small C or shell tool with stdenv.mkDerivation, name every default phase, and know when to override hooks vs rewrite the whole build.

Note

Stage V starts after you have run a real host. Packaging now is craft, not theory before you felt closure size.

Why this day exists

Most “how do I package X?” questions collapse into one skill: drive stdenv phases deliberately. Language helpers (buildGoModule, buildPythonPackage, …) are thin wrappers on the same lifecycle. If you cannot read a failing phase log, every later day in Stage V will feel random.


Theory 1 — What stdenv.mkDerivation actually is

A derivation is a pure build plan: inputs → sandbox → outputs under /nix/store.
stdenv.mkDerivation fills that plan with a bash driver (genericBuild) and a default toolchain (compiler, coreutils, patch, make, …).

{ stdenv, fetchurl }:
stdenv.mkDerivation rec {
  pname = "hello";
  version = "2.12.1";
  src = fetchurl {
    url = "mirror://gnu/hello/hello-${version}.tar.gz";
    hash = "sha256-…"; # replace with real FOD hash after first fail
  };
  meta = {
    description = "GNU Hello prints a friendly greeting";
    license = lib.licenses.gpl3Plus;
  };
}
Concept Meaning
pname / version Human labels; feed name as ${pname}-${version} by default
src Input path (usually a fixed-output fetcher)
buildInputs Host libraries linked into the result (runtime + link time on native)
nativeBuildInputs Tools that run on the build machine (compilers, cmake, pkg-config)
propagated* Rare; pollutes dependents—prefer explicit deps

buildInputs vs nativeBuildInputs (cross-aware)

Attr Role Cross example
nativeBuildInputs Execute during build pkg-config, cmake, host gcc when cross-compiling
buildInputs Link / use as target libraries openssl, zlib for the target system
depsBuildBuild Advanced bootstrap Tools for the build→build hop

For native-only packaging you can get away with sloppy placement; cross and static punish it. Prefer correct classification from day one.


Theory 2 — Default phase order

genericBuild runs roughly:

unpackPhase
patchPhase
updateAutotoolsGnuConfigScriptsPhase   # often invisible
configurePhase
buildPhase
checkPhase          # if doCheck = true
installPhase
fixupPhase
installCheckPhase   # if doInstallCheck = true
distPhase           # rare for binary packages

Hooks fire before and after many phases (preConfigure, postInstall, …). Prefer hooks over replacing an entire phase when you only need a few lines.

Phase responsibilities

Phase Default intent Common overrides
unpack Extract src into $sourceRoot sourceRoot, setSourceRoot, custom unpackPhase for flat files
patch Apply patches list prePatch to fix line endings
configure ./configure --prefix=$out if script exists cmakeConfigurePhase, meson, or dontConfigure
build make buildPhase = "go build …" for tiny tools (prefer language helpers)
check make check Gate with doCheck = true
install make install Copy binaries into $out/bin
fixup Strip, compress man, move docs, patch shebangs dontStrip, separateDebugInfo

Theory 3 — Important environment variables in the sandbox

During the build, the driver exports paths you should treat as API:

# always available in phases
echo "$out"          # primary output
echo "$src"          # unpacked-from input
echo "$NIX_BUILD_TOP"
echo "$sourceRoot"

Multiple outputs (Day 50) add $dev, $man, $doc, …. For today, stick to $out.

Shebang and RPATH fixups happen in fixupPhase / patchShebangs. If a script still points at /usr/bin/env incorrectly, you usually forgot nativeBuildInputs for the interpreter or skipped fixup.


Theory 4 — Minimal patterns you will reuse forever

Shell-only “package” (no compile)

{ stdenv, lib, bash }:
stdenv.mkDerivation {
  pname = "greet";
  version = "0.1.0";
  dontUnpack = true;
  nativeBuildInputs = [ ];
  installPhase = ''
    runHook preInstall
    mkdir -p $out/bin
    cat > $out/bin/greet <<'EOF'
    #!${bash}/bin/bash
    echo "hello from Stage V"
    EOF
    chmod +x $out/bin/greet
    runHook postInstall
  '';
  meta = {
    description = "Trivial greet script packaged with stdenv";
    license = lib.licenses.mit;
    mainProgram = "greet";
  };
}

Autotools / simple C

{ stdenv, fetchurl, lib }:
stdenv.mkDerivation rec {
  pname = "hello";
  version = "2.12.1";
  src = fetchurl {
    url = "mirror://gnu/hello/hello-${version}.tar.gz";
    hash = "sha256-jZkUKv2SV28wsM18tCqNxoCZmLxdYH2Idh9RLibH2yA=";
  };
  doCheck = true;
  meta = {
    homepage = "https://www.gnu.org/software/hello/";
    license = lib.licenses.gpl3Plus;
    mainProgram = "hello";
  };
}

Single C file without autotools

{ stdenv, lib }:
stdenv.mkDerivation {
  pname = "addone";
  version = "0.1.0";
  src = ./src; # directory with addone.c
  buildPhase = ''
    runHook preBuild
    $CC addone.c -O2 -o addone
    runHook postBuild
  '';
  installPhase = ''
    runHook preInstall
    install -Dm755 addone $out/bin/addone
    runHook postInstall
  '';
  meta = {
    description = "Adds one to an integer argument";
    license = lib.licenses.mit;
    mainProgram = "addone";
  };
}

Always call runHook pre* / runHook post* when you override a phase so preInstall hooks from dependencies still run.


Worked example — flake that exports the package

# flake.nix
{
  description = "Day 45 — stdenv packaging lab";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";

  outputs = { self, nixpkgs }:
    let
      systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ];
      forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f (
        import nixpkgs { inherit system; }
      ));
    in {
      packages = forAllSystems (pkgs: {
        default = pkgs.callPackage ./pkgs/greet.nix { };
        greet = pkgs.callPackage ./pkgs/greet.nix { };
        hello = pkgs.hello; # control sample
      });

      checks = forAllSystems (pkgs: {
        greet-runs = pkgs.runCommand "greet-runs" {
          buildInputs = [ self.packages.${pkgs.system}.greet ];
        } ''
          greet | grep -q "Stage V"
          touch $out
        '';
      });
    };
}
nix build .#greet
./result/bin/greet
nix log ./result     # if build failed earlier
nix flake check

Lab — multi-step

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

Step 1 — Scaffold

mkdir -p ~/lab/90daysofx/02-nixos/day45/{pkgs,src}
cd ~/lab/90daysofx/02-nixos/day45

Create src/addone.c:

#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
  if (argc != 2) { fprintf(stderr, "usage: %s N\n", argv[0]); return 1; }
  printf("%d\n", atoi(argv[1]) + 1);
  return 0;
}

Step 2 — Write three packages

  1. pkgs/greet.nix — shell script package
  2. pkgs/addone.nix — single-file C
  3. Optional: package hello yourself with fetchurl (compare to pkgs.hello)

Step 3 — Break phases on purpose

In addone.nix, set temporarily:

installPhase = "false"; # fail install

Build, read the log, note which phase failed:

nix build .#addone --print-build-logs || true
# restore installPhase with runHook wrappers

Step 4 — Flip input kinds

Move a tool between buildInputs and nativeBuildInputs. On native Linux both may “work”; write one sentence on why native is still correct for compilers.

Step 5 — Phase introspection

nix develop .#default   # if you add a shell with the package
# or
nix build .#greet -L
nix path-info -r ./result | head

Document: unpack → build → install → fixup for each of your packages (what actually ran).

Step 6 — Host consumption (optional but good)

Wire environment.systemPackages = [ inputs.self.packages.${system}.greet ]; on a lab host or run via nix shell .#greet.


Common gotchas

Symptom / mistake What to do
builder for '…' failed with exit code 1 with no clue Rebuild with -L / --print-build-logs; jump to last phase name in log
install: missing destination Create $out/bin before install; use install -Dm755
Binary runs in build sandbox but not after install Forgot install into $out; or RPATH/interpreter wrong—check ldd / file
patchShebangs left bad paths Ensure interpreter package is a build input; call patchShebangs $out
Overrode buildPhase without runHook Add runHook preBuild / postBuild
Used buildInputs for cmake/pkg-config Prefer nativeBuildInputs
src is a single file, unpack fails unpackPhase = "cp $src ./foo.c"; sourceRoot = "."; or dontUnpack + copy in install
Name collisions / impure timestamps in C Avoid embedding timestamps; prefer -D defines from version only

Checkpoint

  • Can list default phase order from memory
  • greet and addone build with nix build
  • At least one intentional phase failure diagnosed from logs
  • nativeBuildInputs vs buildInputs explained in your notes
  • Phases documented for each package

Commit

git add .
git commit -m "day45: stdenv phases — greet + addone packages"

Write three personal gotchas before continuing.

Tomorrow

Day 46 — fetchers & FODs: hashes, fetchFromGitHub, and the mismatch workflow that keeps the store honest.


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.


Day 47 — Language ecosystem A

Stage V · ~4h (lab-heavy)
Goal: Package a real tool in your primary language using the nixpkgs language builder (buildGoModule, buildRustPackage, buildPythonApplication, …)—not raw stdenv unless the ecosystem forces it.

Tip

Pick one primary ecosystem today. Day 48 is a second, smaller transfer exercise. Depth beats collecting half-broken scaffolds for five languages.

Why this day exists

stdenv taught phases. Production packaging is almost always ecosystem helpers: they know how to vendor deps, set LDFLAGS, wrap interpreters, and place modules. Mastering one helper end-to-end is how Stage V becomes useful at work.


Theory 1 — How language builders relate to stdenv

buildGoModule / buildRustPackage / buildPythonApplication / …
        │
        ▼
  stdenv.mkDerivation (+ setup hooks)
        │
        ▼
  phases + FODs (module vendor hashes, cargoLock, …)

You still have pname, version, meta, patches, and hooks—but fetching language deps is specialized.

Ecosystem Primary builder (nixpkgs) Extra hash / lock concept
Go buildGoModule vendorHash
Rust rustPlatform.buildRustPackage cargoHash / cargoLock
Python buildPythonApplication / buildPythonPackage pyproject / fetchPypi / overrides
Node buildNpmPackage / importNpmLock npmDepsHash
Zig buildZigPackage (where available) / stdenv Fetch deps carefully
C/C++ stdenv + cmake/meson Usually no vendor hash

Theory 2 — Go path (buildGoModule) — default if you know Go

{ lib, buildGoModule, fetchFromGitHub }:
buildGoModule rec {
  pname = "hello-go";
  version = "0.1.0";

  src = fetchFromGitHub {
    owner = "you";
    repo = "hello-go";
    rev = "v${version}";
    hash = "sha256-…";
  };

  vendorHash = "sha256-…"; # FOD of the vendored module graph

  ldflags = [
    "-s" "-w"
    "-X main.version=${version}"
  ];

  meta = {
    description = "Tiny Go greeter";
    license = lib.licenses.mit;
    mainProgram = "hello-go";
  };
}

vendorHash workflow

  1. Set vendorHash = lib.fakeHash; (or null only if truly vendorless—rare for modules).
  2. Build; read got: hash.
  3. Paste; rebuild.
  4. When go.mod / go.sum change, vendorHash changes.
nix build .#hello-go -L

Useful knobs

Attr Purpose
subPackages Build only ./cmd/foo
proxyVendor Vendor via module proxy inside FOD
doCheck = false Skip tests if network-bound (prefer fixing tests)
env.CGO_ENABLED = "0" Static-ish pure Go when appropriate

Theory 3 — Rust path (buildRustPackage)

{ lib, rustPlatform, fetchFromGitHub }:
rustPlatform.buildRustPackage rec {
  pname = "hello-rs";
  version = "0.1.0";

  src = fetchFromGitHub {
    owner = "you";
    repo = "hello-rs";
    rev = "v${version}";
    hash = "sha256-…";
  };

  cargoHash = "sha256-…";

  meta = {
    description = "Tiny Rust greeter";
    license = lib.licenses.mit;
    mainProgram = "hello-rs";
  };
}

Alternatively lock from a committed Cargo.lock with cargoLock = { lockFile = ./Cargo.lock; }; patterns used widely in nixpkgs—follow current nixpkgs examples for 26.05 if cargoHash vs git deps bit you.

Git dependencies in Cargo

Vendoring git crates often needs cargoLock.outputHashes for each git source. Expect Day 51-level debugging the first time.


Theory 4 — Python path (application vs library)

{ lib, python3Packages, fetchPypi }:
python3Packages.buildPythonApplication rec {
  pname = "hello-py";
  version = "0.1.0";
  pyproject = true;

  src = fetchPypi {
    inherit pname version;
    hash = "sha256-…";
  };

  build-system = with python3Packages; [ setuptools ];
  dependencies = with python3Packages; [ click ];

  meta = {
    description = "Tiny Click CLI";
    license = lib.licenses.mit;
    mainProgram = "hello-py";
  };
}
Builder When
buildPythonApplication CLI / end-user tool on PATH
buildPythonPackage Library for others to depend on

Prefer existing python3Packages.* deps over vendoring wheels by hand.


Theory 5 — Node (buildNpmPackage) sketch

{ lib, buildNpmPackage, fetchFromGitHub }:
buildNpmPackage rec {
  pname = "hello-node";
  version = "0.1.0";
  src = fetchFromGitHub { /* … */ hash = "sha256-…"; };
  npmDepsHash = "sha256-…";
  meta = { mainProgram = "hello-node"; license = lib.licenses.mit; };
}

package-lock.json (or supported lock) must exist; hash tracks the npm dependency FOD.


Worked example — flake with Go tool + check

{
  description = "Day 47 language ecosystem A";
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";

  outputs = { self, nixpkgs }:
    let
      system = "x86_64-linux"; # or genAttrs
      pkgs = import nixpkgs { inherit system; };
    in {
      packages.${system}.default = pkgs.callPackage ./pkgs/hello-go.nix { };

      apps.${system}.default = {
        type = "app";
        program = "${self.packages.${system}.default}/bin/hello-go";
      };

      checks.${system}.hello-go-version = pkgs.runCommand "hello-go-version" {
        nativeBuildInputs = [ self.packages.${system}.default ];
      } ''
        hello-go --version || hello-go | tee out
        touch $out
      '';

      devShells.${system}.default = pkgs.mkShell {
        packages = [
          pkgs.go
          pkgs.gopls
          self.packages.${system}.default
        ];
      };
    };
}

Local module without GitHub (lab friendly):

{ lib, buildGoModule }:
buildGoModule {
  pname = "hello-go";
  version = "0.1.0";
  src = ../src/hello-go;
  vendorHash = null; # only if no external modules
  meta = { mainProgram = "hello-go"; license = lib.licenses.mit; };
}

If you import modules, use a real vendorHash.


Lab — multi-step

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

Step 1 — Choose primary language

Write the choice in NOTES.md: Go / Rust / Python / Node / other. Stick to it for all steps.

Step 2 — Minimal upstream project

Either:

  • Create a tiny local project under src/, or
  • Fetch a small tagged tool you actually use.

Requirements: builds offline once vendored; one CLI entrypoint; license known.

Step 3 — First package expression

Use the matching builder. Start with lib.fakeHash / fake vendor hashes as appropriate.

nix build .#default -L

Iterate until green.

Step 4 — Wire meta.mainProgram and run

nix run .#
# or
./result/bin/<name> --help

Step 5 — Version injection

Pass version via ldflags (Go), env (Rust), or __version__ (Python). Prove tool --version matches derivation version.

Step 6 — Tests

Enable package checks if feasible (doCheck, cargoTest, pytestCheckHook). If tests need network, patch or disable with a comment explaining why—not silence.

Step 7 — Consume from a devShell and document host install

nix develop
which <tool>

Optional: add to a NixOS environment.systemPackages via flake package output (pattern for Day 56 gate).

Step 8 — Break the vendor hash

Edit a dependency version or go.sum line; rebuild; observe hash mismatch; restore or update intentionally.


Common gotchas

Symptom / mistake What to do
vendorHash / cargoHash / npmDepsHash mismatch Update after intentional lock change; never ignore silently
Go build tries network in buildPhase Modules must be vendored via FOD; check proxyVendor / vendor dir
Rust git dependency hash missing Add outputHashes for the git crate
Python module import works in tests but CLI missing Use buildPythonApplication + meta.mainProgram
Node package missing lockfile Generate supported lock; don’t hand-wave node_modules
mainProgram wrong Fix meta; nix run depends on it for default bin
CGO / shared libs surprise Set CGO_ENABLED, add buildInputs for libs
Building entire monorepo subPackages / cargoBuildFlags / workspace members

Checkpoint

  • Primary language chosen and recorded
  • Package builds with official language helper
  • Vendor/cargo/npm hash workflow demonstrated
  • nix run or ./result/bin works
  • Version string controlled from Nix where possible
  • Notes on how this maps back to stdenv phases

Commit

git add .
git commit -m "day47: language ecosystem A package"

Write three personal gotchas before continuing.

Tomorrow

Day 48 — Language ecosystem B: a second ecosystem for pattern transfer (smaller package).