# 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`. 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 ```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 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 {} }: 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/-/` 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 ` 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 or Try Next - Open `nix repl`, type `:l `, 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.