Add note files about packaging and multi-system support in Flake

This commit is contained in:
Hassan Abedi 2026-04-16 14:13:28 +02:00
parent 8e7af06409
commit 3dad7fbe82
7 changed files with 365 additions and 0 deletions

27
02-package/flake.lock generated Normal file
View File

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1776169885,
"narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

47
02-package/flake.nix Normal file
View File

@ -0,0 +1,47 @@
{
# Packages a shell script as a Nix derivation.
# Try: `nix build`, then `./result/bin/greet`
# Or: `nix run`
description = "Package a script with stdenv.mkDerivation";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
in {
packages.${system}.default = pkgs.stdenv.mkDerivation {
pname = "greet";
version = "0.1.0";
# `src = ./.` copies the flake directory into the store.
# The build then runs inside that copy.
src = ./.;
# Skip the default unpack/build phases; this example has no C code.
dontUnpack = true;
dontBuild = true;
# installPhase is a shell snippet. `$out` is the output store path.
installPhase = ''
mkdir -p $out/bin
cp ${./greet.sh} $out/bin/greet
chmod +x $out/bin/greet
# Patch the shebang so the script uses a store-path bash,
# not /bin/sh (which may not exist on a pure NixOS system).
substituteInPlace $out/bin/greet \
--replace-fail "#!/usr/bin/env bash" "#!${pkgs.bash}/bin/bash"
'';
};
# `nix run` looks here. `program` must be a store path to an executable.
apps.${system}.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/greet";
};
};
}

4
02-package/greet.sh Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
# A trivial script to be packaged by the flake.
name="${1:-world}"
echo "hello, ${name}! (built by nix)"

27
03-multi-system/flake.lock generated Normal file
View File

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1776169885,
"narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

52
03-multi-system/flake.nix Normal file
View File

@ -0,0 +1,52 @@
{
# Makes outputs available on multiple platforms using a hand-rolled
# `forAllSystems` helper (no extra dependencies).
# Compare with the `flake-utils` approach described in notes/005-multi-system.md.
description = "Multi-system outputs with forAllSystems";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs, ... }:
let
# List every system you want to support.
supportedSystems = [
"x86_64-linux"
"aarch64-linux"
"aarch64-darwin"
"x86_64-darwin"
];
# Helper: apply a function to each system and collect results into an attrset.
# `nixpkgs.lib.genAttrs` turns a list of keys + a function into { key = f key; … }.
forAllSystems = f:
nixpkgs.lib.genAttrs supportedSystems (system:
f (import nixpkgs { inherit system; })
);
in {
# Every output that varies per system goes through `forAllSystems`.
packages = forAllSystems (pkgs: {
default = pkgs.writeShellScriptBin "hello-multi" ''
echo "hello from $(uname -s)/$(uname -m)"
'';
});
devShells = forAllSystems (pkgs: {
default = pkgs.mkShell {
packages = with pkgs; [ jq ripgrep ];
shellHook = ''
echo "dev shell on $(uname -s)/$(uname -m)"
'';
};
});
# `nix flake check` evaluates everything under `checks`.
# Wrapping a simple test here shows the pattern.
checks = forAllSystems (pkgs: {
runs = pkgs.runCommand "hello-multi-runs" {} ''
${self.packages.${pkgs.system}.default}/bin/hello-multi > $out
'';
});
};
}

90
notes/004-packaging.md Normal file
View File

@ -0,0 +1,90 @@
# Packaging
This note covers `02-package/`, which builds a shell script into a Nix store path using `stdenv.mkDerivation`.
---
## 1. What `nix build` Does
When you run `nix build` inside `02-package/`:
1. Nix evaluates `flake.nix` and produces a `.drv` file (the build recipe).
2. Nix realises the `.drv`: it runs the install phase inside a sandbox with no network and no access to files outside the declared inputs.
3. The output lands in `/nix/store/<hash>-greet-0.1.0/`.
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.
---
## 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:
1. `unpackPhase`: extracts `src` (tarball, git checkout, or copied path).
2. `patchPhase`: applies patches from the `patches` list.
3. `configurePhase`: runs `./configure` or cmake or similar.
4. `buildPhase`: runs `make` or equivalent.
5. `installPhase`: copies outputs into `$out`.
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`.
---
## 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.
`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.).
`apps.<system>.default`: the `nix run` command looks for this attribute. Its `program` field must be an absolute store path to an executable.
---
## 4. The Build Sandbox
By default, Nix builds run inside a sandbox:
- No network access (except fixed-output derivations).
- No access to the host filesystem beyond explicitly declared inputs.
- No access to environment variables from the host.
- A restricted `/tmp` for scratch space.
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.
---
## 5. Commands to Try
```bash
cd 02-package
nix build # build and create ./result symlink
./result/bin/greet # run directly
./result/bin/greet "nix learner" # pass an argument
nix run # build + run via apps.default
nix run . -- "nix learner" # pass arguments after --
nix path-info ./result # show the store path
nix path-info -rS ./result # show closure size (all transitive deps)
nix derivation show ./result # inspect the .drv (inputs, env, builder)
```
---
## 6. Beyond Shell Scripts
The same `stdenv.mkDerivation` pattern scales to compiled languages. nixpkgs provides wrappers:
- `rustPlatform.buildRustPackage` for Rust (Cargo).
- `buildGoModule` for Go.
- `python3Packages.buildPythonPackage` for Python.
- `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.

118
notes/005-multi-system.md Normal file
View File

@ -0,0 +1,118 @@
# Multi-System Outputs
This note covers `03-multi-system/`, which makes a flake's packages, dev shells, and checks available on multiple platforms from a single `flake.nix`.
---
## 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.
To support multiple systems, you need to generate the per-system attrset for each platform you care about.
---
## 2. The `forAllSystems` Pattern
The `03-multi-system` example defines a local helper:
```nix
forAllSystems = f:
nixpkgs.lib.genAttrs supportedSystems (system:
f (import nixpkgs { inherit system; })
);
```
`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
packages = forAllSystems (pkgs: {
default = pkgs.writeShellScriptBin "hello-multi" ''
echo "hello from $(uname -s)/$(uname -m)"
'';
});
```
This produces:
```
packages.x86_64-linux.default
packages.aarch64-linux.default
packages.aarch64-darwin.default
packages.x86_64-darwin.default
```
No external dependencies. You control the system list explicitly.
---
## 3. The `flake-utils` Alternative
`flake-utils` is a community flake that provides `eachDefaultSystem`, which does the same thing with less boilerplate:
```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;
});
```
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.
- 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.
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.
---
## 4. The `checks` Output
`03-multi-system` also demonstrates a `checks` output:
```nix
checks = forAllSystems (pkgs: {
runs = pkgs.runCommand "hello-multi-runs" {} ''
${self.packages.${pkgs.system}.default}/bin/hello-multi > $out
'';
});
```
`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.
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.
---
## 5. Commands to Try
```bash
cd 03-multi-system
nix flake show # see outputs for all four systems
nix build # build default package for your system
nix run # build + run
nix flake check # build all checks (proves the script runs)
# Inspect what other systems would get (eval only, no cross-build):
nix eval .#packages.aarch64-darwin.default.name
```
---
## 6. When Cross-System Matters
Multi-system outputs are useful when:
- You collaborate across macOS and Linux machines.
- CI runs on a different platform than your laptop.
- 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.
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.