build phases

unpack, configure, build, install, fixup - the lifecycle of a nix build.

genericBuild runs a sequence of shell functions called phases. each has a default implementation, pre/post hooks, and can be replaced entirely.

genericBuild
phases
stdenv build lifecycle
unpackPhase
extracts $src into a build directory. tar, zip, git checkouts.
hooks: preUnpackpostUnpack
patchPhase
configurePhase
buildPhase
checkPhase
installPhase
fixupPhase
installCheckPhase
extracting source archive into build directory.

unpackPhase

extracts the source. nix looks at the file extension and picks the right tool: tar for .tar.*, unzip for .zip, cp -r for directories. the result is a directory named after the source.

src = fetchurl {
  url = "https://curl.se/download/curl-8.18.0.tar.xz";
  sha256 = "...";
};

after unpacking, nix cds into the source directory. if there are multiple source directories, set sourceRoot to tell it which one.

patchPhase

applies patches in order:

patches = [
  ./fix-build.patch
  (fetchpatch {
    url = "https://github.com/curl/curl/commit/abc123.patch";
    sha256 = "...";
  })
];

each patch is applied with patch -p1. the phase runs after unpack and before configure, so patches can fix anything in the source tree: broken configure scripts, hardcoded paths, missing includes.

postPatch is the most common hook in nixpkgs. it runs arbitrary shell after patches are applied:

postPatch = ''
  substituteInPlace Makefile --replace-fail "/usr/local" "$out"
'';

substituteInPlace is a nix-provided helper. with --replace-fail, it does what sed -i does but fails the build if the string is not found. you want that. a silent sed that matches nothing is a silent bug. (the older --replace flag only warns.)

configurePhase

runs ./configure if the file exists. nix passes --prefix=$out so the build installs into the store path, not /usr/local. it also adds --disable-dependency-tracking (autoconf rebuilds are useless in a sandbox) and --disable-static (static archives are rarely wanted alongside shared libs). set dontDisableStatic = true or dontAddDisableDepTrack = true to override.

configureFlags = [
  "--with-openssl=${openssl.dev}"
  "--disable-ldap"
];

flags are appended to the configure command. paths to dependencies are full store paths. no pkg-config guessing, no -L/usr/lib pointing at the wrong library. the path is exact.

for cmake projects, set cmakeFlags. for meson, mesonFlags. stdenv detects the build system and dispatches accordingly (if you use cmake or meson in nativeBuildInputs).

when there is no configure script

some projects use plain Makefiles with no autoconf. set configurePhase to skip it:

configurePhase = "true";

or if you need to generate a config file:

configurePhase = ''
  cat > config.mk <<EOF
  PREFIX=$out
  CC=gcc
  EOF
'';

replacing a phase replaces it entirely. the default is gone. hooks still run.

buildPhase

runs make by default. parallel builds are not automatic; set enableParallelBuilding = true and nix passes -j$NIX_BUILD_CORES.

makeFlags = [ "CC=gcc" "PREFIX=$(out)" ];

$(out) in makeFlags is expanded by make, not by the shell. use $out in shell strings, $(out) in make variables.

most of the time you do not touch buildPhase. the defaults work for autotools and cmake projects. when they do not:

buildPhase = ''
  runHook preBuild
  cargo build --release
  runHook postBuild
'';

runHook preBuild / runHook postBuild calls the pre/post hooks. if you replace a phase, call these yourself. other packages might inject behavior through setup hooks that expect them to run.

checkPhase

runs make check or make test. skipped by default. enable it with:

doCheck = true;

why off by default? some test suites need network access, running services, or resources that do not exist in the sandbox. enabling tests per-package is a deliberate choice. when tests pass in the sandbox, they pass everywhere.

checkFlags = [ "VERBOSE=1" ];
preCheck = ''
  export HOME=$TMPDIR
'';

some test suites write to $HOME. the sandbox sets HOME=/homeless-shelter, which does not exist. pointing it at $TMPDIR is a common fix.

installPhase

runs make install. the output goes to $out because configure set --prefix=$out.

for projects without a make install target:

installPhase = ''
  runHook preInstall
  mkdir -p $out/bin
  cp hello $out/bin/
  runHook postInstall
'';

the install phase is where your output takes shape. after this, $out contains whatever the package ships: binaries, libraries, headers, data files.

fixupPhase

the cleanup pass. runs automatically after install. you rarely override it, but it does real work:

strip: removes debug symbols from binaries and libraries. saves space. dontStrip = true to keep them.

patchelf (linux): rewrites RPATHs in ELF binaries to point at store paths. this is how curl finds libssl.so at /nix/store/...-openssl-3.6.1/lib/libssl.so instead of searching /usr/lib. the binary carries its own dependency paths.

install_name_tool (macOS): same idea, different tool. rewrites dylib load paths.

wrap scripts: if a binary needs specific environment variables at runtime, fixup can generate a wrapper script that sets them before exec.

this is the phase that connects the sandbox to the reference scanner . patchelf writes store paths into the binary. the reference scanner finds them. the closure is computed. the dependency chain is sealed.

the full sequence

genericBuild:
  $prePhases
  unpackPhase
  patchPhase
  configurePhase
  buildPhase
  checkPhase
  installPhase
  fixupPhase
  installCheckPhase
  distPhase
  $postPhases

$prePhases and $postPhases let you inject custom phases at the start or end. distPhase runs make dist for source distributions; almost nobody uses it. the ones that matter are unpack through fixup.

each phase is a shell function. replace it by setting the attribute to a string. hook into it with preBuild, postInstall, etc. the whole system is shell functions called in order.

setup hooks

dependencies can inject behavior into the build. a package that provides a setup hook has a file at $out/nix-support/setup-hook that gets sourced during the setup phase.

pkg-config adds itself to PKG_CONFIG_PATH. cmake adds its prefix path. python adds its site-packages. these hooks run automatically when the package appears in buildInputs or nativeBuildInputs.

you do not call them. you do not configure them. you add the package to your inputs and the hook fires. this is how mkDerivation stays simple while supporting dozens of build systems.

fixed-output derivations are the exception to everything you have seen so far. fetchers need network access, so they play by different rules.