Day 29 — flake-parts or discipline
Day 29 — flake-parts or discipline
Stage III · ~4h
Goal: Choose a house style for multi-output flakes—either adopt flake-parts to reduce boilerplate, or enforce a hand-rolled structure with equal consistency—and apply it to your Stage III flake.
There is no moral victory in either choice. There is only consistency. Mixing half flake-parts and half ad-hoc outputs = args: … is the failure mode.
Why this day exists
By now your flake may export:
nixosConfigurations.*
devShells.*(host-level tooling)
- Soon:
checks.*, maybepackages.*,formatter
Raw outputs functions grow nested let blocks and copy-pasted system lists. Two responses exist in the ecosystem:
| Style | Idea |
|---|---|
| flake-parts | Framework: modules for flakes; per-system helper; community modules |
| Hand-rolled discipline | Small local helpers (forAllSystems), strict file layout, no new dependency |
Today you pick one and document it.
Theory 1 — Pain that motivates structure
Typical messy outputs:
outputs = { self, nixpkgs, home-manager, ... }@inputs:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
in {
nixosConfigurations.lab = /* ... */;
devShells.${system}.default = /* ... */;
# forgot aarch64; forgot checks; formatter missing
};Problems:
- Single-system assumptions
- Repeated pkgs construction
- Hard to share patterns across repos
- Easy to forget an output class
Theory 2 — Hand-rolled discipline (no flake-parts)
Pattern used widely:
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
# ...
outputs = { self, nixpkgs, ... }@inputs:
let
systems = [ "x86_64-linux" "aarch64-linux" ];
forAllSystems = nixpkgs.lib.genAttrs systems;
pkgsFor = system: import nixpkgs {
inherit system;
# overlays = [ self.overlays.default ];
};
in {
nixosConfigurations.lab = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit inputs; };
modules = [ ./hosts/lab /* hm module wiring */ ];
};
devShells = forAllSystems (system:
let pkgs = pkgsFor system; in {
default = pkgs.mkShell {
packages = with pkgs; [ nixpkgs-fmt nil ];
};
});
formatter = forAllSystems (system: (pkgsFor system).nixpkgs-fmt);
};
}House rules for hand-rolled
- One
systemslist
- One
pkgsFor/ overlay injection point
- Host modules stay under
hosts/; flake stays wiring
- New output types get a comment in
docs/flake-style.md
- No deep logic in
flake.nix—call into./libor./flake/if needed
Theory 3 — flake-parts approach
flake-parts turns outputs into a module system for flakes.
{
description = "lab with flake-parts";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
flake-parts.url = "github:hercules-ci/flake-parts";
home-manager.url = "github:nix-community/home-manager/release-26.05";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [ "x86_64-linux" "aarch64-linux" ];
perSystem = { pkgs, system, ... }: {
devShells.default = pkgs.mkShell {
packages = with pkgs; [ nixpkgs-fmt nil ];
};
formatter = pkgs.nixpkgs-fmt;
};
flake = {
nixosConfigurations.lab = inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit inputs; };
modules = [
./hosts/lab
inputs.home-manager.nixosModules.home-manager
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.alice = import ./homes/alice;
}
];
};
};
};
}| Concept | Role |
|---|---|
perSystem |
Outputs that vary by system (packages, devShells, checks, formatter) |
flake = { ... } |
Top-level flake attrs like nixosConfigurations |
| Modules | You can split flake-parts config across files |
Costs
- Another input to pin and learn
- Stack traces include framework layers
- Overkill for a single
nixosConfigurationsonly—but you are past that
Theory 4 — Decision rubric
| Choose flake-parts if… | Choose hand-rolled if… |
|---|---|
| You expect many per-system outputs | You want minimal dependencies |
| You like module-shaped config everywhere | You already understand raw flakes well |
| You will copy patterns across many repos | One lab flake is enough for months |
| You accept framework upgrades | You prefer total local control |
Book stance: either is valid. Capstone graders care that you have a style, not which logo you use.
Theory 5 — What not to do
- Rewrite layout (Day 23) and adopt flake-parts in the same panicked hour without rebuild
- Import flake-parts and still paste a second full
outputstree beside it
- Put NixOS module logic inside
perSystemincorrectly (hosts are usually underflake.nixosConfigurations)
- Enable every community flake-parts module on day one
Theory 6 — Formatter as a style signal
Regardless of framework, expose a formatter:
nix fmtWith formatter output set, this becomes a team habit before Day 30’s checks.
Worked example — Split hand-rolled helpers
# lib/flake-utils.nix
{ nixpkgs }:
rec {
systems = [ "x86_64-linux" "aarch64-linux" ];
forAllSystems = nixpkgs.lib.genAttrs systems;
pkgsFor = system: import nixpkgs { inherit system; };
}# flake.nix uses:
# let u = import ./lib/flake-utils.nix { inherit nixpkgs; }; in …Keeps flake.nix readable without flake-parts.
Lab 1 — Audit current outputs
cd /path/to/your-flake
nix flake showJournal:
- Which outputs exist today?
- Which will exist by Day 32 (checks, shells, …)?
- Is
flake.nixalready painful?
Lab 2 — Pick and write the decision
Create docs/flake-style.md:
# Flake style
Decision: hand-rolled | flake-parts
Date: YYYY-MM-DD
Rationale: …
Rules:
- …Commit this even before code changes—forces intentionality.
Lab 3A — If flake-parts: adopt on a branch
git checkout -b experiment/flake-parts
# add input, rewrite outputs via mkFlake
nix flake lock
nix flake show
nixos-rebuild build --flake .#lab
sudo nixos-rebuild switch --flake .#lab # if build OKProve HM + host still activate.
Lab 3B — If hand-rolled: extract helpers
git checkout -b experiment/flake-discipline
# add systems + forAllSystems + formatter + devShells
nix flake show
nix fmt 2>/dev/null || true
nix develop -c true
nixos-rebuild build --flake .#labLab 4 — Merge the winner
Merge the experiment branch to your main lab branch. Delete half-finished alternatives. One style in flake.nix.
Lab 5 — Teach the style to “future you”
In docs/flake-style.md, document how to:
- Add a new
nixosConfigurationshost
- Add a per-system
devShell
- Update flake-parts (if used) or helpers (if not)
Common gotchas
| Symptom / mistake | What to do |
|---|---|
perSystem vs flake confusion |
Hosts go under flake; shells under perSystem |
Lost specialArgs / inputs |
Thread inputs explicitly after rewrite |
| HM module dropped in refactor | Re-check modules list post-migration |
| Double nixpkgs without follows | Keep follows on all companion inputs |
nix flake show empty-ish |
Eval error—run build to see full trace |
| Framework without docs | Write house docs the same day |
Checkpoint
- Explicit house style documented
nix flake showreflects multi-output intent
- Host rebuild still works
formatterand/ordevShellsexist in the chosen style
- No hybrid mess left on main branch
- How-to for adding a host written
Commit
git add .
git commit -m "day29: flake-parts or hand-rolled discipline"Write three personal gotchas before continuing.
Tomorrow
Day 30 — Flake checks: make nix flake check fail when format/eval assumptions break—CI-shaped discipline before packaging stage.