The Derivation Primitive

Updated

September 12, 2026

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 1: A one-file banner

Save as minimal.nix:

# minimal.nix
let
  pkgs = import <nixpkgs> {};
in
derivation {
  name = "desk-banner";
  system = builtins.currentSystem;
  builder = "${pkgs.bash}/bin/bash";
  args = [
    "-c"
    "echo 'Desk System Online' > $out"
  ];
}
nix-build minimal.nix
cat result

Output:

these 1 derivations will be built:
  /nix/store/…-desk-banner.drv
building '/nix/store/…-desk-banner.drv'...
/nix/store/…-desk-banner
Desk System Online

result is a GC root pointing at $out.

Case 2: Instantiate versus build

nix-instantiate minimal.nix
cat $(nix-instantiate minimal.nix) | head

The .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 result

Output:

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 result

Same $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.nix

Output:

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 derivation once, so the .drv is not magic. Then runCommand / mkDerivation.
  • builder is 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 ship builtins.currentSystem.
  • result is a GC root. Delete it when you are done poking.

Try this

  1. Change the echo text in minimal.nix, rebuild, and confirm the store path hash changed.
  2. nix-instantiate minimal.nix and search the .drv for bash. It must be a /nix/store/…-bash-… path.
  3. Rewrite minimal.nix as runCommand (Case 4) and diff the two cat result outputs — they should match.
  4. In env.nix, add deskShift = "morning"; and print both variables. Confirm a rebuild produced a new path.