flake-parts (optional structure)
flake-parts (optional structure)
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 host 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 chapter 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 |
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";
# home-manager ...
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 multi-output flakes benefit
Split flake-parts modules (optional)
# flake.nix
outputs = inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [
./flake-modules/devshell.nix
./flake-modules/hosts.nix
];
systems = [ "x86_64-linux" ];
};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. What matters is that you have a style and stick to it.
Theory 5 — What not to do
- Rewrite layout (hosts/modules/homes) 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
- Change house style every weekend
Theory 6 — Formatter as a style signal
Regardless of framework, expose a formatter:
nix fmtWith formatter output set, this becomes a team habit before checks enforce it.
| Formatter | Notes |
|---|---|
nixpkgs-fmt |
Common, stable |
alejandra |
Opinionated alternative |
nixfmt |
Ecosystem evolving—pin deliberately |
Pick one; put it in formatter and in the devShell.
Theory 7 — Overlay injection point
Whichever style you pick, define one place overlays are applied to pkgs:
pkgsFor = system: import nixpkgs {
inherit system;
overlays = [ (import ./overlays) ];
};flake-parts: configure perSystem pkgs via its nixpkgs module patterns, or construct consistently—document how.
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.
Worked example — Decision record template
# Flake style
Decision: hand-rolled | flake-parts
Date: YYYY-MM-DD
Rationale: …
Rules:
- systems = …
- overlays applied in …
- hosts defined under …
- how to add a new nixosConfiguration
- how to add a devShellExercises
Exercise 1 — Audit current outputs
cd /path/to/your-flake
nix flake showJournal:
- Which outputs exist today?
- Which will exist after checks/overlays chapters?
- Is
flake.nixalready painful?
Exercise 2 — Pick and write the decision
Create docs/flake-style.md with the template above. Commit this even before code changes—forces intentionality.
Exercise 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.
Exercise 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 .#labExercise 4 — Merge the winner
Merge the experiment branch to your main lab branch. Delete half-finished alternatives. One style in flake.nix.
Exercise 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)
Exercise 6 — Formatter end-to-end
nix fmt
git diffFormat should be deterministic. Fix any files the formatter rewrites.
Exercise 7 — specialArgs survival
After refactor, confirm modules that need inputs still receive them (HM, future sops).
Exercise 8 — Show vs build
nix flake show
nixos-rebuild build --flake .#lab -LIf show looks empty-ish, build often has the real trace.
Exercise 9 — follows still holds
nix flake metadataConfirm companion inputs still follows nixpkgs after the rewrite.
Exercise 10 — Negative hybrid cleanup
Search for leftover dual patterns (outputs = dead code, unused flake-parts import). Remove.
Exercise 11 — Systems honesty
If you only ever build x86_64-linux, either keep one system or document why two systems exist without CI.
Exercise 12 — Style PR description
Write a short paragraph as if reviewing your own style adoption: risks, rollback, smoke tests.
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 |
| Layout + framework same commit | Split; prove rebuild between |
| Formatter wars in one PR | Pick one tool; migrate separately |
| Community modules kitchen-sink | Add one optional module at a time |
| Infinite recursion after pkgs hook | Simplify pkgs construction |
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
- specialArgs/inputs still correct for HM
- follows discipline intact
Journal (optional)
Write three personal gotchas before continuing.