From d2814acdc9a3771cddd571bb7d5b4117e508cfd8 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Wed, 15 Apr 2026 13:44:59 +0200 Subject: [PATCH] WIP --- 01-devshell/flake.nix | 4 +-- notes/001-glossary.md | 68 ++++++++++++++++++++--------------------- notes/002-nix-primer.md | 62 ++++++++++++++++++------------------- notes/003-flakes.md | 62 ++++++++++++++++++------------------- 4 files changed, 98 insertions(+), 98 deletions(-) diff --git a/01-devshell/flake.nix b/01-devshell/flake.nix index f02adcc..4aae071 100644 --- a/01-devshell/flake.nix +++ b/01-devshell/flake.nix @@ -1,8 +1,8 @@ { - description = "A minimal dev shell — your first flake"; + 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 + # `nixpkgs` is the main 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"; diff --git a/notes/001-glossary.md b/notes/001-glossary.md index 1300123..35ba31a 100644 --- a/notes/001-glossary.md +++ b/notes/001-glossary.md @@ -1,52 +1,52 @@ # Glossary -Core vocabulary you'll hit in the first hour of Nix. Skim, don't memorize — come back as needed. +Core vocabulary you'll hit in the first hour of Nix. Skim, don't memorize; come back as needed. -## The store and builds +## 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. +- **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, covering inputs, builder script, env vars, and 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. +- **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, for example `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, so 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 +## 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). +- **nixpkgs**: the 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 or 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, and `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. +- **Flake**: a directory with a `flake.nix`. Has pinned `inputs` and structured `outputs`. Reproducible, composable, and 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://…`, or `nixpkgs` (registry alias). +- **Registry**: short-name to flake-ref mapping. `nixpkgs` resolves via the global registry by default. +- **Pure evaluation**: flakes evaluate in a sandbox with no env vars, no arbitrary filesystem reads, and only declared inputs. This is what makes them reproducible. -## NixOS & home-manager +## NixOS and 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. +- **NixOS module**: a function `{ config, lib, pkgs, ... }: { options = …; config = …; }`. The unit of NixOS configuration. +- **home-manager**: user-level declarative config for dotfiles, per-user packages, and services. Works standalone or as a NixOS module. -## CLI: old vs. new +## CLI: Old vs. New | Old (pre-flakes) | New (flakes) | |---|---| diff --git a/notes/002-nix-primer.md b/notes/002-nix-primer.md index 9115355..6f4d78e 100644 --- a/notes/002-nix-primer.md +++ b/notes/002-nix-primer.md @@ -9,9 +9,9 @@ Confusingly, both are called "Nix." Separate them in your head and everything ge --- -## 1. The mental model: eval, then build +## 1. The Mental Model: Eval, Then Build -Every `nix build` / `nix develop` runs in two phases: +Every `nix build` or `nix develop` runs in two phases: ``` .nix source ──(evaluate)──▶ .drv file ──(realise)──▶ /nix/store/...-output @@ -23,13 +23,13 @@ Every `nix build` / `nix develop` runs in two phases: - **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. +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 +## 2. The Language in 5 Minutes -### Primitives and collections +### Primitives and Collections ```nix 42 # integer @@ -40,7 +40,7 @@ You can evaluate without building (`nix eval`, `nix-instantiate`) and you can bu true false null [ 1 2 3 ] # list (space-separated, NOT comma) { a = 1; b = "two"; } # attrset -/foo/bar # path (literal — not a string!) +/foo/bar # path (literal, not a string!) ./relative/path # path relative to current file ``` @@ -69,7 +69,7 @@ The `...` means "ignore extra attrs." Without it, passing extras is an error. ```nix let x = 1; - y = x + 1; # can reference earlier bindings (and each other — lazy) + y = x + 1; # can reference earlier bindings (and each other: lazy) in x + y ``` @@ -78,7 +78,7 @@ in x + y { inherit (pkgs) hello jq; } # pulls hello/jq from pkgs into this attrset ``` -### `with` (use cautiously) +### `with` (Use Cautiously) ```nix with pkgs; [ hello jq ripgrep ] @@ -86,11 +86,11 @@ 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. +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. +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` @@ -105,7 +105,7 @@ import ./foo.nix { x = 1; } # if foo.nix is a function, also call it --- -## 3. Derivations — the core primitive +## 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). @@ -117,7 +117,7 @@ Minimal example: pkgs.stdenv.mkDerivation { pname = "hello-note"; version = "0.1"; - src = ./.; # path — gets copied into the store + src = ./.; # path: gets copied into the store installPhase = '' mkdir -p $out/bin echo '#!/bin/sh' > $out/bin/hello-note @@ -132,40 +132,40 @@ 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. +- **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 +## 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. +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 / 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. +- **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 +## 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. +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 +## 6. Minimum Useful Commands ```bash # Language / introspection @@ -187,7 +187,7 @@ nix why-depends ./result nixpkgs#glibc # trace dependency chains --- -## 7. Gotchas that trip up newcomers +## 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. @@ -198,8 +198,8 @@ nix why-depends ./result nixpkgs#glibc # trace dependency chains --- -## 8. What to read/try next +## 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. +- 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 index 5bca173..c4c5c10 100644 --- a/notes/003-flakes.md +++ b/notes/003-flakes.md @@ -2,12 +2,12 @@ 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. +- **Pinned inputs** via `flake.lock` for reproducible builds. +- **A fixed output schema** so tools know where to look (`devShells..default`, `packages..default`, and so on). +- **Composability**, since flakes depend on other flakes by URL. +- **Pure evaluation**, with 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. +Flakes are still marked "experimental" but are now the way most new Nix code is written. --- @@ -26,7 +26,7 @@ Flakes are still marked "experimental" but are overwhelmingly the way new Nix co }; outputs = { self, nixpkgs, flake-utils, ... }: { - # arbitrary attrs here — but names with meaning to `nix` CLI must match the schema + # arbitrary attrs here, but names with meaning to `nix` CLI must match the schema }; } ``` @@ -60,8 +60,8 @@ A function: `{ self, ...inputs }: `. The attrset keys are *conventions* | Output path | What it is | Command | |---|---|---| -| `devShells..` | A `mkShell` — dev environment | `nix develop [.#]` | -| `packages..` | A derivation — build target | `nix build [.#]` | +| `devShells..` | A `mkShell`, a dev environment | `nix develop [.#]` | +| `packages..` | A derivation, a 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 .#` | @@ -73,13 +73,13 @@ A function: `{ self, ...inputs }: `. The attrset keys are *conventions* 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`. +`.` is the platform triple: `x86_64-linux`, `aarch64-linux`, `aarch64-darwin`, or `x86_64-darwin`. --- -## 2. The lockfile +## 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`. +`flake.lock` pins the exact commit and NAR hash of every input, transitively. Generated or updated automatically on first use or via `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). @@ -88,7 +88,7 @@ Default attrs `default` are picked when you omit the name: `nix build` ≡ `nix --- -## 3. Multi-system support +## 3. Multi-System Support A flake output has to enumerate systems explicitly. Two common patterns: @@ -121,11 +121,11 @@ outputs = { self, nixpkgs, flake-utils }: }); ``` -Less boilerplate, but adds a dependency. For bigger projects, `flake-parts` is increasingly popular — it gives you a module system for flakes themselves. +Less boilerplate, at the cost of one dependency. For larger projects, `flake-parts` is increasingly popular: it gives you a module system for flakes themselves. --- -## 4. Flake URIs: `.#` syntax +## 4. Flake URIs: `.#` Syntax Everywhere you point `nix` at a flake, the form is `#`: @@ -138,26 +138,26 @@ 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. +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 +## 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. +- `builtins.currentTime` and `builtins.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. +In exchange: anyone with your `flake.nix` plus `flake.lock` gets bit-identical evaluation. --- -## 6. Common commands +## 6. Common Commands ```bash nix flake init # scaffold a flake.nix in current dir @@ -176,9 +176,9 @@ nix run # run apps..default (or packages..d --- -## 7. Recurring patterns worth recognizing +## 7. Recurring Patterns Worth Recognizing -### Rust / Go / Python dev shell +### Rust / Go / Python Dev Shell ```nix devShells.default = pkgs.mkShell { packages = [ pkgs.cargo pkgs.rustc pkgs.rust-analyzer ]; @@ -187,7 +187,7 @@ devShells.default = pkgs.mkShell { }; ``` -### Package a local source tree +### Package a Local Source Tree ```nix packages.default = pkgs.rustPlatform.buildRustPackage { pname = "my-tool"; @@ -197,13 +197,13 @@ packages.default = pkgs.rustPlatform.buildRustPackage { }; ``` -### Expose a NixOS module +### Expose a NixOS Module ```nix nixosModules.default = import ./module.nix; # Consumer does: imports = [ inputs.my-flake.nixosModules.default ]; ``` -### NixOS system config as a flake +### NixOS System Config as a Flake ```nix nixosConfigurations.my-laptop = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; @@ -212,7 +212,7 @@ nixosConfigurations.my-laptop = nixpkgs.lib.nixosSystem { # Apply with: nixos-rebuild switch --flake .#my-laptop ``` -### Overlay as an output +### Overlay as an Output ```nix overlays.default = final: prev: { my-patched-foo = prev.foo.overrideAttrs (old: { … }); @@ -221,21 +221,21 @@ overlays.default = final: prev: { --- -## 8. Debugging tips +## 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. +- **`nix repl .`** loads your flake's outputs into a REPL: useful 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 +## 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. +- Running a one-off tool: `nix run nixpkgs#ripgrep`, no flake needed. +- A tiny personal script: plain `default.nix` plus `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. +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.