stdenv and mkDerivation
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.
what stdenv is
stdenv is a derivation itself. it lives in the store like any other package. its output contains:
- a C/C++ compiler (gcc on linux, clang on Darwin)
- core unix tools (coreutils, findutils, sed, grep, awk, tar)
- make, patch, and binutils
- a setup script (
setup.sh) that defines the build phases
when 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.
how mkDerivation works
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:
nameis computed frompnameandversion:"curl-8.18.0"builderis set tostdenv.shell(bash)argsis set to[ "-e" source-stdenv.sh defaultBuilder ].source-stdenv.shsources$stdenv/setup, thendefaultBuildercallsgenericBuildbuildInputspaths are joined into a space-separated string in the env var. the setup script splits this and adds theirbin/toPATH, theirlib/to linker paths, theirinclude/to compiler include pathsnativeBuildInputsworks the same way but for tools that run on the build machine. whendoCheck = true,checkInputsget merged intonativeBuildInputsautomatically- every other attribute becomes an environment variable. strings pass through. paths get copied to the store. lists are joined with spaces.
truebecomes"1",falseandnullbecome""
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 still comes from builtins.derivation. mkDerivation is convenience, not a separate build model.
buildInputs vs nativeBuildInputs
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 setup script
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:
- sources
setup-hooksfrom all declared dependencies (packages can inject behavior into the build) - constructs
PATH,PKG_CONFIG_PATH,CMAKE_PREFIX_PATH, etc. from dependency outputs - defines the phase functions:
unpackPhase,configurePhase,buildPhase,installPhase,fixupPhase - defines
genericBuild, which calls each phase in order
genericBuild is the entry point. it runs the phases sequentially. each phase has a default implementation and can be overridden.
overriding
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.
genericBuild actually runs. each phase is a shell function with hooks you can tap into.