diff --git a/README.md b/README.md index db478ab0..3cf5c982 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,8 @@ source by. | `colmena` | `colmena` | a hive written for colmena, taken as it is, with each node a NixOS configuration; for porting an existing hive and the modules written for it | | `caisson-disko` | `disko` | disk layouts, with the NixOS configurations installed onto each declared inside it; a machine gets the layout's file systems and no disko scripts; the disko integration we recommend for new configurations | | `disko` | `disko` | disk layouts written as disko reads them, whose machines import disko's NixOS module, so disko's command, `disko-install` and its image builder work on them | +| `caisson-facter` | none | the hardware of a machine, a nixos-facter report checked against the report versions caisson knows and extended with a project's own options, with the NixOS configurations that run on it declared inside it; a machine gets the report through nixpkgs' `hardware.facter` | +| `facter` | none | a `facter.json` as nixos-facter writes it, with the NixOS configurations that run on it declared inside it; a machine gets it as `hardware.facter.reportPath` | | `system-manager` | `system-manager` | the systems of machines running another Linux distribution, published as the `systemConfigs` that `system-manager switch` reads | | `structural` | none | a configuration that only exports and holds configurations beneath it: the top of a repository whose point is what it exports, such as caisson itself, or a layer at any depth | diff --git a/configs/structural/impl/default.nix b/configs/structural/impl/default.nix index 9f7d8b4e..0fd0cff4 100644 --- a/configs/structural/impl/default.nix +++ b/configs/structural/impl/default.nix @@ -29,6 +29,8 @@ colmena caisson-disko disko + caisson-facter + facter darwin terranix system-manager diff --git a/docs/development/repository-structure.md b/docs/development/repository-structure.md index 59d72bf5..6c42ecd2 100644 --- a/docs/development/repository-structure.md +++ b/docs/development/repository-structure.md @@ -27,7 +27,8 @@ without nixpkgs can depend on it directly. The layer users reach for: the integrations (`structural`, `flake-parts`, `nixpkgs`, `nixos`, `nixos-minimal`, `home-manager`, `home-manager-minimal`, `darwin`, `hjem`, `caisson-colmena`, `colmena`, -`caisson-disko`, `disko`, `terranix`, `system-manager`), each +`caisson-disko`, `disko`, `caisson-facter`, `facter`, `terranix`, +`system-manager`), each a library overlay contributing its `lib.caisson` namespace, registering its module class where it has a class (`nixos-minimal` and `home-manager-minimal` diff --git a/lib-overlays/caisson-facter/default.nix b/lib-overlays/caisson-facter/default.nix new file mode 100644 index 00000000..3d06f5b9 --- /dev/null +++ b/lib-overlays/caisson-facter/default.nix @@ -0,0 +1,286 @@ +# SPDX-License-Identifier: MIT +# +# The caisson-facter integration, the native facter integration: it +# owns the `caisson-facter` class. A facter configuration is the +# hardware of a machine: a module of that class with `facter.report`, +# the hardware report nixos-facter writes, as data. It declares the +# NixOS configurations that run on that hardware the way any +# configuration declares NixOS configurations, under +# `caisson.nixos.configurations`. +# +# The evaluation is caisson's own `evalModules` over the report +# options, the assertions, the framework modules, the selection and the +# configuration's module, on the composed library. A project extends +# the class with options of its own about the hardware, beside the +# report. The outputs are the report, checked against the report +# versions the integration knows, the report as the file nixos-facter +# writes, the configuration as data and its hash. Every output fails +# with the messages of the assertions that do not hold. +# +# The default module of the class adds caisson's `caisson-facter-hardware` +# NixOS module to the NixOS configurations declared inside it. That +# module hands the report to nixpkgs' `hardware.facter` module, which +# turns it into the NixOS settings of the hardware: kernel modules, +# firmware, microcode, graphics, networking. +# +# A facter configuration is published under no output attribute set: +# no tool reads a facter configuration from a flake. Its outputs are on +# its manifest. +{ + mkLibOverlay, + ... +}: +{ + + imports = [ + # The library of nixpkgs, which this overlay calls through the + # composed library, imported by key so it is composed wherever + # this is. + ((mkLibOverlay ../nixpkgs-lib) // { key = "nixpkgs-lib"; }) + # What an integration is written from, imported by key so it is + # composed wherever this integration is. + ((mkLibOverlay ../integrations) // { key = "integrations"; }) + ]; + + overlay = + final: prev: + let + context = "lib.caisson.caisson-facter"; + + # The versions of the report nixos-facter writes that the + # integration knows. A report of another version is refused, so + # an unknown report is not handed to nixpkgs' `hardware.facter`. + supportedReportVersions = [ 1 ]; + + # The report options of the class: the report as data, and the + # file nixos-facter writes, which the report defaults to. + reportOptions = + { config, lib, ... }: + { + options.facter = { + reportPath = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + The hardware report nixos-facter writes, `facter.json`. + `facter.report` defaults to its content. + ''; + }; + report = lib.mkOption { + type = lib.types.attrsOf lib.types.anything; + default = + if config.facter.reportPath == null then + { } + else + builtins.fromJSON (builtins.readFile config.facter.reportPath); + defaultText = lib.literalMD "the content of `facter.reportPath`, or `{ }`"; + description = "The hardware report nixos-facter writes, as data."; + }; + }; + }; + + # The assertions of the class: a module of the class adds one to + # refuse the hardware, with a message saying why. + assertionsOptions = + { lib, ... }: + { + options.assertions = lib.mkOption { + type = lib.types.listOf ( + lib.types.submodule { + options = { + assertion = lib.mkOption { + type = lib.types.bool; + description = "Whether the hardware is accepted."; + }; + message = lib.mkOption { + type = lib.types.str; + description = "Why the hardware is refused, when `assertion` is false."; + }; + }; + } + ); + default = [ ]; + description = '' + Conditions the hardware must meet. Every output of the facter + configuration fails with the message of each assertion that is + false. + ''; + }; + }; + + # A value as data: attributes whose names begin with `_`, and + # functions, left out. + stripInternal = + value: + if builtins.isAttrs value then + final.concatMapAttrs ( + name: inner: + if final.hasPrefix "_" name || builtins.isFunction inner then + { } + else + { ${name} = stripInternal inner; } + ) value + else if builtins.isList value then + builtins.map stripInternal (builtins.filter (inner: !builtins.isFunction inner) value) + else + value; + + # The outputs of an evaluated facter configuration, from its + # `config`. + hardwareOf = + config: + let + inherit (config.facter) report; + failed = + builtins.map (assertion: assertion.message) ( + builtins.filter (assertion: !assertion.assertion) config.assertions + ) + ++ ( + if report == { } || builtins.elem (report.version or null) supportedReportVersions then + [ ] + else + [ + "the facter report has version ${ + builtins.toJSON (report.version or null) + }, and the versions caisson knows are ${builtins.toJSON supportedReportVersions}" + ] + ); + checked = + value: + if failed == [ ] then + value + else + throw '' + ${context}: the hardware is refused: + ${builtins.concatStringsSep "\n" (builtins.map (message: "- ${message}") failed)} + ''; + serializableConfig = checked (stripInternal (builtins.removeAttrs config [ "caisson" ])); + in + { + report = checked report; + # The report as the file nixos-facter writes. + reportFile = builtins.toFile "facter.json" (builtins.toJSON (checked report)); + # The configuration as data: the report and what the modules of + # the class declare about the hardware, without caisson's + # options. + inherit serializableConfig; + # The identity of the hardware: the hash of the configuration + # as data. + hash = builtins.hashString "sha256" (builtins.toJSON serializableConfig); + }; + + # The evaluator's call, on the lib of the view being evaluated. + # `ecosystemArgs`, which only the twin's pattern admits, is merged + # over the call last. + evaluate = + args: + { lib, manifest }: + let + selection = lib.caisson.integrations; + registry = lib.caisson.modules.caisson-facter or { }; + frameworkModules = selection.frameworkModules "caisson-facter" registry; + moduleImports = selection.moduleImportsOf "caisson-facter" { inherit lib manifest; } args; + + # The configuration's module: the module passed, else the + # configuration registered under the configuration's name + # (`configs/caisson-facter/`), else none. + name = manifest.name or null; + registered = lib.caisson.configs.caisson-facter or { }; + configModule = + if (args.configModule or null) != null then + args.configModule + else if name != null then + registered.${name} or null + else + null; + + evaluated = lib.evalModules ( + { + class = "caisson-facter"; + specialArgs = { + inherit lib; + } + // (if (args.specialArgs or null) != null then args.specialArgs else { }); + modules = [ + reportOptions + assertionsOptions + ] + ++ frameworkModules + ++ moduleImports registry + ++ (if configModule == null then [ ] else [ configModule ]); + } + // (if (args.ecosystemArgs or null) != null then args.ecosystemArgs else { }) + ); + in + { + value = evaluated; + outputs = hardwareOf evaluated.config; + }; + + configuration = + args: + final.caisson.integrations.mkModuleConfiguration { + type = "caisson-facter"; + defaultPkgs = args.defaultPkgs or null; + evaluate = evaluate args; + }; + + integration = final.caisson.integrations.mkIntegration { + name = "caisson-facter"; + class = "caisson-facter"; + # What these return is a configuration, a function of + # `{ name, parent }`: the parent that declares it under + # `caisson.caisson-facter.configurations.` finalizes it. + mkConfiguration = + { + # The facter configuration's module: `facter.report` or + # `facter.reportPath`, and the NixOS configurations that run + # on the hardware under `caisson.nixos.configurations`. When + # absent, the configuration registered under the + # configuration's name (`lib.caisson.configs.caisson-facter.`), + # if any. Further modules of the class are selected with + # `moduleImports`, from the registry. + configModule ? null, + # The package set the configurations beneath this one run + # on: a function that receives the package sets available + # to each, by package config name, and returns the set to + # run on. + defaultPkgs ? null, + # The selection over the caisson-facter class of the + # registry. It replaces the default of the class, which is + # every entry named `default` followed by what the + # configurations above added. + moduleImports ? null, + # A selection added to that selection, whichever it is. + extraModuleImports ? null, + # Extra module arguments, merged over those the framework + # supplies. + specialArgs ? null, + }@args: + configuration args; + # The same arguments and `ecosystemArgs`, the evaluator's + # arguments merged over the composed call last. + mkConfigurationWithEcosystemArgs = + { + configModule ? null, + defaultPkgs ? null, + moduleImports ? null, + extraModuleImports ? null, + specialArgs ? null, + ecosystemArgs ? null, + }@args: + configuration args; + extra = { + # The versions of the report nixos-facter writes that the + # integration accepts. + inherit supportedReportVersions; + }; + }; + in + { + caisson = (prev.caisson or { }) // { + caisson-facter = integration.namespace; + }; + }; + +} diff --git a/lib-overlays/facter/default.nix b/lib-overlays/facter/default.nix new file mode 100644 index 00000000..a5225c56 --- /dev/null +++ b/lib-overlays/facter/default.nix @@ -0,0 +1,170 @@ +# SPDX-License-Identifier: MIT +# +# The facter integration, the upstream-compatible facter integration: +# it owns the `facter` class. A facter configuration is a hardware +# report as nixos-facter writes it, `facter.json`, named by the one +# option of the class, `reportPath`. It declares the NixOS +# configurations that run on that hardware under +# `caisson.nixos.configurations`. +# +# The default module of the class adds caisson's NixOS module +# `facter-hardware` to the NixOS configurations declared inside it. +# That module sets nixpkgs' `hardware.facter.reportPath` to the report, +# the line a NixOS configuration written for nixos-facter carries, so +# nixpkgs' `hardware.facter` module reads the file as it does for any +# NixOS configuration. +# +# The outputs are `reportPath` and `report`, the content of the file. A +# facter configuration is published under no output attribute set. +# For a report extended with options of a project, and checked against +# the report versions caisson knows, see the caisson-facter integration. +{ + mkLibOverlay, + ... +}: +{ + + imports = [ + # The library of nixpkgs, which this overlay calls through the + # composed library, imported by key so it is composed wherever + # this is. + ((mkLibOverlay ../nixpkgs-lib) // { key = "nixpkgs-lib"; }) + # What an integration is written from, imported by key so it is + # composed wherever this integration is. + ((mkLibOverlay ../integrations) // { key = "integrations"; }) + ]; + + overlay = + final: prev: + let + context = "lib.caisson.facter"; + + # The option of the class: the report file. + reportOptions = + { lib, ... }: + { + options.reportPath = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + The hardware report nixos-facter writes, `facter.json`, which + the NixOS configurations declared inside this configuration + get as `hardware.facter.reportPath`. + ''; + }; + }; + + evaluate = + args: + { lib, manifest }: + let + selection = lib.caisson.integrations; + registry = lib.caisson.modules.facter or { }; + frameworkModules = selection.frameworkModules "facter" registry; + moduleImports = selection.moduleImportsOf "facter" { inherit lib manifest; } args; + + # The configuration's module: the module passed, else the + # configuration registered under the configuration's name + # (`configs/facter/`), else none. + name = manifest.name or null; + registered = lib.caisson.configs.facter or { }; + configModule = + if (args.configModule or null) != null then + args.configModule + else if name != null then + registered.${name} or null + else + null; + + evaluated = lib.evalModules ( + { + class = "facter"; + specialArgs = { + inherit lib; + } + // (if (args.specialArgs or null) != null then args.specialArgs else { }); + modules = [ + reportOptions + ] + ++ frameworkModules + ++ moduleImports registry + ++ (if configModule == null then [ ] else [ configModule ]); + } + // (if (args.ecosystemArgs or null) != null then args.ecosystemArgs else { }) + ); + inherit (evaluated.config) reportPath; + in + { + value = evaluated; + outputs = { + inherit reportPath; + report = + if reportPath == null then + throw "${context}: the facter configuration `${toString name}` names no `reportPath`." + else + builtins.fromJSON (builtins.readFile reportPath); + }; + }; + + configuration = + args: + final.caisson.integrations.mkModuleConfiguration { + type = "facter"; + defaultPkgs = args.defaultPkgs or null; + evaluate = evaluate args; + }; + + integration = final.caisson.integrations.mkIntegration { + name = "facter"; + class = "facter"; + # What these return is a configuration, a function of + # `{ name, parent }`: the parent that declares it under + # `caisson.facter.configurations.` finalizes it. + mkConfiguration = + { + # The facter configuration's module: `reportPath`, and the + # NixOS configurations that run on the hardware under + # `caisson.nixos.configurations`. When absent, the + # configuration registered under the configuration's name + # (`lib.caisson.configs.facter.`), if any. Further + # modules of the class are selected with `moduleImports`, + # from the registry. + configModule ? null, + # The package set the configurations beneath this one run + # on: a function that receives the package sets available + # to each, by package config name, and returns the set to + # run on. + defaultPkgs ? null, + # The selection over the facter class of the registry. It + # replaces the default of the class, which is every entry + # named `default` followed by what the configurations above + # added. + moduleImports ? null, + # A selection added to that selection, whichever it is. + extraModuleImports ? null, + # Extra module arguments, merged over those the framework + # supplies. + specialArgs ? null, + }@args: + configuration args; + # The same arguments and `ecosystemArgs`, the evaluator's + # arguments merged over the composed call last. + mkConfigurationWithEcosystemArgs = + { + configModule ? null, + defaultPkgs ? null, + moduleImports ? null, + extraModuleImports ? null, + specialArgs ? null, + ecosystemArgs ? null, + }@args: + configuration args; + }; + in + { + caisson = (prev.caisson or { }) // { + facter = integration.namespace; + }; + }; + +} diff --git a/lib-overlays/integrations/default.nix b/lib-overlays/integrations/default.nix index 90a3c88c..7faa2afd 100644 --- a/lib-overlays/integrations/default.nix +++ b/lib-overlays/integrations/default.nix @@ -544,25 +544,29 @@ declared ? configurations && declared ? exported ) (builtins.attrNames (options.caisson or { })); + # Of `integrationsIn options`, the integrations whose + # `caisson..configurations` a module of the evaluation + # defines: those whose highest priority is above the priority of + # an option default. The others hold no configurations, so neither + # their configurations nor their `exported` selector is read, which + # spares merging those options in every evaluation for every + # integration the evaluation could hold. + integrationsDeclaringIn = + options: + builtins.filter (name: options.caisson.${name}.configurations.highestPrio < 1500) ( + integrationsIn options + ); + # The configurations an evaluated configuration declares beneath # it, by integration and then name, as the `children` an # integration's `evaluate` returns: the values of its # `caisson..configurations` options, which finalize # each entry when read. An integration with none declared is left - # out. An option with no definition but its default, whose highest - # priority is the priority of an option default, is not read at - # all, which spares merging it in every evaluation for every - # integration the evaluation could hold. + # out. childrenOf = { options, config }: - let - optionDefaultPriority = 1500; - declaring = builtins.filter ( - name: options.caisson.${name}.configurations.highestPrio < optionDefaultPriority - ) (integrationsIn options); - in filterAttrs (_: declared: declared != { }) ( - genAttrs declaring (name: config.caisson.${name}.configurations) + genAttrs (integrationsDeclaringIn options) (name: config.caisson.${name}.configurations) ); # The nearest configuration above `manifest` whose integration is @@ -831,6 +835,7 @@ entriesOf configurationsModuleFor integrationsIn + integrationsDeclaringIn nearestOf mkEvaluation publish diff --git a/modules/caisson-facter/core b/modules/caisson-facter/core new file mode 120000 index 00000000..3715354a --- /dev/null +++ b/modules/caisson-facter/core @@ -0,0 +1 @@ +../generic/core \ No newline at end of file diff --git a/modules/caisson-facter/default/default.nix b/modules/caisson-facter/default/default.nix new file mode 100644 index 00000000..4a325317 --- /dev/null +++ b/modules/caisson-facter/default/default.nix @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: MIT +# +# The default module of the caisson-facter class. +# +# The NixOS configurations declared inside a facter configuration run +# on its hardware. This module adds caisson's NixOS module +# `caisson-facter-hardware`, which hands the facter report to nixpkgs' +# `hardware.facter` module, to the modules that a NixOS configuration +# declared inside this facter configuration imports by default. A +# NixOS configuration that passes `moduleImports` selects its modules +# itself, and then it lists `caisson-facter-hardware` itself, as +# `caisson/caisson-facter-hardware` in a flake that lists caisson in +# `projects`. +# +# `closure-lib` is the library of caisson, where +# `caisson-facter-hardware` is registered under that name. +{ closure-lib, ... }: +{ ... }: +{ + # The options under `caisson` are declared by the core module of the + # class. + imports = [ closure-lib.caisson.modules.caisson-facter.core ]; + + caisson.forChildren.extraDefaultModuleImports.nixos = _lib: [ + closure-lib.caisson.modules.nixos.caisson-facter-hardware + ]; +} diff --git a/modules/facter/core b/modules/facter/core new file mode 120000 index 00000000..3715354a --- /dev/null +++ b/modules/facter/core @@ -0,0 +1 @@ +../generic/core \ No newline at end of file diff --git a/modules/facter/default/default.nix b/modules/facter/default/default.nix new file mode 100644 index 00000000..6c88192b --- /dev/null +++ b/modules/facter/default/default.nix @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: MIT +# +# The default module of the facter class. +# +# The NixOS configurations declared inside a facter configuration run +# on its hardware. This module adds caisson's NixOS module +# `facter-hardware`, which sets `hardware.facter.reportPath` to the +# report file, to the modules that a NixOS configuration declared inside +# this facter configuration imports by default. A NixOS configuration +# that passes `moduleImports` selects its modules itself, and then it +# lists `facter-hardware` itself, as `caisson/facter-hardware` in a +# flake that lists caisson in `projects`. +# +# `closure-lib` is the library of caisson, where `facter-hardware` is +# registered under that name. +{ closure-lib, ... }: +{ ... }: +{ + # The options under `caisson` are declared by the core module of the + # class. + imports = [ closure-lib.caisson.modules.facter.core ]; + + caisson.forChildren.extraDefaultModuleImports.nixos = _lib: [ + closure-lib.caisson.modules.nixos.facter-hardware + ]; +} diff --git a/modules/generic/core/caisson/configurations.nix b/modules/generic/core/caisson/configurations.nix index 3445eec4..06b02d47 100644 --- a/modules/generic/core/caisson/configurations.nix +++ b/modules/generic/core/caisson/configurations.nix @@ -18,7 +18,10 @@ let manifest = lib.caisson-core.evalManifest; - integrations = lib.caisson.integrations.integrationsIn options; + # The integrations whose configurations a module of this evaluation + # declares. An integration with none declared passes nothing up, + # whatever its `exported` selects, so it is not read. + integrations = lib.caisson.integrations.integrationsDeclaringIn options; isManifest = value: builtins.isAttrs value && (value._type or null) == "caisson-manifest"; diff --git a/modules/generic/core/caisson/exports.nix b/modules/generic/core/caisson/exports.nix index b4a12b9c..68de1be2 100644 --- a/modules/generic/core/caisson/exports.nix +++ b/modules/generic/core/caisson/exports.nix @@ -13,7 +13,7 @@ in options.caisson.exports = { libOverlays = lib.mkOption { - type = lib.types.attrsOf types.libOverlay; + type = types.libOverlays; description = "The registered library overlays `caisson.libOverlays.exported` selected, merged with what lies beneath."; }; @@ -35,7 +35,7 @@ in }; pkgOverlays = lib.mkOption { - type = lib.types.attrsOf types.pkgOverlay; + type = types.pkgOverlays; description = "The registered package overlays `caisson.pkgOverlays.exported` selected, merged with what lies beneath."; }; diff --git a/modules/generic/core/caisson/libOverlays.nix b/modules/generic/core/caisson/libOverlays.nix index 0a906565..ff53d9c2 100644 --- a/modules/generic/core/caisson/libOverlays.nix +++ b/modules/generic/core/caisson/libOverlays.nix @@ -8,7 +8,7 @@ in export.enabled = lib.mkEnableOption "lib overlay export"; exported = lib.mkOption { - type = lib.types.functionTo (lib.types.attrsOf types.libOverlay); + type = lib.types.functionTo types.libOverlays; description = '' Function that selects which registered library overlays to export. Receives the set of overlays registered via `mkLib` and diff --git a/modules/generic/core/default.nix b/modules/generic/core/default.nix index ffc7bf5c..9e8d3a2f 100644 --- a/modules/generic/core/default.nix +++ b/modules/generic/core/default.nix @@ -30,9 +30,11 @@ (closure-lib.caisson.integrations.configurationsModuleFor [ "caisson-colmena" "caisson-disko" + "caisson-facter" "colmena" "darwin" "disko" + "facter" "flake-parts" "hjem" "home-manager" diff --git a/modules/generic/core/types.nix b/modules/generic/core/types.nix index 7a76a42e..f98deeb9 100644 --- a/modules/generic/core/types.nix +++ b/modules/generic/core/types.nix @@ -12,6 +12,42 @@ let && builtins.isFunction v.overlay && builtins.isList (v.imports or [ ]) && builtins.all isLibOverlay (v.imports or [ ]); + + isPkgOverlay = v: builtins.isAttrs v && builtins.isFunction (v.overlay or null); + + # An attribute set of registered entries, merged as one attribute set. + # The definitions are joined with `//`, and an entry is checked when + # it is read. Every evaluation defines the exports of its library, so + # merging each entry as an option of its own would cost a merge per + # entry in every evaluation. + # + # An option of such a type can have more than one definition of the + # same entry. A configuration and the configurations declared inside + # it share one library. Each of them exports the entries of that + # library, and a configuration also passes up what the configurations + # inside it export. So the same entry reaches a configuration from two + # places, and either definition is the entry. + entries = + { + name, + description, + checkEntry, + }: + lib.mkOptionType { + inherit name; + description = "attribute set of ${description}"; + descriptionClass = "composite"; + check = builtins.isAttrs; + merge = + loc: defs: + builtins.mapAttrs ( + entryName: entry: + if checkEntry entry then + entry + else + throw "The entry `${entryName}` of `${lib.showOption loc}` is not a ${description}." + ) (builtins.foldl' (merged: def: merged // def.value) { } defs); + }; in { @@ -23,23 +59,24 @@ in }; # A registered package overlay, as a configuration exports it. - # - # An option of this type can have more than one definition of the - # same entry. A configuration and the configurations declared inside - # it share one library. Each of them exports the package overlays of - # that library, and a configuration also passes up what the - # configurations inside it export. So the same entry reaches a - # configuration from two places. - # - # The type has no merge function of its own, so the module system - # merges the definitions as attribute sets. The definitions are the - # same entry, and the result is that entry. `libOverlay` above works - # the same way. The type `raw` would refuse the second definition. pkgOverlay = lib.mkOptionType { name = "pkgOverlay"; description = "package overlay ({ imports ? [ ], overlay })"; descriptionClass = "noun"; - check = v: builtins.isAttrs v && builtins.isFunction (v.overlay or null); + check = isPkgOverlay; + }; + + # The registered library overlays and package overlays, by name, as a + # configuration exports them. + libOverlays = entries { + name = "libOverlays"; + description = "library overlays ({ imports ? [ ], overlay })"; + checkEntry = isLibOverlay; + }; + pkgOverlays = entries { + name = "pkgOverlays"; + description = "package overlays ({ imports ? [ ], overlay })"; + checkEntry = isPkgOverlay; }; # The manifest type: structural, checked on the export side only. @@ -64,8 +101,7 @@ in && builtins.all builtins.isAttrs (builtins.attrValues v.modules) && builtins.isAttrs (v.configs or { }) && builtins.all builtins.isAttrs (builtins.attrValues (v.configs or { })) - && builtins.isAttrs (v.libOverlays or null) - && builtins.all isLibOverlay (builtins.attrValues v.libOverlays); + && builtins.isAttrs (v.libOverlays or null); }; } diff --git a/modules/nixos/caisson-facter-hardware/default.nix b/modules/nixos/caisson-facter-hardware/default.nix new file mode 100644 index 00000000..a0d774a0 --- /dev/null +++ b/modules/nixos/caisson-facter-hardware/default.nix @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: MIT +# +# A NixOS module for a NixOS configuration that runs on the hardware of +# a caisson-facter configuration: a NixOS configuration declared inside +# one. It hands the facter report of the hardware, checked by the +# caisson-facter configuration, to nixpkgs' `hardware.facter` module as +# `hardware.facter.report`, at default priority. nixpkgs' module turns +# the report into the NixOS settings of the hardware. +# +# The default module of the caisson-facter class adds this module to +# every NixOS configuration declared inside a caisson-facter +# configuration. A NixOS configuration that passes `moduleImports` +# selects its modules itself, and then it lists this module itself. A +# module of the NixOS configuration that needs more of its hardware +# reads it through `lib.caisson-core.evalManifest.nearest.caisson-facter`. +{ ... }: +{ lib, ... }: +let + hardware = + lib.caisson-core.evalManifest.nearest.caisson-facter or (throw '' + caisson's `caisson-facter-hardware` NixOS module gives a NixOS configuration + the facter report of the hardware it runs on, and this NixOS configuration + is not declared inside a caisson-facter configuration. + ''); +in +{ + _file = "caisson:caisson-facter-hardware"; + + hardware.facter.report = lib.mkDefault hardware.outputs.report; +} diff --git a/modules/nixos/facter-hardware/default.nix b/modules/nixos/facter-hardware/default.nix new file mode 100644 index 00000000..ddad9c67 --- /dev/null +++ b/modules/nixos/facter-hardware/default.nix @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: MIT +# +# A NixOS module for a NixOS configuration that runs on the hardware of +# a facter configuration: a NixOS configuration declared inside one. It +# sets `hardware.facter.reportPath` to the report file of the facter +# configuration, at default priority, the line a NixOS configuration +# written for nixos-facter carries. nixpkgs' `hardware.facter` module +# reads the file and turns it into the NixOS settings of the hardware. +# +# The default module of the facter class adds this module to every +# NixOS configuration declared inside a facter configuration. A NixOS +# configuration that passes `moduleImports` selects its modules itself, +# and then it lists this module itself. +{ ... }: +{ lib, ... }: +let + hardware = + lib.caisson-core.evalManifest.nearest.facter or (throw '' + caisson's `facter-hardware` NixOS module gives a NixOS configuration the + facter report of the hardware it runs on, and this NixOS configuration is + not declared inside a facter configuration. + ''); +in +{ + _file = "caisson:facter-hardware"; + + hardware.facter.reportPath = lib.mkDefault hardware.outputs.reportPath; +} diff --git a/tests/argument-errors/check.sh b/tests/argument-errors/check.sh index e6278da7..1e1aa83a 100755 --- a/tests/argument-errors/check.sh +++ b/tests/argument-errors/check.sh @@ -36,6 +36,8 @@ namespaces=( caisson-colmena caisson-disko disko + caisson-facter + facter terranix system-manager ) diff --git a/tests/pinned-world/default.nix b/tests/pinned-world/default.nix index b039ce86..0a92fa96 100644 --- a/tests/pinned-world/default.nix +++ b/tests/pinned-world/default.nix @@ -66,6 +66,8 @@ let registered."caisson/colmena" registered."caisson/caisson-disko" registered."caisson/disko" + registered."caisson/caisson-facter" + registered."caisson/facter" registered."caisson/darwin" registered."caisson/terranix" registered."caisson/system-manager" @@ -82,6 +84,7 @@ let expectedCaissonNames = [ "caisson-colmena" "caisson-disko" + "caisson-facter" "callConsumerFlake" "callFlake" "colmena" @@ -92,6 +95,7 @@ let "elide" "eval-weight" "evalManifest" + "facter" "finalizeTop" "flake-parts" "hjem" @@ -471,6 +475,8 @@ let "caisson/colmena" "caisson/caisson-disko" "caisson/disko" + "caisson/caisson-facter" + "caisson/facter" "caisson/darwin" "caisson/terranix" "caisson/system-manager" @@ -568,6 +574,8 @@ let "colmena" "caisson-disko" "disko" + "caisson-facter" + "facter" "darwin" "terranix" "system-manager" @@ -1526,6 +1534,100 @@ let ) && builtins.attrNames probe.top.hjemConfigurations == [ "probe" ]; + # A machine inside a caisson-facter configuration and one inside a + # facter configuration, from the report of nixpkgs' facter test, get + # from nixpkgs' `hardware.facter` module what a NixOS configuration + # that sets `hardware.facter.reportPath` itself gets: the same + # report and the same kernel modules for the initrd. The + # caisson-facter configuration refuses a report of a version it does + # not know, and a hardware an assertion refuses. + facterHandsTheReportToNixpkgs = + let + reportPath = "${inputs.nixpkgs}/nixos/tests/facter/facter.json"; + machine = { + boot.loader.grub.enable = false; + fileSystems."/" = { + device = "none"; + fsType = "tmpfs"; + }; + system.stateVersion = "25.05"; + }; + upstream = import "${inputs.nixpkgs}/nixos/lib/eval-config.nix" { + system = "x86_64-linux"; + modules = [ + machine + { hardware.facter.reportPath = reportPath; } + ]; + }; + top = hiveLib.caisson.structural.mkTopConfiguration { + moduleImports = _modules: [ ]; + configModule = + { lib, ... }: + { + caisson.caisson-facter.configurations.native = lib.caisson.caisson-facter.mkConfiguration { + configModule = + { lib, ... }: + { + facter.reportPath = reportPath; + caisson.nixos.configurations.native = lib.caisson.nixos.mkConfiguration { + configModule = machine; + }; + }; + }; + caisson.facter.configurations.upstream = lib.caisson.facter.mkConfiguration { + configModule = + { lib, ... }: + { + inherit reportPath; + caisson.nixos.configurations.upstream = lib.caisson.nixos.mkConfiguration { + configModule = machine; + }; + }; + }; + }; + }; + native = top.nixosConfigurations.native.config; + compatible = top.nixosConfigurations.upstream.config; + kernelModules = config: builtins.sort builtins.lessThan config.boot.initrd.availableKernelModules; + hardwareOf = + configModule: + (hiveLib.caisson.structural.mkTopConfiguration { + moduleImports = _modules: [ ]; + configModule = + { lib, ... }: + { + caisson.caisson-facter.configurations.probe = lib.caisson.caisson-facter.mkConfiguration { + inherit configModule; + }; + }; + }).caisson.manifest.children.caisson-facter.probe.outputs; + throws = expr: !(builtins.tryEval (builtins.deepSeq expr true)).success; + in + builtins.toJSON native.hardware.facter.report + == builtins.toJSON upstream.config.hardware.facter.report + && compatible.hardware.facter.reportPath == upstream.config.hardware.facter.reportPath + && kernelModules upstream.config != [ ] + && kernelModules native == kernelModules upstream.config + && kernelModules compatible == kernelModules upstream.config + && + builtins.attrNames top.nixosConfigurations == [ + "native" + "upstream" + ] + && (hardwareOf { facter.reportPath = reportPath; }).report.version == 1 + && throws (hardwareOf { facter.report.version = 99; }).report + && + throws + (hardwareOf { + facter.reportPath = reportPath; + assertions = [ + { + assertion = false; + message = "refused"; + } + ]; + }).hash; + # A terranix configuration that is a top is its `config.tf.json` # file, built from the package set of the composition with the # evaluated configuration in its `passthru`, and its modules get the diff --git a/tests/unit/lib-overlays.nix b/tests/unit/lib-overlays.nix index 8199fae8..9fb0442e 100644 --- a/tests/unit/lib-overlays.nix +++ b/tests/unit/lib-overlays.nix @@ -122,6 +122,8 @@ let "caisson-colmena" "caisson-disko" "disko" + "caisson-facter" + "facter" "terranix" "system-manager" ]; @@ -5317,6 +5319,8 @@ in "home-manager-minimal" "terranix" "system-manager" + "caisson-facter" + "facter" ]; setsFromTheComposition = builtins.elem name [ "nixos" @@ -5327,6 +5331,8 @@ in "home-manager-minimal" "terranix" "system-manager" + "caisson-facter" + "facter" ]; in signature ? configModule