From 3b36d69ac526edcbc85e9509960ce707f02f5c2c Mon Sep 17 00:00:00 2001 From: Chris Hodapp Date: Sat, 17 Oct 2026 01:36:25 -0700 Subject: [PATCH 1/3] perf(core): merge the exported overlays as one attribute set `caisson.exports.libOverlays`, `caisson.exports.pkgOverlays` and `caisson.libOverlays.exported` were attribute sets of an overlay type, so every evaluation merged each registered overlay as an option of its own, and the type check of the manifest walked every overlay and its imports. The exports are now merged once, joining the definitions with `//`, and an entry is checked when it is read. The check of the manifest no longer walks the overlays it holds. Co-Authored-By: Claude Opus 5.5 --- modules/generic/core/caisson/exports.nix | 4 +- modules/generic/core/caisson/libOverlays.nix | 2 +- modules/generic/core/types.nix | 66 +++++++++++++++----- 3 files changed, 54 insertions(+), 18 deletions(-) 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/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); }; } From b250a6cbfddb0c12115b7becbc4cb8561e3ae539 Mon Sep 17 00:00:00 2001 From: Chris Hodapp Date: Sat, 17 Oct 2026 01:36:25 -0700 Subject: [PATCH 2/3] feat(facter): add the caisson-facter and facter integrations A caisson-facter configuration is the hardware of a machine: a module of the `caisson-facter` class with `facter.report`, the report nixos-facter writes, or `facter.reportPath`. caisson evaluates it with its own `evalModules`, so a project extends the class with options of its own about the hardware. The outputs are the report, checked against the report versions caisson knows, the report as a file, the configuration as data and its hash; every output fails with the messages of the class's `assertions`. The NixOS configurations declared inside it get caisson's NixOS module `caisson-facter-hardware`, which hands the report to nixpkgs' `hardware.facter.report`. A facter configuration is a `facter.json` as nixos-facter writes it, the `reportPath` of the `facter` class. The NixOS configurations declared inside it get `facter-hardware`, which sets nixpkgs' `hardware.facter.reportPath` to that file. Neither is published under an output attribute set. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 + configs/structural/impl/default.nix | 2 + docs/development/repository-structure.md | 3 +- lib-overlays/caisson-facter/default.nix | 286 ++++++++++++++++++ lib-overlays/facter/default.nix | 170 +++++++++++ modules/caisson-facter/core | 1 + modules/caisson-facter/default/default.nix | 27 ++ modules/facter/core | 1 + modules/facter/default/default.nix | 26 ++ modules/generic/core/default.nix | 2 + .../nixos/caisson-facter-hardware/default.nix | 30 ++ modules/nixos/facter-hardware/default.nix | 28 ++ tests/argument-errors/check.sh | 2 + tests/pinned-world/default.nix | 102 +++++++ tests/unit/lib-overlays.nix | 6 + 15 files changed, 687 insertions(+), 1 deletion(-) create mode 100644 lib-overlays/caisson-facter/default.nix create mode 100644 lib-overlays/facter/default.nix create mode 120000 modules/caisson-facter/core create mode 100644 modules/caisson-facter/default/default.nix create mode 120000 modules/facter/core create mode 100644 modules/facter/default/default.nix create mode 100644 modules/nixos/caisson-facter-hardware/default.nix create mode 100644 modules/nixos/facter-hardware/default.nix 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/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/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/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 From e6ad2c959747c2a2c7538aeb80cece2ee4ecefb8 Mon Sep 17 00:00:00 2001 From: Chris Hodapp Date: Sat, 17 Oct 2026 01:40:36 -0700 Subject: [PATCH 3/3] perf(integrations): read the configurations options only where a module defines them Re-export read `caisson..exported` and `caisson..configurations` for every integration in every full evaluation. It now reads them only for the integrations whose configurations a module of the evaluation defines, the integrations `childrenOf` already reads, found with the new `caisson.integrations.integrationsDeclaringIn`. An integration with no configurations declared passes nothing up whatever its selector selects. Co-Authored-By: Claude Opus 5.5 --- lib-overlays/integrations/default.nix | 27 +++++++++++-------- .../generic/core/caisson/configurations.nix | 5 +++- 2 files changed, 20 insertions(+), 12 deletions(-) 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/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";