build phases
unpack, configure, build, install, fixup - the lifecycle of a nix build.
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.
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.
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.)
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).
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.
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.
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.
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.
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 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.
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.
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.