7.1 KiB
Nix Primer
Nix is two things bundled:
- A pure, lazy functional language for describing builds.
- A package manager that takes those descriptions and produces content-addressed store paths.
Confusingly, both are called "Nix." Separate them in your head and everything gets easier.
1. The Mental Model: Eval, Then Build
Every nix build or nix develop runs in two phases:
.nix source ──(evaluate)──▶ .drv file ──(realise)──▶ /nix/store/...-output
│ │ │
└── Nix language └── Recipe └── Actual files
(pure, lazy) (on disk) (content-addressed)
- Evaluation runs the Nix language. Pure, in-memory, fast. Produces
.drvfiles. - Realisation actually builds things. Sandboxed. Slow. Outputs go to
/nix/store.
You can evaluate without building (nix eval, nix-instantiate) and you can build without touching the language if you already have a .drv. Most
errors you'll see are eval errors: typos in attrsets, missing arguments, and undefined names.
2. The Language in 5 Minutes
Primitives and Collections
42 # integer
3.14 # float
"hello ${name}" # string with interpolation
''multi-line # indented string (strips common leading whitespace)
string''
true false null
[ 1 2 3 ] # list (space-separated, NOT comma)
{ a = 1; b = "two"; } # attrset
/foo/bar # path (literal, not a string!)
./relative/path # path relative to current file
Paths are their own type. When used, they're copied into the store and replaced with the store path.
Functions
Single-argument, curried:
add = x: y: x + y;
add 2 3 # => 5
Attrset destructuring (how "named args" work):
mkUser = { name, age ? 0, ... }: { inherit name age; };
mkUser { name = "ada"; } # => { name = "ada"; age = 0; }
The ... means "ignore extra attrs." Without it, passing extras is an error.
Bindings
let
x = 1;
y = x + 1; # can reference earlier bindings (and each other: lazy)
in x + y
{ inherit x y; } # ≡ { x = x; y = y; }
{ inherit (pkgs) hello jq; } # pulls hello/jq from pkgs into this attrset
with (Use Cautiously)
with pkgs; [ hello jq ripgrep ]
# ≡
[ pkgs.hello pkgs.jq pkgs.ripgrep ]
Convenient in package lists. Avoid at the top of a file: it silently shadows names and makes debugging hard.
Laziness
Nothing is evaluated until needed. This is why you can have large attrsets like nixpkgs (100k+ packages) and only pay for what you use.
let broken = throw "nope"; ok = 1; in ok # => 1, never evaluates `broken`
import
import ./foo.nix # evaluate foo.nix, return its value
import ./foo.nix { x = 1; } # if foo.nix is a function, also call it
3. Derivations: The Core Primitive
A derivation is "a build, described as data." You create one with derivation { … } (low-level) or pkgs.stdenv.mkDerivation { … } (the nixpkgs
wrapper you'll actually use).
Minimal example:
{ pkgs ? import <nixpkgs> {} }:
pkgs.stdenv.mkDerivation {
pname = "hello-note";
version = "0.1";
src = ./.; # path: gets copied into the store
installPhase = ''
mkdir -p $out/bin
echo '#!/bin/sh' > $out/bin/hello-note
echo 'echo hello from nix' >> $out/bin/hello-note
chmod +x $out/bin/hello-note
'';
}
Key ideas:
$outis the output store path the build writes into.- Phases (
unpackPhase,buildPhase,installPhase, …) are shell snippetsstdenvruns in order. Override the ones you need. - No network in the build sandbox, except for fixed-output derivations (whose hash you declare up front).
- Every input (source, compiler, env vars) becomes part of the hash. Change anything and you get a fresh store path.
4. Store Model: Why This Is All Different
Traditional package managers put files in /usr/bin, /usr/lib, and similar shared locations. One version at a time. Upgrades mutate shared state.
Nix puts every build in /nix/store/<hash>-<name>/ with the hash derived from all its inputs (recursively). That gives you:
- Atomic upgrades and rollbacks: switching "versions" is just switching symlinks.
- Multiple versions coexisting: they live at different hashes.
- Reproducibility: same inputs produce the same hash and the same output (in theory; in practice, 99% there).
- Binary caching: if someone else already built exactly these inputs, you can download their output. This is what
cache.nixos.orgdoes. - Garbage-collectable: anything not referenced by a "GC root" can be deleted safely.
5. How You'll Actually Use Nix Day-to-Day
Ordered roughly by power-to-complexity:
nix run nixpkgs#cowsay -- moo: run a package without installing.nix shell nixpkgs#jq nixpkgs#ripgrep: throwaway shell with those tools.- Dev shell via
flake.nix: per-project pinned toolchain. This is the most common thing flakes are used for. nix build: produce artifacts (binaries, containers, and ISOs).- NixOS: declare your whole OS config (packages, services, users, and network) in Nix.
- home-manager: same idea, at the user level.
Each step up locks in more of your environment. You don't need to adopt all of it at once.
6. Minimum Useful Commands
# Language / introspection
nix repl # REPL. Try `:l <nixpkgs>` then `hello`.
nix eval --expr '1 + 1'
nix-instantiate --eval -E '{ a = 1; }.a'
# Running & building
nix run nixpkgs#hello
nix shell nixpkgs#jq
nix build nixpkgs#hello # writes ./result symlink
nix-store --query --references ./result # show closure
# Housekeeping
nix store gc # garbage collect
nix store optimise # dedupe identical files via hardlinks
nix why-depends ./result nixpkgs#glibc # trace dependency chains
7. Gotchas That Trip Up Newcomers
- Lists use spaces, not commas.
[ 1 2 3 ]. Commas are a syntax error. rec { }makes an attrset self-referential. Withoutrec, attrs can't reference each other. You'll see this in package definitions.- Paths vs. strings.
./foois a path (copied to store on use)."./foo"is a string (not auto-copied). Mixing them causes surprises. builtins.*is the always-available low-level namespace.lib.*(from nixpkgs) is the high-level helper library. Both exist; they're different.- Evaluation errors are lazy. An error deep in an unused branch only fires when you actually use it.
nix-instantiate --eval --strictforces full evaluation. - Hash errors on first build of an FOD: you'll see
got: sha256-…. Copy that hash into your expression. This is expected workflow.
8. What to Read or Try Next
- Open
nix repl, type:l <nixpkgs>, thenhello,hello.drv,hello.outPath. Poke around. - Read
nix.dev, the current best introduction. - See
003-flakes.mdfor the flakes-specific layer on top of all of this.