An opinionated flake module for structuring Nix projects: it imports your flake outputs from a conventional directory layout, and keeps evaluation lean by placing every part of that layout in its own flake-parts partition, so that evaluating one output never has to fetch or evaluate the inputs of another. This page covers the layout, the recipes, and the sharp edges; the flake-parts documentation is a good primer if partitions are new to you.
Important
This is the second generation (v2) of nix-utilities, a complete rewrite of
the original. See RELEASE-NOTES.md for what changed;
the previous generation remains available on the
legacy
branch.
{
inputs = {
flake-parts = {
url = "github:hercules-ci/flake-parts";
inputs.nixpkgs-lib.follows = "nixpkgs-lib";
};
nix-utilities = {
url = "github:neodyme-labs/nix-utilities";
inputs = {
flake-parts.follows = "flake-parts";
nixpkgs-lib.follows = "nixpkgs-lib";
};
};
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
nixpkgs-lib.url = "github:nix-community/nixpkgs.lib";
};
outputs =
inputs@{ flake-parts, nix-utilities, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [ nix-utilities.flakeModules.default ];
nixUtilities.root = ./nix;
systems = [ "x86_64-linux" ];
};
}The flake-parts.follows line is load-bearing - see "One flake-parts
instance" under caveats. inputs.nixpkgs is the fallback package set for
NixOS systems and home configurations; partitions that need nothing from it
never fetch it.
All paths live under nixUtilities.root and can be overridden individually
through the nixUtilities.paths.* options (shown in parentheses):
<root>/
+-- apps/ apps partition (paths.appsDirectory)
+-- dev/ dev partition (paths.devDirectory)
| +-- checks/
| +-- shells/
| +-- formatter.nix
+-- flake-modules/ flakeModules partition (paths.flakeModulesDirectory)
+-- home-manager/ homeManager partition (paths.homeManagerDirectory)
| +-- home-configurations/
| +-- homes/
| +-- modules/
+-- nixos/ nixos partition (paths.nixosDirectory)
| +-- modules/
| +-- systems/
+-- overlays/ overlays partition (paths.overlaysDirectory)
+-- packages/ packages partition (paths.pkgsDirectory)
Every directory is optional: a partition only exists when its directory does, and an output attribute is only claimed when discovery finds content backing it (see "Partitions and output claiming").
Drop a file (or a directory with a default.nix) into the matching
directory; it is exposed under its name with the .nix suffix dropped:
packages/->perSystem.packages, called incallPackagestyle: package inputs come frompkgs, and the flake-parts perSystem module arguments,inputs,self,libandnix-utils-libare available as automatic arguments. A standard{ stdenv, lib }: ...file works unchanged;.overridekeeps working. Where names collide,pkgswins:configandlibmean what they mean in nixpkgs.apps/->perSystem.apps;dev/checks/->perSystem.checks;dev/shells/->perSystem.devShells. Files are called (vianix-utils-lib.callWith) with exactly the arguments they ask for, selected from the same set as packages plus everything else inallModuleArgs.dev/formatter.nix->perSystem.formatter, called like a package, at priority 1001 - one step weaker thanlib.mkDefault, so modules like treefmt-nix can take over the formatter without friction.
overlays/ -> flake.overlays:
- A file is a whole overlay (
final: prev: { ... }), optionally wrapped per the extraArgs convention (extraArgs: final: prev: { ... }). - A directory becomes a sub-partition (injected arguments:
overlayName,overlayPath). Itsdefault.nix, if present, is the whole overlay; otherwise every file is an ordinary attrset-returning overlay part, and all parts are composed (vialib.composeManyExtensions) in alphabetical order, later parts seeing earlier ones throughprev.
nixos/modules/ -> flake.nixosModules:
- Files support the extraArgs convention and are exposed directly.
- Directories become sub-partitions (injected arguments:
nixosModuleName,nixosModulePath). The module tree is discovered as described under "Discovery rules" and exposed as a single module.
nixos/systems/ -> flake.nixosConfigurations. A system is a directory
containing a system-metadata.nix; the tree is searched recursively, so
FQDN-structured layouts work - systems/one/example/host/ becomes
nixosConfigurations."host.example.one":
systems/
+-- <host>/
| +-- system-metadata.nix - Required; see below
| +-- ... - Module tree, discovered as usual
+-- <tld>/<domain>/<host>/
-- same, for FQDN-structured layouts --
Each system is a sub-partition (injected arguments:
nixosConfigurationName, nixosConfigurationPath); the configuration is
built with nixpkgs.lib.nixosSystem from the discovered module tree plus
the metadata below.
system-metadata.nix contains an attribute set, or a function to one
supporting the extraArgs argument set:
| Attribute | Optional | Usage |
|---|---|---|
hostPlatform |
No | Platform (system) of the configuration. |
modules |
Yes | List of modules included in the configuration. |
nixpkgs |
Yes | The nixpkgs flake being used. Falls back to inputs.nixpkgs. |
overlays |
Yes | List of overlays applied to the nixpkgs. |
extra |
Yes | Arbitrary host facts exposed via nixosConfigurationExtras. |
Two extra pieces of plumbing:
flake.nixosConfigurationExtras.<name>mirrors each host'sextrametadata. Reading it never forces that host's module tree, unlikenixosConfigurations.<name>.config, which makes it cheap ground truth for tooling that walks every host (CODEOWNERS generators, secrets tooling, ...).- Modules inside a configuration receive the specialArg
hostInputs: the host's own fully resolved inputs (root inputs merged with the host'sflake.nixinputs, if any). It is deliberately named differently from a module's own eagerly resolvedinputs, so a shared module can prefer a host's override while falling back to what it already has, e.g.inputs // hostInputs.
Also injected as module arguments: nixosConfigurationName,
nixosConfigurationPath and nixosConfigurationExtra (this host's own
extra).
home-manager/homes/ -> flake.homes: one entry per user home, with
exactly the same shape as modules (files support the extraArgs convention;
directories become sub-partitions with injected arguments homeName,
homePath, and are exposed as a single module).
A home is a home-manager module, not a built configuration - homes are
typically embedded into hosts, where they can read osConfig and use each
host's own package set:
home-manager.users.alice = self.homes.alice;homes/ is deliberately separate from modules/: in multi-user
environments, per-person homes and shared building blocks diverge - homes
reference modules, carry per-person inputs, and are managed per user, while
home-manager/modules/ -> flake.homeModules holds the shared modules
they import.
home-manager/home-configurations/ -> flake.homeConfigurations: a
configuration is a directory containing a home-metadata.nix, built with
home-manager.lib.homeManagerConfiguration into a real (switchable)
configuration for home-manager switch --flake.
Configurations pull nothing in implicitly: name what you want, typically a
home from homes/, in the metadata's modules -
# home-manager/home-configurations/alice/home-metadata.nix
{ self, ... }:
{
hostPlatform = "x86_64-linux";
modules = [ self.homes.alice ];
}- plus the configuration directory's own module tree, discovered as usual.
Because the build bakes in one platform and has no host to read
osConfigfrom, host-adaptive homes belong inhomes/and get embedded; standalone configurations suit self-contained setups (a laptop, a dev container).
home-metadata.nix contains an attribute set, or a function to one
supporting the extraArgs argument set:
| Attribute | Optional | Usage |
|---|---|---|
hostPlatform |
No | Platform (system) of the configuration. |
modules |
Yes | Modules included in the configuration (e.g. self.homes.<name>). |
nixpkgs |
Yes | The nixpkgs flake to use. Falls back to inputs.nixpkgs. |
home-manager |
Yes | The home-manager flake to use. Falls back to inputs.home-manager. |
config |
Yes | nixpkgs configuration (allowUnfree, ...) for the package set. |
overlays |
Yes | Overlays applied to the package set. |
extraSpecialArgs |
Yes | Extra specialArgs passed to the home-manager modules. |
extra |
Yes | Arbitrary facts exposed via homeConfigurationExtras. |
flake.homeConfigurationExtras.<name> mirrors each configuration's
extra without forcing its build, like nixosConfigurationExtras.
Configurations receive the specialArg hostInputs and the module arguments
homeConfigurationName, homeConfigurationPath and
homeConfigurationExtra.
Every (sub-)partition directory supports two special files:
flake.nixtogether withflake.lock: additional inputs available (merged over the root inputs) only within that partition. Both files must be present. This repository's dev directory pins its development dependencies this way.flake-module.nix: replaces our implementation of the partition with your own flake-parts module. Injected module arguments (such asnixosConfigurationPath) stay available. This repository's dev/flake-module.nix uses this to add extra tooling on top of the stock dev partition. Note that which flake output attributes are claimed from the partition still follows the stock directory conventions - a replacement implementation has to keep the layout, or wire diverging outputs itself.
To add to or change a generated output, do it inside the partition: drop a
file into its directory, or override the partition with a
flake-module.nix. Defining the same attribute at the flake's top level
does not work (see caveats).
Each top-level directory becomes a flake-parts partition: a separate module system evaluation providing a fixed set of flake output attributes. Sub-directories of the homeManager, nixos and overlays partitions become nested sub-partitions of their own.
An output attribute exists exactly when discovery finds content backing it:
the claiming logic in
flake-modules/default.nix probes each
attribute's backing directory with the same discovery rules its partition
uses (shared via
flake-modules/internal/lib.nix), so an
existing-but-empty directory claims nothing and the flake never grows empty
outputs. The one exception is dev: dev/ existing claims checks,
devShells and formatter outright, because their content typically comes
from flake-module.nix modules (git-hooks adding a pre-commit check,
treefmt-nix providing the formatter), which discovery cannot see.
Partitioned attributes are wired into the flake with lib.mkForce
(flake-parts' partitionedAttrs machinery).
- Regular files are exposed under their name with the
.nixsuffix dropped; directories under their plain name. - Two entries mapping to the same name (
foo.nixnext tofoo/) are an error, not a silent shadowing. - Which directories are included depends on the partition. The modules,
homes and overlays walks take any directory carrying a
.nixfile at any depth (flake.nixdoes not count), so asset directories (patch collections, keys, ...) stay invisible. apps, packages, dev and flake-modules only take directories with adefault.nix; systems and home-configurations only take directories with their metadata file. - Symlinks count as their target: a
.nixname is treated as a file, anything else as a directory candidate. Symlinked directories are never recursed into, and only count where the inclusion probe passes through the link (adefault.nixor metadata file behind it) - the modules, homes and overlays walks cannot inspect a link's target and never take symlinked directories. A dangling.nixlink fails at import time; other dangling links are ignored. - Where a directory is imported as a module tree, a
default.nixtakes precedence: if present it is imported as the whole tree, otherwise all.nixfiles and includible directories are collected recursively.
Files documented above as supporting extra arguments may optionally be wrapped in one more function layer:
# Plain form
{ config, ... }: { ... }
# Wrapped form - extraArgs receives _module.args // _module.specialArgs
# // { inherit nix-utils-lib; } of the surrounding partition
{ inputs, nix-utils-lib, ... }: { config, ... }: { ... }The wrapper is detected by speculatively probing the function's arity; see the caveats for the two limits this brings.
nix-utilities.lib (also injected everywhere as the module argument
nix-utils-lib) carries the helpers the partitions are built from, usable
on their own:
files.verifyFileType: existence-and-type check that treats symlinks as their targets.functions.callWith/callWithContext: call a function with only the arguments it asks for; the context variant names the call site in missing-argument errors.functions.callWithIfNestedFunc/callWithIfNestedFuncContext: the extraArgs wrapper detection described above.imports.readImportablePaths: the discovery walk (exclusions, recursion, symlink and includibility rules).imports.importAsAttrs: discovery plus import into an attribute set.imports.isDirectoryIncludible,imports.dirContainsNixFiles: includibility checks.imports.uniqueListToAttrs:listToAttrsthat reports name collisions instead of shadowing.imports.stripNixSuffix: the naming rule for discovered entries.
The set is built with lib.makeExtensible, so it can be extended via
nix-utilities.lib.extend.
- One flake-parts instance. The
flake-parts.followsline in the getting-started snippet keeps a single flake-parts in play. nix-utilities imports the partitions module from its ownflake-partsinput; without the follows this still evaluates as long as nothing else importsflakeModules.partitions, but you are then running another revision's partition machinery inside yourmkFlake, and the moment any other module importsflakeModules.partitionsfrom your own flake-parts evaluation fails with "optionpartitionsis already declared". The module system rejects duplicate option declarations even when the copies are identical; it only deduplicates imports of the same store path. - Top-level definitions of claimed attributes do not merge. For
attributes flake-parts declares options for (
packages,checks,devShells,formatter,apps,overlays,nixosConfigurations,nixosModules) a top-level definition is silently discarded by themkForcewiring. The freeform attributes (homes,homeConfigurations,homeModules,flakeModules,nixosConfigurationExtras,homeConfigurationExtras) take exactly one definition: the top-level definition and the partition's collide inside the partition evaluation, and reading the attribute fails with "definitions can't be merged automatically". Add content inside the partition instead. - dev claims coarsely.
dev/existing claimschecks,devShellsandformattereven for missing sub-directories, so module-contributed content survives - and so root-level definitions of those three are discarded wheneverdev/exists. - Ellipsis-only wrappers are not detected. The arity probe cannot
distinguish
{ ... }: ...from a module, so an extraArgs wrapper must either be a plain lambda (extraArgs: ...) or declare at least one named argument. A body that fails with something other thanthrow/assertwhile being probed aborts evaluation (seecallWithIfNestedFuncContextin lib/functions.nix). callWithpasses only named arguments. An ellipsis in a called file's pattern receives nothing extra: name every argument you want, the...is only tolerated.- Symlink semantics differ per Nix implementation. Whether a dangling
non-
.nixsymlink is dropped during discovery or reaches the inclusion probe depends onpathExists(upstream Nix follows links, Determinate Nix lstats them); the bundled probes tolerate both.verifyFileTypeaccepts a symlink for both"regular"and"directory"- a wrong-kind target surfaces at use time. - Metadata files are special at the root only. A
system-metadata.nixorhome-metadata.nixnested deeper in a tree is an ordinary file and gets imported as part of the module tree like any other. libis injected, not native.libis a module-system built-in, so it appears in neither_module.argsnorspecialArgs; nix-utilities injects it explicitly so discovered files can ask for it. IncallPackage-style calls (packages,dev/formatter.nix)pkgswins name collisions -configandlibkeep their nixpkgs meaning there.
flakeModules.precommit-treefmt (not imported automatically) wires
git-hooks.nix and
treefmt-nix together with sane
defaults: the treefmt pre-commit hook runs the project's actual treefmt
wrapper, nixfmt runs in strict mode, and a default dev shell carrying
the pre-commit hook is added at mkDefault priority.
The consumer must import the git-hooks.nix and treefmt-nix flake
modules itself (they need their own inputs); this module only configures
them.