stdenv and mkDerivation
the standard build environment and how most packages are built.
the standard build environment and how most packages are built.
builtins.derivation gives you nothing. no compiler, no mkdir, no cp. you provide everything yourself.
stdenv.mkDerivation sets up a full build environment.
stdenv is a derivation itself. it lives in the store like any other package. its output contains:
setup.sh) that defines the build phaseswhen you write stdenv.mkDerivation { ... }, nix adds stdenv to inputDrvs. the builder script sources setup.sh from stdenv's output, which sets up PATH from all your declared dependencies and runs through the build phases.
mkDerivation takes your attribute set (or a function, for the finalAttrs self-referencing pattern) and transforms it before calling builtins.derivation:
stdenv.mkDerivation {
pname = "curl";
version = "8.18.0";
src = fetchurl { ... };
buildInputs = [ openssl zlib ];
nativeBuildInputs = [ pkg-config ];
configureFlags = [ "--with-openssl" ];
}
what happens:
name is computed from pname and version: "curl-8.18.0"builder is set to stdenv.shell (bash)args is set to [ "-e" source-stdenv.sh defaultBuilder ]. source-stdenv.sh sources $stdenv/setup, then defaultBuilder calls genericBuildbuildInputs paths are joined into a space-separated string in the env var. the setup script splits this and adds their bin/ to PATH, their lib/ to linker paths, their include/ to compiler include pathsnativeBuildInputs works the same way but for tools that run on the build machine. when doCheck = true, checkInputs get merged into nativeBuildInputs automaticallytrue becomes "1", false and null become ""two attributes are special: meta (license, description, maintainers) and passthru (extra attributes like tests and updateScript) are stripped before calling builtins.derivation. they do not become env vars, do not appear in the .drv, and do not affect the output hash. changing a package's description does not trigger a rebuild.
mkDerivation also sets NIX_HARDENING_ENABLE with a default set of compiler hardening flags: stack protector, fortify source, position-independent code, RELRO, and more. these are on by default for all packages. hardeningDisable = [ "all" ] turns them off.
the resulting .drv has builtins.derivation at the bottom. mkDerivation is convenience, not magic.
for native builds (not cross-compiling), the distinction barely matters. both end up on PATH.
for cross-compilation, it is the difference between working and broken:
nativeBuildInputs: tools that run during the build. compilers, code generators, pkg-config. these run on the build machinebuildInputs: libraries the output links against. these are for the target architecture{
nativeBuildInputs = [ pkg-config cmake ]; # run at build time
buildInputs = [ openssl zlib ]; # linked into the output
}
if you put a build tool in buildInputs during cross-compilation, nix provides the target-architecture binary. it cannot execute on your build machine. the build fails.
the args are two scripts. source-stdenv.sh runs first:
source $stdenv/setup
source $1 # the actual builder
and default-builder.sh is just:
genericBuild
source $stdenv/setup loads the setup script from stdenv's store path. this script:
setup-hooks from all declared dependencies (packages can inject behavior into the build)PATH, PKG_CONFIG_PATH, CMAKE_PREFIX_PATH, etc. from dependency outputsunpackPhase, configurePhase, buildPhase, installPhase, fixupPhasegenericBuild, which calls each phase in ordergenericBuild is the entry point. it runs the phases sequentially. each phase has a default implementation and can be overridden.
any attribute in mkDerivation can be overridden after the fact:
hello.overrideAttrs (old: {
configureFlags = (old.configureFlags or []) ++ [ "--disable-nls" ];
})
overrideAttrs takes a function from the old attributes to a set of overrides. the override is merged (shallow //) with the original. the result is a new derivation with a new hash.
the original derivation is untouched. laziness means the original attributes are thunks that never get forced if the override replaces them.
build phases are what genericBuild actually runs. each phase is a shell function with hooks you can tap into.