The base commit

This commit is contained in:
Hassan Abedi 2026-04-15 11:49:36 +02:00
commit 35402cccda
11 changed files with 1041 additions and 0 deletions

22
.editorconfig Normal file
View File

@ -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

38
.gitattributes vendored Normal file
View File

@ -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

85
.gitignore vendored Normal file
View File

@ -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

44
.pre-commit-config.yaml Normal file
View File

@ -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 ]

33
01-devshell/flake.nix Normal file
View File

@ -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.<system>.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"
'';
};
};
}

170
AGENTS.md Normal file
View File

@ -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-<topic>/`: 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-<short-topic>/` 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.<system>.*`, 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.<system>.*` 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-<topic>/`.
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

133
Makefile Normal file
View File

@ -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=<dir>
@if [ -z "$(EXAMPLE)" ]; then echo "Usage: make update-one EXAMPLE=<dir>"; exit 1; fi
@(cd "$(EXAMPLE)" && $(NIX) flake update)
.PHONY: dev
dev: ## Enter `nix develop` in EXAMPLE=<dir>
@if [ -z "$(EXAMPLE)" ]; then echo "Usage: make dev EXAMPLE=<dir>"; 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."

59
notes/001-glossary.md Normal file
View File

@ -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 |
| `<nixpkgs>` (NIX_PATH) | `inputs.nixpkgs` |
Both still work. Flakes are the direction of travel; old commands aren't going away soon.

205
notes/002-nix-primer.md Normal file
View File

@ -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 <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.

241
notes/003-flakes.md Normal file
View File

@ -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.<system>.default`, `packages.<system>.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 }: <attrset>`. The attrset keys are *conventions* the `nix` CLI knows about:
| Output path | What it is | Command |
|---|---|---|
| `devShells.<system>.<name>` | A `mkShell` — dev environment | `nix develop [.#<name>]` |
| `packages.<system>.<name>` | A derivation — build target | `nix build [.#<name>]` |
| `apps.<system>.<name>` | `{ type = "app"; program = …; }` | `nix run [.#<name>]` |
| `nixosConfigurations.<host>` | `nixpkgs.lib.nixosSystem { … }` | `nixos-rebuild switch --flake .#<host>` |
| `homeConfigurations.<user>` | home-manager config | `home-manager switch --flake .#<user>` |
| `nixosModules.<name>` | reusable NixOS module | imported by other flakes |
| `overlays.<name>` | nixpkgs overlay | imported by other flakes |
| `formatter.<system>` | a formatter derivation | `nix fmt` |
| `checks.<system>.<name>` | derivations run by `nix flake check` | `nix flake check` |
| `templates.<name>` | scaffold for `nix flake init` | `nix flake init -t .#<name>` |
Default attrs `default` are picked when you omit the name: `nix build``nix build .#default`.
`.<system>` 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: `.#<name>` syntax
Everywhere you point `nix` at a flake, the form is `<flake-ref>#<attr>`:
```bash
nix build . # current dir, packages.<sys>.default
nix build .#foo # packages.<sys>.foo
nix build github:owner/repo#foo # remote flake
nix develop .#ci # devShells.<sys>.ci
nix run nixpkgs#hello # pkgs.hello from the nixpkgs registry flake
nixos-rebuild switch --flake .#my-host
```
The `<system>` 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 `<nixpkgs>`.
- 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.<sys>.default
nix develop .#ci # named shell
nix build # build packages.<sys>.default → ./result
nix run # run apps.<sys>.default (or packages.<sys>.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 <name>` 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.

11
pyproject.toml Normal file
View File

@ -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)",
]