Flake checks
Flake checks
Goal: Add flake checks so nix flake check exercises eval/format/build assumptions locally—the same shape CI will use later.
Why this chapter exists
A flake that only switches on your laptop is a private ritual. Checks turn “I hope this still evaluates” into a command:
nix flake checkThis chapter is not full nixosTest mastery (ops/testing chapters). It is lightweight gates: format, build a shell, evaluate a host, maybe build a package.
Theory 1 — What checks are
Flake schema: checks.<system>.<name> → derivations. nix flake check builds them (with some special cases).
| Check type | Purpose |
|---|---|
| Format / lint | Style consistency |
nixosSystem eval/build |
Config still evaluates |
mkShell build |
DevShell inputs resolve |
| Small package build | Packaging sanity |
nixosTest |
VM integration (later parts) |
Failed check → non-zero exit → CI red.
Theory 2 — Hand-rolled checks skeleton
# inside outputs, with forAllSystems / pkgsFor from the flake-parts chapter
checks = forAllSystems (system:
let
pkgs = pkgsFor system;
in {
# 1) formatter check
format = pkgs.runCommand "check-format" {
nativeBuildInputs = [ pkgs.nixpkgs-fmt ];
src = pkgs.lib.cleanSource ./.;
} ''
set -euo pipefail
cp -r $src tree
chmod -R u+w tree
cd tree
find . -name '*.nix' -print0 | xargs -0 nixpkgs-fmt --check
mkdir -p $out
touch $out/ok
'';
# 2) devShell builds
devshell = self.devShells.${system}.default;
});Building the full host toplevel is an excellent check but can be slow and architecture-bound. For a laptop-only x86_64-linux lab, pinning checks to that system is fine.
Host toplevel (strong, heavy)
// nixpkgs.lib.optionalAttrs (system == "x86_64-linux") {
lab-toplevel = self.nixosConfigurations.lab.config.system.build.toplevel;
}Gate by system so aarch64 eval of an x86-only host is not nonsense.
Lighter host check — eval-oriented
Some teams use CI scripts (nixos-rebuild build) without putting toplevel in checks. Either works if documented.
Theory 3 — flake-parts checks
Under perSystem:
perSystem = { self', pkgs, ... }: {
checks = {
devshell = self'.devShells.default;
# treefmt / git-hooks modules exist in the ecosystem—optional
};
};Community modules (treefmt-nix, git-hooks.nix) are optional accelerators—do not require them if time is short.
Theory 4 — Format check that actually fails
A useful format check must see your sources and use the formatter’s check mode.
Notes:
--checkmust exist for your formatter CLI (nixpkgs-fmtsupports it; other formatters differ)- Copying entire flake including
.gitcan be heavy—trim withlib.cleanSource - IFD and huge contexts slow checks—keep them lean
src = pkgs.lib.cleanSource ./.;Alternative: treefmt (awareness)
treefmt + treefmt-nix can unify formatters. Optional complexity—only adopt if you want multi-language format in one gate.
Theory 5 — What belongs in check vs push-time CI only
Local nix flake check |
CI later |
|---|---|
| Format | Same + matrix systems |
| Shell / small builds | Cache push |
| Optional host toplevel | Full VM tests |
| Fast enough to run pre-commit | Longer integration |
Keep local checks fast enough that you run them. A 40-minute check nobody runs is theater.
| Budget (guideline) | Contents |
|---|---|
| < 1–2 min warm | format + shell |
| < 10–15 min | + host toplevel on one system |
| CI nightly | tests, multi-system, slow builds |
Theory 6 — nix flake check UX
nix flake check
nix flake check -L # logs
nix build .#checks.x86_64-linux.format -LIndividual check attrs help debug without running the full set.
Common failure modes
| Failure | Meaning |
|---|---|
| format | Style drift |
| devshell | Missing package / bad mkShell |
| lab-toplevel | Host module error |
| Eval error before build | checks attrset itself broken |
Theory 7 — Checks and secrets
Checks often copy sources into the builder. Do not place plaintext secrets in the flake tree. Encrypted sops files are ciphertext (OK); private keys and .env must stay out.
.env
*.pem
keys.txt
Theory 8 — Wiring consistency with pkgs/overlays
If overlays affect hello-lab or formatters, pkgsFor in checks must match host/devShell construction—or document intentional differences.
pkgsFor = system: import nixpkgs {
inherit system;
overlays = import ./overlays;
};Worked example — Practical minimal set
checks = forAllSystems (system:
let pkgs = pkgsFor system; in {
shell = self.devShells.${system}.default;
} // nixpkgs.lib.optionalAttrs (system == "x86_64-linux") {
# lab-toplevel = self.nixosConfigurations.lab.config.system.build.toplevel;
});Uncomment toplevel when you accept the build cost.
Worked example — Script wrapper
# tools/check.sh
#!/usr/bin/env bash
set -euo pipefail
nix flake check -Lchmod +x tools/check.sh
./tools/check.shExercises
Exercise 1 — Baseline without checks
cd /path/to/your-flake
nix flake show
nix flake check || trueNote current behavior (may pass with zero checks or warn).
Exercise 2 — Add devShell as a check
Wire checks.<system>.shell = self.devShells.${system}.default (or flake-parts equivalent).
nix build .#checks.x86_64-linux.shell -LExercise 3 — Add a format check that can fail
- Implement format check on
*.nix - Run
nix flake check - Break formatting in a temp edit
- Confirm check fails
- Restore format
Exercise 4 — Optional host toplevel check
If disk/time allow:
nix build .#checks.x86_64-linux.lab-toplevel -LRecord wall-clock time. If over your patience budget, document “CI-only toplevel” and keep shell+format local.
Exercise 5 — Developer loop script (optional)
Add tools/check.sh or a Makefile target. Not required if you prefer raw commands.
Exercise 6 — Journal: CI shape
Write the future CI job in five lines:
checkout
install nix
nix flake check
# later: deploy job separate
Exercise 7 — Single-attr debug
nix build .#checks.x86_64-linux.format -LPractice isolating one check.
Exercise 8 — cleanSource impact
If format check is slow or includes junk, switch to lib.cleanSource and remeasure.
Exercise 9 — System gate
If you list multiple systems, ensure host toplevel is only on the host architecture.
Exercise 10 — Pre-commit habit
Run nix flake check before a real commit. Note anything surprising (downloads, rebuilds).
Exercise 11 — Document check inventory
In docs/flake-style.md or docs/checks.md, list each check and what failure means.
Exercise 12 — Secret scan of check context
Confirm no private key paths are required for checks to pass.
Exercise 13 — Fail-closed demo
Leave a format break on a throwaway branch; show nix flake check non-zero; fix on mainline branch only.
Exercise 14 — Overlay consistency
If you have a wrapper package from overlays, add a tiny check that builds it—or document why not.
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| Checks empty / not discovered | Must be under checks output correctly |
| IFD surprises slow checks | Avoid unnecessary import-from-derivation |
| Building host on wrong system | optionalAttrs gate |
| Format check always succeeds | You didn’t pass real sources |
| Check context includes secrets | Don’t; keep secrets out of flake source |
nix flake check downloads the world |
First run cold; then cached—note substituters |
| Top-level too slow → abandoned | Split local vs CI checks |
| flake-parts checks in wrong section | Use perSystem.checks |
Formatter mismatch vs nix fmt |
Same package in formatter + format check |
| Broken self reference | Ensure self.devShells exists before check |
Checkpoint
- At least two meaningful checks (e.g. shell + format)
nix flake checkfails when format intentionally broken- Know how to build one check attribute
- Host toplevel check added or explicitly deferred with reason
- Future CI sketch written
- Checks are fast enough to run before commit
- Documented inventory of checks
- No secrets required to evaluate checks
Journal (optional)
Write three personal gotchas before continuing.