commit 35402cccdabaa031761ced2e96c62f888a71ff9a Author: Hassan Abedi Date: Wed Apr 15 11:49:36 2026 +0200 The base commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..61849ab --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{rs,py}] +max_line_length = 100 + +[*.md] +max_line_length = 150 +trim_trailing_whitespace = false + +[*.sh] +indent_size = 2 + +[*.{yaml,yml,json}] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..afc5177 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,38 @@ +* text=auto eol=lf + +*.go text +*.mod text +*.sum text +*.md text +*.rst text +*.json text +*.yaml text +*.yml text +*.toml text +*.sh text eol=lf +*.html text +*.css text +*.js text +*.svg text +*.xml text + +*.png filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text +*.ico filter=lfs diff=lfs merge=lfs -text +*.mp4 filter=lfs diff=lfs merge=lfs -text +*.mov filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.ttf filter=lfs diff=lfs merge=lfs -text +*.woff filter=lfs diff=lfs merge=lfs -text +*.woff2 filter=lfs diff=lfs merge=lfs -text +*.exe filter=lfs diff=lfs merge=lfs -text +*.dll filter=lfs diff=lfs merge=lfs -text +*.so filter=lfs diff=lfs merge=lfs -text +*.out filter=lfs diff=lfs merge=lfs -text +*.a filter=lfs diff=lfs merge=lfs -text +*.o filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..04b2bed --- /dev/null +++ b/.gitignore @@ -0,0 +1,85 @@ +# Python specific +__pycache__/ +*.py[cod] +*$py.class + +# Virtual environments +.env/ +env/ +.venv/ +venv/ + +# Packaging and distribution files +.Python +build/ +dist/ +*.egg-info/ +*.egg +MANIFEST + +# Dependency directories +develop-eggs/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +.installed.cfg + +# Test and coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# IDE specific files and directories +.idea/ +*.iml +.vscode/ + +# Jupyter Notebook files +.ipynb_checkpoints + +# Temporary files created by editors and the system and folders to ignore +*.swp +*~ +*.bak +*.tmp +temp/ +tmp/ + +# Database files (SQLite, DuckDB, etc.) +*.duckdb +*.db +*.wal +*.sqlite + +# Dependency lock files (uncomment to ignore) +poetry.lock + +# Rust specific +/target/ +.cargo-ok +cobertura.xml +tarpaulin-report.html + +# Comment out the next line if you want to checkin your lock file for Cargo +Cargo.lock + +# Misc +*.proptest-regressions +.DS_Store +.benchmarks +.env +.claude/ +.codex diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..60a3d84 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,44 @@ +default_stages: [ pre-commit ] +fail_fast: false +exclude: '^\.idea/' + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + args: [ --markdown-linebreak-ext=md ] + - id: end-of-file-fixer + - id: mixed-line-ending + - id: check-merge-conflict + - id: check-added-large-files + - id: detect-private-key + - id: check-yaml + - id: check-toml + - id: check-json + + - repo: local + hooks: + - id: nix-fmt-check + name: Check Nix Formatting + entry: make fmt-check + language: system + pass_filenames: false + files: \.nix$ + stages: [ pre-commit ] + + - id: nix-lint + name: Lint Nix Files + entry: make lint + language: system + pass_filenames: false + files: \.nix$ + stages: [ pre-commit ] + + - id: nix-flake-check + name: Run Nix Flake Checks + entry: make check + language: system + pass_filenames: false + files: \.(nix|lock)$ + stages: [ pre-push ] diff --git a/01-devshell/flake.nix b/01-devshell/flake.nix new file mode 100644 index 0000000..f02adcc --- /dev/null +++ b/01-devshell/flake.nix @@ -0,0 +1,33 @@ +{ + description = "A minimal dev shell — your first flake"; + + # Inputs: other flakes this one depends on. + # `nixpkgs` is the big package set. `follows` isn't used here, but you'll + # see it a lot when composing flakes (e.g. `inputs.foo.inputs.nixpkgs.follows = "nixpkgs";`). + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + # Outputs: a function that receives all inputs and returns an attrset. + # The attrset's structure is what `nix` CLI commands look for (e.g. `devShells..default`). + outputs = { self, nixpkgs, ... }: + let + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + in { + # `nix develop` picks this up by default. + devShells.${system}.default = pkgs.mkShell { + packages = with pkgs; [ + cowsay + jq + ripgrep + ]; + + # Runs once when you enter the shell. + shellHook = '' + echo "welcome to your first nix dev shell" + cowsay "hello, flakes" + ''; + }; + }; +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c4b6e4d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,170 @@ +# AGENTS.md + +This file provides guidance to coding agents collaborating on this repository. + +## Mission + +`nix-playground` is a personal learning playground for Nix and flakes. + The goal is not production software but clear, runnable, progressively more advanced examples plus prose notes that explain them. + +Priorities, in order: + +1. Correctness: examples must actually evaluate and build. +2. Clarity: each example teaches one concept; names, comments, and directory structure should make that concept obvious. +3. Minimality: prefer the shortest flake or expression that demonstrates the idea. +4. Accuracy of notes: prose under `notes/` must not describe behavior the examples do not demonstrate. +5. Reproducibility: every flake commits its `flake.lock`; nothing depends on ambient state. + +## Core Rules + +- Use English for code, comments, and prose. +- Keep each numbered example self-contained: its own `flake.nix`, own `flake.lock`, no cross-example imports. +- Prefer small, focused changes over broad rewrites across examples. +- Add comments only when they clarify non-obvious Nix behavior (laziness, `rec`, string vs. path, `with` scoping, etc.). +- Do not describe Nix features in notes or comments as if they were implemented in an example unless the example actually uses them. +- When an example grows beyond one concept, split it into a new numbered directory rather than expanding the existing one. + +Quick examples: + +- Good: add `03-multi-system/` that demonstrates `forAllSystems` in isolation. +- Good: add a `checks` output to an existing flake with a one-line comment explaining what `nix flake check` will do with it. +- Bad: combine overlays, NixOS modules, and home-manager into one "comprehensive" example. +- Bad: edit `notes/` to describe an approach no example in the repo uses. + +## Writing Style + +- Use Oxford commas in inline lists: "a, b, and c" not "a, b, c". +- Do not use em dashes. Restructure the sentence, or use a colon or semicolon instead. +- Avoid colorful adjectives and adverbs. Write "dev shell" not "lightweight dev shell", "overlay" not "flexible overlay". +- Use noun phrases for checklist items, not imperative verbs. Write "input pinning" not "pin inputs". +- Headings in Markdown files must be in title case: "Build from Source" not "Build from source". Minor words (a, an, the, and, but, or, for, in, on, at, to, by, of) stay lowercase unless they are the first word. + +## Repository Layout + +- `01-devshell/`, `02-*/`, `NN-/`: self-contained numbered examples. Each directory is a flake root. +- `notes/`: prose companions numbered to match reading order. + - `001-glossary.md`: vocabulary reference. + - `002-nix-primer.md`: the Nix language and store model. + - `003-flakes.md`: flake anatomy, schema, and common patterns. +- `Makefile`: discovery-based helpers that run formatting, linting, and `nix flake check` across all examples. +- `AGENTS.md`: this file. +- `.pre-commit-config.yaml`, `.editorconfig`, `.gitattributes`, `.gitignore`: repository hygiene. +- `pyproject.toml`: Python environment metadata used only to install `pre-commit`. + +New examples follow `NN-/` where `NN` is a two-digit ordinal. Topics grow roughly from simpler to more involved: dev shell, package, multi-system, NixOS module, home-manager, overlay. + +## Example Layout Constraints + +- Each example owns exactly one `flake.nix` at its root and commits its `flake.lock`. +- Examples do not import each other. Copy and adapt if a pattern needs to be shown twice. +- An example may depend only on flakes it declares in its own `inputs`. +- Prefer `nixpkgs` pinned to `nixos-unstable` for consistency across examples unless the example's point is pinning strategy. +- Keep the `outputs` attrset flat enough that `nix flake show` reads as a single screen. +- If an example exposes `checks..*`, those checks must pass under `nix flake check`. + +## Nix and Flake Conventions + +- Target Nix with `experimental-features = nix-command flakes` enabled (already the case on this machine). +- Prefer `pkgs.mkShell` for dev shells; reach for `mkShellNoCC` only when explaining the distinction. +- Use `nixpkgs.lib.genAttrs` or `flake-utils.lib.eachDefaultSystem` for multi-system outputs; pick one per example and say which in a comment. +- Use `follows` to unify transitive `nixpkgs` inputs when pulling in ecosystem flakes. +- Prefer `inherit` over repetition in attrsets. +- Avoid top-level `with` statements; keep `with` narrowly scoped to package lists. +- Format every `.nix` file with `nixfmt` (RFC 166 style) before committing. + +## Required Validation + +Run these checks for any non-trivial change: + +1. `make fmt-check` +2. `make lint` +3. `make check` + +These map to `nixfmt --check`, `statix check` plus `deadnix`, and `nix flake check` across every numbered example. + +For notes-only changes, `make fmt-check` and a manual read-through suffice. + +## First Contribution Flow + +Use this sequence for your first change: + +1. Read the relevant `notes/` file and the nearest existing example. +2. Add the smallest possible flake or expression demonstrating the new concept. +3. Add a short header comment in the new `flake.nix` stating what the example teaches. +4. Run `nix flake check` inside the new example directory. +5. Run `make fmt-check` and `make lint` from the repository root. +6. Add or update the matching entry in `notes/` if the concept is not yet covered there. + +Example scopes that are good first tasks: + +- Add `02-package/` with a trivial `stdenv.mkDerivation` and one-line install phase. +- Add a `checks` output to `01-devshell/` that asserts a tool is on `$PATH`. +- Add a short section to `notes/003-flakes.md` referencing a newly added example. +- Convert an existing example from a hand-rolled `forAllSystems` to `flake-utils`, or vice versa, with a comment explaining the tradeoff. + +## Testing Expectations + +- This repository has no runtime test suite; "tests" are `nix flake check` outcomes and successful builds of each example's default output. +- Any example that exposes non-trivial behavior (a derivation, a module) should expose a `checks..*` attribute that `nix flake check` exercises. +- Do not merge changes that regress `make check`. + +## Change Design Checklist + +Before coding: + +1. Identify which existing example or notes file the change belongs to, or whether it needs a new `NN-/`. +2. Confirm the change teaches one concept, not several. +3. Confirm `nixpkgs` input choice is consistent with surrounding examples. + +Before submitting: + +1. Verify `make fmt-check`, `make lint`, and `make check` pass. +2. Verify every modified flake's `flake.lock` is committed. +3. Verify `notes/` accurately reflects what the examples now demonstrate. + +## Review Guidelines (P0/P1 Focus) + +Review output should be concise and only include critical issues. + +- `P0`: must-fix defects (a flake fails to evaluate, an example documents the wrong mechanism, notes contradict the code). +- `P1`: high-priority defects (eval warnings, missing `flake.lock`, unpinned or inconsistent inputs, misleading comment). + +Do not include: + +- style-only nitpicks, +- praise or summary of what is already good, +- exhaustive restatement of the patch. + +Use this review format: + +1. `Severity` (`P0`/`P1`) +2. `File:line` +3. `Issue` +4. `Why it matters` +5. `Minimal fix direction` + +## Practical Notes for Agents + +- Prefer targeted edits over broad mechanical rewrites across examples. +- If two examples disagree on a convention, prefer the newer one and update the older example in a dedicated commit. +- When uncertain whether a concept deserves its own example, start by expanding the notes; promote to an example once the idea stabilizes. +- Keep presentational prose in `notes/`. Keep runnable material in numbered directories. Do not cross the streams. +- Keep user-facing naming consistent with the repository name: `nix-playground`. The directory spelling `nix-playgraound` is intentional and should not be "fixed". + +## Commit and PR Hygiene + +- Keep commits scoped to one logical change: one example, one notes update, one convention shift. +- Commit `flake.lock` in the same commit that introduces or updates the `flake.nix` it belongs to. +- PR descriptions should include: + 1. what concept the change teaches or clarifies, + 2. which example directories or notes files are affected, + 3. any new `inputs` added and why, + 4. output of `make check` (pass/fail). + +Suggested PR checklist: + +- [ ] `make fmt-check` passes +- [ ] `make lint` passes +- [ ] `make check` passes +- [ ] `flake.lock` committed for every new or updated `flake.nix` +- [ ] Notes updated where the change introduces or changes a concept diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d7c3ca9 --- /dev/null +++ b/Makefile @@ -0,0 +1,133 @@ +# Variables +# Every numbered directory at the repository root is treated as a self-contained flake example. +EXAMPLES := $(sort $(wildcard [0-9][0-9]-*)) + +# Tools are invoked via `nix run` so nothing needs to be pre-installed beyond Nix itself. +# Override any of these if you have the tools on $PATH and want faster startup. +NIX ?= nix +NIXFMT ?= $(NIX) run nixpkgs#nixfmt-rfc-style -- +STATIX ?= $(NIX) run nixpkgs#statix -- +DEADNIX ?= $(NIX) run nixpkgs#deadnix -- + +# Selector for single-example targets (dev, show, update-one). +EXAMPLE ?= + +# Default target +.DEFAULT_GOAL := help + +.PHONY: help +help: ## Show help messages for all available targets + @grep -E '^[a-zA-Z_-]+:.*## .*$$' Makefile | \ + awk 'BEGIN {FS = ":.*## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +.PHONY: examples +examples: ## List discovered example directories + @echo "Examples:" + @for e in $(EXAMPLES); do echo " - $$e"; done + +.PHONY: fmt +fmt: ## Format every .nix file with nixfmt (RFC 166 style) + @echo "Formatting .nix files..." + @find . -type f -name '*.nix' -not -path './.git/*' -print0 | xargs -0 -r $(NIXFMT) + +.PHONY: fmt-check +fmt-check: ## Check formatting of every .nix file (non-zero exit on drift) + @echo "Checking .nix formatting..." + @find . -type f -name '*.nix' -not -path './.git/*' -print0 | xargs -0 -r $(NIXFMT) --check + +.PHONY: lint +lint: ## Run statix and deadnix against every example + @echo "Running statix..." + @$(STATIX) check . + @echo "Running deadnix..." + @$(DEADNIX) --fail . + +.PHONY: fix-lint +fix-lint: ## Apply statix and deadnix autofixes where possible + @echo "Applying statix fixes..." + @$(STATIX) fix . + @echo "Applying deadnix fixes..." + @$(DEADNIX) --edit . + +.PHONY: check +check: ## Run `nix flake check` in every example directory + @if [ -z "$(EXAMPLES)" ]; then echo "No examples found."; exit 0; fi + @set -e; for e in $(EXAMPLES); do \ + echo ">>> nix flake check $$e"; \ + (cd "$$e" && $(NIX) flake check); \ + done + +.PHONY: build +build: ## Run `nix build` on the default package of every example (skips those without one) + @if [ -z "$(EXAMPLES)" ]; then echo "No examples found."; exit 0; fi + @set -e; for e in $(EXAMPLES); do \ + if $(NIX) eval --no-warn-dirty "$$e#packages.$$($(NIX) eval --impure --raw --expr 'builtins.currentSystem').default" --apply 'x: true' >/dev/null 2>&1; then \ + echo ">>> nix build $$e"; \ + (cd "$$e" && $(NIX) build --no-link); \ + else \ + echo "--- $$e has no default package, skipping"; \ + fi; \ + done + +.PHONY: show +show: ## Print `nix flake show` for every example + @if [ -z "$(EXAMPLES)" ]; then echo "No examples found."; exit 0; fi + @for e in $(EXAMPLES); do \ + echo ">>> nix flake show $$e"; \ + (cd "$$e" && $(NIX) flake show) || true; \ + done + +.PHONY: update +update: ## Run `nix flake update` in every example (updates all flake.lock files) + @if [ -z "$(EXAMPLES)" ]; then echo "No examples found."; exit 0; fi + @set -e; for e in $(EXAMPLES); do \ + echo ">>> nix flake update $$e"; \ + (cd "$$e" && $(NIX) flake update); \ + done + +.PHONY: update-one +update-one: ## Run `nix flake update` in EXAMPLE= + @if [ -z "$(EXAMPLE)" ]; then echo "Usage: make update-one EXAMPLE="; exit 1; fi + @(cd "$(EXAMPLE)" && $(NIX) flake update) + +.PHONY: dev +dev: ## Enter `nix develop` in EXAMPLE= + @if [ -z "$(EXAMPLE)" ]; then echo "Usage: make dev EXAMPLE="; exit 1; fi + @(cd "$(EXAMPLE)" && $(NIX) develop) + +.PHONY: metadata +metadata: ## Print `nix flake metadata` for every example + @for e in $(EXAMPLES); do \ + echo ">>> nix flake metadata $$e"; \ + (cd "$$e" && $(NIX) flake metadata) || true; \ + done + +.PHONY: clean +clean: ## Remove `result` symlinks produced by `nix build` + @echo "Removing result symlinks..." + @find . -maxdepth 3 -type l \( -name 'result' -o -name 'result-*' \) -print -delete + +.PHONY: gc +gc: ## Run `nix store gc` to garbage collect unreferenced store paths + @echo "Running nix store gc..." + @$(NIX) store gc + +.PHONY: setup-hooks +setup-hooks: ## Install Git hooks (pre-commit) + @echo "Setting up Git hooks..." + @if ! command -v pre-commit >/dev/null 2>&1; then \ + echo "pre-commit not found. Install it via 'pip install pre-commit' or enter a shell that provides it."; \ + exit 1; \ + fi + @pre-commit install --hook-type pre-commit + @pre-commit install --hook-type pre-push + @pre-commit install-hooks + +.PHONY: test-hooks +test-hooks: ## Run pre-commit hooks across all files + @echo "Running pre-commit hooks on all files..." + @pre-commit run --all-files --show-diff-on-failure + +.PHONY: all +all: fmt-check lint check ## Run format check, lint, and flake checks + @echo "All checks passed." diff --git a/notes/001-glossary.md b/notes/001-glossary.md new file mode 100644 index 0000000..1300123 --- /dev/null +++ b/notes/001-glossary.md @@ -0,0 +1,59 @@ +# Glossary + +Core vocabulary you'll hit in the first hour of Nix. Skim, don't memorize — come back as needed. + +## The store and builds + +- **Nix store** — `/nix/store`. Immutable, content-addressed directory where *everything* Nix builds lives. Each entry is prefixed by a hash of its inputs. +- **Store path** — a single entry under `/nix/store`, e.g. `/nix/store/abc123…-hello-2.12.1`. The hash makes it unique per set of build inputs. +- **Derivation (`.drv`)** — a *recipe* for a build: inputs, builder script, env vars, outputs. Produced by evaluating Nix code. Not the built artifact itself. +- **Realisation / "realising a derivation"** — actually executing the `.drv` to produce the output store path(s). "Build" colloquially means this. +- **Output path** — the store path a derivation produces. A derivation can have multiple outputs (e.g. `out`, `dev`, `man`, `lib`). +- **Closure** — a store path plus all its runtime dependencies, transitively. What you need to copy to another machine to actually use something. +- **Hash** — content/input hash that names store paths. Changing any input changes the hash, which changes the path — this is how Nix guarantees no cache collisions. +- **Fixed-output derivation (FOD)** — a derivation whose output hash you declare up front (e.g. source tarballs, `fetchurl`). Allowed network access during build; everything else is sandboxed offline. + +## Language + +- **Nix (the language)** — lazy, purely functional, dynamically typed. Every `.nix` file is one expression. +- **Attrset** — `{ a = 1; b = "x"; }`. The main data structure. +- **Lambda** — `x: x + 1` or `{ a, b }: a + b`. Functions take one argument; multi-arg functions are attrset-destructured. +- **`let ... in`** — local bindings: `let x = 1; in x + 1`. +- **`with expr; body`** — brings `expr`'s attrs into scope for `body`. Common in `with pkgs; [ hello jq ]`. Useful but can shadow bindings — use sparingly. +- **`inherit`** — shorthand to pull names from outer scope into an attrset: `{ inherit pkgs system; }` ≡ `{ pkgs = pkgs; system = system; }`. +- **`import`** — evaluate another `.nix` file. `import ./foo.nix { }` imports and calls it. +- **`callPackage`** — a nixpkgs convention that auto-supplies function arguments from a package set. You'll see it everywhere in nixpkgs. + +## Package management + +- **nixpkgs** — the big community-maintained collection of Nix expressions for ~100k packages. Lives at `github:NixOS/nixpkgs`. +- **Channel** — a named, periodically-updated snapshot of nixpkgs (e.g. `nixos-unstable`, `nixos-25.11`). Pre-flakes way to pin. Flakes mostly replace this with lockfiles. +- **Overlay** — a function that extends/overrides a package set. Lets you patch, pin, or add packages without forking nixpkgs. +- **Profile** — a symlink tree representing "what's installed" for a user or system. `nix-env`, `nix profile`, NixOS, and home-manager all manage profiles. +- **Generation** — a versioned snapshot of a profile. Every change creates a new generation; old ones stay until garbage-collected. This is how rollback works. +- **Garbage collection (`nix-collect-garbage`)** — deletes store paths not reachable from any "GC root" (profiles, running processes, `result` symlinks). + +## Flakes + +- **Flake** — a directory with a `flake.nix`. Has pinned `inputs` and structured `outputs`. Reproducible, composable, schema-driven. +- **`flake.lock`** — JSON file pinning the exact revision (and hash) of each input. Commit it. +- **Flake reference** — URL-like string identifying a flake: `github:NixOS/nixpkgs`, `path:./foo`, `git+https://…`, `nixpkgs` (registry alias). +- **Registry** — short-name → flake-ref mapping. `nixpkgs` resolves via the global registry by default. +- **Pure evaluation** — flakes evaluate in a sandbox: no env vars, no arbitrary filesystem reads, only declared inputs. This is what makes them reproducible. + +## NixOS & home-manager + +- **NixOS module** — a function `{ config, lib, pkgs, ... }: { options = …; config = …; }`. The unit of NixOS configuration. +- **home-manager** — user-level declarative config (dotfiles, per-user packages, services). Works standalone or as a NixOS module. + +## CLI: old vs. new + +| Old (pre-flakes) | New (flakes) | +|---|---| +| `nix-build` | `nix build` | +| `nix-shell` | `nix develop` | +| `nix-env -iA` | `nix profile install` | +| `nix-channel` | `flake.lock` + inputs | +| `` (NIX_PATH) | `inputs.nixpkgs` | + +Both still work. Flakes are the direction of travel; old commands aren't going away soon. diff --git a/notes/002-nix-primer.md b/notes/002-nix-primer.md new file mode 100644 index 0000000..9115355 --- /dev/null +++ b/notes/002-nix-primer.md @@ -0,0 +1,205 @@ +# 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 {} }: + +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/-/` 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 ` 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 `, 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. diff --git a/notes/003-flakes.md b/notes/003-flakes.md new file mode 100644 index 0000000..5bca173 --- /dev/null +++ b/notes/003-flakes.md @@ -0,0 +1,241 @@ +# Flakes + +A **flake** is a directory containing `flake.nix` (and, once evaluated, `flake.lock`). It's a packaging convention on top of regular Nix that gives you: + +- **Pinned inputs** via `flake.lock` → reproducible builds. +- **A fixed output schema** → tools know where to look (`devShells..default`, `packages..default`, etc.). +- **Composability** → flakes depend on other flakes by URL. +- **Pure evaluation** → no silent dependence on `NIX_PATH`, env vars, or random files. + +Flakes are still marked "experimental" but are overwhelmingly the way new Nix code is written. + +--- + +## 1. Anatomy of a `flake.nix` + +```nix +{ + description = "…"; # optional human-readable summary + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + # Pattern: make a transitive input follow yours, to avoid duplicates + flake-utils.url = "github:numtide/flake-utils"; + flake-utils.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = { self, nixpkgs, flake-utils, ... }: { + # arbitrary attrs here — but names with meaning to `nix` CLI must match the schema + }; +} +``` + +Two top-level keys matter: `inputs` and `outputs`. `description` is cosmetic. + +### Inputs + +Each input has a **flake reference** URL. Common forms: + +``` +github:owner/repo # latest default branch +github:owner/repo/branch-or-tag # specific ref +github:owner/repo/abc123 # specific commit +git+https://example.com/repo.git +git+ssh://git@example.com/repo +path:./local-flake # local path +tarball+https://…/src.tar.gz +nixpkgs # registry alias (resolves via `nix registry`) +``` + +Non-flake sources (a repo without `flake.nix`): +```nix +inputs.some-src = { url = "github:owner/repo"; flake = false; }; +# Access raw files via self.inputs.some-src +``` + +### Outputs + +A function: `{ self, ...inputs }: `. The attrset keys are *conventions* the `nix` CLI knows about: + +| Output path | What it is | Command | +|---|---|---| +| `devShells..` | A `mkShell` — dev environment | `nix develop [.#]` | +| `packages..` | A derivation — build target | `nix build [.#]` | +| `apps..` | `{ type = "app"; program = …; }` | `nix run [.#]` | +| `nixosConfigurations.` | `nixpkgs.lib.nixosSystem { … }` | `nixos-rebuild switch --flake .#` | +| `homeConfigurations.` | home-manager config | `home-manager switch --flake .#` | +| `nixosModules.` | reusable NixOS module | imported by other flakes | +| `overlays.` | nixpkgs overlay | imported by other flakes | +| `formatter.` | a formatter derivation | `nix fmt` | +| `checks..` | derivations run by `nix flake check` | `nix flake check` | +| `templates.` | scaffold for `nix flake init` | `nix flake init -t .#` | + +Default attrs `default` are picked when you omit the name: `nix build` ≡ `nix build .#default`. + +`.` is the platform triple: `x86_64-linux`, `aarch64-linux`, `aarch64-darwin`, `x86_64-darwin`. + +--- + +## 2. The lockfile + +`flake.lock` pins the exact commit + NAR hash of every input, transitively. Generated/updated automatically on first use or `nix flake update`. + +- **Commit it.** Without it, "my flake" means different things on different machines. +- **Update explicitly.** `nix flake update` (all inputs) or `nix flake update nixpkgs` (one input). +- **Inspect.** `nix flake metadata` prints the resolved URLs and revisions. +- **`follows`** deduplicates: if both `flake-utils` and `foo` want their own `nixpkgs`, `follows = "nixpkgs";` forces them to use yours. Without this, you can end up with multiple nixpkgs copies and subtle version skew. + +--- + +## 3. Multi-system support + +A flake output has to enumerate systems explicitly. Two common patterns: + +### (a) Manual `forAllSystems` + +```nix +outputs = { self, nixpkgs }: + let + systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ]; + forAllSystems = f: + nixpkgs.lib.genAttrs systems (system: f (import nixpkgs { inherit system; })); + in { + packages = forAllSystems (pkgs: { default = pkgs.hello; }); + devShells = forAllSystems (pkgs: { default = pkgs.mkShell { packages = [ pkgs.jq ]; }; }); + }; +``` + +Zero external deps, explicit. + +### (b) `flake-utils` / `flake-parts` + +```nix +inputs.flake-utils.url = "github:numtide/flake-utils"; + +outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let pkgs = import nixpkgs { inherit system; }; in { + packages.default = pkgs.hello; + devShells.default = pkgs.mkShell { packages = [ pkgs.jq ]; }; + }); +``` + +Less boilerplate, but adds a dependency. For bigger projects, `flake-parts` is increasingly popular — it gives you a module system for flakes themselves. + +--- + +## 4. Flake URIs: `.#` syntax + +Everywhere you point `nix` at a flake, the form is `#`: + +```bash +nix build . # current dir, packages..default +nix build .#foo # packages..foo +nix build github:owner/repo#foo # remote flake +nix develop .#ci # devShells..ci +nix run nixpkgs#hello # pkgs.hello from the nixpkgs registry flake +nixos-rebuild switch --flake .#my-host +``` + +The `` portion is auto-inferred from your machine — you don't spell it out in the CLI. + +--- + +## 5. Pure evaluation — what you lose, what you gain + +Flakes evaluate in pure mode: + +- No reading `NIX_PATH` or ``. +- No arbitrary `builtins.getEnv` or reading files outside the flake. +- `builtins.currentTime` / `currentSystem` are blocked. +- Source is pulled from the flake's input tree, not the working directory, unless you're in `self`. Files not tracked by git are **invisible** to the flake by default. + +This last point surprises people: **`git add` a file before `nix build` can see it.** (`nix build --impure` escapes this, but then you're not reproducible.) + +In exchange: anyone with your `flake.nix` + `flake.lock` gets bit-identical evaluation. + +--- + +## 6. Common commands + +```bash +nix flake init # scaffold a flake.nix in current dir +nix flake init -t github:nix-community/templates#rust +nix flake show # tree of all outputs +nix flake metadata # locked inputs + paths +nix flake check # evaluate & build all `checks` outputs +nix flake update # bump all inputs +nix flake update nixpkgs # bump one input +nix flake lock --override-input nixpkgs github:NixOS/nixpkgs/nixos-24.11 +nix develop # enter devShells..default +nix develop .#ci # named shell +nix build # build packages..default → ./result +nix run # run apps..default (or packages..default) +``` + +--- + +## 7. Recurring patterns worth recognizing + +### Rust / Go / Python dev shell +```nix +devShells.default = pkgs.mkShell { + packages = [ pkgs.cargo pkgs.rustc pkgs.rust-analyzer ]; + env.RUST_LOG = "debug"; + shellHook = ''export FOO=bar''; +}; +``` + +### Package a local source tree +```nix +packages.default = pkgs.rustPlatform.buildRustPackage { + pname = "my-tool"; + version = "0.1.0"; + src = ./.; + cargoLock.lockFile = ./Cargo.lock; +}; +``` + +### Expose a NixOS module +```nix +nixosModules.default = import ./module.nix; +# Consumer does: imports = [ inputs.my-flake.nixosModules.default ]; +``` + +### NixOS system config as a flake +```nix +nixosConfigurations.my-laptop = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ ./configuration.nix ]; +}; +# Apply with: nixos-rebuild switch --flake .#my-laptop +``` + +### Overlay as an output +```nix +overlays.default = final: prev: { + my-patched-foo = prev.foo.overrideAttrs (old: { … }); +}; +``` + +--- + +## 8. Debugging tips + +- **`nix flake show --all-systems`** to see outputs for every platform, not just yours. +- **`nix eval .#packages.x86_64-linux.default.drvPath`** to get the `.drv` without building. +- **`nix eval --json .#devShells.x86_64-linux.default.buildInputs | jq`** to inspect shell deps. +- **`--show-trace`** on any command for full eval backtraces. +- **`nix repl .`** loads your flake's outputs into a REPL — great for poking at attrs. +- If an input seems stuck on an old rev, check `flake.lock`; `nix flake update ` fixes it. + +--- + +## 9. When flakes are overkill + +- Running a one-off tool: `nix run nixpkgs#ripgrep` — no flake needed. +- A tiny personal script: plain `default.nix` + `nix-build` is still fine. +- Large monorepos with complex CI: consider `flake-parts` or hybrid setups (flake for the interface, regular Nix for guts). + +Flakes are a **packaging interface**, not a requirement. The old non-flake world still works — flakes just compose better once you have more than one Nix project. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5d4b699 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "nix-playground" +version = "0.1.0" +description = "The Python environment for the `nix-playground` project" + +requires-python = ">=3.10,<4.0" + +dependencies = [ + "pre-commit (>=4.2.0,<5.0.0)", + "icecream (>=2.1.4,<3.0.0)", +]