Derivations_explained

Introduction to Nix Derivations

gruv10
  • A derivation in Nix is a fundamental concept that describes how to build a piece of software or a resource (e.g., a package, library, or configuration file). Think of it as a recipe for creating something within the Nix ecosystem.

  • For beginners, the analogy of a cooking recipe is helpful:

    • Ingredients (Dependencies): What other software or libraries are needed.
    • Steps (Build Instructions): The commands to compile, configure, and install.
    • Final Dish (Output): The resulting package or resource.
  • A Nix derivation encapsulates all this information, telling Nix what inputs to use, how to build it, and what the final output should be.

Creating Derivations in Nix

  • The primary way to define packages in Nix is through the mkDerivation function, which is part of the standard environment (stdenv). While a lower-level derivation function exists for advanced use cases, mkDerivation simplifies the process by automatically managing dependencies and the build environment.

  • mkDerivation (and derivation) takes a set of attributes as its argument. At a minimum, you’ll often encounter these essential attributes:

    1. name: A human-readable identifier for the derivation (e.g., “foo”, “hello.txt”). This helps you and Nix refer to the package.
    2. system: Specifies the target architecture for the build (e.g., builtins.currentSystem for your current machine).
    3. builder: Defines the program that will execute the build instructions (e.g., bash).

Our First Simple Derivation: Understanding the Builder

  • To understand how derivations work, let’s create a very basic example using a bash script as our builder.

Why a Builder Script?

  • The builder attribute in a derivation tells Nix how to perform the build steps. A simple and common way to define these steps is with a bash script.

The Challenge with Shebangs in Nix

  • In typical Unix-like systems, you might start a bash script with a shebang (#!/bin/bash or #!/usr/bin/env bash) to tell the system how to execute it. However, in Nix derivations, we generally avoid this.

  • Reason: Nix builds happen in an isolated environment where the exact path to common tools like bash isn’t known beforehand (it resides within the Nix store). Hardcoding a path or relying on the system’s PATH would break Nix’s stateless property.

The Importance of Statelessness in Nix

  • Stateful Systems (Traditional): When you install software traditionally, it often modifies the core system environment directly. This can lead to dependency conflicts and makes rollbacks difficult.

  • Stateless Systems (Nix): Nix takes a different approach. When installing a package, it creates a unique, immutable directory in the Nix store. This means:

    • No Conflicts: Different versions of the same package can coexist without interfering with each other.
    • Reliable Rollback: You can easily switch back to previous versions without affecting system-wide files.
    • Reproducibility: Builds are more likely to produce the same result across different machines if they are “pure” (don’t rely on external system state).

Our builder Script

  • For our first derivation, we’ll create a simple builder.sh file in the current directory:
# builder.sh
declare -xp
echo foo > $out
  • The command declare -xp lists exported variables (it’s a bash builtin function).

  • Nix needs to know where the final built product (the “cake” in our earlier analogy) should be placed. So, during the derivation process, Nix calculates a unique output path within the Nix store. This path is then made available to our builder script as an environment variable named $out. The .drv file, which is the recipe, contains instructions for the builder, including setting up this $out variable. Our builder script will then put the result of its work (in this case, the “foo” file) into this specific $out directory.

  • As mentioned earlier we need to find the nix store path to the bash executable, common way to do this is to load Nixpkgs into the repl and check:

nix-repl> :l <nixpkgs>
Added 3950 variables.
nix-repl> "${bash}"
"/nix/store/ihmkc7z2wqk3bbipfnlh0yjrlfkkgnv6-bash-4.2-p45"

So, with this little trick we are able to refer to bin/bash and create our derivation:

nix-repl> d = derivation { name = "foo"; builder = "${bash}/bin/bash";
 args = [ ./builder.sh ]; system = builtins.currentSystem; }
nix-repl> :b d
[1 built, 0.0 MiB DL]

this derivation produced the following outputs:
  out -> /nix/store/gczb4qrag22harvv693wwnflqy7lx5pb-foo
  • Boom! The contents of /nix/store/w024zci0x1hh1wj6gjq0jagkc1sgrf5r-foo is really foo! We’ve built our first derivation.

  • Derivations are the primitive that Nix uses to define packages. “Package” is a loosely defined term, but a derivation is simply the result of calling builtins.derivation.

Our Second Derivation

The following is a simple hello-drv derivation:

nix-repl> hello-drv = nixpkgs.stdenv.mkDerivation {
            name = "hello.txt";
            unpackPhase = "true";
            installPhase = ''
              echo -n "Hello World!" > $out
            '';
          }

nix-repl> hello-drv
«derivation /nix/store/ad6c51ia15p9arjmvvqkn9fys9sf1kdw-hello.txt.drv»
  • Derivations have a .drv suffix, as you can see the result of calling hello-drv is the nix store path to a derivation.