Add note files about packaging and multi-system support in Flake
This commit is contained in:
parent
66d368555d
commit
0e84296b79
27
02-package/flake.lock
generated
Normal file
27
02-package/flake.lock
generated
Normal 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
47
02-package/flake.nix
Normal 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
4
02-package/greet.sh
Normal 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
27
03-multi-system/flake.lock
generated
Normal 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
52
03-multi-system/flake.nix
Normal 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
90
notes/004-packaging.md
Normal 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
118
notes/005-multi-system.md
Normal 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.
|
||||
Loading…
x
Reference in New Issue
Block a user