Day 30 — Flake checks

Updated

July 30, 2026

Day 30 — Flake checks

Stage III · ~4h
Goal: Add flake checks so nix flake check exercises eval/format/build assumptions locally—the same shape CI will use later.

Why this day 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 check

Today is not full nixosTest mastery (Stage VI). Today 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)

Failed check → non-zero exit → CI red.


Theory 2 — Hand-rolled checks skeleton

# inside outputs, with forAllSystems / pkgsFor from Day 29
checks = forAllSystems (system:
  let
    pkgs = pkgsFor system;
  in {
    # 1) formatter dry-run style check
    format = pkgs.runCommand "check-format" {
      buildInputs = [ pkgs.nixpkgs-fmt ];
    } ''
      set -euo pipefail
      # copy sources; fail if nixpkgs-fmt wants changes
      mkdir -p $out
      # For a real check, point at your nix files via path builtins carefully.
      # Simpler starter: succeed if formatter exists
      command -v nixpkgs-fmt > $out/fmt-path
    '';

    # 2) devShell builds
    devshell = self.devShells.${system}.default;

    # 3) host evaluation via build of system toplevel (heavy but strong)
    # lab-host = self.nixosConfigurations.lab.config.system.build.toplevel;
  });

Building the full 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.

Lighter host check — eval only patterns

Some people use pkgs.writeText of a JSON dump of options they care about, or nixos-rebuild build in CI scripts 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 / pre-commit style modules exist in the ecosystem—optional
  };
};

Community modules (treefmt-nix, git-hooks.nix / pre-commit-hooks) are optional accelerators—do not require them today if time is short.


Theory 4 — Format check that actually fails

A useful format check:

format = pkgs.runCommand "check-nix-fmt" {
  nativeBuildInputs = [ pkgs.nixpkgs-fmt ];
  src = ./.;  # careful: large contexts; often better to pass explicit file list
} ''
  set -euo pipefail
  cp -r $src tree
  chmod -R u+w tree
  cd tree
  # list your nix files
  find . -name '*.nix' -print0 | xargs -0 nixpkgs-fmt --check
  mkdir -p $out
  touch $out/ok
'';

Notes:

  • --check must exist for your formatter CLI (nixpkgs-fmt supports check mode; nix fmt workflows vary)
  • Copying entire flake including .git can be heavy—trim with lib.cleanSource
src = lib.cleanSource ./.;

Theory 5 — What belongs in check vs push-time CI only

Local nix flake check CI later (Stage VI)
Format Same + matrix systems
Shell / small builds Cache push
Optional host toplevel Full VM tests

Keep local checks fast enough that you run them. A 40-minute check nobody runs is theater.


Theory 6 — nix flake check UX

nix flake check
nix flake check -L          # logs
nix build .#checks.x86_64-linux.format -L

Individual check attrs help debug without running the full set.


Worked example — Practical minimal set for Stage III

checks = forAllSystems (system:
  let pkgs = pkgsFor system; in {
    shell = self.devShells.${system}.default;
    # only on the host architecture you actually use:
  }) // {
  # top-level non-per-system alternative patterns exist; prefer per-system consistency
};

Plus on x86_64-linux only:

# conceptual
lab-toplevel = self.nixosConfigurations.lab.config.system.build.toplevel;

If aarch64 eval of an x86-only host is nonsense, do not genAttrs that check onto all systems blindly—gate by system.

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;
  });

Lab 1 — Baseline without checks

cd /path/to/your-flake
nix flake show
nix flake check || true

Note current behavior (may pass with zero checks or warn).


Lab 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 -L

Lab 3 — Add a format check that can fail

  1. Implement format check on *.nix
  2. Run nix flake check
  3. Break formatting in a temp edit
  4. Confirm check fails
  5. Restore format
# example break
# add ugly spacing in a nix file, then:
nix flake check

Lab 4 — Optional host toplevel check

If disk/time allow:

nix build .#checks.x86_64-linux.lab-toplevel -L

Record wall-clock time. If > your patience budget, document “CI-only toplevel” and keep shell+format local.


Lab 5 — Developer loop script (optional)

# tools/check.sh
#!/usr/bin/env bash
set -euo pipefail
nix flake check -L

Not required if you prefer raw commands.


Lab 6 — Journal: CI shape

Write the future CI job in five lines:

checkout
install nix
nix flake check
# later: deploy job separate

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

Checkpoint

  • At least two meaningful checks (e.g. shell + format)
  • nix flake check fails 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

Commit

git add .
git commit -m "day30: flake checks for format and shell"

Write three personal gotchas before continuing.


Tomorrow

Day 31 — Overlays light: pin or patch one package for host and shell without forking all of nixpkgs.