The Derivation Build Process
The Derivation Build Process
stdenv.mkDerivation is how Unix software becomes a store path. The boring default is: keep the standard phases, override only the ones you must, and let stdenv set $CC, $out, RPATH, and the sandbox.
Raw derivation { } (fundamentals) is the primitive. This chapter is what you write for real C.
Mental model
| Phase | Default |
|---|---|
unpackPhase |
Unpack $src |
patchPhase |
patch -p1 each of patches |
configurePhase |
./configure --prefix=$out if that script exists |
buildPhase |
make |
checkPhase |
make check if doCheck = true |
installPhase |
make install |
fixupPhase |
patchelf RPATH, strip, gzip man pages |
Skip a phase with dontConfigure = true; (or dontBuild, dontInstall — rarely). Override a phase with buildPhase = ''…'';. Always mkdir -p $out/bin before cp if you write installPhase by hand.
$CC is the stdenv compiler (gcc or clang), already wrapped with hardening flags. Do not call gcc by name.
nativeBuildInputs run on the build machine (make, pkg-config, $CC). buildInputs are linked into the output (zlib, openssl). Mixing them is why a cross build links host openssl. strictDeps = true; makes that mix an eval/build error — turn it on for new C packages.
enableParallelBuilding = true; (and enableParallelChecking) is make -j. Default is often already on in 26.05 stdenv; if a Makefile is racy, set it false and say why.
Custom phases must still fire hooks:
buildPhase = ''
runHook preBuild
$CC -O2 -o desk-status-c main.c
runHook postBuild
'';Skipping runHook drops preBuild from nativeBuildInputs (code generators, autoreconfHook).
$src → unpack → patch → configure → build → check → install → fixup → $out
Worked examples
Case 1: One C file, no autotools
Save as c_tool.nix:
# c_tool.nix
{ lib, stdenv }:
stdenv.mkDerivation {
pname = "desk-status-c";
version = "1.0";
src = ./.;
dontConfigure = true;
buildPhase = ''
cat > main.c << 'EOF'
#include <stdio.h>
int main(void) {
puts("Desk C worker active");
return 0;
}
EOF
$CC -O2 -o desk-status-c main.c
'';
installPhase = ''
mkdir -p $out/bin
cp desk-status-c $out/bin/
'';
meta = {
description = "Compiled C desk status check";
license = lib.licenses.mit;
};
}Save as default.nix:
# default.nix
{ pkgs ? import <nixpkgs> { } }:
pkgs.callPackage ./c_tool.nix { }nix-build default.nix
./result/bin/desk-status-cOutput:
Desk C worker active
Case 2: doCheck
# in c_tool.nix
doCheck = true;
checkPhase = ''
./desk-status-c
'';checkPhase runs before installPhase, in the build directory. The binary is still ./desk-status-c, not $out/bin.
nix-build default.nixA failing check fails the derivation. Good.
Case 3: $src as a real tree
Put main.c in git and drop the cat from buildPhase:
stdenv.mkDerivation {
pname = "desk-status-c";
version = "1.0";
src = ./src;
dontConfigure = true;
buildPhase = ''
$CC -O2 -o desk-status-c main.c
'';
installPhase = ''
mkdir -p $out/bin
cp desk-status-c $out/bin/
'';
}Filter src (source-filtering chapter) so .git is not an input.
Case 4: Inspect the wrapped compiler
nix-build default.nix
nix-store -q --references resultYou should see glibc (or musl), not gcc. gcc is a build-time input; fixupPhase did not copy it into the runtime closure.
readelf -d result/bin/desk-status-c | grep PATHRUNPATH points at store libc. That is stdenv, not you.
Case 5: outputs (preview)
outputs = [ "out" "dev" ];Multiple outputs split runtime (out) from headers (dev). Skip until a closure-size measurement says headers are the problem. One $out is the desk default.
The trap
The trap is installPhase without mkdir -p $out/bin. cp: cannot create … No such file. The build log is long; the last line is the one that matters.
The other trap is gcc main.c instead of $CC. Cross and clang stdenvs then break. $CC is the contract.
The boring rule
- Standard phases.
dontConfigurewhen there is no./configure. $CC, notgcc.$out, not/usr.nativeBuildInputsvsbuildInputs.strictDepson new C.- Custom phases call
runHook preBuild/postInstall. mkdir -pbeforecpin a custominstallPhase.doCheck = truewhen tests exist and are hermetic.- Language builders (
buildGoModule,buildRustPackage) wrap this. Use them when they exist.
Try this
- Add Case 2;
nix-build; thencheckPhase = "false";and confirm failure. - Replace
$CCwithgcc; cross later will hurt — put$CCback. nix path-info -Shr resultvsnix-store -q --deriver resultreferences — runtime vs build.- Forget
mkdir; save the error; restore.