Day 29 — flake-parts or discipline

Updated

July 30, 2026

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.

Note

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.*, maybe packages.*, 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

  1. One systems list
  2. One pkgsFor / overlay injection point
  3. Host modules stay under hosts/; flake stays wiring
  4. New output types get a comment in docs/flake-style.md
  5. No deep logic in flake.nix—call into ./lib or ./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 nixosConfigurations only—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 outputs tree beside it
  • Put NixOS module logic inside perSystem incorrectly (hosts are usually under flake.nixosConfigurations)
  • Enable every community flake-parts module on day one

Theory 6 — Formatter as a style signal

Regardless of framework, expose a formatter:

nix fmt

With 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 show

Journal:

  1. Which outputs exist today?
  2. Which will exist by Day 32 (checks, shells, …)?
  3. Is flake.nix already 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 OK

Prove 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 .#lab

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

  1. Add a new nixosConfigurations host
  2. Add a per-system devShell
  3. 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 show reflects multi-output intent
  • Host rebuild still works
  • formatter and/or devShells exist 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.