Flake-Parts Expert
Specialized guidance for the flake-parts framework — a modular system for organizing Nix flakes. It applies the NixOS module system to flake organization and eliminates forAllSystems boilerplate: define a package once in perSystem and it is generated for every system in systems.
Structure with mkFlake
{
inputs.flake-parts.url = "github:hercules-ci/flake-parts";
outputs = inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
# Target architectures - define once
systems = [ "x86_64-linux" "aarch64-linux" ];
# External modules
imports = [ ./modules/packages.nix ];
# Multi-system configuration (defined once, generated for all systems)
perSystem = { config, pkgs, ... }: {
packages.hello = pkgs.hello;
devShells.default = pkgs.mkShell {
packages = [ config.packages.hello ];
};
};
# Traditional flake-level attributes (single-system)
flake = {
nixosConfigurations.machine = { };
};
};
}Generated structure:
# Input:
perSystem.packages.hello = pkgs.hello;
# Output:
packages.x86_64-linux.hello = <derivation>;
packages.aarch64-linux.hello = <derivation>;perSystem vs flake
Use perSystem for things that build across multiple platforms:
- Packages, devShells, apps
- Formatters, checks
- Anything that should exist per-system
Use flake for unique, non-system-specific outputs:
nixosConfigurations(each machine is unique)homeConfigurations(each config is unique)- Custom flake outputs
Standard perSystem Options
packages- Derivations to build (e.g.,packages.myapp = pkgs.hello;)apps- Executable applications (fornix run)devShells- Development environments (fornix develop)checks- Tests and validation (run withnix flake check)formatter- Code formatter (single package, run withnix fmt)legacyPackages- Nested, unvalidated package sets; traversed cheaply (name-level) bynix flake check, skipped bynix flake showunless--legacy
All are automatically generated for each system in the systems list.
Module Arguments
Only arguments you name in the function signature are passed — the module system inspects signatures with builtins.functionArgs. A catch-all args: receives none of the special arguments.
| Argument | Scope | Purpose |
|---|---|---|
pkgs | perSystem | nixpkgs for the current system |
system | perSystem | Current architecture string |
config | both | Access other values in the current configuration |
inputs' | perSystem | Inputs with system auto-selected (inputs'.foo.packages.bar) |
self' | perSystem | This flake’s outputs with system pre-selected |
final | perSystem (with easyOverlay) | Package set after overlays — see overlays.md |
inputs | top level only | The flake’s inputs attrset |
withSystem | top level only | Enter a system’s scope (see below) |
getSystem | top level only | Retrieve a specific system’s perSystem config |
moduleWithSystem | top level only | Bring perSystem arguments into a top-level module |
inputs, self, withSystem, getSystem, and moduleWithSystem are top-level arguments only — current flake-parts throws if you name them in a perSystem signature. Inside perSystem, use inputs' and self'.
perSystem = { pkgs, system, inputs', self', ... }: {
packages.myapp = pkgs.writeShellScriptBin "myapp" "echo hello on ${system}";
packages.tool = inputs'.some-flake.packages.default;
devShells.default = pkgs.mkShell {
packages = [ self'.packages.myapp ];
};
};The @ pattern accesses both scopes without shadowing:
{ config, ... }: {
myTopLevelOption = "foo";
perSystem = toplevel@{ config, pkgs, ... }: {
# config = per-system config
# toplevel.config = top-level config
packages.example = pkgs.writeText "value" toplevel.config.myTopLevelOption;
};
}Full reference — _module.args customization, functionArgs inspection, pitfalls — in module-arguments.md.
withSystem: bridging to single-system outputs
withSystem enters a system’s scope so flake-level outputs (like NixOS configs) can use perSystem values. Bind inputs at the top level; the callback only receives perSystem arguments:
top@{ inputs, ... }: {
flake.nixosConfigurations.machine = withSystem "x86_64-linux" (
{ config, ... }:
inputs.nixpkgs.lib.nixosSystem {
modules = [{
environment.systemPackages = [ config.packages.myapp ];
}];
}
);
}Without withSystem: self.packages.x86_64-linux.myapp (repetitive and verbose).
Converting a Standard Flake
Before converting: confirm the flake is under version control with a clean tree (commit or stash first). Migrate one output at a time and validate with nix flake check --no-build between steps — evaluation-only, so you get the same module-system validation without building every checks derivation at each step. New files must be git-tracked before the flake can see them (git add -N file.nix). For final validation run a full nix flake check --all-systems (by default flake check omits non-current systems), or nix-fast-build in CI for parallel eval+build.
Before:
outputs = { nixpkgs, ... }:
let
systems = [ "x86_64-linux" "aarch64-linux" ];
forAllSystems = nixpkgs.lib.genAttrs systems;
in {
packages = forAllSystems (system: {
hello = nixpkgs.legacyPackages.${system}.hello;
});
};After:
outputs = inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [ "x86_64-linux" "aarch64-linux" ];
perSystem = { pkgs, ... }: {
packages.hello = pkgs.hello;
};
};Why perSystem over eachSystem helpers: flake-parts transposes per-system outputs lazily — requesting packages.x86_64-linux.foo evaluates only that system. flake-utils’ eachSystem eagerly fold-merges every system’s outputs, which measured 3.2× slower on a full nix flake check at 8 systems (point evals are equal), and every consumer adds its own flake-utils entry to the lock file. Prefer perSystem or plain lib.genAttrs.
Step-by-step migration, incremental old/new mixing, and a checklist: advanced.md.
Beyond the Basics
- module-arguments.md - Complete reference for all module arguments, the @ pattern,
functionArgsinspection, and_module.argscustomization - overlays.md - easyOverlay: auto-generate overlays from perSystem packages, the
finalvspkgsdistinction, pitfalls. Import it at the top level (imports = [ inputs.flake-parts.flakeModules.easyOverlay ];), never insideperSystem - modular-organization.md -
importApplyfor passing flake-level context (likewithSystem) into imported modules; reusableflakeModules; dogfooding your own modules - advanced.md - Partitions (isolated evaluation via
partitionedAttrs), custom outputs, debug mode, migration from standard flakes
Best Practices
- Don’t traverse inputs - Never iterate through
inputswithmapAttrsor similar (causes unnecessary fetching and evaluation) - Namespace options - Use
mymodule.foonot justfooto avoid collisions - Favor perSystem - Most work happens there (packages, shells, checks)
- Use specific options - Prefer
foo.packageoverfoo.flakefor better granularity - Use
'suffixed arguments - Preferinputs'andself'over manual system selection
Debugging
Enable debug mode:
{
debug = true;
# ... rest of config
}Inspect with nix repl:
nix repl
:lf .
currentSystem.allModuleArgs.pkgs # Inspect current system pkgs
debug.allSystems.x86_64-linux # Inspect specific system
currentSystem.options.packages.files # See where values are defined
debug.options.systems.declarations # See where options are declaredDebug mode adds no measurable evaluation overhead, but it publishes debug, allSystems, and currentSystem (impure contexts only) as public flake outputs — turn it off when you’re done for output hygiene, not speed.
The nix-module-options skill builds on this debug = true infrastructure to query upstream NixOS/Home Manager options via currentSystem.allModuleArgs.
Common Issues
“path does not exist” Error
Files must be git-tracked for flakes to see them:
git add <your-module-files>
# OR for quick testing:
git add -N file.nix # Track without staging contentCircular Dependency
Don’t access self directly in modules. In perSystem, use self' or config; from flake level, use withSystem.
Wrong Module Context
Use @ syntax to access both top-level and perSystem config:
perSystem = toplevel@{ config, ... }: {
# config = perSystem config
# toplevel.config = top-level config
}Undefined Variable in Module Argument
The module system only passes arguments you explicitly name:
# ✅ CORRECT
{ pkgs, system, config, ... }: { }
# ❌ WRONG
args: { } # Won't receive special argumentsRelated Skills
- nix-dendritic-flake - Convert flakes to the dendritic pattern (every file is a top-level flake-parts module)
- nix-module-options - Query upstream NixOS/Home Manager module options programmatically (uses this skill’s
debug = trueoutputs)