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

@ -12,180 +12,23 @@
# See the License for the specific language governing permissions and
# limitations under the License.
{
this,
self,
inputs,
systems,
nt,
mix,
...
} @ args: let
inherit
(builtins)
all
attrNames
elem
mapAttrs
warn
;
inherit (inputs.nixpkgs) lib;
inherit (nt) findImport;
in
mix.newMixture args (mixture: let
inherit (mixture) mapNodes;
in {
includes.private = [
./lib/nodes.nix
];
includes = {
private = [
./lib/nodes.nix
];
public = [
./flake
];
};
inherit findImport;
# snow.flake
flake = flakeInputs: root: let
module = lib.evalModules {
class = "snowflake";
# TODO: abort if inputs contains reserved names
specialArgs =
(flakeInputs
// {
inherit systems root;
inherit (this) snow;
inputs = flakeInputs;
})
|> (x: builtins.removeAttrs x ["self" "nodes"]);
modules = [
./module.nix
({config, ...}: {
_module.args = {
self = config;
nodes = config.nodes.nodes;
};
})
];
};
nodes = module.config.nodes;
in rec {
nixosConfigurations = mapNodes nodes (
{
base,
lib,
name,
node,
groupModules,
...
}: let
homeManager =
if node.homeManager != null
then node.homeManager
else if nodes.homeManager != null
then nodes.homeManager
else
warn ''
[snowflake] Neither `nodes.homeManager` nor `nodes.nodes.${name}.homeManager` were specified!
[snowflake] home-manager will NOT be used! User configuration will be ignored!
''
null;
userArgs = nodes.args // node.args;
ceruleanArgs = {
inherit systems root base nodes node;
inherit (node) system;
inherit (this) snow;
hostname = name;
_cerulean = {
inherit inputs userArgs ceruleanArgs homeManager;
specialArgs = userArgs // ceruleanArgs;
};
};
specialArgs = assert (userArgs
|> attrNames
|> all (argName:
! ceruleanArgs ? argName
|| abort ''
`specialArgs` are like super important to Cerulean my love... </3
But `args.${argName}` is a reserved argument name :(
''));
ceruleanArgs._cerulean.specialArgs;
in
lib.nixosSystem {
inherit (node) system;
inherit specialArgs;
modules =
[
self.nixosModules.default
(findImport /${root}/hosts/${name})
]
++ (groupModules root)
++ node.modules
++ nodes.modules;
}
);
deploy.nodes = mapNodes nodes ({
name,
node,
...
}: let
inherit
(node.deploy)
ssh
user
interactiveSudo
remoteBuild
rollback
autoRollback
magicRollback
activationTimeout
confirmTimeout
;
nixosFor = system: inputs.deploy-rs.lib.${system}.activate.nixos;
in {
hostname =
if ssh.host != null
then ssh.host
else "";
profilesOrder = ["default"]; # profiles priority
profiles.default = {
path = nixosFor node.system nixosConfigurations.${name};
user = user;
sudo = "sudo -u";
interactiveSudo = interactiveSudo;
fastConnection = false;
autoRollback = autoRollback -> rollback;
magicRollback = magicRollback -> rollback;
activationTimeout = activationTimeout;
confirmTimeout = confirmTimeout;
remoteBuild = remoteBuild;
sshUser = ssh.user;
sshOpts =
ssh.opts
++ (
if elem "-p" ssh.opts
then []
else ["-p" (toString ssh.port)]
)
++ (
if elem "-A" ssh.opts
then []
else ["-A"]
);
};
});
checks =
inputs.deploy-rs.lib
|> mapAttrs (system: deployLib:
deployLib.deployChecks deploy);
};
})

View file

@ -0,0 +1,38 @@
{
this,
inputs,
systems,
...
}: let
inherit (inputs.nixpkgs) lib;
in {
# snow.flake
flake = flakeInputs: root: let
snowflake = lib.evalModules {
class = "snowflake";
# XXX: TODO: abort if inputs contains reserved names
specialArgs =
(flakeInputs
// {
inherit (this) snow;
inherit systems root;
inputs = flakeInputs;
})
# XXX: TODO:
# |> (x: builtins.removeAttrs x ["self" "nodes"]);
|> (x: builtins.removeAttrs x ["self"]);
modules = [
./module.nix
({config, ...}: {
_module.args = {
self = config;
# XXX: TODO:
# nodes = config.nodes.nodes;
};
})
];
};
in
snowflake.config.outputs;
}

318
nix/snow/flake/lib.nix.bak Normal file
View file

@ -0,0 +1,318 @@
{ lib
# Optionally a string with extra version info to be included in the error message
# in case is lib is out of date. Empty or starts with space.
, revInfo ? ""
}:
let
inherit (lib)
mkOption
mkOptionType
defaultFunctor
isAttrs
isFunction
showOption
throwIf
types
warnIf
getAttrFromPath
setAttrByPath
attrByPath
optionalAttrs
;
inherit (lib.modules)
mkAliasAndWrapDefsWithPriority;
inherit (lib.types)
path
submoduleWith
;
# Polyfill isFlake until Nix with https://github.com/NixOS/nix/pull/7207 is common
isFlake = maybeFlake:
if maybeFlake ? _type
then maybeFlake._type == "flake"
else maybeFlake ? inputs && maybeFlake ? outputs && maybeFlake ? sourceInfo;
/**
Deprecated for any use except type-merging into `perSystem`.
Use `lib.types.deferredModuleWith` instead, and add `apply = m: [ m ];` if needed.
The deferredModule type was pioneered in flake-parts for the `perSystem` option.
The Nixpkgs version has an improved merge function that returns a single module,
whereas this version returns a list. The flake-parts version was not updated to
match this improvement in Nixpkgs.
# History
This predates `lib.types.deferredModuleWith`, added in Nixpkgs 22.11
(https://github.com/NixOS/nixpkgs/pull/163617).
Documented as deprecated in flake-parts in January 2026.
*/
deferredModuleWith =
attrs@{ staticModules ? [ ] }: mkOptionType {
name = "deferredModule";
description = "module";
check = x: isAttrs x || isFunction x || path.check x;
merge = loc: defs: staticModules ++ map (def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value) defs;
inherit (submoduleWith { modules = staticModules; })
getSubOptions
getSubModules;
substSubModules = m: deferredModuleWith (attrs // {
staticModules = m;
});
functor = defaultFunctor "deferredModuleWith" // {
type = deferredModuleWith;
payload = {
inherit staticModules;
};
binOp = lhs: rhs: {
staticModules = lhs.staticModules ++ rhs.staticModules;
};
};
};
# Internal: preserves legacy list-merge behavior for perSystem type-merging.
mkLegacyDeferredModuleType =
module:
deferredModuleWith {
staticModules = [ module ];
};
errorExample = ''
For example:
outputs = inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } { /* module */ };
To avoid an infinite recursion, *DO NOT* pass `self.inputs` and
*DO NOT* pass `inherit (self) inputs`, but pass the output function
arguments as `inputs` like above.
'';
flake-parts-lib = rec {
evalFlakeModule =
args@
{ inputs ? self.inputs
, specialArgs ? { }
# legacy
, self ? inputs.self or (throw ''
When invoking flake-parts, you must pass all the flake output arguments,
and not just `self.inputs`.
${errorExample}
'')
, moduleLocation ? "${self.outPath}/flake.nix"
}:
let
inputsPos = builtins.unsafeGetAttrPos "inputs" args;
errorLocation =
# Best case: user makes it explicit
args.moduleLocation or (
# Slightly worse: Nix does not technically commit to unsafeGetAttrPos semantics
if inputsPos != null
then inputsPos.file
# Slightly worse: self may not be valid when an error occurs
else if args?inputs.self.outPath
then args.inputs.self.outPath + "/flake.nix"
# Fallback
else "<mkFlake argument>"
);
in
throwIf
(!args?self && !args?inputs) ''
When invoking flake-parts, you must pass in the flake output arguments.
${errorExample}
''
warnIf
(!args?inputs) ''
When invoking flake-parts, it is recommended to pass all the flake output
arguments in the `inputs` parameter. If you only pass `self`, it's not
possible to use the `inputs` module argument in the module `imports`.
Please pass the output function arguments. ${errorExample}
''
(module:
lib.evalModules {
specialArgs = {
inherit self flake-parts-lib moduleLocation;
inputs = args.inputs or /* legacy, warned above */ self.inputs;
} // specialArgs;
modules = [ ./all-modules.nix (lib.setDefaultModuleLocation errorLocation module) ];
class = "flake";
}
);
# Function to extract the default flakeModule from
# what may be a flake, returning the argument unmodified
# if it's not a flake.
#
# Useful to map over an 'imports' list to make it less
# verbose in the common case.
defaultModule = maybeFlake:
if isFlake maybeFlake
then maybeFlake.flakeModules.default or maybeFlake
else maybeFlake;
mkFlake = args: module:
let
eval = flake-parts-lib.evalFlakeModule args module;
in
eval.config.flake;
/**
Deprecated. Declare options directly, e.g. `options.foo.bar = mkOption { ... }`,
provided that `foo` is already declared as a submodule option.
In flake-parts, `flake` is declared as a submodule option by the core modules,
so `options.flake.<name>` declarations work directly.
This function wraps option declarations in a submodule, allowing them to
be merged into an existing submodule option. For example, if `foo` is
already declared as a submodule option, using
`options.foo = mkSubmoduleOptions { bar = mkOption {...}; }` would add
`bar` to the `foo` submodule.
# History
This was a workaround for https://github.com/NixOS/nixpkgs/issues/146882,
fixed in Nixpkgs 22.05 by https://github.com/NixOS/nixpkgs/pull/156533.
With the fix, declaring `options.foo.bar` directly works when `foo` is
already a submodule option. Documented as deprecated in flake-parts in January 2026.
*/
mkSubmoduleOptions =
options:
mkOption {
type = types.submoduleWith {
modules = [{ inherit options; }];
};
};
/**
Deprecated. Use mkPerSystemType/mkPerSystemOption for `perSystem` type-merging, or
use Nixpkgs `types.deferredModule` directly, noting the lack of list wrapping;
see `deferredModuleWith` docs.
*/
mkDeferredModuleType = mkLegacyDeferredModuleType;
/**
Given a module, construct an option type suitable for type-merging into `perSystem`'s type.
*/
mkPerSystemType = mkLegacyDeferredModuleType;
/**
Deprecated. Use mkPerSystemOption for `perSystem` type-merging, or
use `mkOption` and Nixpkgs `types.deferredModule` directly, noting the
lack of list wrapping; see `deferredModuleWith` docs.
*/
mkDeferredModuleOption =
module:
mkOption {
type = flake-parts-lib.mkPerSystemType module;
};
/**
Given a module, construct an option declaration suitable for merging into the core `perSystem` module.
*/
mkPerSystemOption = mkDeferredModuleOption;
# Polyfill https://github.com/NixOS/nixpkgs/pull/344216
# Nixpkgs master 2024-12-09, Nixpkgs 25.05
attrsWith = types.attrsWith or ({ elemType, lazy ? false, placeholder ? "name" }:
if lazy then types.attrsOf elemType else types.lazyAttrsOf elemType);
# Helper function for defining a per-system option that
# gets transposed by the usual flake system logic to a
# top-level flake attribute.
mkTransposedPerSystemModule = { name, option, file }: {
_file = file;
options = {
flake.${name} = mkOption {
type = attrsWith {
elemType = option.type;
lazy = true;
placeholder = "system";
};
default = { };
description = ''
See {option}`perSystem.${name}` for description and examples.
'';
};
perSystem = flake-parts-lib.mkPerSystemOption {
_file = file;
options.${name} = option;
};
};
config = {
transposition.${name} = { };
};
};
# Needed pending https://github.com/NixOS/nixpkgs/pull/198450
mkAliasOptionModule = from: to: { config, options, ... }:
let
fromOpt = getAttrFromPath from options;
toOf = attrByPath to
(abort "Renaming error: option `${showOption to}' does not exist.");
toType = let opt = attrByPath to { } options; in opt.type or (types.submodule { });
in
{
options = setAttrByPath from (mkOption
{
visible = true;
description = "Alias of {option}`${showOption to}`.";
apply = x: (toOf config);
} // optionalAttrs (toType != null) {
type = toType;
});
config = mkAliasAndWrapDefsWithPriority (setAttrByPath to) fromOpt;
};
# Helper function for importing while preserving module location. To be added
# in nixpkgs: https://github.com/NixOS/nixpkgs/pull/230588
# I expect these functions to remain identical. This one will stick around
# for a while to support older nixpkgs-lib.
importApply =
modulePath: staticArgs:
lib.setDefaultModuleLocation modulePath (import modulePath staticArgs);
inherit (import ./lib/memoize/memoize.nix {
inherit lib;
}) memoizeStr;
/**
`importAndPublish name module` returns a module that both imports the `module`, and exposes it as flake attribute `modules.flake.${name}`.
This also imports the optional [`modules`](https://flake.parts/options/flake-parts-modules.html) module to support that.
*/
importAndPublish = name: module: { lib, ... }: {
_class = "flake";
imports = [
module
./extras/modules.nix
];
flake.modules.flake.${name} = module;
};
};
# A best effort, lenient estimate. Please use a recent nixpkgs lib if you
# override it at all.
minVersion = "23.05pre-git";
in
if builtins.compareVersions lib.version minVersion < 0
then
abort ''
The nixpkgs-lib dependency of flake-parts was overridden but is too old.
The minimum supported version of nixpkgs-lib is ${minVersion},
but the actual version is ${lib.version}${revInfo}.
''
else
flake-parts-lib

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;
};
};
}

View file

@ -0,0 +1,5 @@
checks =
inputs.deploy-rs.lib
|> mapAttrs (system: deployLib:
deployLib.deployChecks deploy);

View file

@ -0,0 +1,57 @@
deploy.nodes = mapNodes nodes ({
name,
node,
...
}: let
inherit
(node.deploy)
ssh
user
interactiveSudo
remoteBuild
rollback
autoRollback
magicRollback
activationTimeout
confirmTimeout
;
nixosFor = system: inputs.deploy-rs.lib.${system}.activate.nixos;
in {
hostname =
if ssh.host != null
then ssh.host
else "";
profilesOrder = ["default"]; # profiles priority
profiles.default = {
path = nixosFor node.system nixosConfigurations.${name};
user = user;
sudo = "sudo -u";
interactiveSudo = interactiveSudo;
fastConnection = false;
autoRollback = autoRollback -> rollback;
magicRollback = magicRollback -> rollback;
activationTimeout = activationTimeout;
confirmTimeout = confirmTimeout;
remoteBuild = remoteBuild;
sshUser = ssh.user;
sshOpts =
ssh.opts
++ (
if elem "-p" ssh.opts
then []
else ["-p" (toString ssh.port)]
)
++ (
if elem "-A" ssh.opts
then []
else ["-A"]
);
};
});

View file

@ -0,0 +1,67 @@
# {
# _module = { ... };
# _type = "configuration";
# class = null;
# config = { ... };
# extendModules = «lambda extendModules @ /nix/store/9hfp0agnm43kz72l5lpfn9var5p0x2fa-source/lib/modules.nix:340:9»;
# graph = [ ... ];
# options = { ... };
# type = { ... };
# }
nixosConfigurations = mapNodes nodes (
{
base,
lib,
name,
node,
groupModules,
...
}: let
homeManager =
if node.homeManager != null
then node.homeManager
else if nodes.homeManager != null
then nodes.homeManager
else
warn ''
[snowflake] Neither `nodes.homeManager` nor `nodes.nodes.${name}.homeManager` were specified!
[snowflake] home-manager will NOT be used! User configuration will be ignored!
''
null;
userArgs = nodes.args // node.args;
ceruleanArgs = {
inherit systems root base nodes node;
inherit (node) system;
inherit (this) snow;
hostname = name;
_cerulean = {
inherit inputs userArgs ceruleanArgs homeManager;
specialArgs = userArgs // ceruleanArgs;
};
};
specialArgs = assert (userArgs
|> attrNames
|> all (argName:
! ceruleanArgs ? argName
|| abort ''
`specialArgs` are like super important to Cerulean my love... </3
But `args.${argName}` is a reserved argument name :(
''));
ceruleanArgs._cerulean.specialArgs;
in
lib.nixosSystem {
inherit (node) system;
inherit specialArgs;
modules =
[
self.nixosModules.default
(this.findImport /${root}/hosts/${name})
]
++ (groupModules root)
++ node.modules
++ nodes.modules;
}
);