Day 6 — Derivations as idea
Day 6 — Derivations as idea
Stage I · ~4h (theory-heavy)
Goal: Treat a derivation as a pure build plan: relate evaluation → .drv → store outputs; run a tiny build; read logs; explain sandbox and substitutes at a conceptual level.
Deep stdenv phase mastery is Stage V. Today is the idea and one successful build loop.
Why this day exists
Without “derivation,” the store (Day 2) is a pile of directories and flakes are just JSON with extra steps. Derivations are the contracts that produce store paths.
Theory 1 — Derivation = build recipe with a promised output path
A derivation answers:
- What inputs (other store paths, sources)?
- What builder runs (usually bash + stdenv)?
- What environment (env vars, sandbox)?
- What output path(s) will hold results?
Informally:
derivation D
inputs: I1, I2, …
builder: B
→ output path P = hash(inputs, builder, args, system, …)
In the common input-addressed model, the hash is about the plan, not the bytes of the result (contrast content-addressed—later).
Theory 2 — Eval vs realize (operational)
| Phase | Artifact | Command vibes |
|---|---|---|
| Evaluate | Nix values + derivation objects | nix eval, instantiate |
| Low-level plan | .drv file in the store |
nix derivation show |
| Realize | Output path directories | nix build |
| Run | Binaries inside outputs | nix run, ./result/bin/… |
.drv is not the package
/nix/store/xxxx-hello-2.12.drv ← plan
/nix/store/yyyy-hello-2.12 ← output (realized)
You ship and cache outputs; .drv explains how they were supposed to be built.
Theory 3 — mkDerivation mental model (lite)
Most software uses stdenv.mkDerivation:
stdenv.mkDerivation {
pname = "mytool";
version = "0.1.0";
src = ./src;
buildInputs = [ /* runtime/build deps */ ];
# phases: unpack, patch, configure, build, install, fixup, …
}For learning today, prefer trivial builders that avoid compilers:
pkgs.writeText "hello.txt" "hello from nix\n"
pkgs.writeShellScriptBin "greet" ''
echo "hello from store script"
''These still produce real store paths and teach the loop without a C toolchain fight.
Theory 4 — Sandbox: why builds are fenced
The build sandbox aims to make builds pure:
| Restriction (typical Linux sandbox) | Why |
|---|---|
| Limited filesystem view | No sneaking /usr/lib deps |
| No network (usually) | Downloads must be fixed-output derivations |
| Clean env | Fewer “works on my machine” env vars |
| Only declared inputs visible | Graph stays honest |
Fixed-output derivations (FOD) — awareness
Fetchers (fetchurl, fetchFromGitHub) are special: they may use network but must declare a hash of the result. Wrong hash → fail. That is how purity reconciles with downloading sources.
fetchurl {
url = "https://example.com/foo.tar.gz";
sha256 = "…"; # wrong → error
}Theory 5 — Substituters vs local build
Realization of path P:
1. Is P already in the local store? → done
2. Can a trusted substituter provide P? → download
3. Else build from .drv locally
Implications
- First build can be “download a lot from cache.nixos.org”
- Changing one input changes the hash → cache miss → compile
- Trust is about signatures, not “HTTP has a file”
Theory 6 — Outputs and multiple outputs (preview)
A derivation can have several outputs (out, dev, doc, …):
P-out, P-dev, P-man
Closure size discipline later depends on not pulling dev into runtime. Today: know out is the default.
Theory 7 — What you inspect after a build
| Tool | Use |
|---|---|
nix build … --print-out-paths |
Output path |
ls result |
Symlink to output |
nix log <path or installable> |
Build log |
nix derivation show |
JSON-ish plan |
nix path-info -rS |
Closure size |
Worked examples bank
Example A — Build hello from nixpkgs
nix build nixpkgs#hello --print-out-paths
./result/bin/helloExample B — Trivial local package via flake-less expr
trivial.nix:
let
pkgs = import (builtins.getFlake "nixpkgs") { system = builtins.currentSystem; };
# If getFlake is awkward offline, use: nix build -f '<nixpkgs>' hello (literacy)
in
pkgs.writeShellScriptBin "greet" ''
echo "greetings from a derivation"
''Modern simple approach with nix build and expression:
nix build --impure --expr 'with import (builtins.getFlake "nixpkgs") { }; writeText "t" "hi\n"'Prefer the flake-based lab below for cleanliness.
Example C — Read derivation metadata
OUT=$(nix build nixpkgs#hello --print-out-paths)
nix derivation show nixpkgs#hello | head -c 2000
nix log "$OUT" 2>/dev/null || nix log nixpkgs#hello | tailExample D — Sandbox failure story (conceptual)
Builder does: curl http://example.com/dep.so
→ network blocked (non-FOD)
→ build fails
→ declare dep as input or use fetchurl with hash
Lab 1 — Workspace + flake shell for builds
mkdir -p ~/lab/90daysofx/02-nixos/day06
cd ~/lab/90daysofx/02-nixos/day06Create flake.nix (preview of Day 8—minimal):
{
description = "day06 trivial derivations";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux"; # change if needed: aarch64-linux, aarch64-darwin, …
pkgs = import nixpkgs { inherit system; };
in {
packages.${system} = {
greet = pkgs.writeShellScriptBin "greet" ''
echo "hello from day06 derivation"
'';
note = pkgs.writeText "day06-note.txt" ''
Derivations plan store paths.
'';
default = self.packages.${system}.greet;
};
};
}If your system is not x86_64-linux, edit system. On multi-arch learning, you can also use builtins.currentSystem in impure experiments—but pin explicitly when you can.
nix build .#greet
./result/bin/greet
nix build .#note
cat result # may be the text file path content via result symlinkCommit flake.lock when generated.
Lab 2 — Inspect plan and log
nix build .#greet --print-out-paths
nix derivation show .#greet | head -n 80
nix log .#greet | tail -n 40Journal:
- Output path hash/name
- Whether the “build” was essentially a write to the store
- One field you saw in
derivation showthat looks like an input
Lab 3 — Compare to nixpkgs hello closure
H=$(nix build nixpkgs#hello --print-out-paths)
G=$(nix build .#greet --print-out-paths)
echo "hello requisites: $(nix-store -q --requisites "$H" | wc -l)"
echo "greet requisites: $(nix-store -q --requisites "$G" | wc -l)"
nix path-info -rS "$H" | tail -3
nix path-info -rS "$G" | tail -3Explain size difference in one sentence (runtime interpreter/libs vs tiny script).
Lab 4 — Break a FOD on purpose (optional)
nix build --expr 'let pkgs = import (builtins.getFlake "nixpkgs") { system = "x86_64-linux"; }; in pkgs.fetchurl { url = "https://example.com"; sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; }' 2>&1 | tailYou want a hash mismatch or fetch failure—not a success. Note the error class for later packaging days.
Lab 5 — Teach-back
Define without notes:
- Derivation
.drvvs output path
- Why sandbox blocks network by default
- Substituter vs local build
- Why
writeShellScriptBinis still a “real” package
Common gotchas
| Symptom | Theory |
|---|---|
Wrong system in flake |
Outputs empty / wrong attr path |
result pins GC root |
rm result when done experimenting |
| Edited files in store | Immutable—change expression, rebuild |
| “Build” always compiles | Substituters often download |
| Impure path surprises | Unqualified imports / currentSystem |
| Darwin vs Linux scripts | shebangs and system must match host |
Checkpoint
- Built a local trivial package via flake output
- Ran the resulting binary/script
- Inspected derivation show + log
- Compared closure size to
hello
- Explained sandbox + FOD at awareness level
flake.lockpresent
Commit
cd ~/lab/90daysofx/02-nixos/day06
git init 2>/dev/null || true
git add .
git commit -m "day06: derivations idea trivial build logs"Tomorrow
Day 7 — Modern CLI fluency. Make nix build / run / develop / shell / flake / path-info second nature—same store, better UX.