Standard Derivation

builtins.derivation provides the minimal functionality to define a Nix derivation. However all dependencies have to be manually managed, which can be pretty cumbersome. In practice, most Nix derivations are built on top of stdenv.mkDerivation, which provide many battery-included functionalities that helps make defining derivations easy.

The tradeoff is that stdenv.mkDerivation is much more complex than builtins.derivation. With the detour to understand builtins.derivation first, we can hopefully have an easier time understanding stdenv.mkDerivation

Inspecting Build Environment

We can inspect the standard environment in similar way.

nix-repl> nixpkgs.stdenv.mkDerivation {
            name = "inspect";
            unpackPhase = "true";

            buildPhase = ''
              set -x
              ls -la .
              ls -la /
              env
              set +x
            '';

            installPhase = "touch $out";
          }
«derivation /nix/store/vdyp9cxs0li87app03vm8zbxmq0lhw5l-inspect.drv»
$ nix-build /nix/store/vdyp9cxs0li87app03vm8zbxmq0lhw5l-inspect.drv
these derivations will be built:
  /nix/store/vdyp9cxs0li87app03vm8zbxmq0lhw5l-inspect.drv
building '/nix/store/vdyp9cxs0li87app03vm8zbxmq0lhw5l-inspect.drv'...
unpacking sources
patching sources
configuring
no configure script, doing nothing
building
++ ls -la .
total 16
drwx------ 2 nixbld nixbld 4096 Nov 30 19:42 .
drwxr-x--- 9 nixbld nixbld 4096 Nov 30 19:42 ..
-rw-r--r-- 1 nixbld nixbld 5013 Nov 30 19:42 env-vars
++ ls -la /
total 32
drwxr-x---   9 nixbld nixbld  4096 Nov 30 19:42 .
drwxr-x---   9 nixbld nixbld  4096 Nov 30 19:42 ..
drwxr-xr-x   2 nixbld nixbld  4096 Nov 30 19:42 bin
drwx------   2 nixbld nixbld  4096 Nov 30 19:42 build
drwxr-xr-x   4 nixbld nixbld  4096 Nov 30 19:42 dev
drwxr-xr-x   2 nixbld nixbld  4096 Nov 30 19:42 etc
drwxr-xr-x   3 nixbld nixbld  4096 Nov 30 19:42 nix
dr-xr-xr-x 405 nobody nogroup    0 Nov 30 19:42 proc
drwxrwxrwt   2 nixbld nixbld  4096 Nov 30 19:42 tmp
++ env
...
unpackPhase=true
propagatedBuildInputs=
stdenv=/nix/store/ajq5dfwn4hzlx1qf2xxwb6rj8a7s65nm-stdenv-linux
TZ=UTC
OLDPWD=/build
out=/nix/store/a226brzfy71vr6vkfy4m188qs9f7k7g7-inspect
CONFIG_SHELL=/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash
buildInputs=
builder=/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash
...
buildPhase=set -x
ls -la .
ls -la /
env
set +x

PATH=/nix/store/cr86kfhzfwa558mzav4rnfkbz00hw27w-patchelf-0.12/bin:/nix/store/ppfvi0cfcpdr83klw5kx6si2l260n1gh-gcc-wrapper-9.3.0/bin:...
NIX_BUILD_TOP=/build
depsBuildTargetPropagated=
NIX_ENFORCE_PURITY=1
SIZE=size
nativeBuildInputs=
LD=ld
patches=
depsTargetTargetPropagated=
_=/nix/store/2shqhfsyzz4rnfyysbzgyp5kbfk29750-coreutils-8.32/bin/env
++ set +x
installing
post-installation fixup
shrinking RPATHs of ELF executables and libraries in /nix/store/a226brzfy71vr6vkfy4m188qs9f7k7g7-inspect
strip is /nix/store/bnjps68g8ax6abzvys2xpx12imrx8949-binutils-2.31.1/bin/strip
patching script interpreter paths in /nix/store/a226brzfy71vr6vkfy4m188qs9f7k7g7-inspect
checking for references to /build/ in /nix/store/a226brzfy71vr6vkfy4m188qs9f7k7g7-inspect...
/nix/store/a226brzfy71vr6vkfy4m188qs9f7k7g7-inspect

As we can see, our standard environment is quite more complicated than the minimal environment provided by builtins.derivation. We also have a number of executables added to $PATH, which we can use without specifying them as dependencies.

Capturing the Build Environment

nix-repl> nixpkgs.stdenv.mkDerivation {
            name = "env";
            unpackPhase = "true";
            installPhase = "env > $out";
          }
«derivation /nix/store/5rgcvwndbc4525ypbb0r1vgqpbxgcy2g-env.drv»
$ cat $(nix-build /nix/store/5rgcvwndbc4525ypbb0r1vgqpbxgcy2g-env.drv)
...
unpackPhase=true
propagatedBuildInputs=
stdenv=/nix/store/ajq5dfwn4hzlx1qf2xxwb6rj8a7s65nm-stdenv-linux
TZ=UTC
OLDPWD=/build
out=/nix/store/rkjhcjhdj6ba7r7n7fasq8gmzxi5hk72-env
CONFIG_SHELL=/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash
buildInputs=
builder=/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash
...

Build Inputs

stdenv.mkDerivation also provides a convenient way of adding dependencies to appropriate environment variables with the buildInputs attribute.

nix-repl> nixpkgs.stdenv.mkDerivation {
            name = "greet-alice";
            buildInputs = [ greet ];

            unpackPhase = "true";
            installPhase = "greet Alice > $out";
          }
«derivation /nix/store/in40c5fl13ziqzds3wfg2ag7ax2xmq5l-greet-alice.drv»
$ nix-build /nix/store/in40c5fl13ziqzds3wfg2ag7ax2xmq5l-greet-alice.drv
these derivations will be built:
  /nix/store/in40c5fl13ziqzds3wfg2ag7ax2xmq5l-greet-alice.drv
building '/nix/store/in40c5fl13ziqzds3wfg2ag7ax2xmq5l-greet-alice.drv'...
...
/nix/store/kp32rzq63barqa55q3mf761gsggi2bq6-greet-alice

$ cat /nix/store/kp32rzq63barqa55q3mf761gsggi2bq6-greet-alice
Hello, Alice!

We can check that greet is added to $PATH using Nix shell:

$ drv=/nix/store/in40c5fl13ziqzds3wfg2ag7ax2xmq5l-greet-alice.drv

$ nix-shell $drv --pure --run "command -v greet"
/nix/store/l6xy4qjr8x3ni16skfilw0fvnda13szq-greet/bin/greet

$ nix-shell $drv --pure --run 'echo $PATH' | tr ':' '\n'
...
/nix/store/2shqhfsyzz4rnfyysbzgyp5kbfk29750-coreutils-8.32/bin
/nix/store/l6xy4qjr8x3ni16skfilw0fvnda13szq-greet/bin
...

stdenv also adds the build inputs to other variables.

$ nix-shell $drv --pure --run 'echo $NIX_LDFLAGS'
-rpath /nix/store/kp32rzq63barqa55q3mf761gsggi2bq6-greet-alice/lib64 -rpath /nix/store/kp32rzq63barqa55q3mf761gsggi2bq6-greet-alice/lib

Note that the paths /nix/store/kp32rzq63barqa55q3mf761gsggi2bq6-greet-alice/lib does not exist, but stdenv still sets the variables anyway.

Stdenv Script

How do stdenv.mkDerivation do the magic compared to builtins.derivation? We can find out by first inspecting the derivation:

$ nix show-derivation $drv
{
  "/nix/store/in40c5fl13ziqzds3wfg2ag7ax2xmq5l-greet-alice.drv": {
    ...
    "builder": "/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash",
    "args": [
      "-e",
      "/nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh"
    ],
    "env": {
      "buildInputs": "/nix/store/l6xy4qjr8x3ni16skfilw0fvnda13szq-greet",
      "builder": "/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash",
      ...
      "installPhase": "greet Alice > $out",
      "name": "greet-alice",
      ...
      "stdenv": "/nix/store/ajq5dfwn4hzlx1qf2xxwb6rj8a7s65nm-stdenv-linux",
      ...
    }

  }
}

stdenv is also using bash as the builder, and have it evaluate the script at /nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh. Let's see what's inside there:

$ cat /nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh
source $stdenv/setup
genericBuild

So the magic is hidden inside $stdenv/setup, with the $stdenv variable set to /nix/store/ajq5dfwn4hzlx1qf2xxwb6rj8a7s65nm-stdenv-linux.

We can open it and see what's inside.

nix-shell $drv --run 'cat $stdenv/setup'

There are quite a lot of shell scripts happening. If we search through the script, we can see that environment variables such as buildInputs, buildPhase, and installPhase are being referred inside $stdenv/setup.

In other words, instead of having to figure how to setup various environment variables to work with various dependencies, $stdenv/setup provides a higher level abstraction of doing the setup for us. We just have to define the build inputs and steps that we need, and $stdenv/setup will fill in the missing pieces from us.

In fact, $stdenv/setup is also being sourced when we enter a Nix shell of a stdenv derivation. From the nix-shell manual:

The command nix-shell will build the dependencies of the specified derivation, but not the derivation itself. It will then start an interactive shell in which all environment variables defined by the derivation path have been set to their corresponding values, and the script $stdenv/setup has been sourced. This is useful for reproducing the environment of a derivation for development.

Deriving Environment at Build Time

One question we might ask is, why is stdenv doing the heavy lifting steps only at build time inside a shell script. We could as well parse the dependencies inside Nix at evaluation time, and produce a derivation with everything setup already.

However recall from the previous example of fib-serialized.nix. If we try to peek into the content of a dependency derivation, that would instead become an evaluation time dependency. If stdenv is looking into the content of all dependencies inside Nix, then we can only know how to build the derivation after all dependencies have been built.

Instead, stdenv avoids this to allow the derivation dependencies to be built in parallel by Nix. With that, we can only read the content of our dependencies at build time, which happens inside the build script.