From 3e29615db9ac072029557b031ee814f820e49791 Mon Sep 17 00:00:00 2001 From: _cry64 Date: Mon, 9 Mar 2026 02:46:41 +1000 Subject: [PATCH] example refactor snowflake module system --- nix/snow/default.nix | 173 +--------- nix/snow/flake/default.nix | 38 +++ nix/snow/flake/lib.nix.bak | 318 ++++++++++++++++++ nix/snow/{ => flake}/module.nix | 0 nix/snow/flake/modules/apps.nix | 61 ++++ nix/snow/flake/modules/checks.nix | 21 ++ nix/snow/flake/modules/debug.nix | 78 +++++ nix/snow/flake/modules/devShells.nix | 30 ++ nix/snow/flake/modules/flake.nix | 47 +++ nix/snow/flake/modules/formatter.nix | 52 +++ nix/snow/flake/modules/legacyPackages.nix | 21 ++ nix/snow/flake/modules/moduleWithSystem.nix | 32 ++ .../flake/modules/nixosConfigurations.nix | 36 ++ nix/snow/flake/modules/nixosModules.nix | 26 ++ nix/snow/flake/modules/nixpkgs.nix | 26 ++ nix/snow/flake/modules/overlays.nix | 32 ++ nix/snow/flake/modules/packages.nix | 23 ++ nix/snow/flake/modules/perSystem.nix | 159 +++++++++ nix/snow/flake/modules/transposition.nix | 132 ++++++++ nix/snow/flake/modules/withSystem.nix | 37 ++ nix/snow/{ => flake}/nodes/default.nix | 0 nix/snow/{ => flake}/nodes/shared.nix | 0 nix/snow/{ => flake}/nodes/submodule.nix | 0 nix/snow/flake/outputs/checks.nix | 5 + nix/snow/flake/outputs/deploy.nix | 57 ++++ .../flake/outputs/nixosConfigurations.nix | 67 ++++ 26 files changed, 1306 insertions(+), 165 deletions(-) create mode 100644 nix/snow/flake/default.nix create mode 100644 nix/snow/flake/lib.nix.bak rename nix/snow/{ => flake}/module.nix (100%) create mode 100644 nix/snow/flake/modules/apps.nix create mode 100644 nix/snow/flake/modules/checks.nix create mode 100644 nix/snow/flake/modules/debug.nix create mode 100644 nix/snow/flake/modules/devShells.nix create mode 100644 nix/snow/flake/modules/flake.nix create mode 100644 nix/snow/flake/modules/formatter.nix create mode 100644 nix/snow/flake/modules/legacyPackages.nix create mode 100644 nix/snow/flake/modules/moduleWithSystem.nix create mode 100644 nix/snow/flake/modules/nixosConfigurations.nix create mode 100644 nix/snow/flake/modules/nixosModules.nix create mode 100644 nix/snow/flake/modules/nixpkgs.nix create mode 100644 nix/snow/flake/modules/overlays.nix create mode 100644 nix/snow/flake/modules/packages.nix create mode 100644 nix/snow/flake/modules/perSystem.nix create mode 100644 nix/snow/flake/modules/transposition.nix create mode 100644 nix/snow/flake/modules/withSystem.nix rename nix/snow/{ => flake}/nodes/default.nix (100%) rename nix/snow/{ => flake}/nodes/shared.nix (100%) rename nix/snow/{ => flake}/nodes/submodule.nix (100%) create mode 100644 nix/snow/flake/outputs/checks.nix create mode 100644 nix/snow/flake/outputs/deploy.nix create mode 100644 nix/snow/flake/outputs/nixosConfigurations.nix diff --git a/nix/snow/default.nix b/nix/snow/default.nix index 6993ff1..15c67da 100644 --- a/nix/snow/default.nix +++ b/nix/snow/default.nix @@ -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... 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); - }; }) diff --git a/nix/snow/flake/default.nix b/nix/snow/flake/default.nix new file mode 100644 index 0000000..fad3c6d --- /dev/null +++ b/nix/snow/flake/default.nix @@ -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; +} diff --git a/nix/snow/flake/lib.nix.bak b/nix/snow/flake/lib.nix.bak new file mode 100644 index 0000000..c8ea2ba --- /dev/null +++ b/nix/snow/flake/lib.nix.bak @@ -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 "" + ); + 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.` 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 diff --git a/nix/snow/module.nix b/nix/snow/flake/module.nix similarity index 100% rename from nix/snow/module.nix rename to nix/snow/flake/module.nix diff --git a/nix/snow/flake/modules/apps.nix b/nix/snow/flake/modules/apps.nix new file mode 100644 index 0000000..3030d32 --- /dev/null +++ b/nix/snow/flake/modules/apps.nix @@ -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 . + + 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 ``. + ''; + example = lib.literalExpression '' + { + default.program = "''${config.packages.hello}/bin/hello"; + } + ''; + }; + file = ./apps.nix; +} diff --git a/nix/snow/flake/modules/checks.nix b/nix/snow/flake/modules/checks.nix new file mode 100644 index 0000000..9e7ceae --- /dev/null +++ b/nix/snow/flake/modules/checks.nix @@ -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; +} diff --git a/nix/snow/flake/modules/debug.nix b/nix/snow/flake/modules/debug.nix new file mode 100644 index 0000000..995f13b --- /dev/null +++ b/nix/snow/flake/modules/debug.nix @@ -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.` 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.` 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; + }; + }; +} diff --git a/nix/snow/flake/modules/devShells.nix b/nix/snow/flake/modules/devShells.nix new file mode 100644 index 0000000..c0cc6c5 --- /dev/null +++ b/nix/snow/flake/modules/devShells.nix @@ -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 .#`](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-develop.html) will run `devShells.`. + ''; + example = literalExpression '' + { + default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ wget bat cargo ]; + }; + } + ''; + }; + file = ./devShells.nix; +} diff --git a/nix/snow/flake/modules/flake.nix b/nix/snow/flake/modules/flake.nix new file mode 100644 index 0000000..871f04d --- /dev/null +++ b/nix/snow/flake/modules/flake.nix @@ -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;}; +} diff --git a/nix/snow/flake/modules/formatter.nix b/nix/snow/flake/modules/formatter.nix new file mode 100644 index 0000000..e2959ed --- /dev/null +++ b/nix/snow/flake/modules/formatter.nix @@ -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}; + }; + + }; +} diff --git a/nix/snow/flake/modules/legacyPackages.nix b/nix/snow/flake/modules/legacyPackages.nix new file mode 100644 index 0000000..fb17e14 --- /dev/null +++ b/nix/snow/flake/modules/legacyPackages.nix @@ -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 .#`](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-build.html). + ''; + }; + file = ./legacyPackages.nix; +} diff --git a/nix/snow/flake/modules/moduleWithSystem.nix b/nix/snow/flake/modules/moduleWithSystem.nix new file mode 100644 index 0000000..e5c7008 --- /dev/null +++ b/nix/snow/flake/modules/moduleWithSystem.nix @@ -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) + ]; + }; + }; + }; +} diff --git a/nix/snow/flake/modules/nixosConfigurations.nix b/nix/snow/flake/modules/nixosConfigurations.nix new file mode 100644 index 0000000..597132d --- /dev/null +++ b/nix/snow/flake/modules/nixosConfigurations.nix @@ -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 + ]; + }; + } + ''; + }; + }; +} diff --git a/nix/snow/flake/modules/nixosModules.nix b/nix/snow/flake/modules/nixosModules.nix new file mode 100644 index 0000000..86ee9cc --- /dev/null +++ b/nix/snow/flake/modules/nixosModules.nix @@ -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. + ''; + }; + }; +} diff --git a/nix/snow/flake/modules/nixpkgs.nix b/nix/snow/flake/modules/nixpkgs.nix new file mode 100644 index 0000000..44df915 --- /dev/null +++ b/nix/snow/flake/modules/nixpkgs.nix @@ -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 + ); + }; + }; + }; +} diff --git a/nix/snow/flake/modules/overlays.nix b/nix/snow/flake/modules/overlays.nix new file mode 100644 index 0000000..172336c --- /dev/null +++ b/nix/snow/flake/modules/overlays.nix @@ -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`. + ''; + }; + }; +} diff --git a/nix/snow/flake/modules/packages.nix b/nix/snow/flake/modules/packages.nix new file mode 100644 index 0000000..20f0071 --- /dev/null +++ b/nix/snow/flake/modules/packages.nix @@ -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 .#` will build `packages.`. + ''; + }; + file = ./packages.nix; +} diff --git a/nix/snow/flake/modules/perSystem.nix b/nix/snow/flake/modules/perSystem.nix new file mode 100644 index 0000000..d4890ef --- /dev/null +++ b/nix/snow/flake/modules/perSystem.nix @@ -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..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 `` 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); + }; + +} diff --git a/nix/snow/flake/modules/transposition.nix b/nix/snow/flake/modules/transposition.nix new file mode 100644 index 0000000..d532e76 --- /dev/null +++ b/nix/snow/flake/modules/transposition.nix @@ -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.`. + + The stub option declaration does not support merging and lacks + documentation, so you are recommended to declare the {option}`perSystem.` + 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. is defined (and in this instance 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 + ); + }; + }; +} diff --git a/nix/snow/flake/modules/withSystem.nix b/nix/snow/flake/modules/withSystem.nix new file mode 100644 index 0000000..161eece --- /dev/null +++ b/nix/snow/flake/modules/withSystem.nix @@ -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; + }; + }; +} diff --git a/nix/snow/nodes/default.nix b/nix/snow/flake/nodes/default.nix similarity index 100% rename from nix/snow/nodes/default.nix rename to nix/snow/flake/nodes/default.nix diff --git a/nix/snow/nodes/shared.nix b/nix/snow/flake/nodes/shared.nix similarity index 100% rename from nix/snow/nodes/shared.nix rename to nix/snow/flake/nodes/shared.nix diff --git a/nix/snow/nodes/submodule.nix b/nix/snow/flake/nodes/submodule.nix similarity index 100% rename from nix/snow/nodes/submodule.nix rename to nix/snow/flake/nodes/submodule.nix diff --git a/nix/snow/flake/outputs/checks.nix b/nix/snow/flake/outputs/checks.nix new file mode 100644 index 0000000..21fd677 --- /dev/null +++ b/nix/snow/flake/outputs/checks.nix @@ -0,0 +1,5 @@ + checks = + inputs.deploy-rs.lib + |> mapAttrs (system: deployLib: + deployLib.deployChecks deploy); + diff --git a/nix/snow/flake/outputs/deploy.nix b/nix/snow/flake/outputs/deploy.nix new file mode 100644 index 0000000..08caa8f --- /dev/null +++ b/nix/snow/flake/outputs/deploy.nix @@ -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"] + ); + }; + }); + diff --git a/nix/snow/flake/outputs/nixosConfigurations.nix b/nix/snow/flake/outputs/nixosConfigurations.nix new file mode 100644 index 0000000..799758d --- /dev/null +++ b/nix/snow/flake/outputs/nixosConfigurations.nix @@ -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...