Day 3 — Derivations, Modern CLI & First Flake
Day 3 — Derivations, Modern CLI & First Flake
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.
Day 7 — Modern CLI fluency
Stage I · ~4h (theory-heavy + drills)
Goal: Prefer the modern nix CLI for all daily tasks: build, run, develop, shell, flake inspection, store queries—and know which command matches which intent.
Why this day exists
Docs and blog posts mix eras. Muscle memory on the wrong CLI makes flakes feel harder than they are. Today is deliberate fluency so Days 8–10 feel like product work, not archaeology.
Theory 1 — One CLI, many intents
| Intent | Modern command | Rough meaning |
|---|---|---|
Build installable → result |
nix build |
Realize outputs |
| Run app without installing | nix run |
Build/realize + exec |
| Project dev environment | nix develop |
Shell with build inputs (flake devShell) |
| Ad-hoc package shell | nix shell |
Temporary PATH with packages |
| Explore flake outputs | nix flake show |
Schema of outputs |
| Update lock | nix flake update |
Refresh inputs (careful) |
| Metadata / eval | nix eval |
Evaluate expressions |
| Store facts | nix path-info |
Size, refs, sigs |
| Search | nix search |
Find packages |
| Profile (user env) | nix profile |
Generational user installs |
| Format / check (later) | nix fmt, nix flake check |
Quality gates |
Classic cousins (nix-build, nix-shell, …) wait until Day 9.
Theory 2 — Installables: how you name what you want
Modern CLI takes installables:
| Form | Example | Meaning |
|---|---|---|
| Flake output | .#greet |
Current flake, package greet |
| Flake + attr | .#packages.x86_64-linux.greet |
Fully spelled |
| Indirect flake | nixpkgs#hello |
Registry/indirect nixpkgs |
| Git flake | github:NixOS/nixpkgs/nixos-26.05#hello |
Explicit |
| Path flake | ./myproj#default |
Local |
| File / expr | -f file.nix, --expr '…' |
Non-flake (still useful) |
# splits flake ref from attribute
nixpkgs#hello
│ │
│ └─ attribute path inside outputs (with defaults)
└─ flake reference
Theory 3 — nix build deep enough for daily use
nix build .#greet
nix build .#greet --print-out-paths
nix build .#greet -o greet-result
nix build nixpkgs#hello| Flag | Role |
|---|---|
--print-out-paths |
Print store paths |
-o name |
Name the result symlink |
--no-link |
Realize without result |
-L / --print-build-logs |
Stream logs |
--rebuild |
Force rebuild (when applicable) |
Discipline: temporary builds → --no-link or rm result to avoid GC root clutter (Day 2).
Theory 4 — nix run vs nix shell vs nix develop
nix run
nix run nixpkgs#hello
nix run .#greet
nix run nixpkgs#cowsay -- "moo"Builds/downloads if needed, runs the package’s main program, does not require a permanent profile entry.
nix shell
nix shell nixpkgs#git nixpkgs#jq
# temporary shell with git + jq on PATHAd-hoc tools. Great for “I need jq once.”
nix develop
nix develop # .#devShells.default or similar
nix develop .#nameEnters the flake’s devShell—meant for project dependencies, compilers, env vars. Day 8 makes one.
| Command | Sticky? | Typical use |
|---|---|---|
run |
No | One-shot CLI tools |
shell |
Until exit | Ad-hoc package mix |
develop |
Until exit | Project toolchain |
profile install |
Yes | User-global tools (use sparingly early) |
Theory 5 — Flake subcommands
nix flake init # scaffold (optional)
nix flake new mod # template
nix flake show # outputs tree
nix flake metadata # resolved inputs, locks
nix flake check # run checks (later quality)
nix flake update # update all inputs
nix flake update nixpkgs # selective update
nix flake lock # refresh lock without full update semantics as neededshow vs metadata
| Command | Answers |
|---|---|
show |
What outputs exist? |
metadata |
What are inputs pinned to? |
Theory 6 — Eval and debugging eval
nix eval .#packages.x86_64-linux.greet.name
nix eval --raw .#packages.x86_64-linux.greet
nix eval --json nixpkgs#hello.meta | headnix repl
:lf .
:lf nixpkgsREPL remains the best theory laboratory.
Theory 7 — Store queries you will reuse
nix path-info -Sh ./result
nix path-info -rS ./result | tail
nix store gc --dry-run # careful; prefer dry concepts early
nix store delete … # advanced; skip casuallynix-store -q still works; modern path-info is friendlier.
Theory 8 — Profiles without making them a lifestyle
nix profile list
nix profile install nixpkgs#jq
nix profile remove … This volume’s preference: project flakes + devShells over growing a huge imperative profile. Profiles are fine for a few personal tools; they are not a substitute for declarative hosts (Stage II).
Worked examples bank
Example A — Same package, three ways
nix run nixpkgs#hello
nix shell nixpkgs#hello -c hello
nix build nixpkgs#hello --print-out-pathsExample B — Flake introspection
cd ~/lab/90daysofx/02-nixos/day06 # or day07 copy
nix flake show
nix flake metadataExample C — Logs streaming
nix build nixpkgs#hello -LExample D — JSON for scripting
nix flake metadata --json | head -c 400
nix path-info --json ./result | head -c 400Lab 1 — Cheat-sheet construction
mkdir -p ~/lab/90daysofx/02-nixos/day07
cd ~/lab/90daysofx/02-nixos/day07Create CHEATSHEET.md and fill by running each command at least once:
build: …
run: …
shell: …
develop: … (may wait until day08 if no shell yet)
show: …
metadata: …
eval: …
path-info:…
search: …
Paste the exact successful command lines you used.
Lab 2 — Copy day06 flake and operate only with modern CLI
cp -a ~/lab/90daysofx/02-nixos/day06/* ~/lab/90daysofx/02-nixos/day07/ 2>/dev/null || true
# or recreate minimal flake from day06Tasks:
nix flake show
nix build .#greet --print-out-paths
nix run .#greet
nix path-info -rS ./result | tail
rm -f resultand rebuild with--no-link
Lab 3 — Ad-hoc nix shell workflow
nix shell nixpkgs#jq nixpkgs#ripgrep -c bash -c 'jq --version; rg --version'Solve a tiny task: query a JSON blob with jq without installing jq into a profile permanently.
Lab 4 — Search + run loop
nix search nixpkgs cowsay | head
nix run nixpkgs#cowsay -- "modern cli"Record the attribute name you used.
Lab 5 — Intent mapping drill (closed book)
Write the best command for each:
- Temporarily need
ffmpeg
- See outputs of current flake
- Build package without leaving
result
- Run project’s default app
- See what nixpkgs revision lock pins
Common gotchas
| Symptom | Theory |
|---|---|
experimental features errors |
nix-command / flakes not enabled |
| Attribute not found | Wrong system triple / output layout |
nix run can’t find binary |
Package has no main program meta |
Mixing nix-env -i habits |
Prefer run/shell/develop |
flake show empty packages |
system key mismatch |
Slow nix search |
First-time indexing/eval |
Checkpoint
- Cheat-sheet written from real command output
- Can choose run vs shell vs develop correctly
- Built with and without
resultlink
- Used
flake show+metadata
- Used
path-infoon a real path
- No new dependency on classic CLI
Commit
cd ~/lab/90daysofx/02-nixos/day07
git init 2>/dev/null || true
git add .
git commit -m "day07: modern cli cheatsheet drills"Tomorrow
Day 8 — First project flake. Author inputs / outputs / flake.lock properly with devShells.default for a real project directory.
Day 8 — First project flake
Stage I · ~4h (theory + project)
Goal: Author a real project flake with pinned inputs, clear outputs, committed flake.lock, and a working devShells.default—before any NixOS host work.
Why this day exists
Flakes are the modern collaboration unit: reproducible inputs + named outputs. A devShell is the highest-value first output—shared tools without polluting a global profile. OS configuration comes in Stage II; project pinning comes now.
Theory 1 — What a flake is
A flake is a directory (usually a git tree) with:
| File | Role |
|---|---|
flake.nix |
Declarative inputs + outputs function |
flake.lock |
Exact pinned revisions/hashes of inputs |
Evaluation model
flake.nix outputs = inputs: { … }
↓
lock pins input trees
↓
outputs provide packages, shells, apps, nixosConfigurations, …
Punchline: without the lock file, “same flake.nix” is not the same system.
Theory 2 — inputs
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
# optional:
# flake-utils.url = "github:numtide/flake-utils";
};
}Common URL forms
| URL | Meaning |
|---|---|
github:Owner/Repo |
Default branch (avoid for pins—use lock) |
github:Owner/Repo/ref |
Branch/tag/rev |
git+https://… |
Arbitrary git |
path:./vendor/x |
Local path input |
Following transitive inputs
inputs.foo.inputs.nixpkgs.follows = "nixpkgs";Keeps a single nixpkgs when dependencies would otherwise multiply.
Theory 3 — outputs
outputs = { self, nixpkgs, ... }@inputs:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
in {
packages.${system}.default = pkgs.hello;
devShells.${system}.default = pkgs.mkShell {
packages = [ pkgs.git pkgs.hello pkgs.jq ];
};
};Output schema (common)
| Output | Purpose |
|---|---|
packages.<system>.<name> |
Buildable packages |
devShells.<system>.<name> |
nix develop environments |
apps.<system>.<name> |
nix run apps |
formatter.<system> |
nix fmt |
checks.<system>.<name> |
nix flake check |
nixosConfigurations.<name> |
NixOS hosts (Stage II) |
overlays.default |
Package overlays (later) |
self
self is the flake’s own source (after copy to store). Used for src = self; packaging and referring to other outputs: self.packages.${system}.foo.
Theory 4 — Systems and portability
Hard-coding one system is fine for a first lab. Better patterns later: flake-utils, flake-parts, or explicit map over systems.
systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ];For today: pick your system correctly or multi-define the same shell for two systems if you dual-boot.
Discover:
nix eval --impure --expr 'builtins.currentSystem'Theory 5 — mkShell / mkShellNoCC
pkgs.mkShell {
packages = [ pkgs.go pkgs.git ];
shellHook = ''
echo "welcome to the dev shell"
export DEMO=1
'';
}| Field | Role |
|---|---|
packages / nativeBuildInputs |
Tools on PATH |
buildInputs |
Libraries (more package-oriented) |
shellHook |
Runs on enter |
Prefer putting project tools here, not every CLI you like globally.
Theory 6 — Lock discipline (intro)
nix flake lock # ensure lock exists/up to date with urls
nix flake update # bump inputs (all)
nix flake update nixpkgs # bump one input| Rule | Why |
|---|---|
Commit flake.lock |
Reproducibility |
| Review lock diffs | Security + surprise upgrades |
| Selective update | Smaller change sets |
Day 24 goes deeper; today: lock must exist and be committed.
Theory 7 — Flakes and git
Flakes generally want a git repository. Untracked files may be invisible to evaluation depending on setup.
git init
git add flake.nix
# after first lock:
git add flake.lockIf something “missing,” check git status—not only the filesystem.
Theory 8 — Registries (light awareness)
nix registry listnixpkgs#hello resolves via registry → often a floating ref until you pin your project’s own input. Project flakes should not rely on registry drift; they declare inputs.nixpkgs.url.
Worked examples bank
Example A — Minimal full flake
{
description = "day08 project shell";
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 system);
in {
devShells = forAllSystems (system:
let pkgs = import nixpkgs { inherit system; };
in {
default = pkgs.mkShell {
packages = with pkgs; [ git hello jq ];
};
});
};
}Example B — Enter and verify
nix develop
hello
git --version
jq --version
exitExample C — Metadata
nix flake metadata
nix flake showLab 1 — Create the project
mkdir -p ~/lab/90daysofx/02-nixos/day08/myproject
cd ~/lab/90daysofx/02-nixos/day08/myproject
git initWrite flake.nix (start from Example A; trim systems if you want).
nix flake lock
nix flake show
nix develop -c hellogit add flake.nix flake.lock
git commit -m "day08: initial project flake with devShell"Lab 2 — Make it your project
Add tools you actually want for a small project (pick a lane):
| Lane | Example packages |
|---|---|
| Go | go, gopls, git |
| Rust | rustc, cargo, rust-analyzer |
| Python | python3, uv or poetry |
| Ops | jq, ripgrep, kubectl (if desired) |
Edit packages = [ … ], re-enter shell, verify versions.
Add a trivial project file README.md describing how to enter the shell.
Lab 3 — shellHook and env
shellHook = ''
export PROJECT_ROOT="$PWD"
echo "devShell ready for $PROJECT_ROOT"
'';nix develop -c bash -c 'echo $PROJECT_ROOT'Lab 4 — Offline-capable after populate (preview of gate)
- Enter
nix developonce online (populate store).
- Note
nix flake metadataresolved revisions.
- Optional: airplane mode / disconnect and
nix develop --offline(or equivalent) if supported in your setup.
Journal what still requires network (usually nothing if all paths realized).
Lab 5 — Teach-back
- Why
flake.lockis mandatory in git
- Difference between
packagesanddevShells
- What
nix developuses by default
- Why registry
nixpkgs#≠ your flake’s pinned nixpkgs
Common gotchas
| Symptom | Theory |
|---|---|
| File not found in flake eval | Not git-added |
| Wrong system attr | devShells.<system> mismatch |
nix develop empty |
No default shell |
| Divergent tools across machines | Lock not committed / not pulled |
| Huge shell closure | Too many packages; split shells later |
| Dirty tree warnings | Commit or understand copy semantics |
Checkpoint
flake.nixwith nixpkgs 26.05 input
flake.lockcommitted
nix developprovides ≥2 real tools
nix flake showlists devShell
- README explains entry
- Understand lock vs registry
Commit
cd ~/lab/90daysofx/02-nixos/day08/myproject
git add .
git commit -m "day08: first project flake devShell lock"Tomorrow
Day 9 — Classic CLI literacy. Map old nix-build / nix-shell / channels onto modern commands once so old docs stop scaring you.