206 lines
7.2 KiB
Markdown
206 lines
7.2 KiB
Markdown
# 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` / `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`.
|
|
|
|
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, undefined names.
|
|
|
|
---
|
|
|
|
## 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 huge 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
|
|
|
|
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:
|
|
|
|
```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`, etc. 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 / rollbacks** — switching "versions" is just switching symlinks.
|
|
- **Multiple versions coexisting** — they live at different hashes.
|
|
- **Reproducibility** — same inputs → same hash → 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, ISOs).
|
|
5. **NixOS** — declare your whole OS config (packages, services, users, 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.
|
|
- **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.
|
|
- **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/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.
|