Why reproducibility matters

A cure for your 7 page HowTo 

Almut Lütge, almut.luetge@sdsc.ethz.ch

Gabriel Nützi, gabriel.nuetzi@sdsc.ethz.ch

Cyril Matthey-Doret, cyril.matthey-doret@epfl.ch

Aug 31st, 2026, Repository, Slides

Nix in the “RSE profession and Career” track? 🤨

“I hope […] it convinces you to see your home environment as your career-long companion and thus worthy of a long-running personal project.”

— Luc Perkins - Determinate Systems Blog

Help

How to use these slides:

  • S: See the speaker notes.
  • Esc: See all slides and jump around.
  • Space: Go forward.
  • Shift + Space: Go backward.

Acknowledgment

Thanks to the following contributors:

Resources

Slides and the original workshop can be found here:

Motivation

Install Dependecies = Good Luck 🤞🏼

 Why Nix?

You start python and you get this:

>>> import numpy
Segmentation fault (core dumped)

💣 It Works on My Machine

Sure, but it is useful …

🧪 Development Setups


Feature Local Development Mamba Devcontainer Nix/DevShell
Maintenance ⚠️ High ⚠️ Medium ⚠️ Medium ⚠️ Medium
Reproducibility ❌ Low ⚠️ Medium ⚠️ Medium ✅ Very high
Ease of Use ❌ Low ✅ Easy ✅ Ok ❌ Low
Dep. Mgmt. ❌ ✅ ⚠️ ✅
Portability 💣 ⚠️ ✅ ✅
CI Stability 💣 ⚠️ ⚠️ ✅ Almost Perfect

Full Table

 What is Nix?

Nix started 2006 …

 The Nix Ecosystem

Main projects related to Nix:

  • 🎁 Nix(Package Manager): Turns a configuration into a software application.

  • 💬 Nix(Programming Language): Functional programming language to write a software/system configuration.

  • 📦 Nixpkgs: Standard library for Nix + build functions for >140k packages.

  • 💻 NixOS: Declarative Linux distribution.

Nix Basics

Same, same, but different! 🧙‍♀️

Example: A Simple Shell Script.

I am building a script to find my IP:

#! /usr/bin/env bash
curl -s http://ipinfo.io | jq --raw-output .ip
12.24.14.88

What guarantees that jq or curl is available?

Package It with Nix (1)

let
  system = builtins.currentSystem; # e.g. `x86_64-linux`

  # Download something into the `/nix/store/...-source`
  src = builtins.fetchTarball
    "https://github.com/NixOS/nixpkgs/archive/9684b53175fc6c09581e94cc85f05ab77464c7e3.tar.gz";

  # Import the `default.nix` in the `/nix/store/...-source`
  f = import src;

  pkgs = f { inherit system; }; # This is the package attribute set of `nixpkgs`.
in
pkgs.writeShellScriptBin "what-is-my-ip" ''
  ${pkgs.curl}/bin/curl -s http://ipinfo.io | \
    ${pkgs.jq}/bin/jq --raw-output .ip
''

Package It with Nix (2)

Building this Nix code gives you a store path:

nix build -f ./examples/what-is-my-ip.nix --print-out-paths

> "/nix/store/7x9hf9g95d4wjjvq853x25jhakki63bz-what-is-my-ip"

which contains the script and all needed dependencies

#!/nix/store/mc4485g4apaqzjx59dsmqscls1zc3p2w-bash-5.2p37/bin/bash

/nix/store/zl7h70n70g5m57iw5pa8gqkxz6y0zfcf-curl-8.12.1-bin/bin/curl \
  -s "http://ipinfo.io" | \
  /nix/store/y50rkdixqzgdgnps2vrc8g0f0kyvpb9w-jq-1.7.1-bin/bin/jq \
    --raw-output ".ip"

Nix has encoded the used executables with store paths (/nix/store).

What Is This Hash ?

/nix/store/7x9hf9g95d4wjjvq853x25jhakki63bz-what-is-my-ip

The hash 7x9hf9g95d4wjjvq853x25jhakki63bz of your built package depends on:

  • source code of the package (the bash script)
  • dependencies down to the commit level (curl, jq)
  • all build instructions (nix code, including dependencies)

⛓️ Deterministic software packaging.

Package It with Nix (3)

Getting back to: whats-is-my-ip.nix:

let
  system = builtins.currentSystem; # e.g. `x86_64-linux`

  # Download something into the `/nix/store/...-source`
  src = builtins.fetchTarball
    "https://github.com/NixOS/nixpkgs/archive/9684b53175fc6c09581e94cc85f05ab77464c7e3.tar.gz";

  # Import the `default.nix` in the `/nix/store/...-source`
  f = import src;

  pkgs = f { inherit system; }; # This is the package attribute set of `nixpkgs`.
in
pkgs.writeShellScriptBin "what-is-my-ip" ''
  ${pkgs.curl}/bin/curl -s http://ipinfo.io | \
    ${pkgs.jq}/bin/jq --raw-output .ip
''

Wait! What is github.com/NixOS/nixpkgs ?

󰳏 nixpgkgs is a mono-repository with Nix code to build software packages (> 130k).

 A commit in nixpkgs represents the version of all packages at that commit (a big tree).

Package It with Nix (4)

pkgs.writeShellScriptBin "what-is-my-ip" ''
  ${pkgs.curl}/bin/curl -s http://ipinfo.io | \
    ${pkgs.jq}/bin/jq --raw-output .ip
''

What Is a Flake?

A flake.nix [1, 2]

  • provides a deterministic way to manage dependencies and configurations [1].

  • comes with a flake.lock file which locks dependencies.

Remember fetchTarball ".../NixOS/nixpkgs/..." in what-is-my-ip.nix.
🌻 A flake.nix is a better method for locked inputs.

What Is a Flake? (2)

A flake flake.nix:

  • References external Nix code - called inputs.
    • Other repositories, local files, or URLs with a flake.nix.
  • Defines structured outputs (a function).
    • Specifies what the flake provides.
# flake.nix
{
  inputs = { /* ... */ };

  outputs = inputs: {
    packages = /* implementation */

    # ... other output attributes ...
  }
}

Flake outputs

Output What it does Invoked with
packages.<system>.<name> Buildable derivation — the installable package nix build .#name
devShells.<system>.<name> Reproducible shell with dev dependencies in $PATH nix develop
apps.<system>.<name> Executable entry point for the flake nix run .#name
nixosConfigurations.<host> / homeConfigurations.<name> Full system or user-level declarative config nixos-rebuild switch / home-manager switch

Questions ?

Outlook

Goodies 🍬 from SDSC

We maintain well-structured, state-of-the-art, nix-enabled repository templates for toolchains like:

The templates provide CI out of the box and are ready to use!

Your Nix Journey

  • 🌍 Tackling non-reproducible software distribution is hard — but essential

  • 🚀 Embrace Nix to gain

    • reproducibility
    • consistency
    • healthy local development
    • CI for free (when you fix something with  its fixed!)
  • 🤝 The Nix community is welcoming and supportive — don’t hesitate to ask!

  • ✅ Check the references/resources provided (🤷‍♀️ sometimes its a mess, but gets better).

References

Appendix

 The Nix Language

  • Domain-specific functional language (no side-effects).

  • Structurally similar to JSON but with functions.

  • Fundamental data types such as string, integer, path, list, and attribute set.

  • Lazy Evaluated: expression evaluation delayed until needed.

  • ⚠️Specifically designed for deterministic/reproducible software deployment.

What Is a Nix Derivation?

A derivation is a

  • specialized attribute set, describes how to build a Nix package.

    { type = "derivation"; ... }

    Check nix repl -f <nixpkgs> and type pkgs.curl.type.

Whats a Derivation?

A derivation is a build instruction to realize a package in the /nix/store using a special derivation function.

  • Can depend on multiple other derivations.
  • Produce one or more outputs.

The complete set of dependencies required to build a derivation—including its transitive dependencies—is called a closure. [Ref]

Evaluate & Build a Derivation

  • Evaluating: Store build instructions in the /nix/store
    (a store derivation *.drv, more details).

  • Building: Realizing outputs of the derivation in the /nix/store. This can literally be anything!

What is github.com/NixOS/nixpkgs ?

Nix Development Shells

What Is a Nix DevShell?

Its a Nix derivation in the output attribute set devShells of the flake.nix:

{
  inputs = { /* ... */ };
  outputs = inputs: {
    packages.x86_64-linux = {
      mytool = /* derivation */
    };
    devShells.x86_64-linux = {
      banana-shell = /* derivation */
    };
    # ... other outputs ...
  }
}

The banana-shell derivation is meant to be consumed by nix develop.

Use devenv.sh for Nix DevShells

  • 🚧 Nix DevShells from nixpkgs (pkgs.mkShell) are raw and too simplistic.

  • 🌻Nix DevShells from devenv.sh provides more concise configuration.

    • ❤️‍🔥Configuration based on mechanics which drive NixOS (NixOS Modules).

Fixed Point Combinator 🤯

In maths a fix point x of a function F is defined as:

\[ x = F(x). \]

In functional programming a fix-point combinator fix is a higher-order function.
It returns the fix point of a function F:

fix = F: let x = F x in x

Fixed-Point Combinator 🤯

That is how recursive self-referential sets can be defined.

let
  fix = F: let x = F x in x;

  # Define the constructor of the set.
  newSet = self: { path = "/bin"; full = self.path + "/my-app"; };

  mySet = fix newSet; # fulfills: mySet == fix mySet;
in
  mySet.full

Seems recursive in let x = F x but but isn’t 🤯, because its lazy evaluated. What you need to know about laziness..

Used in pkgs.callPackage in nixpkgs.

Why Nix is Lazy Evaluated?

The choice for lazy evaluation allows us to write Nix expressions in a convenient and elegant style: Packages are described by Nix expressions and these Nix expressions can freely be passed around in a Nix program – as long as we do not access the contents of the package, no evaluation and thus no build will occur. […] At the call site of a function, we can supply all potential dependencies without having to worry that unneeded dependencies might be evaluated. For instance, the whole of the Nix packages collection is essentially one attribute set where each attribute maps to one package contained in the collection. It is very convenient that at this point, we do not have to deal with the administration of what exactly will be needed where. Another benefit is that we can easily store meta information with packages, such as name, version number, homepage, license, description and maintainer. Without any extra effort, we can access such meta-information without having to build the whole package. [Paper]

The builtin function derivation

This is what pkgs.writeShellScriptBin would expand to: (see ./examples/what-is-my-ip-orig.nix):

derivation {
  inherit system;

  name = "what-is-my-ip";
  builder = "/bin/sh";

  args = [
    "-c"
    ''
      ${pkgs.coreutils}/bin/mkdir -p $out/bin

      {
        echo '#!/bin/sh'
        echo '${pkgs.curl}/bin/curl -s http://ipinfo.io | \
        ${pkgs.jq}/bin/jq --raw-output .ip'
      } > $out/bin/what-is-my-ip

      ${pkgs.coreutils}/bin/chmod +x $out/bin/what-is-my-ip
    ''
  ];

  outputs = [ "out" ];
}

Store Derivation

A store derivation (*.drv) contains only build instructions for Nix to realize/build it. This can be literally anything, e.g. a software package, a wrapper shell script or only source files.

Evaluate & Build a Derivation

# Evaluate it.
drvPath=$(nix eval "./examples/flake-simple#packages.x86_64-linux.mytool" --raw)
# Realize it.
nix build -L "$drvPath" --print-out-paths --out-link ./mytool

Inspect a Derivation

nix eval "./examples/flake-simple#packages.x86_64-linux.mytool"

> «derivation /nix/store/l8pma77py04gd5819zkk3h7jx0bgxqgm-mytool.drv»

./examples/flake-simple#packages.x86_64-linux.mytool is an installable. More later!

# Inspect the store derivation.
cat /nix/store/l8pma77py04gd5819zkk3h7jx0bgxqgm-mytool.drv

Inspect a Derivation (2)

> Derive([("out","/nix/store/5rvqlxk2vx0hx1yk8qdll2l8l62pfn8n-treefmt","","")],
[("/nix/store/1fmb3b4cmr1bl1v6vgr8plw15rqw5jhf-treefmt.toml.drv",["out"]),
("/nix/store/3avbfsh9rjq8psqbbplv2da6dr679cib-treefmt-2.1.0.drv",["out"]),
("/nix/store/61fjldjpjn6n8b037xkvvrgjv4q8myhl-bash-5.2p37.drv",["out"]),
("/nix/store/gp6gh2jn0x7y7shdvvwxlza4r5bmh211-stdenv-linux.drv",["out"])]
,["/nix/store/v6x3cs394jgqfbi0a42pam708flxaphh-default-builder.sh"]
,"x86_64-linux","/nix/store/8vpg72ik2kgxfj05lc56hkqrdrfl8xi9-bash-5.2p37/bin/bash",
["-e","/nix/store/v6x3cs394jgqfbi0a42pam708flxaphh-default-builder.sh"],
[ ("__structuredAttrs",""),("allowSubstitutes",""),
("buildCommand","target=$out/bin/treefmt\nmkdir -p \"$(dirname \"$target\")\"\n\nif [ -e \"$textPath\" ]; then\n  mv \"$textPath\" \"$target\"\nelse\n  echo -n \"$text\" > \"$target\"\nfi\n\nif [ -n \"$executable\" ]; then\n  chmod +x \"$target\"\nfi\n\neval \"$checkPhase\"\n"),("buildInputs",""),("builder","/nix/store/8vpg72ik2kgxfj05lc56hkqrdrfl8xi9-bash-5.2p37/bin/bash"),("checkPhase","/nix/store/8vpg72ik2kgxfj05lc56hkqrdrfl8xi9-bash-5.2p37/bin/bash -n -O extglob \"$target\"\n"),("cmakeFlags",""),("configureFlags",""),("depsBuildBuild",""),("depsBuildBuildPropagated",""),("depsBuildTarget",""),("depsBuildTargetPropagated",""),("depsHostHost",""),("depsHostHostPropagated",""),("depsTargetTarget",""),("depsTargetTargetPropagated",""),("doCheck",""),("doInstallCheck",""),("enableParallelBuilding","1"),("enableParallelChecking","1"),("enableParallelInstalling","1"),("executable","1"),("mesonFlags",""),("name","treefmt"),("nativeBuildInputs",""),("out","/nix/store/5rvqlxk2vx0hx1yk8qdll2l8l62pfn8n-treefmt"),("outputs","out"),("passAsFile","buildCommand text"),("patches",""),("preferLocalBuild","1"),("propagatedBuildInputs",""),("propagatedNativeBuildInputs",""),("stdenv","/nix/store/hsxp8g7zdr6wxk1mp812g8nbzvajzn4w-stdenv-linux"),("strictDeps",""),("system","x86_64-linux"),("text","#!/nix/store/8vpg72ik2kgxfj05lc56hkqrdrfl8xi9-bash-5.2p37/bin/bash\nset -euo pipefail\nunset PRJ_ROOT\nexec /nix/store/0jcp33pgf85arjv3nbghws34mrmy7qq5-treefmt-2.1.0/bin/treefmt \\\n  --config-file=/nix/store/qk8rqccch6slk037dhnprryqwi8mv0xs-treefmt.toml \\\n  --tree-root-file=.git/config \\\n  \"$@\"\n\n")])

JSON output of the above:

nix derivation show /nix/store/l8pma77py04gd5819zkk3h7jx0bgxqgm-mytool.drv

Inspect a Derivation (3)

{
  "/nix/store/l8pma77py04gd5819zkk3h7jx0bgxqgm-mytool.drv": {
    "args": [
      "-e",
      "/nix/store/vj1c3wf9c11a0qs6p3ymfvrnsdgsdcbq-source-stdenv.sh",
      "/nix/store/shkw4qm9qcw5sc5n1k5jznc83ny02r39-default-builder.sh"
    ],
    "builder": "/nix/store/9nw8b61s8lfdn8fkabxhbz0s775gjhbr-bash-5.2p37/bin/bash",
    "env": {
      "__structuredAttrs": "",
      "allowSubstitutes": "",
      "buildCommand": "target=$out/bin/mytool\nmkdir -p \"$(dirname \"$target\")\"\n\nif [ -e \"$textPath\" ]; then\n  mv \"$textPath\" \"$target\"\nelse\n  echo -n \"$text\" > \"$target\"\nfi\n\nif [ -n \"$executable\" ]; then\n  chmod +x \"$target\"\nfi\n\neval \"$checkPhase\"\n",
      "buildInputs": "",
      "builder": "/nix/store/9nw8b61s8lfdn8fkabxhbz0s775gjhbr-bash-5.2p37/bin/bash",
      "checkPhase": "/nix/store/9nw8b61s8lfdn8fkabxhbz0s775gjhbr-bash-5.2p37/bin/bash -n -O extglob \"$target\"\n",
      "cmakeFlags": "",
      "configureFlags": "",
      "depsBuildBuild": "",
      "depsBuildBuildPropagated": "",
      "depsBuildTarget": "",
      "depsBuildTargetPropagated": "",
      "depsHostHost": "",
      "depsHostHostPropagated": "",
      "depsTargetTarget": "",
      "depsTargetTargetPropagated": "",
      "doCheck": "",
      "doInstallCheck": "",
      "enableParallelBuilding": "1",
      "enableParallelChecking": "1",
      "enableParallelInstalling": "1",
      "executable": "1",
      "mesonFlags": "",
      "name": "mytool",
      "nativeBuildInputs": "",
      "out": "/nix/store/blm702jzcwfppwrrj9925ivd9gxp4c9n-mytool",
      "outputs": "out",
      "passAsFile": "buildCommand text",
      "patches": "",
      "preferLocalBuild": "1",
      "propagatedBuildInputs": "",
      "propagatedNativeBuildInputs": "",
      "stdenv": "/nix/store/npp9k9062ny7w0k1i03ij6xvqb7vhvjh-stdenv-linux",
      "strictDeps": "",
      "system": "x86_64-linux",
      "text": "#!/nix/store/9nw8b61s8lfdn8fkabxhbz0s775gjhbr-bash-5.2p37/bin/bash\n\"/nix/store/xkk1gr9bw2dbdjna8391rj1zl1l3dmhq-cowsay-3.8.4/bin/cowsay\" \"Hello there ;)\"\necho \"-------------------------------------\"\n\"/nix/store/4ydiim4lfk6nyab4pdkjj9s33pgbigfd-figlet-2.2.5/bin/figlet\" \"Do you expect\"\n\"/nix/store/4ydiim4lfk6nyab4pdkjj9s33pgbigfd-figlet-2.2.5/bin/figlet\" \"something \"\n\"/nix/store/4ydiim4lfk6nyab4pdkjj9s33pgbigfd-figlet-2.2.5/bin/figlet\" \"useful ? \"\n\n"
    },
    "inputDrvs": {
      "/nix/store/1fsd2cb5ab7ci01ks4j0gbbq254jw6sk-stdenv-linux.drv": {
        "dynamicOutputs": {},
        "outputs": ["out"]
      },
      "/nix/store/lrf9kbhlaf5mkvnlf3zr9wzvk7c2z72l-bash-5.2p37.drv": {
        "dynamicOutputs": {},
        "outputs": ["out"]
      },
      "/nix/store/phq4wh4490manblg905xixpc3gvwr149-figlet-2.2.5.drv": {
        "dynamicOutputs": {},
        "outputs": ["out"]
      },
      "/nix/store/wdpicivrj0bmzh935rr1hm1vlk18j0mp-cowsay-3.8.4.drv": {
        "dynamicOutputs": {},
        "outputs": ["out"]
      }
    },
    "inputSrcs": [
      "/nix/store/shkw4qm9qcw5sc5n1k5jznc83ny02r39-default-builder.sh",
      "/nix/store/vj1c3wf9c11a0qs6p3ymfvrnsdgsdcbq-source-stdenv.sh"
    ],
    "name": "mytool",
    "outputs": {
      "out": {
        "path": "/nix/store/blm702jzcwfppwrrj9925ivd9gxp4c9n-mytool"
      }
    },
    "system": "x86_64-linux"
  }
}

What Is an Installable

The path ./examples/flake-simple#packages.x86_64-linux.mytool is referred to as a Flake output attribute installable, or simply an installable.

An installable is a Flake output that can be realized in the Nix store.

  • ./examples/flake-simple refers to this repository’s flake.nix directory.
  • packages.x86_64-linux.mytool following # is an output attribute defined within the flake.

Most modern Nix commands accept installables as input, making them a fundamental concept in working with Flakes. You should only use the modern commands, e.g. nix <subcommand>. Stay away from the command nix-env.

More on Strings

Nix distinguishes between

  • normal strings: just text,
  • and strings with context : text + set of references to store paths.

Strings with context are always created when derivations are involved:

{pkgs, ...}: {
  mytool = pkgs.writeShellScriptBin "tool"
    # String with context, due to referencing `pkgs.coreutils`:
    ''
      echo "hello" | "${pkgs.coreutils}/bin/tee" -a test.log
    '';
}

Note: Use builtins.getContext to inspect a string with context.