The Derivation Primitive
The Derivation Primitive
Every package, every system, every mkShell eventually becomes a derivation { ... }. The boring default is: know the three required fields, write one by hand once, then use pkgs.runCommand / stdenv.mkDerivation for the rest of your career.
Mental model
derivation is a builtin. It does not compile C. It produces a .drv file describing a build:
| Field | Required | Meaning |
|---|---|---|
name |
yes | Label in the store path |
system |
yes | "x86_64-linux", "aarch64-linux", "aarch64-darwin", … |
builder |
yes | Store path of the executable that will run |
args |
no | Arguments to the builder |
| other attrs | no | Become environment variables inside the sandbox |
$out is set by Nix to the output path before the builder runs. The builder’s job is to create that file or directory. If $out is missing when the builder exits 0, the build fails.
builder = "/bin/sh" is a lie on a sandboxed Linux: /bin/sh is not in the sandbox. Point builder at ${pkgs.bash}/bin/bash.
derivation { … }
│
▼
.drv (instantiate)
│
▼
sandbox runs builder
│
▼
$out registered in the store
Worked examples
Case 2: Instantiate versus build
nix-instantiate minimal.nix
cat $(nix-instantiate minimal.nix) | headThe .drv is text. You will see builder, system, and name. nix-build is instantiate + realise.
Case 3: Environment attributes
Save as env.nix:
# env.nix
let
pkgs = import <nixpkgs> {};
in
derivation {
name = "desk-env";
system = builtins.currentSystem;
builder = "${pkgs.bash}/bin/bash";
deskWindow = "A";
args = [ "-c" "echo window=$deskWindow > $out" ];
}nix-build env.nix && cat resultOutput:
window=A
Every extra attribute is an environment variable. Nested attribute sets are not allowed here (the builtin is strict about types). That is one reason stdenv.mkDerivation exists: it accepts a richer attrset and flattens it.
Case 4: The runCommand spelling you will actually write
Save as run.nix:
# run.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.runCommand "desk-banner" { } ''
echo 'Desk System Online' > $out
''nix-build run.nix && cat resultSame $out idea. runCommand fills in builder, system, and stdenv so you do not hard-code bash. Prefer this in real files. Use raw derivation when you are explaining Nix, not when you are packaging the desk API.
Case 5: Failure when $out is missing
Save as missing.nix:
# missing.nix
let
pkgs = import <nixpkgs> {};
in
derivation {
name = "desk-missing";
system = builtins.currentSystem;
builder = "${pkgs.bash}/bin/bash";
args = [ "-c" "echo forgot" ];
}nix-build missing.nixOutput:
building '/nix/store/…-desk-missing.drv'...
forgot
error: builder for '/nix/store/…-desk-missing.drv' failed to produce output path '/nix/store/…-desk-missing'
The builder exited 0. Nix still failed. $out is a contract.
builtins.currentSystem is impure. It is fine in this chapter’s throwaway files. A flake must name system (x86_64-linux, …) so CI and the laptop agree. passAsFile exists when an env var would exceed ARG_MAX; stdenv.mkDerivation uses it so you do not think about it. Multiple outputs (outputs = [ "out" "dev" ]) split runtime vs headers — hello does not need that; glibc does.
The trap
The trap is builder = "/bin/sh" copied from an ancient gist. It works only with the sandbox off, or on some macOS setups with a magic /bin/sh. On NixOS it fails with No such file or directory. Use ${pkgs.bash}/bin/bash or runCommand.
The other trap is writing to $out and then also expecting a directory layout without mkdir. If $out should be a directory, mkdir -p $out first; do not echo into $out and then mkdir $out/bin.
The boring rule
- Raw
derivationonce, so the.drvis not magic. ThenrunCommand/mkDerivation. builderis a store path, not/bin/sh.- Create
$out. That is the whole job. - Extra attributes become env vars. Keep them strings, paths, or lists of those.
- Flakes name
system; do not shipbuiltins.currentSystem. resultis a GC root. Delete it when you are done poking.
Try this
- Change the
echotext inminimal.nix, rebuild, and confirm the store path hash changed. nix-instantiate minimal.nixand search the.drvforbash. It must be a/nix/store/…-bash-…path.- Rewrite
minimal.nixasrunCommand(Case 4) anddiffthe twocat resultoutputs — they should match. - In
env.nix, adddeskShift = "morning";and print both variables. Confirm a rebuild produced a new path.