Add a more advanced haskell project

This commit is contained in:
Hassan Abedi 2026-04-21 14:19:00 +02:00
parent cc55974109
commit 660ba99f17
19 changed files with 409 additions and 76 deletions

View File

@ -8,7 +8,7 @@ indent_size = 4
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.{rs,py}] [*.{nix,flake,hs}]
max_line_length = 100 max_line_length = 100
[*.md] [*.md]

View File

@ -0,0 +1,23 @@
# 06-haskell-shellfor
This example shows a Haskell dev shell built with `shellFor`.
It includes:
- a local package added to the Haskell package set,
- a dev shell derived from that package with `shellFor`, and
- a small test suite run by `nix flake check`.
Useful commands:
```bash
nix develop
cabal run
cabal test
nix build
./result/bin/mini-shellfor
nix run
nix flake check
```

25
07-haskell-deps/README.md Normal file
View File

@ -0,0 +1,25 @@
# 07-haskell-deps
This example shows a small Haskell project that uses external libraries from the package set.
It includes:
- a library that parses JSON with `aeson`,
- `text` and `bytestring` usage in the library and CLI,
- an executable under `app/`, and
- a test suite run by `nix flake check`.
Useful commands:
```bash
nix develop
cabal run
cabal run -- '{"name":"flakes"}'
cabal test
nix build
./result/bin/mini-json '{"name":"flakes"}'
nix run . -- '{"name":"flakes"}'
nix flake check
```

View File

@ -0,0 +1,18 @@
module Main where
import qualified Data.ByteString.Char8 as ByteString
import MiniJson.Greeting (greetFromJson)
import System.Environment (getArgs)
import System.Exit (die)
main :: IO ()
main = do
args <- getArgs
let input =
case args of
[] -> "{\"name\":\"learner\"}"
firstArg : _ -> firstArg
case greetFromJson (ByteString.pack input) of
Left err -> die err
Right message -> putStrLn message

27
07-haskell-deps/flake.lock generated Normal file
View File

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1776548001,
"narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

40
07-haskell-deps/flake.nix Normal file
View File

@ -0,0 +1,40 @@
{
# Builds a small Haskell program that uses external libraries from the
# package set, so the example shows how Cabal dependencies flow through Nix.
description = "A Haskell project with external dependencies";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs =
{ self, nixpkgs, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
inherit (pkgs) haskellPackages;
project = haskellPackages.callCabal2nix "mini-json" ./. { };
checkedProject = pkgs.haskell.lib.doCheck project;
in
{
packages.${system}.default = project;
apps.${system}.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/mini-json";
meta.description = "Run the Haskell JSON parsing example.";
};
devShells.${system}.default = pkgs.mkShell {
packages = [
haskellPackages.ghc
pkgs.cabal-install
pkgs.haskell-language-server
];
};
# `doCheck` runs the Cabal test suite, which exercises both valid and
# invalid JSON input through the library function.
checks.${system}.test-suite = checkedProject;
};
}

View File

@ -0,0 +1,33 @@
cabal-version: 2.4
name: mini-json
version: 0.1.0.0
build-type: Simple
library
exposed-modules: MiniJson.Greeting
hs-source-dirs: src
build-depends:
aeson,
base >=4.14 && <5,
bytestring,
text
default-language: Haskell2010
executable mini-json
main-is: Main.hs
hs-source-dirs: app
build-depends:
base >=4.14 && <5,
bytestring,
mini-json
default-language: Haskell2010
test-suite mini-json-test
type: exitcode-stdio-1.0
main-is: Main.hs
hs-source-dirs: test
build-depends:
base >=4.14 && <5,
bytestring,
mini-json
default-language: Haskell2010

View File

@ -0,0 +1,20 @@
{-# LANGUAGE OverloadedStrings #-}
module MiniJson.Greeting where
import Data.Aeson ((.:), FromJSON (parseJSON), eitherDecodeStrict', withObject)
import Data.ByteString (ByteString)
import Data.Text (Text)
import qualified Data.Text as Text
data GreetingRequest = GreetingRequest
{ name :: Text
}
instance FromJSON GreetingRequest where
parseJSON = withObject "GreetingRequest" (\object -> GreetingRequest <$> object .: "name")
greetFromJson :: ByteString -> Either String String
greetFromJson input = do
request <- eitherDecodeStrict' input
pure ("hello, " ++ Text.unpack (name request) ++ ", from aeson")

View File

@ -0,0 +1,14 @@
module Main where
import qualified Data.ByteString.Char8 as ByteString
import MiniJson.Greeting (greetFromJson)
import System.Exit (die)
main :: IO ()
main =
case
( greetFromJson (ByteString.pack "{\"name\":\"flakes\"}")
, greetFromJson (ByteString.pack "{\"missing\":\"name\"}")
) of
(Right "hello, flakes, from aeson", Left _) -> putStrLn "test passed"
_ -> die "unexpected parser result"

View File

@ -37,7 +37,8 @@ Quick examples:
- Do not use em dashes. Restructure the sentence, or use a colon or semicolon instead. - 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". - 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". - 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. - 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 ## Repository Layout
@ -51,7 +52,8 @@ Quick examples:
- `.pre-commit-config.yaml`, `.editorconfig`, `.gitattributes`, `.gitignore`: repository hygiene. - `.pre-commit-config.yaml`, `.editorconfig`, `.gitattributes`, `.gitignore`: repository hygiene.
- `pyproject.toml`: Python environment metadata used only to install `pre-commit`. - `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. 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 ## Example Layout Constraints
@ -105,7 +107,8 @@ Example scopes that are good first tasks:
## Testing Expectations ## Testing Expectations
- This repository has no runtime test suite; "tests" are `nix flake check` outcomes and successful builds of each example's default output. - 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. - 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`. - Do not merge changes that regress `make check`.
## Change Design Checklist ## Change Design Checklist
@ -149,7 +152,8 @@ Use this review format:
- If two examples disagree on a convention, prefer the newer one and update the older example in a dedicated commit. - 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. - 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 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". - 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 ## Commit and PR Hygiene

View File

@ -6,12 +6,15 @@ Core vocabulary you'll hit in the first hour of Nix. Skim, don't memorize; come
- Nix store: `/nix/store`. Immutable, content-addressed directory where *everything* Nix builds lives. Each entry is prefixed by a hash of its inputs. - 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. - 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. - 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, or "realising a derivation": actually executing the `.drv` to produce the output store path(s). "Build" colloquially means this. - Realisation, or "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`). - 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. - 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. - 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
- 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. 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 ## Language
@ -19,7 +22,8 @@ Core vocabulary you'll hit in the first hour of Nix. Skim, don't memorize; come
- Attrset: `{ a = 1; b = "x"; }`. The main data structure. - 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. - 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`. - `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. - `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; }`. - `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. - `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. - `callPackage`: a nixpkgs convention that auto-supplies function arguments from a package set. You'll see it everywhere in nixpkgs.
@ -27,11 +31,14 @@ Core vocabulary you'll hit in the first hour of Nix. Skim, don't memorize; come
## Package Management ## Package Management
- nixpkgs: the community-maintained collection of Nix expressions for ~100k packages. Lives at `github:NixOS/nixpkgs`. - 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. - 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. - 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. - 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. - Generation: a versioned snapshot of a profile. Every change creates a new generation; old ones stay until garbage-collected. This is how rollback
- Garbage collection (`nix-collect-garbage`): deletes store paths not reachable from any "GC root" (profiles, running processes, and `result` symlinks). works.
- Garbage collection (`nix-collect-garbage`): deletes store paths not reachable from any "GC root" (profiles, running processes, and `result`
symlinks).
## Flakes ## Flakes
@ -39,7 +46,8 @@ Core vocabulary you'll hit in the first hour of Nix. Skim, don't memorize; come
- `flake.lock`: JSON file pinning the exact revision (and hash) of each input. Commit it. - `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). - 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. - 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. - 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 and home-manager ## NixOS and home-manager
@ -49,7 +57,7 @@ Core vocabulary you'll hit in the first hour of Nix. Skim, don't memorize; come
## CLI: Old vs. New ## CLI: Old vs. New
| Old (pre-flakes) | New (flakes) | | Old (pre-flakes) | New (flakes) |
|---|---| |------------------------|-----------------------|
| `nix-build` | `nix build` | | `nix-build` | `nix build` |
| `nix-shell` | `nix develop` | | `nix-shell` | `nix develop` |
| `nix-env -iA` | `nix profile install` | | `nix-env -iA` | `nix profile install` |

View File

@ -23,7 +23,8 @@ Every `nix build` or `nix develop` runs in two phases:
- Evaluation runs the Nix language. Pure, in-memory, fast. Produces `.drv` files. - Evaluation runs the Nix language. Pure, in-memory, fast. Produces `.drv` files.
- Realisation actually builds things. Sandboxed. Slow. Outputs go to `/nix/store`. - Realisation actually builds things. Sandboxed. Slow. Outputs go to `/nix/store`.
You can evaluate without building (`nix eval`, `nix-instantiate`) and you can build without touching the language if you already have a `.drv`. Most errors you'll see are *eval* errors: typos in attrsets, missing arguments, and undefined names. 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.
--- ---
@ -107,7 +108,8 @@ 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). 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: Minimal example:
@ -193,7 +195,8 @@ nix why-depends ./result nixpkgs#glibc # trace dependency chains
- `rec { }` makes an attrset self-referential. Without `rec`, attrs can't reference each other. You'll see this in package definitions. - `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. - 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. - `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. - 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. - Hash errors on first build of an FOD: you'll see `got: sha256-…`. Copy that hash into your expression. This is expected workflow.
--- ---

View File

@ -49,6 +49,7 @@ nixpkgs # registry alias (resolves via `nix registr
``` ```
Non-flake sources (a repo without `flake.nix`): Non-flake sources (a repo without `flake.nix`):
```nix ```nix
inputs.some-src = { url = "github:owner/repo"; flake = false; }; inputs.some-src = { url = "github:owner/repo"; flake = false; };
# Access raw files via self.inputs.some-src # Access raw files via self.inputs.some-src
@ -59,7 +60,7 @@ inputs.some-src = { url = "github:owner/repo"; flake = false; };
A function: `{ self, ...inputs }: <attrset>`. The attrset keys are *conventions* the `nix` CLI knows about: A function: `{ self, ...inputs }: <attrset>`. The attrset keys are *conventions* the `nix` CLI knows about:
| Output path | What it is | Command | | Output path | What it is | Command |
|---|---|---| |------------------------------|--------------------------------------|-----------------------------------------|
| `devShells.<system>.<name>` | A `mkShell`, a dev environment | `nix develop [.#<name>]` | | `devShells.<system>.<name>` | A `mkShell`, a dev environment | `nix develop [.#<name>]` |
| `packages.<system>.<name>` | A derivation, a build target | `nix build [.#<name>]` | | `packages.<system>.<name>` | A derivation, a build target | `nix build [.#<name>]` |
| `apps.<system>.<name>` | `{ type = "app"; program = …; }` | `nix run [.#<name>]` | | `apps.<system>.<name>` | `{ type = "app"; program = …; }` | `nix run [.#<name>]` |
@ -79,12 +80,14 @@ Default attrs `default` are picked when you omit the name: `nix build` ≡ `nix
## 2. The Lockfile ## 2. The Lockfile
`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`. `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. - 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). - Update explicitly. `nix flake update` (all inputs) or `nix flake update nixpkgs` (one input).
- Inspect. `nix flake metadata` prints the resolved URLs and revisions. - 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. - `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.
--- ---
@ -121,7 +124,8 @@ outputs = { self, nixpkgs, flake-utils }:
}); });
``` ```
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. 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.
--- ---
@ -149,9 +153,11 @@ Flakes evaluate in pure mode:
- No reading `NIX_PATH` or `<nixpkgs>`. - No reading `NIX_PATH` or `<nixpkgs>`.
- No arbitrary `builtins.getEnv` or reading files outside the flake. - No arbitrary `builtins.getEnv` or reading files outside the flake.
- `builtins.currentTime` and `builtins.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. - 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.) 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` plus `flake.lock` gets bit-identical evaluation. In exchange: anyone with your `flake.nix` plus `flake.lock` gets bit-identical evaluation.
@ -179,6 +185,7 @@ nix run # run apps.<sys>.default (or packages.<sys>.d
## 7. Recurring Patterns Worth Recognizing ## 7. Recurring Patterns Worth Recognizing
### Rust / Go / Python Dev Shell ### Rust / Go / Python Dev Shell
```nix ```nix
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
packages = [ pkgs.cargo pkgs.rustc pkgs.rust-analyzer ]; packages = [ pkgs.cargo pkgs.rustc pkgs.rust-analyzer ];
@ -188,6 +195,7 @@ devShells.default = pkgs.mkShell {
``` ```
### Package a Local Source Tree ### Package a Local Source Tree
```nix ```nix
packages.default = pkgs.rustPlatform.buildRustPackage { packages.default = pkgs.rustPlatform.buildRustPackage {
pname = "my-tool"; pname = "my-tool";
@ -198,12 +206,14 @@ packages.default = pkgs.rustPlatform.buildRustPackage {
``` ```
### Expose a NixOS Module ### Expose a NixOS Module
```nix ```nix
nixosModules.default = import ./module.nix; nixosModules.default = import ./module.nix;
# Consumer does: imports = [ inputs.my-flake.nixosModules.default ]; # Consumer does: imports = [ inputs.my-flake.nixosModules.default ];
``` ```
### NixOS System Config as a Flake ### NixOS System Config as a Flake
```nix ```nix
nixosConfigurations.my-laptop = nixpkgs.lib.nixosSystem { nixosConfigurations.my-laptop = nixpkgs.lib.nixosSystem {
system = "x86_64-linux"; system = "x86_64-linux";
@ -213,6 +223,7 @@ nixosConfigurations.my-laptop = nixpkgs.lib.nixosSystem {
``` ```
### Overlay as an Output ### Overlay as an Output
```nix ```nix
overlays.default = final: prev: { overlays.default = final: prev: {
my-patched-foo = prev.foo.overrideAttrs (old: { … }); my-patched-foo = prev.foo.overrideAttrs (old: { … });
@ -238,4 +249,5 @@ overlays.default = final: prev: {
- A tiny personal script: plain `default.nix` plus `nix-build` is still fine. - 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). - 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.

View File

@ -13,13 +13,15 @@ When you run `nix build` inside `02-package/`:
3. The output lands in `/nix/store/<hash>-greet-0.1.0/`. 3. The output lands in `/nix/store/<hash>-greet-0.1.0/`.
4. A `./result` symlink is created pointing to that store path. 4. A `./result` symlink is created pointing to that store path.
You can then run `./result/bin/greet` directly, or use `nix run` which does step 1 through 3 and then executes the `apps.<system>.default.program` path. You can then run `./result/bin/greet` directly, or use `nix run` which does step 1 through 3 and then executes the `apps.<system>.default.program`
path.
--- ---
## 2. `stdenv.mkDerivation` in Detail ## 2. `stdenv.mkDerivation` in Detail
`stdenv.mkDerivation` is the main builder in nixpkgs. It wraps the low-level `derivation` builtin with a phased build system. The default phases, in order: `stdenv.mkDerivation` is the main builder in nixpkgs. It wraps the low-level `derivation` builtin with a phased build system. The default phases, in
order:
1. `unpackPhase`: extracts `src` (tarball, git checkout, or copied path). 1. `unpackPhase`: extracts `src` (tarball, git checkout, or copied path).
2. `patchPhase`: applies patches from the `patches` list. 2. `patchPhase`: applies patches from the `patches` list.
@ -28,17 +30,22 @@ You can then run `./result/bin/greet` directly, or use `nix run` which does step
5. `installPhase`: copies outputs into `$out`. 5. `installPhase`: copies outputs into `$out`.
6. `fixupPhase`: strips binaries, patches RPATHs, shrink-wraps references. 6. `fixupPhase`: strips binaries, patches RPATHs, shrink-wraps references.
For a simple script, most of these are irrelevant. The `02-package` example disables unpack and build with `dontUnpack = true` and `dontBuild = true`, then writes a custom `installPhase`. For a simple script, most of these are irrelevant. The `02-package` example disables unpack and build with `dontUnpack = true` and `dontBuild = true`,
then writes a custom `installPhase`.
--- ---
## 3. Key Concepts in the Example ## 3. Key Concepts in the Example
`$out`: every derivation has at least one output. `$out` is the default output path. The build must place files under `$out` or the derivation produces nothing. `$out`: every derivation has at least one output. `$out` is the default output path. The build must place files under `$out` or the derivation
produces nothing.
`substituteInPlace`: a nixpkgs helper that does in-place string replacement. The example uses it to rewrite the shebang from `#!/usr/bin/env bash` to a store-path bash. This matters because `/usr/bin/env` may not exist on a pure NixOS system, and even where it does, it would pick up whichever `bash` happens to be on `$PATH` at runtime rather than the pinned one. `substituteInPlace`: a nixpkgs helper that does in-place string replacement. The example uses it to rewrite the shebang from `#!/usr/bin/env bash` to
a store-path bash. This matters because `/usr/bin/env` may not exist on a pure NixOS system, and even where it does, it would pick up whichever `bash`
happens to be on `$PATH` at runtime rather than the pinned one.
`src = ./.`: copies the entire flake directory into the store before the build starts. Changes to any file in the directory change the hash, which triggers a rebuild. For real projects you'd use `lib.fileset` or `lib.cleanSource` to exclude irrelevant files (editor backups, `.git/`, etc.). `src = ./.`: copies the entire flake directory into the store before the build starts. Changes to any file in the directory change the hash, which
triggers a rebuild. For real projects you'd use `lib.fileset` or `lib.cleanSource` to exclude irrelevant files (editor backups, `.git/`, etc.).
`apps.<system>.default`: the `nix run` command looks for this attribute. Its `program` field must be an absolute store path to an executable. `apps.<system>.default`: the `nix run` command looks for this attribute. Its `program` field must be an absolute store path to an executable.
@ -55,7 +62,8 @@ By default, Nix builds run inside a sandbox:
This is what guarantees reproducibility: the build can only use what it declared. This is what guarantees reproducibility: the build can only use what it declared.
If a build needs to download something (a source tarball, a Go module cache), it must be a fixed-output derivation. You provide the expected hash, and Nix verifies the output matches. See `builtins.fetchurl`, `pkgs.fetchFromGitHub`, and similar. If a build needs to download something (a source tarball, a Go module cache), it must be a fixed-output derivation. You provide the expected hash, and
Nix verifies the output matches. See `builtins.fetchurl`, `pkgs.fetchFromGitHub`, and similar.
--- ---
@ -87,4 +95,5 @@ The same `stdenv.mkDerivation` pattern scales to compiled languages. nixpkgs pro
- `python3Packages.buildPythonPackage` for Python. - `python3Packages.buildPythonPackage` for Python.
- `mkDerivation` directly for C/C++ (autotools, cmake, meson). - `mkDerivation` directly for C/C++ (autotools, cmake, meson).
Each wrapper pre-configures the phases for its ecosystem. You supply `src`, a lock file hash, and metadata; the wrapper handles configure/build/install. Each wrapper pre-configures the phases for its ecosystem. You supply `src`, a lock file hash, and metadata; the wrapper handles
configure/build/install.

View File

@ -6,7 +6,8 @@ This note covers `03-multi-system/`, which makes a flake's packages, dev shells,
## 1. The Problem ## 1. The Problem
Flake outputs are keyed by system: `packages.x86_64-linux.default`, `packages.aarch64-darwin.default`, and so on. If you hard-code one system (as `01-devshell` and `02-package` do), the flake only works on that platform. Flake outputs are keyed by system: `packages.x86_64-linux.default`, `packages.aarch64-darwin.default`, and so on. If you hard-code one system (as
`01-devshell` and `02-package` do), the flake only works on that platform.
To support multiple systems, you need to generate the per-system attrset for each platform you care about. To support multiple systems, you need to generate the per-system attrset for each platform you care about.
@ -23,7 +24,8 @@ forAllSystems = f:
); );
``` ```
`genAttrs` takes a list of keys and a function, and returns `{ key1 = f key1; key2 = f key2; … }`. Wrapping it so the callback receives `pkgs` (already imported for the right system) keeps the per-system output definitions clean: `genAttrs` takes a list of keys and a function, and returns `{ key1 = f key1; key2 = f key2; … }`. Wrapping it so the callback receives `pkgs` (
already imported for the right system) keeps the per-system output definitions clean:
```nix ```nix
packages = forAllSystems (pkgs: { packages = forAllSystems (pkgs: {
@ -62,11 +64,14 @@ outputs = { self, nixpkgs, flake-utils }:
Differences from the hand-rolled approach: Differences from the hand-rolled approach:
- `eachDefaultSystem` covers a predefined list of common systems (currently `x86_64-linux`, `aarch64-linux`, `x86_64-darwin`, `aarch64-darwin`). You can use `eachSystem` to specify your own list. - `eachDefaultSystem` covers a predefined list of common systems (currently `x86_64-linux`, `aarch64-linux`, `x86_64-darwin`, `aarch64-darwin`). You
- The callback returns a flat attrset (`{ packages.default = …; devShells.default = …; }`), and `eachDefaultSystem` nests it under each system automatically. can use `eachSystem` to specify your own list.
- The callback returns a flat attrset (`{ packages.default = …; devShells.default = …; }`), and `eachDefaultSystem` nests it under each system
automatically.
- It adds one input to `flake.lock`. Use `follows` to keep its nixpkgs in sync with yours. - It adds one input to `flake.lock`. Use `follows` to keep its nixpkgs in sync with yours.
Both approaches are common. The hand-rolled helper is better when you want full control; `flake-utils` is better when you want less repetition. `flake-parts` is a third option for larger projects that benefit from a module system on top of flakes. Both approaches are common. The hand-rolled helper is better when you want full control; `flake-utils` is better when you want less repetition.
`flake-parts` is a third option for larger projects that benefit from a module system on top of flakes.
--- ---
@ -84,9 +89,11 @@ checks = forAllSystems (pkgs: {
`nix flake check` evaluates and builds everything under `checks.<system>`. If any check derivation fails to build, the command exits non-zero. `nix flake check` evaluates and builds everything under `checks.<system>`. If any check derivation fails to build, the command exits non-zero.
`runCommand` is a convenience wrapper around `mkDerivation` that runs a single shell snippet. It needs to produce `$out` (a file or directory) to succeed. Here it runs the built script and writes its output to `$out`, which proves the binary at least executes without error. `runCommand` is a convenience wrapper around `mkDerivation` that runs a single shell snippet. It needs to produce `$out` (a file or directory) to
succeed. Here it runs the built script and writes its output to `$out`, which proves the binary at least executes without error.
This is the simplest form of a flake-level test. More involved checks might run a test suite, validate config syntax, or compare outputs against expected snapshots. This is the simplest form of a flake-level test. More involved checks might run a test suite, validate config syntax, or compare outputs against
expected snapshots.
--- ---
@ -115,4 +122,5 @@ Multi-system outputs are useful when:
- You publish a flake for others to consume (they expect their system to be present). - You publish a flake for others to consume (they expect their system to be present).
- You want `nix flake check` to evaluate outputs for every platform, catching typos that would only surface on the other OS. - You want `nix flake check` to evaluate outputs for every platform, catching typos that would only surface on the other OS.
If you only ever build on one machine, a single hard-coded system string is fine. You can always add `forAllSystems` later without changing any output behavior on your own platform. If you only ever build on one machine, a single hard-coded system string is fine. You can always add `forAllSystems` later without changing any output
behavior on your own platform.

View File

@ -1,6 +1,7 @@
# NixOS Modules # NixOS Modules
This note covers `04-nixos-module/`, which defines the smallest useful NixOS module: one option, one effect, exposed as a flake output other flakes can import. This note covers `04-nixos-module/`, which defines the smallest useful NixOS module: one option, one effect, exposed as a flake output other flakes
can import.
--- ---
@ -11,7 +12,8 @@ A NixOS module is a function from module arguments (`{ lib, config, pkgs, ... }`
- `options`: declarations of typed configuration points. - `options`: declarations of typed configuration points.
- `config`: values assigned to options declared by this module, and by other modules in the set. - `config`: values assigned to options declared by this module, and by other modules in the set.
The NixOS module system collects every module, merges their `options` declarations, merges their `config` assignments, resolves the merged `config` against the merged `options`, and exposes the final result as a single `config` value. Your system configuration is just another module in the set. The NixOS module system collects every module, merges their `options` declarations, merges their `config` assignments, resolves the merged `config`
against the merged `options`, and exposes the final result as a single `config` value. Your system configuration is just another module in the set.
A module can also appear as a plain attrset when it has no arguments. The function form is the general case. A module can also appear as a plain attrset when it has no arguments. The function form is the general case.
@ -24,9 +26,11 @@ A module can also appear as a plain attrset when it has no arguments. The functi
- `enable`: a boolean toggle, produced by `lib.mkEnableOption`. - `enable`: a boolean toggle, produced by `lib.mkEnableOption`.
- `name`: a string with default `"world"`, produced by `lib.mkOption`. - `name`: a string with default `"world"`, produced by `lib.mkOption`.
When `enable` is true, the `config` block defines `environment.etc."greeting".text` to `"hello, ${cfg.name}"`. `lib.mkIf cfg.enable { ... }` wraps the block so it contributes nothing when disabled, rather than setting options to null or empty values. When `enable` is true, the `config` block defines `environment.etc."greeting".text` to `"hello, ${cfg.name}"`. `lib.mkIf cfg.enable { ... }` wraps the
block so it contributes nothing when disabled, rather than setting options to null or empty values.
The module has no `imports`, no `pkgs` usage, and no `services.*` interaction. That keeps the example focused on declaring an option and conditionally assigning one piece of config. The module has no `imports`, no `pkgs` usage, and no `services.*` interaction. That keeps the example focused on declaring an option and conditionally
assigning one piece of config.
--- ---
@ -64,7 +68,8 @@ Downstream flakes consume it like any other module:
## 4. Verifying a Module Without Building a System ## 4. Verifying a Module Without Building a System
`nix flake check` needs the module to be exercised somehow. The example uses the smallest viable approach: evaluate a throwaway NixOS configuration that imports the module, set the options to known values, and read one attribute back out. `nix flake check` needs the module to be exercised somehow. The example uses the smallest viable approach: evaluate a throwaway NixOS configuration
that imports the module, set the options to known values, and read one attribute back out.
```nix ```nix
testConfig = nixpkgs.lib.nixosSystem { testConfig = nixpkgs.lib.nixosSystem {
@ -89,9 +94,12 @@ checks.${system}.greeting = pkgs.runCommand "greeting-check" {
Three things to notice: Three things to notice:
- The test config never triggers `system.build.toplevel`, so assertions that normally demand `fileSystems."/"`, a bootloader, and a hostname do not fire. Reading a single value through `.config.…` is a pure option lookup. - The test config never triggers `system.build.toplevel`, so assertions that normally demand `fileSystems."/"`, a bootloader, and a hostname do not
- `pkgs.runCommand` receives the computed greeting as an environment variable `got`. The comparison happens at build time, inside the sandbox, so a mismatch fails the build. fire. Reading a single value through `.config.…` is a pure option lookup.
- The check only proves evaluation and one config value. Richer modules deserve richer checks: multiple configurations, negative cases, or a full `pkgs.nixosTest` VM. - `pkgs.runCommand` receives the computed greeting as an environment variable `got`. The comparison happens at build time, inside the sandbox, so a
mismatch fails the build.
- The check only proves evaluation and one config value. Richer modules deserve richer checks: multiple configurations, negative cases, or a full
`pkgs.nixosTest` VM.
--- ---
@ -104,9 +112,12 @@ Three things to notice:
- `types.enum [ "a" "b" ]`, `types.submodule { options = { ... }; }`. - `types.enum [ "a" "b" ]`, `types.submodule { options = { ... }; }`.
- `types.lines`, `types.separatedString <sep>` for accumulating strings. - `types.lines`, `types.separatedString <sep>` for accumulating strings.
Each type defines how multiple assignments merge. Booleans OR together by default, strings with `types.lines` concatenate with newlines, lists with `types.listOf` concatenate, attrsets with `types.attrsOf` deep-merge. When you need to override the merge, use `lib.mkForce`, `lib.mkDefault`, `lib.mkBefore`, or `lib.mkAfter`. Each type defines how multiple assignments merge. Booleans OR together by default, strings with `types.lines` concatenate with newlines, lists with
`types.listOf` concatenate, attrsets with `types.attrsOf` deep-merge. When you need to override the merge, use `lib.mkForce`, `lib.mkDefault`,
`lib.mkBefore`, or `lib.mkAfter`.
This merge behavior is why multiple modules can each add packages to `environment.systemPackages` without stepping on each other: the list type accumulates. This merge behavior is why multiple modules can each add packages to `environment.systemPackages` without stepping on each other: the list type
accumulates.
--- ---
@ -149,4 +160,5 @@ Modules earn their weight in a few recurring situations:
- The integration belongs alongside `services.*`, `environment.*`, `users.*`, or `systemd.*` rather than next to them. - The integration belongs alongside `services.*`, `environment.*`, `users.*`, or `systemd.*` rather than next to them.
- A change set benefits from option merging, so multiple modules can contribute without overwriting each other. - A change set benefits from option merging, so multiple modules can contribute without overwriting each other.
For a one-off tweak to a single host, a plain `configuration.nix` snippet is usually smaller. The module system pays off once the same configuration needs reuse, parameterization, or validation. For a one-off tweak to a single host, a plain `configuration.nix` snippet is usually smaller. The module system pays off once the same configuration
needs reuse, parameterization, or validation.

View File

@ -1,6 +1,7 @@
# Haskell Project # Haskell Project
This note covers `05-haskell/`, which packages a tiny Cabal library and executable with Nix, runs a Cabal test suite during `nix flake check`, and provides a dev shell for editing it. This note covers `05-haskell/`, which packages a tiny Cabal library and executable with Nix, runs a Cabal test suite during `nix flake check`, and
provides a dev shell for editing it.
--- ---
@ -15,7 +16,8 @@ The example combines three pieces that show up in real Haskell projects:
- a flake output that builds that package with `callCabal2nix`, and - a flake output that builds that package with `callCabal2nix`, and
- a dev shell that provides GHC, `cabal-install`, and Haskell Language Server. - a dev shell that provides GHC, `cabal-install`, and Haskell Language Server.
That keeps the example focused on one idea: a flake can describe a small Haskell project end to end, including code, tests, and a development environment. That keeps the example focused on one idea: a flake can describe a small Haskell project end to end, including code, tests, and a development
environment.
--- ---
@ -27,7 +29,8 @@ That keeps the example focused on one idea: a flake can describe a small Haskell
project = haskellPackages.callCabal2nix "mini-haskell" ./. { }; project = haskellPackages.callCabal2nix "mini-haskell" ./. { };
``` ```
The first argument is the package name as it should appear in Nix. The second is the source tree. The third is an attrset of overrides, which this example leaves empty. The first argument is the package name as it should appear in Nix. The second is the source tree. The third is an attrset of overrides, which this
example leaves empty.
In this example, the Cabal package contains: In this example, the Cabal package contains:
@ -47,7 +50,8 @@ The dev shell uses `pkgs.mkShell` and adds the tools you need to edit and run th
- `cabal-install` for local development commands, and - `cabal-install` for local development commands, and
- `haskell-language-server` for editor support. - `haskell-language-server` for editor support.
This keeps the shell small and obvious. For projects with many Haskell dependencies, `shellFor` can construct a shell from the package set itself, but this example stays with `mkShell` to keep the mechanics visible. This keeps the shell small and obvious. For projects with many Haskell dependencies, `shellFor` can construct a shell from the package set itself, but
this example stays with `mkShell` to keep the mechanics visible.
--- ---

View File

@ -8,7 +8,8 @@ This note covers `06-haskell-shellfor/`, which builds a local Haskell package an
`mkShell` is a generic shell constructor. You list tools manually. `mkShell` is a generic shell constructor. You list tools manually.
`shellFor` is specific to Haskell package sets. It starts from one or more Haskell packages and builds a development environment around their dependencies. `shellFor` is specific to Haskell package sets. It starts from one or more Haskell packages and builds a development environment around their
dependencies.
That means the shell tracks the package definition more closely. When the package's Haskell dependencies change, the shell changes with it. That means the shell tracks the package definition more closely. When the package's Haskell dependencies change, the shell changes with it.
@ -26,7 +27,8 @@ haskellPackages = pkgs.haskellPackages.override {
}; };
``` ```
`shellFor` expects packages from the package set it is working with. Defining `mini-shellfor` inside that set makes it available both as a normal package output and as a package that `shellFor` can reference. `shellFor` expects packages from the package set it is working with. Defining `mini-shellfor` inside that set makes it available both as a normal
package output and as a package that `shellFor` can reference.
--- ---
@ -45,7 +47,8 @@ devShells.${system}.default = haskellPackages.shellFor {
}; };
``` ```
`packages = hp: [ hp.mini-shellfor ];` tells `shellFor` which Haskell package should drive the environment. `nativeBuildInputs` adds the interactive tools you still want on top. `packages = hp: [ hp.mini-shellfor ];` tells `shellFor` which Haskell package should drive the environment. `nativeBuildInputs` adds the interactive
tools you still want on top.
--- ---

View File

@ -0,0 +1,70 @@
# Haskell Dependencies
This note covers `07-haskell-deps/`, which builds a small Haskell program that depends on external libraries from the Haskell package set.
---
## 1. What This Example Adds
The earlier Haskell examples stayed close to `base` and local modules.
This example adds three common Haskell libraries:
- `aeson` for JSON decoding,
- `text` for string data in the parsed value, and
- `bytestring` for the raw input passed into the decoder.
That makes the example useful for two reasons:
- it shows how Cabal `build-depends` entries become Nix build inputs automatically, and
- it demonstrates a more realistic program shape than a pure string concatenation example.
---
## 2. The Important Point About Dependencies
The flake still uses the same Nix expression pattern as `05-haskell/`:
```nix
project = haskellPackages.callCabal2nix "mini-json" ./. { };
```
The external libraries are not listed again in `flake.nix`. They are declared in `mini-json.cabal`, and `callCabal2nix` translates that Cabal package
description into a Nix derivation.
That is the main teaching point: Cabal remains the source of truth for Haskell package dependencies, while the flake decides how the package is
exposed as a build, app, dev shell, and check.
---
## 3. The Library Function
The library exposes one function:
```haskell
greetFromJson :: ByteString -> Either String String
```
It decodes a JSON object with a `name` field and returns a greeting string. The executable wraps that function in a small CLI, and the test suite
checks both a valid input and an invalid one.
That keeps the example focused on dependency usage, not CLI design.
---
## 4. Commands to Try
```bash
cd 07-haskell-deps
nix develop
cabal run
cabal run -- '{"name":"flakes"}'
cabal test
nix build
./result/bin/mini-json '{"name":"flakes"}'
nix run . -- '{"name":"flakes"}'
nix flake check
```