example refactor snowflake module system

This commit is contained in:
do butterflies cry? 2026-03-09 02:46:41 +10:00
parent 0314109bc0
commit 3e29615db9
Signed by: cry
GPG key ID: F68745A836CA0412
26 changed files with 1306 additions and 165 deletions

View file

@ -0,0 +1,61 @@
{ lib, flake-parts-lib, ... }:
let
inherit (lib)
mkOption
types
;
inherit (flake-parts-lib)
mkTransposedPerSystemModule
;
programType = lib.types.coercedTo derivationType lib.getExe lib.types.str;
derivationType = lib.types.package // {
check = lib.isDerivation;
};
appType = lib.types.submodule {
options = {
type = mkOption {
type = lib.types.enum [ "app" ];
default = "app";
description = ''
A type tag for `apps` consumers.
'';
};
program = mkOption {
type = programType;
description = ''
A path to an executable or a derivation with `meta.mainProgram`.
'';
};
meta = mkOption {
type = types.lazyAttrsOf lib.types.raw;
default = { };
# TODO refer to Nix manual 2.25
description = ''
Metadata information about the app.
Standardized in Nix at <https://github.com/NixOS/nix/pull/11297>.
Note: `nix flake check` is only aware of the `description` attribute in `meta`.
'';
};
};
};
in
mkTransposedPerSystemModule {
name = "apps";
option = mkOption {
type = types.lazyAttrsOf appType;
default = { };
description = ''
Programs runnable with nix run `<name>`.
'';
example = lib.literalExpression ''
{
default.program = "''${config.packages.hello}/bin/hello";
}
'';
};
file = ./apps.nix;
}

View file

@ -0,0 +1,21 @@
{ lib, flake-parts-lib, ... }:
let
inherit (lib)
mkOption
types
;
inherit (flake-parts-lib)
mkTransposedPerSystemModule
;
in
mkTransposedPerSystemModule {
name = "checks";
option = mkOption {
type = types.lazyAttrsOf types.package;
default = { };
description = ''
Derivations to be built by [`nix flake check`](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake-check.html).
'';
};
file = ./checks.nix;
}

View file

@ -0,0 +1,78 @@
{ config, flake-parts-lib, lib, options, getSystem, extendModules, ... }:
let
inherit (lib)
mapAttrs
mkIf
mkOption
optionalAttrs
types
;
inherit (flake-parts-lib)
mkPerSystemOption
;
mkDebugConfig = { config, options, extendModules }: config // {
inherit config;
inherit (config) _module;
inherit options;
inherit extendModules;
};
in
{
options = {
debug = mkOption {
type = types.bool;
default = false;
description = ''
Whether to add the attributes `debug`, `allSystems` and `currentSystem`
to the flake output. When `true`, this allows inspection of options via
`nix repl`.
```
$ nix repl
nix-repl> :lf .
nix-repl> currentSystem._module.args.pkgs.hello
«derivation /nix/store/7vf0d0j7majv1ch1xymdylyql80cn5fp-hello-2.12.1.drv»
```
Each of `debug`, `allSystems.<system>` and `currentSystem` is an
attribute set consisting of the `config` attributes, plus the extra
attributes `_module`, `config`, `options`, `extendModules`. So note that
these are not part of the `config` parameter, but are merged in for
debugging convenience.
- `debug`: The top-level options
- `allSystems`: The `perSystem` submodule applied to the configured `systems`.
- `currentSystem`: Shortcut into `allSystems`. Only available in impure mode.
Works for arbitrary system values.
See [Expore and debug option values](../debug.html) for more examples.
'';
};
perSystem = mkPerSystemOption
({ options, config, extendModules, ... }: {
_file = ./formatter.nix;
options = {
debug = mkOption {
description = ''
Values to return in e.g. `allSystems.<system>` when
[`debug = true`](#opt-debug).
'';
type = types.lazyAttrsOf types.raw;
};
};
config = {
debug = mkDebugConfig { inherit config options extendModules; };
};
});
};
config = mkIf config.debug {
flake = {
debug = mkDebugConfig { inherit config options extendModules; };
allSystems = mapAttrs (_s: c: c.debug) config.allSystems;
} // optionalAttrs (builtins?currentSystem) {
currentSystem = (getSystem builtins.currentSystem).debug;
};
};
}

View file

@ -0,0 +1,30 @@
{ lib, flake-parts-lib, ... }:
let
inherit (lib)
mkOption
types
literalExpression
;
inherit (flake-parts-lib)
mkTransposedPerSystemModule
;
in
mkTransposedPerSystemModule {
name = "devShells";
option = mkOption {
type = types.lazyAttrsOf types.package;
default = { };
description = ''
An attribute set of packages to be used as shells.
[`nix develop .#<name>`](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-develop.html) will run `devShells.<name>`.
'';
example = literalExpression ''
{
default = pkgs.mkShell {
nativeBuildInputs = with pkgs; [ wget bat cargo ];
};
}
'';
};
file = ./devShells.nix;
}

View file

@ -0,0 +1,47 @@
{
lib,
config,
...
}: let
inherit
(lib)
mkOption
types
;
flake = mkOption {
type = types.submoduleWith {
modules = [
{
freeformType =
types.lazyAttrsOf
(types.unique
{
message = ''
No option has been declared for this flake output attribute, so its definitions can't be merged automatically.
Possible solutions:
- Load a module that defines this flake output attribute
Many modules are listed at https://flake.parts
- Declare an option for this flake output attribute
- Make sure the output attribute is spelled correctly
- Define the value only once, with a single definition in a single module
'';
}
types.raw);
}
];
};
description = ''
Raw flake output attributes. Any attribute can be set here, but some
attributes are represented by options, to provide appropriate
configuration merging.
'';
};
in {
options = {
inherit flake;
output = {inherit flake;};
};
config = {inherit (config) flake;};
}

View file

@ -0,0 +1,52 @@
{ config, lib, flake-parts-lib, ... }:
let
inherit (lib)
filterAttrs
mapAttrs
mkOption
optionalAttrs
types
;
inherit (flake-parts-lib)
mkPerSystemOption
;
in
{
options = {
flake.formatter = mkOption {
type = types.lazyAttrsOf types.package;
default = { };
description = ''
An attribute set of per system a package used by [`nix fmt`](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-fmt.html).
'';
};
perSystem = mkPerSystemOption {
_file = ./formatter.nix;
options = {
formatter = mkOption {
type = types.nullOr types.package;
default = null;
description = ''
A package used by [`nix fmt`](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-fmt.html).
'';
};
};
};
};
config = {
flake.formatter =
mapAttrs
(k: v: v.formatter)
(filterAttrs
(k: v: v.formatter != null)
config.allSystems
);
perInput = system: flake:
optionalAttrs (flake?formatter.${system}) {
formatter = flake.formatter.${system};
};
};
}

View file

@ -0,0 +1,21 @@
{ lib, flake-parts-lib, ... }:
let
inherit (lib)
mkOption
types
;
inherit (flake-parts-lib)
mkTransposedPerSystemModule
;
in
mkTransposedPerSystemModule {
name = "legacyPackages";
option = mkOption {
type = types.lazyAttrsOf types.raw;
default = { };
description = ''
An attribute set of unmergeable values. This is also used by [`nix build .#<attrpath>`](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-build.html).
'';
};
file = ./legacyPackages.nix;
}

View file

@ -0,0 +1,32 @@
{ withSystem, ... }:
{
config = {
_module.args = {
moduleWithSystem =
module:
{ config, ... }:
let
system =
config._module.args.system or
config._module.args.pkgs.stdenv.hostPlatform.system or
(throw "moduleWithSystem: Could not determine the configuration's system parameter for this module system application.");
allArgs = withSystem system (args: args);
lazyArgsPerParameter = f: builtins.mapAttrs
(k: v: allArgs.${k} or (throw "moduleWithSystem: module argument `${k}` does not exist."))
(builtins.functionArgs f);
# Use reflection to make the call lazy in the argument.
# Restricts args to the ones declared.
callLazily = f: a: f (lazyArgsPerParameter f);
in
{
imports = [
(callLazily module allArgs)
];
};
};
};
}

View file

@ -0,0 +1,36 @@
{ lib, ... }:
let
inherit (lib)
mkOption
types
literalExpression
;
in
{
options = {
flake.nixosConfigurations = mkOption {
type = types.lazyAttrsOf types.raw;
default = { };
description = ''
Instantiated NixOS configurations. Used by `nixos-rebuild`.
`nixosConfigurations` is for specific machines. If you want to expose
reusable configurations, add them to [`nixosModules`](#opt-flake.nixosModules)
in the form of modules (no `lib.nixosSystem`), so that you can reference
them in this or another flake's `nixosConfigurations`.
'';
example = literalExpression ''
{
my-machine = inputs.nixpkgs.lib.nixosSystem {
# system is not needed with freshly generated hardware-configuration.nix
# system = "x86_64-linux"; # or set nixpkgs.hostPlatform in a module.
modules = [
./my-machine/nixos-configuration.nix
config.nixosModules.my-module
];
};
}
'';
};
};
}

View file

@ -0,0 +1,26 @@
{ self, lib, moduleLocation, ... }:
let
inherit (lib)
mapAttrs
mkOption
types
;
in
{
options = {
flake.nixosModules = mkOption {
type = types.lazyAttrsOf types.deferredModule;
default = { };
apply = mapAttrs (k: v: {
_class = "nixos";
_file = "${toString moduleLocation}#nixosModules.${k}";
imports = [ v ];
});
description = ''
NixOS modules.
You may use this for reusable pieces of configuration, service modules, etc.
'';
};
};
}

View file

@ -0,0 +1,26 @@
#
# Nixpkgs module. The only exception to the rule.
#
# Provides a `pkgs` argument in `perSystem`.
#
# Arguably, this shouldn't be in flake-parts, but in nixpkgs.
# Nixpkgs could define its own module that does this, which would be
# a more consistent UX, but for now this will do.
#
# The existence of this module does not mean that other flakes' logic
# will be accepted into flake-parts, because it's against the
# spirit of Flakes.
#
{
config = {
perSystem = { inputs', lib, ... }: {
config = {
_module.args.pkgs = lib.mkOptionDefault (
builtins.seq
(inputs'.nixpkgs or (throw "flake-parts: The flake does not have a `nixpkgs` input. Please add it, or set `perSystem._module.args.pkgs` yourself."))
inputs'.nixpkgs.legacyPackages
);
};
};
};
}

View file

@ -0,0 +1,32 @@
{ lib, ... }:
let
inherit (lib)
mkOption
types
;
in
{
options = {
flake.overlays = mkOption {
# uniq -> ordered: https://github.com/NixOS/nixpkgs/issues/147052
# also update description when done
type = types.lazyAttrsOf (types.uniq (types.functionTo (types.functionTo (types.lazyAttrsOf types.unspecified))));
# This eta expansion exists for the sole purpose of making nix flake check happy.
apply = lib.mapAttrs (_k: f: final: prev: f final prev);
default = { };
example = lib.literalExpression ''
{
default = final: prev: {};
}
'';
description = ''
An attribute set of [overlays](https://nixos.org/manual/nixpkgs/stable/#chap-overlays).
Note that the overlays themselves are not mergeable. While overlays
can be composed, the order of composition is significant, but the
module system does not guarantee sufficiently deterministic
definition ordering, across versions and when changing `imports`.
'';
};
};
}

View file

@ -0,0 +1,23 @@
{ lib, flake-parts-lib, ... }:
let
inherit (lib)
mkOption
types
;
inherit (flake-parts-lib)
mkTransposedPerSystemModule
;
in
mkTransposedPerSystemModule {
name = "packages";
option = mkOption {
type = types.lazyAttrsOf types.package;
default = { };
description = ''
An attribute set of packages to be built by [`nix build`](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-build.html).
`nix build .#<name>` will build `packages.<name>`.
'';
};
file = ./packages.nix;
}

View file

@ -0,0 +1,159 @@
{ config, lib, flake-parts-lib, self, ... }:
let
inherit (lib)
genAttrs
mapAttrs
mkOption
types
;
inherit (lib.strings)
escapeNixIdentifier
;
inherit (flake-parts-lib)
mkPerSystemType
;
rootConfig = config;
# Stubs for self and inputs. While it'd be possible to define aliases
# inside perSystem, that is not a general solution, and it would make
# top.config harder to discover, stretching the learning curve rather
# than flattening it.
throwAliasError' = param:
throw ''
`${param}` (without `'`) is not a `perSystem` module argument, but a
module argument of the top level config.
The following is an example usage of `${param}`. Note that its binding
is in the `top` parameter list, which is declared by the top level module
rather than the `perSystem` module.
top@{ config, lib, ${param}, ... }: {
perSystem = { config, ${param}', ... }: {
# in scope here:
# - ${param}
# - ${param}'
# - config (of perSystem)
# - top.config (note the `top@` pattern)
};
}
'';
throwAliasError = param:
throw ''
`${param}` is not a `perSystem` module argument, but a module argument of
the top level config.
The following is an example usage of `${param}`. Note that its binding
is in the `top` parameter list, which is declared by the top level module
rather than the `perSystem` module.
top@{ config, lib, ${param}, ... }: {
perSystem = { config, ... }: {
# in scope here:
# - ${param}
# - config (of perSystem)
# - top.config (note the `top@` pattern)
};
}
'';
/**
We primarily use `systems` to help memoize the per system context, but that
doesn't extend to arbitrary `system`s.
For that, we use the slightly less efficient, but perfectly acceptable
`memoizeStr` function.
*/
otherMemoizedSystems = flake-parts-lib.memoizeStr config.perSystem;
in
{
options = {
systems = mkOption {
description = ''
All the system types to enumerate in the flake output subattributes.
In other words, all valid values for `system` in e.g. `packages.<system>.foo`.
'';
type = types.listOf types.str;
};
perInput = mkOption {
description = ''
A function that pre-processes flake inputs.
It is called for users of `perSystem` such that `inputs'.''${name} = config.perInput system inputs.''${name}`.
This is used for [`inputs'`](../module-arguments.html#inputs) and [`self'`](../module-arguments.html#self).
The attributes returned by the `perInput` function definitions are merged into a single namespace (per input),
so each module should return an attribute set with usually only one or two predictable attribute names. Otherwise,
the `inputs'` namespace gets polluted.
'';
type = types.functionTo (types.functionTo (types.lazyAttrsOf types.unspecified));
};
perSystem = mkOption {
description = ''
A function from system to flake-like attributes omitting the `<system>` attribute.
Modules defined here have access to the suboptions and [some convenient module arguments](../module-arguments.html).
'';
type = mkPerSystemType ({ config, system, ... }: {
_file = ./perSystem.nix;
config = {
_module.args.inputs' =
mapAttrs
(inputName: input:
builtins.addErrorContext "while retrieving system-dependent attributes for input ${escapeNixIdentifier inputName}" (
if input._type or null == "flake"
then rootConfig.perInput system input
else
throw "Trying to retrieve system-dependent attributes for input ${escapeNixIdentifier inputName}, but this input is not a flake. Perhaps flake = false was added to the input declarations by mistake, or you meant to use a different input, or you meant to use plain old inputs, not inputs'."
)
)
self.inputs;
_module.args.self' =
builtins.addErrorContext "while retrieving system-dependent attributes for a flake's own outputs" (
rootConfig.perInput system self
);
# Custom error messages
_module.args.self = throwAliasError' "self";
_module.args.inputs = throwAliasError' "inputs";
_module.args.getSystem = throwAliasError "getSystem";
_module.args.withSystem = throwAliasError "withSystem";
_module.args.moduleWithSystem = throwAliasError "moduleWithSystem";
};
});
apply = modules: system:
(lib.evalModules {
inherit modules;
prefix = [ "perSystem" system ];
specialArgs = {
inherit system;
};
class = "perSystem";
}).config;
};
allSystems = mkOption {
type = types.lazyAttrsOf types.unspecified;
description = "The system-specific config for each of systems.";
internal = true;
};
};
config = {
allSystems = genAttrs config.systems config.perSystem;
_module.args.getSystem = system: config.allSystems.${system} or (otherMemoizedSystems system);
# The warning is there for a reason. Only use this in situations where the
# performance cost has already been incurred, such as in `flakeModules.easyOverlay`,
# where we run in the context of an overlay, and the performance cost of the
# extra `pkgs` makes the cost of running `perSystem` probably negligible.
_module.args.getSystemIgnoreWarning = system: config.allSystems.${system} or (config.perSystem system);
};
}

View file

@ -0,0 +1,132 @@
{ config, lib, flake-parts-lib, ... }:
let
inherit (lib)
filterAttrs
mapAttrs
mkOption
types
;
inherit (lib.strings)
escapeNixIdentifier
;
transpositionModule = {
options = {
adHoc = mkOption {
type = types.bool;
default = false;
description = ''
Whether to provide a stub option declaration for {option}`perSystem.<name>`.
The stub option declaration does not support merging and lacks
documentation, so you are recommended to declare the {option}`perSystem.<name>`
option yourself and avoid {option}`adHoc`.
'';
};
};
};
perInputAttributeError = { flake, attrName, system, attrConfig }:
# This uses flake.outPath for lack of a better identifier.
# Consider adding a perInput variation that has a normally-redundant argument for the input name.
# Tested manually with
# perSystem = { inputs', ... }: {
# packages.extra = inputs'.nixpkgs.extra;
# packages.default = inputs'.nixpkgs.packages.default;
# packages.veryWrong = (top.config.perInput "x86_64-linux" inputs'.nixpkgs.legacyPackages.hello).packages.default;
# };
# transposition.extra = {};
let
attrPath = "${escapeNixIdentifier attrName}.${escapeNixIdentifier system}";
flakeIdentifier =
if flake._type or null != "flake"
then
throw "An attempt was made to access attribute ${attrPath} on a value that's supposed to be a flake, but may not be a proper flake."
else
builtins.addErrorContext "while trying to find out how to describe what is supposedly a flake, whose attribute ${attrPath} was accessed but does not exist" (
toString flake.outPath
);
# This ought to be generalized by extending attrConfig, but this is the only known and common mistake for now.
alternateAttrNameHint =
if attrName == "packages" && flake?legacyPackages
then # Unfortunately we can't just switch them out, because that will put packages *sets* where single packages are expected in user code, resulting in potentially much worse and more confusing errors down the line.
"\nIt does define legacyPackages; try that instead?"
else "";
in
if flake?${attrName}
then
throw ''
Attempt to access ${attrPath} of flake ${flakeIdentifier}, but it does not have it.
It does have attribute ${escapeNixIdentifier attrName}, so it appears that it does not support system type ${escapeNixIdentifier system}.
''
else
throw ''
Attempt to access ${attrPath} of flake ${flakeIdentifier}, but it does not have attribute ${escapeNixIdentifier attrName}.${alternateAttrNameHint}
'';
in
{
options = {
transposition = lib.mkOption {
description = ''
A helper that defines transposed attributes in the flake outputs.
When you define `transposition.foo = { };`, definitions are added to the effect of (pseudo-code):
```nix
flake.foo.''${system} = (perSystem system).foo;
perInput = system: inputFlake: inputFlake.foo.''${system};
```
Transposition is the operation that swaps the indices of a data structure.
Here it refers specifically to the transposition between
```plain
perSystem: .''${system}.''${attribute}
outputs: .''${attribute}.''${system}
```
It also defines the reverse operation in [{option}`perInput`](#opt-perInput).
'';
type =
types.lazyAttrsOf
(types.submoduleWith { modules = [ transpositionModule ]; });
};
};
config = {
flake =
lib.mapAttrs
(attrName: attrConfig:
mapAttrs
(system: v: v.${attrName} or (
abort ''
Could not find option ${attrName} in the perSystem module. It is required to declare such an option whenever transposition.<name> is defined (and in this instance <name> is ${attrName}).
''))
config.allSystems
)
config.transposition;
perInput =
system: flake:
mapAttrs
(attrName: attrConfig:
flake.${attrName}.${system} or (
throw (perInputAttributeError { inherit system flake attrName attrConfig; })
)
)
config.transposition;
perSystem = {
options =
mapAttrs
(k: v: lib.mkOption { })
(filterAttrs
(k: v: v.adHoc)
config.transposition
);
};
};
}

View file

@ -0,0 +1,37 @@
{ lib, flake-parts-lib, getSystem, ... }:
let
inherit (lib)
mkOption
types
;
inherit (flake-parts-lib)
mkPerSystemOption
;
in
{
options = {
perSystem = mkPerSystemOption ({ config, options, specialArgs, ... }: {
_file = ./perSystem.nix;
options = {
allModuleArgs = mkOption {
type = types.lazyAttrsOf (types.raw or types.unspecified);
internal = true;
readOnly = true;
description = "Internal option that exposes _module.args, for use by withSystem.";
};
};
config = {
allModuleArgs = config._module.args // specialArgs // { inherit config options; };
};
});
};
config = {
_module.args = {
withSystem =
system: f:
f
(getSystem system).allModuleArgs;
};
};
}