Skip to content

Purity and Explicit Inputs

This page explains a core Nix idea:

Reproducible builds require controlling what information a build is allowed to see.

Nix is “strict” on purpose. That strictness is what enables:

  • reproducibility
  • correct caching
  • predictable upgrades
  • fewer “it worked yesterday” failures

We’ll keep this high-level but hands-on, using commands that reveal what Nix is doing.


When we say “pure” in the Nix context, we mean:

  • No hidden dependencies
  • No ambient state
  • No implicit filesystem probing
  • No network access during builds (by default)
  • The build is a function of declared inputs

In a pure build, if something influences the output, it should be declared as an input.


The Basic Problem: Scripts Can See the Whole World

Section titled “The Basic Problem: Scripts Can See the Whole World”

A typical build script can accidentally depend on:

  • whatever is on your PATH
  • whatever is installed globally
  • what time it is
  • what DNS resolves to today
  • what happens to be on disk

That makes builds:

  • hard to reproduce
  • hard to cache correctly
  • hard to debug

Nix’s solution is: restrict what the build can see.


(output) = build(inputs)

If you can’t enumerate the inputs, you can’t claim reproducibility.


Nix has two important phases:

  1. Evaluation (pure): compute a build graph from expressions
  2. Realization (sandboxed): run builders with only declared inputs available

We won’t write Nix code yet—this page is about behaviors you can observe.


Demo 1: “If It’s Not an Input, It Doesn’t Exist”

Section titled “Demo 1: “If It’s Not an Input, It Doesn’t Exist””

This is the most important lived experience for new Nix users.

Step 1: Get a temporary shell with only one tool

Section titled “Step 1: Get a temporary shell with only one tool”
Terminal window
nix shell nixpkgs#bashInteractive -c bash

Inside that shell, run:

Terminal window
which git || echo "git is not available"
which curl || echo "curl is not available"
which python || echo "python is not available"

Now exit:

Terminal window
exit

What this demonstrates:

  • your environment can be intentionally minimal
  • “I assumed X existed” becomes an explicit choice
Terminal window
nix shell nixpkgs#bashInteractive nixpkgs#git nixpkgs#curl -c bash

Now:

Terminal window
which git
which curl

Nix makes you declare tools because undeclared tools are hidden dependencies.


Demo 2: Dependency Closure is Explicit (and Inspectable)

Section titled “Demo 2: Dependency Closure is Explicit (and Inspectable)”

A big reason purity works is that Nix can compute the full closure of a package.

Terminal window
HELLO="$(nix path-info nixpkgs#hello)"
echo "$HELLO"
nix path-info -r "$HELLO" | head -n 20

Now compare with a larger package:

Terminal window
PY="$(nix path-info nixpkgs#python3)"
nix path-info -r "$PY" | wc -l
nix path-info -r "$HELLO" | wc -l

What this demonstrates:

  • dependency graphs are explicit
  • you can audit and reason about “what’s included”
  • inputs are not hidden in global state

Demo 3: “Purity” Enables Correct Caching

Section titled “Demo 3: “Purity” Enables Correct Caching”

Traditional build caching is often heuristic (“did files change?”). Nix caching can be correct because the cache key comes from inputs.

Terminal window
nix build --no-link --print-out-paths nixpkgs#hello
nix build --no-link --print-out-paths nixpkgs#hello

Second run should be instant.

Terminal window
nix build --no-link --print-out-paths nixpkgs/nixos-23.11#hello
nix build --no-link --print-out-paths nixpkgs/nixos-24.05#hello

Different inputs ⇒ different output paths. That’s an important guarantee.


This is where teams often underestimate the problem. Inputs include more than direct dependencies.

  • Source code (obvious)
  • Build tools (make, cmake, maven, gradle)
  • Compiler toolchain (gcc/clang/jdk)
  • System libraries (openssl, libc, zlib)
  • Environment variables that influence the build
  • Patches applied during build
  • Configuration flags
  • The platform/architecture

Nix works because it tries to make these explicit.


Demo 4: Hidden Dependency on PATH (Simple Illustration)

Section titled “Demo 4: Hidden Dependency on PATH (Simple Illustration)”

This demo shows why “ambient PATH” is a build hazard.

Terminal window
command -v gcc || true
command -v make || true
command -v python || true
command -v openssl || true

Now create a very minimal Nix environment and re-check:

Terminal window
nix shell nixpkgs#bashInteractive -c sh -lc 'command -v python || echo "no python"; command -v gcc || echo "no gcc"; command -v openssl || echo "no openssl"'

Now add them explicitly:

Terminal window
nix shell nixpkgs#bashInteractive nixpkgs#gcc nixpkgs#openssl nixpkgs#python -c sh -lc 'command -v gcc; command -v openssl; command -v python3'

Lesson:

  • in Nix, ambient availability disappears
  • build dependencies become deliberate

Demo 5: Network Access and “Impure Inputs”

Section titled “Demo 5: Network Access and “Impure Inputs””

One of the biggest sources of non-reproducibility is downloading things during a build.

If a build needs the internet, the internet becomes an input.

Nix generally pushes network access out of the build step and into controlled fetchers.

Run:

Terminal window
nix build --no-link nixpkgs#hello -L

Watch whether it:

  • downloads a substitute (binary cache)
  • or performs local builds

Either way, Nix is trying to ensure the artifact is a known result, not “whatever the internet served today.”

Note: Nix can still download sources/binaries, but it does so in a controlled way that can be hashed and cached.


Nix has an “impure” mode for evaluation and some commands. This is useful as a contrast.

Evaluate with and without impurity (flake example)

Section titled “Evaluate with and without impurity (flake example)”

Try showing that Nix can reference the current directory as an input:

Terminal window
pwd
ls

Then:

Terminal window
nix flake show

Now compare with:

Terminal window
nix flake show --impure

Depending on your flake and environment, this might not change output—so treat it as optional. The takeaway is:

  • pure evaluation discourages implicit dependency on “whatever directory I’m in”
  • --impure exists, but it’s a tradeoff

The Big Tradeoff: Strictness vs Convenience

Section titled “The Big Tradeoff: Strictness vs Convenience”

This is where you acknowledge the pain point honestly.

  • You must declare tools/dependencies
  • Some “quick hacks” stop working
  • You occasionally need to fight the build boundary
  • Builds become explainable
  • Bugs become repeatable
  • Caches become reliable
  • Upgrades become mechanical (bump inputs → build/test → promote)

Nix doesn’t remove work. It removes classes of compensatory work.


Purity + explicit inputs is what makes these possible:

  • Onboarding: “enter the dev shell” replaces multi-page setup docs
  • CI parity: same inputs in CI and local
  • Safer upgrades: build/test matrices reveal breakage immediately
  • CVE response: update inputs, rebuild everything, promote green

  • Reproducible builds require controlling what a build can see

  • “Explicit inputs” is not ceremony—it’s how you make builds predictable

  • Nix’s strictness is what enables:

    • correct caching
    • reliable reproduction
    • safer upgrades