2026-04-15 11:49:36 +02:00
|
|
|
# Nix Primer
|
|
|
|
|
|
|
|
|
|
Nix is two things bundled:
|
|
|
|
|
|
|
|
|
|
1. A pure, lazy functional language for describing builds.
|
|
|
|
|
2. 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 `.drv` files.
|
|
|
|
|
- Realisation actually builds things. Sandboxed. Slow. Outputs go to `/nix/store`.
|
|
|
|
|
|
2026-04-21 14:19:00 +02:00
|
|
|
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.
|
2026-04-15 11:49:36 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 2. The Language in 5 Minutes
|
|
|
|
|
|
|
|
|
|
### Primitives and Collections
|
|
|
|
|
|
|
|
|
|
```nix
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```nix
|
|
|
|
|
add = x: y: x + y;
|
|
|
|
|
add 2 3 # => 5
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Attrset destructuring (how "named args" work):
|
|
|
|
|
|
|
|
|
|
```nix
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
```nix
|
|
|
|
|
let
|
|
|
|
|
x = 1;
|
|
|
|
|
y = x + 1; # can reference earlier bindings (and each other: lazy)
|
|
|
|
|
in x + y
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```nix
|
|
|
|
|
{ inherit x y; } # ≡ { x = x; y = y; }
|
|
|
|
|
{ inherit (pkgs) hello jq; } # pulls hello/jq from pkgs into this attrset
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `with` (Use Cautiously)
|
|
|
|
|
|
|
|
|
|
```nix
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
```nix
|
|
|
|
|
let broken = throw "nope"; ok = 1; in ok # => 1, never evaluates `broken`
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `import`
|
|
|
|
|
|
|
|
|
|
```nix
|
|
|
|
|
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
|
|
|
|
|
|
2026-04-21 14:19:00 +02:00
|
|
|
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).
|
2026-04-15 11:49:36 +02:00
|
|
|
|
|
|
|
|
Minimal example:
|
|
|
|
|
|
|
|
|
|
```nix
|
|
|
|
|
{ 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:
|
|
|
|
|
|
|
|
|
|
- `$out` is the output store path the build writes into.
|
|
|
|
|
- Phases (`unpackPhase`, `buildPhase`, `installPhase`, …) are shell snippets `stdenv` runs 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.org` does.
|
|
|
|
|
- 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:
|
|
|
|
|
|
|
|
|
|
1. `nix run nixpkgs#cowsay -- moo`: run a package without installing.
|
|
|
|
|
2. `nix shell nixpkgs#jq nixpkgs#ripgrep`: throwaway shell with those tools.
|
|
|
|
|
3. Dev shell via `flake.nix`: per-project pinned toolchain. *This is the most common thing flakes are used for.*
|
|
|
|
|
4. `nix build`: produce artifacts (binaries, containers, and ISOs).
|
|
|
|
|
5. NixOS: declare your whole OS config (packages, services, users, and network) in Nix.
|
|
|
|
|
6. 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
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
# 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. Without `rec`, attrs can't reference each other. You'll see this in package definitions.
|
|
|
|
|
- Paths vs. strings. `./foo` is 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.
|
2026-04-21 14:19:00 +02:00
|
|
|
- Evaluation errors are lazy. An error deep in an unused branch only fires when you actually use it. `nix-instantiate --eval --strict` forces full
|
|
|
|
|
evaluation.
|
2026-04-15 11:49:36 +02:00
|
|
|
- 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>`, then `hello`, `hello.drv`, `hello.outPath`. Poke around.
|
|
|
|
|
- Read [`nix.dev`](https://nix.dev), the current best introduction.
|
|
|
|
|
- See `003-flakes.md` for the flakes-specific layer on top of all of this.
|