Day 18 — Debug Builds, nixpkgs Layout & Binary Caches
Day 18 — Debug Builds, nixpkgs Layout & Binary Caches
Day 51 — Debug builds
Stage V · ~4h (lab-heavy)
Goal: Recover from failed package builds using nix log, -L, --keep-failed, breakpointHook, and structured isolation of phase failures—without disabling the sandbox as a first resort.
Why this day exists
Packaging skill is half writing expressions and half reading sandboxes under stress. Teams lose days because someone only knows “it failed” and rebuilds with random flags. Today you build a deliberate debug playbook.
Theory 1 — Where information lives
| Tool | Shows | When |
|---|---|---|
nix build -L / --print-build-logs |
Live phase output | First failure |
nix log <drv\|path> |
Stored build log | After failure/success |
nix show-derivation |
JSON-ish drv env | Input/env inspection |
--keep-failed |
Leaves tmp build dir |
Interactive inspect |
breakpointHook |
Drops into shell on failure | Deep phase debug |
NIX_LOG_FD / verbose |
Extra noise | Rare |
nix build .#broken -L
nix log .#broken
nix derivation show .#broken | lessModern CLI accepts installables (.#pkg); classic nix-store -l /nix/store/...drv still works.
Theory 2 — Keep failed trees
nix build .#broken --keep-failed -LOn failure, Nix prints a path under /tmp/nix-build-… (or configured temp). You can:
cd /tmp/nix-build-foo-*
# inspect source tree after patchPhase, object files, config.log, etc.The kept tree is not the sandbox re-entered with the same env. It is a corpse for forensics. Re-running make there may work or lie—prefer breakpointHook for a live env.
Theory 3 — breakpointHook
{ stdenv, breakpointHook }:
stdenv.mkDerivation {
pname = "explode";
version = "0.0.1";
dontUnpack = true;
nativeBuildInputs = [ breakpointHook ];
buildPhase = ''
echo "about to fail"
false
'';
}When the build fails, you get a shell inside the build environment (with caveats on platforms and interactive settings). Fix hypotheses, then exit and fix the Nix expression permanently.
Also related: pkgs.busybox / extra tools in nativeBuildInputs temporarily for inspection (strace, file)—remove before merge.
Theory 4 — Common failure classes
| Class | Signals | First moves |
|---|---|---|
| Hash / FOD | hash mismatch |
Day 46 workflow |
| Configure | config.log, missing headers |
nativeBuildInputs, PKG_CONFIG_PATH |
| Compile | error:, missing symbols |
deps, flags, patches |
| Link | undefined reference |
buildInputs, order, static/shared |
| Install | empty $out, wrong paths |
installPhase, multi-out |
| Check | test suite | doCheck, sandbox network, golden files |
| Patch | rejects | refresh patch |
| Purity | banned network/path | FOD or remove impurity |
| Eval | before build | fix Nix syntax/attrs—not nix log of build |
Separate eval failures from build failures immediately:
nix eval .#packages.x86_64-linux.foo.drvPath
# works? then build issue. fails? expression issue.Theory 5 — Sandbox: don’t disable casually
| Flag / knob | Effect | Use? |
|---|---|---|
--option sandbox false |
Allows host impurities | Last resort local diagnosis only |
--no-sandbox |
Same family | Never in CI policy |
require-sigs / substituters |
Cache trust | Unrelated to compile fails |
__noChroot (legacy patterns) |
Avoid | Prefer proper FODs |
If it only builds with sandbox off, your package is not done.
Theory 6 — Reproducing CI failures locally
# Clean-ish rebuild of one attr
nix build .#foo --rebuild -L
# Ignore binary cache for this attr (force local compile)
nix build .#foo --option substitute false -L
# Show why a dep is present
nix why-depends .#foo .#barMatch CI: same flake lock, same system, same Nix version band (book baseline: 2.34.x, nixpkgs 26.05).
Worked example — scripted break/fix series
# pkgs/debug-lab.nix
{ stdenv, lib, breakpointHook }:
stdenv.mkDerivation {
pname = "debug-lab";
version = "0.1.0";
src = ../src/debug-lab; # contains broken.c / fixed later
# Toggle while learning:
# nativeBuildInputs = [ breakpointHook ];
buildPhase = ''
runHook preBuild
$CC broken.c -o debug-lab
runHook postBuild
'';
installPhase = ''
runHook preInstall
install -Dm755 debug-lab $out/bin/debug-lab
runHook postInstall
'';
meta = {
mainProgram = "debug-lab";
license = lib.licenses.mit;
};
}/* broken.c — missing semicolon */
#include <stdio.h>
int main(void) {
puts("debug-lab")
return 0;
}Fix:
puts("debug-lab");Lab — multi-step
Suggested workspace: ~/lab/90daysofx/02-nixos/day51
Step 1 — Baseline success package
Start from Day 45 addone or Day 47 tool. Confirm green build + log:
nix build .#addone -L
nix log .#addone | tail -n 50Step 2 — Inject a compile failure
Break a source file. Capture:
nix build .#addone -L 2>&1 | tee fail-compile.txt
nix log .#addone | tee fail-compile-log.txtStep 3 — --keep-failed
nix build .#addone --keep-failed -L || true
# locate printed /tmp/nix-build-* path; list files; find .o / sourcesDocument what you could see vs not see.
Step 4 — Inject an install failure
Fix compile; break installPhase (mkdir missing, wrong path). Repeat log + keep-failed.
Step 5 — FOD failure recall
Temporarily wrong src.hash; confirm you recognize the error class in under 10 seconds.
Step 6 — breakpointHook (Linux recommended)
Add hook; force false in buildPhase; enter the breakpoint if your environment supports it. Type env | sort | head, inspect $CC, exit.
Step 7 — Write a personal runbook
DEBUG-RUNBOOK.md with your ordered steps (max one page). Example skeleton:
- Eval or build?
-Lfirst failure phase
nix log
- hash vs compile vs install
--keep-failed
- hook / extra tools
- only then consider sandbox experiments
Step 8 — Force local rebuild
nix build .#addone --rebuild --option substitute false -LNote time vs cached build.
Theory 7 — Reading nix log paths
nix log /nix/store/….drv 2>/dev/null || true
# or from failed build output: link to log under /nix/var/log/nix/drvsAlways save the drv path from a failure before GC. Without it, debugging becomes archaeology.
Theory 8 — --keep-failed and dirty trees
nix build -L --keep-failed .#brokenPkg || true
# inspect /tmp/nix-build-… when printedCompare with pure rebuilds—never commit keep-failed trees. They are for diagnosis only.
Worked example — bisect mental model
When a pin update breaks a package:
old lock (green) ── update input ──> new lock (red)
narrow: which input?
then: which package rebuild?
then: which phase?
Day 24 lock discipline + today’s phase logs = faster bisect.
Lab — capture a complete failure dossier
For one intentional break, write:
- Command
- Eval or build?
- Phase (if build)
- First error line
- Fix
- Verification command
Store under notes/debug-dossier.md.
Lab — sandbox proof
# show sandbox setting
nix show-config | rg -i sandboxState whether you ever disabled sandbox (you should not for normal work) and why that is dangerous (undeclared deps “work on my machine”).
Common gotchas
| Symptom / mistake | What to do |
|---|---|
nix log says no build log |
Build never started (eval) or GC’d; rebuild with -L |
| Kept temp already deleted | Re-run with --keep-failed immediately |
| breakpointHook “does nothing” | Platform/interactive limits; use keep-failed + better logs |
Fixed tree by hand in /tmp only |
Always port fix into Nix sources |
| Disabled sandbox “to move on” | Track impurity; fix properly before Day 56 gate |
| CI-only failure | Align system, lock, substituters, case-sensitive paths |
Silent doCheck fails |
Watch for checkPhase in log; set doCheck explicitly |
Checkpoint
- Used
-Landnix logon a real failure
- Used
--keep-failedand inspected the tree
- Classified at least three failure kinds
- Tried
breakpointHookor documented why not
- Personal debug runbook committed
Commit
git add .
git commit -m "day51: debug builds runbook and failure drills"Write three personal gotchas before continuing.
Tomorrow
Day 52 — nixpkgs layout & by-name: conventions for PR-shaped packages.
Day 52 — nixpkgs layout & by-name
Stage V · ~4h
Goal: Navigate modern nixpkgs package layout—especially pkgs/by-name—and shape a package so it could become an upstream PR (submission optional).
Why this day exists
Copy-pasting random default.nix from a blog creates private snowflakes. Knowing where packages live, how they are called, and what reviewers expect makes your flake packages portable and your future contributions reviewable.
Theory 1 — Mental map of nixpkgs (2026)
| Path | Role |
|---|---|
pkgs/by-name/<p>/<pname>/package.nix |
Preferred home for many new single-package expressions |
pkgs/top-level/all-packages.nix |
Historic wiring; less hand-editing for new pkgs |
pkgs/development/… |
Older category tree still holds many packages |
pkgs/applications/… |
Same—legacy category layout |
lib/ |
Nix library functions |
nixos/modules/ |
NixOS modules (not package expressions) |
maintainers/maintainer-list.nix |
Maintainer identities |
by-name rules of thumb
pkgs/by-name/he/hello/package.nix
││ │
││ └─ package directory = pname
└┴─ first two lowercase letters of pname
| Rule | Detail |
|---|---|
| File name | Usually package.nix (not always default.nix) |
| Shard | toLower (substring 0 2 pname) |
| Scope | One package per dir; keep it self-contained |
| Call | Auto-wired for eligible packages—follow current nixpkgs docs if adding upstream |
Your flake can still use callPackage ./pkgs/foo.nix { } without sharding; by-name is the upstream shape.
Theory 2 — Anatomy of a PR-shaped package.nix
{
lib,
stdenv,
fetchFromGitHub,
# only real deps
}:
stdenv.mkDerivation (finalAttrs: {
pname = "cooltool";
version = "1.2.3";
src = fetchFromGitHub {
owner = "acme";
repo = "cooltool";
rev = "v${finalAttrs.version}";
hash = "sha256-…";
};
nativeBuildInputs = [ /* … */ ];
buildInputs = [ /* … */ ];
meta = {
description = "One-line description ending without a period sometimes—match nixpkgs style you see nearby";
homepage = "https://github.com/acme/cooltool";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ /* your handle if upstream */ ];
platforms = lib.platforms.unix;
mainProgram = "cooltool";
};
})Modern habits
| Habit | Why |
|---|---|
finalAttrs: pattern |
Version/src self-reference without rec pitfalls |
| Explicit function head args | callPackage injects deps |
meta.mainProgram |
nix run nixpkgs#cooltool |
Real license |
Policy and binary caches |
| No unrelated formatting of huge trees | Reviewer happiness |
Theory 3 — callPackage and fixed-points
pkgs.callPackage ./package.nix { }
# equivalent spirit: package.nix is a function of deps# Override only one dep
pkgs.callPackage ./package.nix {
openssl = pkgs.openssl_3;
}Overlays often do:
final: prev: {
cooltool = final.callPackage ./cooltool/package.nix { };
}Use final.callPackage so deps see other overlay packages.
Theory 4 — Reading existing packages as literature
# If you have a nixpkgs checkout:
rg -n "pname = \"ripgrep\"" -S pkgs | head
# Or query without full clone:
nix edit nixpkgs#ripgrep
# opens the file in $EDITOR when configuredRead three packages in your Day 47 language:
- Tiny CLI
- One with patches
- One with multiple outputs
Note patterns: hooks, finalAttrs, meta, tests.
Theory 5 — Tests and passthru (awareness)
Many nixpkgs packages expose:
passthru.tests = { /* … */ };
passthru.updateScript = /* nix-update support */;Optional today; required mental model for “done upstream.” Your flake can mirror with checks.
Worked example — flake package laid out like by-name
day52/
flake.nix
pkgs/
by-name/
co/cooltool/package.nix
overlays/default.nix
NOTES.md
# overlays/default.nix
final: prev: {
cooltool = final.callPackage ../pkgs/by-name/co/cooltool/package.nix { };
}# flake.nix
{
description = "Day 52 by-name shaped package";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
overlays = [ (import ./overlays/default.nix) ];
};
in {
packages.${system}.default = pkgs.cooltool;
packages.${system}.cooltool = pkgs.cooltool;
# PR checklist as a check: evaluate meta
checks.${system}.meta-license =
assert pkgs.cooltool.meta ? license;
pkgs.runCommand "meta-ok" { } "touch $out";
};
}Lab — multi-step
Suggested workspace: ~/lab/90daysofx/02-nixos/day52
Step 1 — Locate packages
nix edit nixpkgs#hello || true
nix eval nixpkgs#hello.meta --json | jq .Record: description, license, platforms, maintainers shape.
Step 2 — Reshape your best Stage V package
Take Day 45–48 best package and move it to:
pkgs/by-name/<shard>/<pname>/package.nix
Use finalAttrs if it fits.
Step 3 — Overlay + flake export
callPackage through overlay; nix build .#cooltool.
Step 4 — Meta completeness pass
Ensure:
description
homepage(if any)
license
mainProgram
platformsor rely on sensible default
Step 5 — PR-shaped checklist (write even if not submitting)
PR-CHECKLIST.md:
- Builds on your system
nixpkgs-reviewawareness (optional run if you have checkout)
- No secrets in expression
- Hashes pinned
- Follows naming
- Commit message style (
cooltool: init at 1.2.3)
Step 6 — Optional: clone nixpkgs and dry-place the file
# shallow clone is large; optional
# place file in correct by-name path; do not push unless you intend a real PRStep 7 — Compare category tree vs by-name
Find one package still under pkgs/applications/… and one under by-name. Note one reviewer-facing difference (discovery, automation, structure).
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| Wrong two-letter shard | Use first two chars of pname, lowercase |
default.nix in by-name |
Prefer package.nix per current convention |
Recursive rec version bugs |
Prefer finalAttrs |
| Giant unrelated deps in head | Only list what the function uses |
Meta missing mainProgram |
Multi-bin packages: set correctly or omit carefully |
| Assuming auto-discovery in your flake | Flakes need explicit callPackage/overlay |
Checkpoint
- Explained
by-namesharding
- Package lives in by-name-shaped path
- Overlay +
callPackageworks
- Meta filled to nixpkgs-ish bar
- PR checklist written (submit optional)
Commit
git add .
git commit -m "day52: by-name shaped package + meta"Write three personal gotchas before continuing.
Tomorrow
Day 53 — Binary caches: Cachix, Attic/Harmonia, trust, and push/pull workflows.
Day 53 — Binary caches
Stage V · ~4h (lab-heavy)
Goal: Pull and push substitutes with cache.nixos.org, Cachix, and/or self-hosted Attic / Harmonia—with a clear model of trust and signatures.
Why this day exists
Without substitutes, Stage V and fleet deploys become compile farms. With careless substituters, you outsource trust to whoever can serve a path. Caches are a performance and supply-chain topic, not just “make CI fast.”
Theory 1 — Substituters vs builders
eval → wanted store path P
├─ trusted substituter has P? → download (+ verify sigs)
└─ else → build (local or remote builder)
| Term | Meaning |
|---|---|
| Substituter | HTTP(S) NAR store endpoint (cache.nixos.org) |
| Binary cache | Popular name for substituter + signing |
| Signed NAR | Artifact with signature by a cache key |
| Trusted key | Public keys your Nix is willing to accept |
| Priority | Order among substituters |
# NixOS example
nix.settings = {
substituters = [
"https://cache.nixos.org"
"https://myorg.cachix.org"
];
trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"myorg.cachix.org-1:BASE64KEY…"
];
};Flake/nix.conf user settings similar under ~/.config/nix/nix.conf or daemon config.
Theory 2 — Trust model (non-negotiable)
| Claim | Reality |
|---|---|
| “HTTPS means safe binaries” | No—HTTPS protects transport, not who built the bits |
| “I added a cache URL only” | Incomplete without matching public key (or explicit none) |
| “Private cache = trusted” | Only if builders and keys are controlled |
| “cache.nixos.org is magic” | Trusted by default for official paths; still a third party |
Rule: Every substituter you add is part of your TCB (trusted computing base) for that machine class.
Theory 3 — Cachix (hosted, popular in 2026)
Cachix hosts per-project caches with simple push UX.
# Install client via nix once
nix profile install nixpkgs#cachix
# Auth (token from UI; treat as secret—Day 33–35 patterns)
export CACHIX_AUTH_TOKEN=… # prefer secret manager / GH Actions secret
cachix use myorg # writes substituter + pubkey locally (daemon may need trust)
cachix push myorg ./result
# or
nix build .#foo | cachix push myorgCI sketch:
- uses: cachix/install-nix-action@v31
- uses: cachix/cachix-action@v16
with:
name: myorg
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- run: nix build .#default -L(Pin action versions to what you verify in 2026—don’t invent untrusted SHAs; follow current Cachix docs.)
Theory 4 — Self-hosted: Attic & Harmonia
| Project | Role (approx.) | When |
|---|---|---|
| Attic | Modern multi-tenant binary cache server | Teams wanting self-host push/pull |
| Harmonia | cache.nixos.org-compatible server in Rust | Serve local store as substituter |
| nix-serve / nix-serve-ng | Classic signing servers | Legacy blogs |
Harmonia sketch (NixOS)
services.harmonia = {
enable = true;
# signKeyPath = "/var/lib/secrets/harmonia.secret"; # manage via sops-nix
# settings…
};
# nginx TLS reverse proxy in front in productionAttic sketch (conceptual)
# server configured with tokens / buckets (see upstream Attic docs)
attic login local http://cache.example.internal:8080 "$TOKEN"
attic push local:lab ./resultExact CLI flags evolve—read current Attic docs for 26.05-era packaging; the lab below allows Cachix or a local Harmonia/Attic path.
Theory 5 — Push what matters
| Push | Skip |
|---|---|
| Your packages & CI deps | Entire nixpkgs rebuilds by accident |
| Closure of deploy targets | Personal /tmp experiments |
| Signed artifacts from trusted CI | Unsigned ad-hoc laptop builds for prod (policy choice) |
# Push full runtime closure
nix-store -qR ./result | cachix push myorg
# modern:
nix copy --to 'http://…' ./result # when endpoint supports itWorked examples
NixOS module fragment for team cache
{ lib, ... }:
{
nix.settings = {
substituters = lib.mkAfter [
"https://myorg.cachix.org"
];
trusted-public-keys = lib.mkAfter [
"myorg.cachix.org-1:REPLACE_ME="
];
# optional: allow user to add more without daemon edit fights
trusted-users = [ "root" "@wheel" ];
};
}Query whether a path would substitute
nix path-info --store https://cache.nixos.org --json $(nix build nixpkgs#hello --print-out-paths) | jq .Build without substitutes (contrast)
nix build .#cooltool --option substitute false -LLab — multi-step
Suggested workspace: ~/lab/90daysofx/02-nixos/day53
Step 1 — Observe default cache
nix build nixpkgs#hello -L
# note download lines mentioning cache.nixos.org
nix config show | rg -i 'substituter|trusted-public'Step 2 — Build your Stage V package cold-ish
nix build .#cooltool --rebuild -LTime it roughly.
Step 3 — Choose push target
Path A — Cachix: create free/personal cache if allowed; cachix use; push ./result.
Path B — Self-host lab: enable Harmonia or Attic on a lab VM; configure key; push/copy.
Path C — Document-only: if no account/VM, write full config you would apply and simulate with nix copy --to file:///… local store dump.
# Path C example: local file nar cache
nix copy --to file://$PWD/nar-cache ./result
nix copy --from file://$PWD/nar-cache --to $NIX_STORE ./resultStep 4 — Second machine / second user story
On another user or VM (or after GC of the path if safe):
nix build .#cooltool -L
# should substitute if cache wired and path presentStep 5 — Trust write-up
TRUST.md:
- Who can push?
- Who holds signing keys?
- What hosts trust the pubkey?
- What is the revoke plan?
Step 6 — CI note
Add a short paragraph linking to Day 66: push from GitHub Actions only with secrets, not from random laptops, if that is your house rule.
Step 7 — Negative test
Point at a cache without adding the public key (on a disposable Nix config if you can). Observe failure mode; restore.
Theory 6 — Substituter order and priority
nix.settings = {
substituters = [
"https://cache.nixos.org"
# "https://myorg.cachix.org"
];
trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
# "myorg.cachix.org-1:…"
];
};| Idea | Practice |
|---|---|
| Official cache first | Common default |
| Extra caches append | Org/project binaries |
| Keys must match | Silent ignore / fail if mismatch depending on config |
Never add a substituter without its public key. Trust is cryptographic, not vibes.
Theory 7 — What “cache miss” really means
eval → derive output path hash
→ query substituters for that path
→ hit: download NAR
→ miss: build locally (or remote builder)
A miss is normal for private packages. A miss on hello from cache.nixos.org means network, config, or exotic platform issues.
Worked example — query without installing
nix path-info --store https://cache.nixos.org nixpkgs#hello
nix store ping --store https://cache.nixos.orgLab — document lab cache policy
In docs/cache.md:
- Which substituters are enabled
- Which keys
- Whether you push CI builds
- Who may add a new cache (code review rule)
Lab — force a local build once
nix build nixpkgs#hello --option substitute false -LFeel the difference; re-enable substitutes after. Do not leave substitute false in user config.
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| “don’t know how to trust” / unsigned | Add correct trusted-public-keys or sign |
User nix.conf ignored by daemon |
Configure system/daemon NixOS settings |
| Pushed wrong path | Push derivation outputs you care about; verify with another client |
| Cache hit but old content | Paths are immutable; rebuild means new hash |
| Leaked Cachix token | Rotate; treat like deploy key |
| Priority fights | Order substituters; understand fallback to build |
| Assumed private URL = auth | Many caches need tokens separately from Nix substituter URL |
Checkpoint
- Explain substituter + signature trust
- Built with and without substitutes
- Pushed or file-store simulated push
- Second client pull story demonstrated/documented
TRUST.mdwritten
Commit
git add .
git commit -m "day53: binary caches push/pull and trust notes"Write three personal gotchas before continuing.
Tomorrow
Day 54 — Remote builders: offload builds to another machine safely.