Chapter 7
- Chapter 7
Introduction to Nix Derivations
Nix's build instructions, known as derivations, are defined using the Nix Language. These derivations can describe anything from individual software packages to complete system configurations. The Nix package manager then deterministically "realizes" (builds) these derivations, ensuring consistency because they rely solely on a predefined set of inputs.
-
Most things in NixOS are built around derivations:
- The system configuration (i.e.
/run/current-system
) is a derivation
- The system configuration (i.e.
In NixOS, your entire operating system configuration—including the kernel, system services, installed applications, and their configurations—is defined as a single, overarching derivation. When you build and apply a new NixOS configuration, Nix creates this entire system as an atomic unit within the immutable Nix store.
The /run/current-system
path is a vital symbolic link that always points to
the specific, active version of your system in the Nix store. This gives NixOS
the ability to perform atomic upgrades and rollbacks; changing your system simply
involves building a new system derivation and updating this symlink to point to
the latest, desired version.
ls -lsah /run/current-system 0 lrwxrwxrwx 1 root root 85 May 23 12:11 /run/current-system -> /nix/store/cy2c0kxpjrl7ajlg9v3zh898mhj4dyjv-nixos-system-magic-25.11.20250520.2795c50
-
The
->
indicates a symlink and it's pointing to a store path which is the result of a derivation being built (the system closure) -
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.
-
Nix derivations run in pure, isolated environments, meaning they cannot access the internet during the build phase. This ensures that builds are reproducible -- they don't depend on external sources that might change over time.
- There are
Fixed-output-derivations
that allow fetching resources during the build process by explicitly specifying the expected hash upfront. Just keep this in mind that normal derivations don't have network access.
- There are
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-levelderivation
function exists for advanced use cases,mkDerivation
simplifies the process by automatically managing dependencies and the build environment. -
mkDerivation
(andderivation
) takes a set of attributes as its argument. At a minimum, you'll often encounter these essential attributes:-
name: A human-readable identifier for the derivation (e.g., "foo", "hello.txt"). This helps you and Nix refer to the package.
-
system: Specifies the target architecture for the build (e.g.,
builtins.currentSystem
for your current machine). -
builder: Defines the program that will execute the build instructions (e.g.,
bash
).
-
Our First fake derivation
nix-repl> :l <nixpkgs> # Makes Nixpkgs available for ${pkgs.bash} nix-repl> d = derivation { name = "myname"; builder = "${pkgs.bash}/bin/bash"; system = "mysystem"; } nix-repl> :b d [...] these derivations will be built: error: a 'mysystem' with features {} is required to build '/nix/store/fq6843vfzzbhy3s6iwcd0hm10l578883-myname.drv', but I am a 'x86_64-linux' with features {benchmark, big-parallel, kvm, nixos-test}
- The build failure is expected here due fake system and name attributes, (it didn't build the derivation but it did create a .drv file)
- The
:b
is anix repl
specific command to build a derivation.- To realise this outside of the
nix repl
you can usenix-store -r
:$ nix-store -r /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv
nix derivation show
: Pretty print the contents of a store derivation:$ nix derivation show /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv
-- Nix Pills
-
The above example shows the fundamental structure of a Nix derivation, how it's defined within the
nix-repl
, and the importance of correctly specifying attributes likesystem
. -
.drv
files are intermediate files that describe how to build a derivation; it's the bare minimum information.
When Derivations are Built
Nix doesn't build derivations during the evaluation of your Nix expressions. Instead, it processes your code in two main phases:
-
Evaluation/Instantiate Phase: This is when Nix parses and interprets your .nix expression. The result is a precise derivation description (often represented as a .drv file on disk), and the unique "out paths" where the final built products will go are calculated. No actual code is compiled or executed yet.
-
Realize/Build Phase: Only after a derivation has been fully described does Nix actually execute its build instructions. It first ensures all the derivation's inputs (dependencies) are built, then runs the builder script in an isolated environment, and places the resulting products into their designated "out paths" in the Nix store.
Produce a development shell from a derivation
Building on the concept of a derivation as a recipe, let's create our first practical derivation. This example shows how to define a temporary development environment (a shell) using stdenv.mkDerivation, which is the primary function for defining packages in Nix.
# my-shell.nix
# We use a `let` expression to bring `pkgs` and `stdenv` into scope.
# This is a recommended practice over `with import <nixpkgs> {}`
# for clarity and to avoid potential name collisions.
let
pkgs = import <nixpkgs> {};
stdenv = pkgs.stdenv; # Access stdenv from the imported nixpkgs
in
# Make a new "derivation" that represents our shell
stdenv.mkDerivation {
name = "my-environment";
# The packages in the `buildInputs` list will be added to the PATH in our shell
buildInputs = [
# cowsay is an arbitrary package
# see https://nixos.org/nixos/packages.html to search for more
pkgs.cowsay
pkgs.fortune
];
}
Usage
nix-shell my-shell.nix
fortune | cowsay
_________________________________________
/ "Lines that are parallel meet at \
| Infinity!" Euclid repeatedly, heatedly, |
| urged. |
| |
| Until he died, and so reached that |
| vicinity: in it he found that the |
| damned things diverged. |
| |
\ -- Piet Hein /
-----------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
- To exit type:
exit
This Nix expression defines a temporary development shell. Let's break it down:
-
pkgs = import <nixpkgs> {};
: Standard way to get access to all the packages and helper functions (i.e.nixpkgs.lib
) -
stdenv = pkgs.stdenv;
:stdenv
provides usmkDerivation
and is from thenixpkgs
collection. -
stdenv.mkDerivation { ... };
: This is the core function for creating packages.stdenv
provides a set of common build tools and conventions.
-
mkDerivation
takes an attribute set (a collection of key-value pairs) as its argument. -
name = "my-environment";
: This gives your derivation a human-readable name. -
buildInputs = [ pkgs.cowsay ];
: This is a list of dependencies that will be available in the build environment of this derivation (or in thePATH
if you enter the shell created by this derivation).pkgs.cowsay
refers to thecowsay
package from the importedpkgs
collection.
The command nix-instantiate --eval my-shell.nix
evaluates the Nix expression
in the file. It does not build the derivation. Instead, it returns the Nix value
that the expression evaluates to.
nix-instantiate --eval my-shell.nix
This value is a structured data type that encapsulates all the attributes (like
name
, system
, buildInputs
, etc.) required to build the derivation. Your
output shows this detailed internal representation of the derivation's "recipe"
as understood by Nix. This is useful for debugging and inspecting the
derivation's definition.
Our Second Derivation: Understanding the Builder
Understanding the Builder (Click to Expand)
- 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'sPATH
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).
-
The Isolated Nix Build Environment: A Quick Overview
When Nix executes a builder script, it sets up a highly controlled and pristine environment to ensure reproducibility and isolation. Here's what happens:
-
Fresh Start: Nix creates a temporary, empty directory for the build and makes it the current working directory.
-
Clean Environment: It completely clears the environment variables from your shell.
-
Controlled Inputs: Nix then populates the environment with only the variables essential for the build, such as:
-
$NIX_BUILD_TOP
: The path to the temporary build directory. -
$PATH
: Carefully set to include only the explicitbuildInputs
you've specified, preventing reliance on arbitrary system tools. -
$HOME
: Set to/homeless-shelter
to prevent programs from reading user-specific configuration files. -
Variables for each declared output (
$out
, etc.), indicating where the final results should be placed in the Nix store.
-
-
Execution & Logging: The builder script is run with its specified arguments. All its output (stdout/stderr) is captured in a log.
-
Clean Up & Registration: If successful, the temporary directory is removed. Nix then scans the build outputs for references to other store paths, ensuring all dependencies are correctly tracked for future use and garbage collection. Finally, it normalizes file permissions and timestamps in the output for consistent hashing.
This meticulous setup ensures that your builds are independent of the machine they run on and always produce the same result, given the same inputs.
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
-
The contents of the resulting store path (
/nix/store/...-foo
) now contain the filefoo
, as intended. We have successfully built a 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 Last Derivation
Create a new directory and a hello.nix
with the following contents:
# hello.nix
{
stdenv,
fetchzip,
}:
stdenv.mkDerivation {
pname = "hello";
version = "2.12.1";
src = fetchzip {
url = "https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz";
sha256 = "";
};
}
Save this file to hello.nix
and run nix-build
to observe the build failure:
- Click to expand output:
$ nix-build hello.nix
error: cannot evaluate a function that has an argument without a value ('stdenv')
Nix attempted to evaluate a function as a top level expression; in
this case it must have its arguments supplied either by default
values, or passed explicitly with '--arg' or '--argstr'. See
https://nix.dev/manual/nix/stable/language/constructs.html#functions.
at /home/nix-user/hello.nix:3:3:
2| {
3| stdenv,
| ^
4| fetchzip,
Problem: The expression in hello.nix
is a function, which only produces
it's intended output if it is passed the correct arguments.(i.e. stdenv
is
available from nixpkgs
so we need to import nixpkgs
before we can use
stdenv
):
The recommended way to do this is to create a default.nix
file in the same
directory as the hello.nix
with the following contents:
# default.nix
let
nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-24.05";
pkgs = import nixpkgs { config = {}; overlays = []; };
in
{
hello = pkgs.callPackage ./hello.nix { };
}
This allows you to run nix-build -A hello
to realize the derivation in hello.nix
,
similar to the current convention used in Nixpkgs:
- Click to expand Output:
nix-build -A hello
error: hash mismatch in fixed-output derivation '/nix/store/pd2kiyfa0c06giparlhd1k31bvllypbb-source.drv':
specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
got: sha256-1kJjhtlsAkpNB7f6tZEs+dbKd8z7KoNHyDHEJ0tmhnc=
error: 1 dependencies of derivation '/nix/store/b4mjwlv73nmiqgkdabsdjc4zq9gnma1l-hello-2.12.1.drv' failed to build
- Another way to do this is with nix-prefetch-url It is a utility to calculate the sha beforehand.
nix-prefetch-url https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz
path is '/nix/store/pa10z4ngm0g83kx9mssrqzz30s84vq7k-hello-2.12.1.tar.gz'
086vqwk2wl8zfs47sq2xpjc9k066ilmb8z6dn0q6ymwjzlm196cd
- When you use
nix-prefetch-url
, you get a Base32 hash when nix needs SRI format.
Run the following command to convert from Base32 to SRI:
nix hash to-sri --type sha256 086vqwk2wl8zfs47sq2xpjc9k066ilmb8z6dn0q6ymwjzlm196cd
sha256-jZkUKv2SV28wsM18tCqNxoCZmLxdYH2Idh9RLibH2yA=
-
This actually fetched a different sha than the Nix compiler returned in the example where we replace the empty sha with the one Nix gives us. The difference was that
fetchzip
automatically extracts archives before computing the hash and slight differences in the metadata cause different results. I had to switch fromfetchzip
tofetchurl
to get the correct results.-
Extracted archives can differ in timestamps, permissions, or compression details, causing different hash values.
-
A simple takeaway is to use
fetchurl
when you need an exact match, andfetchzip
when working with extracted contents. -
fetchurl
returns afixed-output derivation
(FOD): A derivation where a cryptographic hash of the output is determined in advance using the outputHash attribute, and where the builder executable has access to the network.
-
Lastly replace the empty sha256 placeholder with the returned value from the last command:
# hello.nix
{
stdenv,
fetchzip,
}:
stdenv.mkDerivation {
pname = "hello";
version = "2.12.1";
src = fetchzip {
url = "https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz";
sha256 = "sha256-1kJjhtlsAkpNB7f6tZEs+dbKd8z7KoNHyDHEJ0tmhnc=";
};
}
Run nix-build -A hello
again and you'll see the derivation successfully builds.
Best Practices
Reproducible source paths: If we built the following derivation in
/home/myuser/myproject
then the store path of src
will be
/nix/store/<hash>-myproject
causing the build to no longer be reproducible:
let pkgs = import <nixpkgs> {}; in
pkgs.stdenv.mkDerivation {
name = "foo";
src = ./.;
}
❗ TIP: Use
builtins.path
with thename
attribute set to something fixed. This will derive the symbolic name of the store path from thename
instead of the working directory:let pkgs = import <nixpkgs> {}; in pkgs.stdenv.mkDerivation { name = "foo"; src = builtins.path { path = ./.; name = "myproject"; }; }
Conclusion
In this chapter, we've laid the groundwork for understanding Nix derivations, the fundamental recipes that define how software and other artifacts are built within the Nix ecosystem. We've explored their key components – inputs, builder, build phases, and outputs – and how they contribute to Nix's core principles of reproducibility and isolated environments. Derivations are the workhorses behind the packages and tools we use daily in Nix.
As you've learned, derivations offer a powerful and principled approach to
software management. However, the way we organize and manage these derivations,
along with other Nix expressions and dependencies, has evolved over time.
Traditionally, Nix projects often relied on patterns involving default.nix
files, channel subscriptions, and manual dependency management.
A more recent and increasingly popular approach to structuring Nix projects and managing dependencies is through Nix Flakes. Flakes introduce a standardized project structure, explicit input tracking, and a more robust way to ensure reproducible builds across different environments.
In our next chapter, Comparing Flakes and Traditional Nix, we will directly compare and contrast these two approaches. We'll examine the strengths and weaknesses of traditional Nix practices in contrast to the benefits and features offered by Nix Flakes. This comparison will help you understand the motivations behind Flakes and when you might choose one approach over the other for your Nix projects.
As you can see below, there is a ton of information on derivations freely available.
Links To Articles about Derivations
Click To Expand Resources
-
Sparky/blog-creatingASuperSimpleDerivation # How to learn Nix