Day 16 — stdenv Phases, Fetchers & Go Packaging
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.
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 checkLab — 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/day45Create 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
pkgs/greet.nix— shell script package
pkgs/addone.nix— single-file C
- Optional: package
helloyourself withfetchurl(compare topkgs.hello)
Step 3 — Break phases on purpose
In addone.nix, set temporarily:
installPhase = "false"; # fail installBuild, read the log, note which phase failed:
nix build .#addone --print-build-logs || true
# restore installPhase with runHook wrappersStep 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 | headDocument: 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
greetandaddonebuild withnix build
- At least one intentional phase failure diagnosed from logs
nativeBuildInputsvsbuildInputsexplained 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
- Trust “got” only after you trust the URL/rev. If the project was compromised, blindly updating the hash pins malware.
- Confirm you intended that
rev/ version.
- Replace
hashwith thegotvalue (or uselib.fakeHash/ empty once to force a fail).
- Rebuild; lock the known-good hash in git.
- Never leave
hash = "";orlib.fakeSha256in 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.gzRecord 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/helloStep 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 fixpatches = [ ./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:
- Where did bytes come from?
- Who could change them?
- What does updating
hashmean operationally?
- Difference between updating
flake.lockand updatingsrc.hash.
Step 7 — Offline check (optional)
After populate:
nix build .#hello-fod --offlineIf 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
fetchurland 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.
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
- Set
vendorHash = lib.fakeHash;(ornullonly if truly vendorless—rare for modules).
- Build; read
got:hash.
- Paste; rebuild.
- When
go.mod/go.sumchange, vendorHash changes.
nix build .#hello-go -LUseful 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 -LIterate until green.
Step 4 — Wire meta.mainProgram and run
nix run .#
# or
./result/bin/<name> --helpStep 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 runor./result/binworks
- 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).