Module Classes
Overview
caisson models modules as class-keyed sets. A class is a string key used to group related modules and control where they are exported in flake outputs.
- Registered modules live under
modules.<class>.<name> - Exported modules are published under
flake.modules.<class>.<name>
This builds on flake-parts’ flake.modules output, which publishes modules under any class name, and adds closed-inputs module normalization.
Registering a module
A flake laid out as modules/<class>/<name>/default.nix registers its
modules by naming the directory. lib.caisson.mkModules reads the
directory into the class-keyed registration, and it reads
configs/<class>/<name> the same way:
modules = lib: lib.caisson.mkModules ./modules;
configs = lib: lib.caisson.mkModules ./configs;
The first directory level is the class, whatever its name, and each
entry is registered with lib.caisson.mkModule, given the name of
that directory as the class. The integration that evaluates a class
names it in its declaration (the class argument of mkIntegration),
and caisson keeps no list of classes beside that. The class-free
generic group holds modules any class may import; the name comes
from flake-parts, whose export leaves those modules unstamped. An
entry is a directory holding a default.nix, a symlink to such a
directory included; anything else in a directory being read is an
error, so a stray file cannot silently vanish from a registry.
A tree with another layout writes the registration by hand, with the
mkModule of the integration that owns the class applied to the
module’s path:
modules = lib: {
flake.default = lib.caisson.flake-parts.mkModule ./modules/flake/default;
nixos.my-service = lib.caisson.nixos.mkModule ./modules/nixos/my-service;
homeManager.shell = lib.caisson.home-manager.mkModule ./modules/home-manager/shell;
hardware.tpmFacts = lib.caisson.mkModule "hardware" ./modules/hardware/tpmFacts;
};
Each integration’s mkModule is lib.caisson.mkModule bound to
that integration’s class:
mkModule = class: freeformModule: ...
The class-string form is written out for a class no integration covers.
Core and default
The entry names core and default carry meaning in every class,
for every project a composition lists (caisson among them, no
differently):
core: the framework module of the class. An integration forces every entry namedcore(core,<project>/core) into every evaluation of its class, before anything the evaluation selects. Registering acoreis the big hammer, for a module the class cannot function without; the core module of caisson declares thecaisson.*options every evaluation carries (the manifest, the registry selectors,caisson.exports).default: the default default. When an evaluation passes nomoduleImports, every entry nameddefault(default,<project>/default) applies; an evaluation that selects by name replaces that default with the list it names. Thedefaultof caisson for theflakeclass carries the nixpkgs integration’s module layer.
The core module of caisson lives as the generic entry core;
modules/structural/core is a symlink to it and modules/flake/core
imports it and adds the flake mechanics, so each class that forces it
registers it as core in that class.
The class-specific normalizer applies the closure attrset
({ closure-inputs, closure-lib, mkModule, ... }) as the module’s first
arg list. The mkModule closure member is bound to the same class, so
nested use of mkModule stays in that class.
Registration APIs
The class-keyed registry is in the manifest of the library, and
lib.caisson.modules reads it from there. Each module there is the
registered module inside a module that imports it and carries the key
modules.<class>.<name>@<project>, the registry key and the project
that registered it. The module system imports the modules that share a
key once, so a registry entry an evaluation selects twice is imported
once. The key is also how the history of an evaluation names the
registry entry each of its modules is (declaration events). Modules
enter the registry in these ways:
- Local registration. The
modulesargument ofmkLibis a function that receives the composedliband returns the class-keyed registration.lib.caisson.mkModulesderives that registration from the conventional layout, and helpers such aslib.caisson.flake-parts.mkModulebuild entries by hand. This is for the flake’s modules. - Project consumption,
mkLib’sprojectshook: registering a whole upstream contribution (projects.my-dep = inputs.my-dep) places its exported modules in the registry under<project>/<name>per class, beside its overlays in the overlay dictionary. Selection stays per item at each use site, and a local registration beats a same-named project entry.
The registry is a shared, class-keyed space per composition. Names
within a class are a flat space: the entries of a project carry its
name as a prefix (my-flake/my-service), and short names are for the
registrations made in the mkLib call itself. Precedence is
deterministic: those local registrations apply last, so a local entry
wins over a project entry of the same name.
Use class flake for flake-parts modules and other class keys for other module ecosystems. The shipped integrations (caisson.nixos, caisson.darwin, caisson.home-manager, caisson.hjem, caisson.terranix, caisson.caisson-colmena, caisson.colmena, caisson.caisson-disko, caisson.disko, caisson.caisson-facter, caisson.facter, caisson.system-manager, caisson.treefmt, caisson.nvf, caisson.nixvim, caisson.topology, caisson.kubenix, caisson.nixidy, and caisson.nixpkgs) each name their class in their declaration (the class of caisson-colmena is caisson-colmena, and the class of caisson-disko is caisson-disko; the nodes of a colmena configuration and the machines of a disko configuration are NixOS configurations); see the library reference.
Consuming Exported Modules
A downstream flake can import modules published under any class via the upstream flake’s modules output:
# In a downstream NixOS configuration:
imports = [ inputs.my-upstream.modules.nixos.myModule ];
# In a downstream home-manager configuration:
imports = [ inputs.my-upstream.modules.homeManager.myModule ];
The exported modules have their inputs already closed over, so importing them is possible without threading the upstream’s dependencies.
Export Controls
Per-class export controls live under:
caisson.modules.<class>.export.enabledcaisson.modules.<class>.exported
The exported modules are in the manifest. mkLib reads them from
there for each entry of projects.
The exported modules are also published under
flake.modules.<class>.<name>, for an import by hand.
caisson.flakeOutputs.modules selects which exported modules that
output holds, and it defaults to every exported module. A module
that only works inside an evaluation that caisson makes can be
exported and left out of the output:
caisson.flakeOutputs.modules =
modules: modules // { nixos = removeAttrs modules.nixos [ "needs-caisson" ]; };
For flake-parts compatibility, flake.flakeModules mirrors
flake.modules.flake, and the flake class always exports a default
entry (an empty module unless the selection provides it) so
flakeModules.default exists for consumers that import it by
convention.
Relationship to flake-parts
The flake.modules output is provided by flake-parts’ modules extra module. When caisson wires exported modules into flake.modules.<class>.<name>, flake-parts stamps each module with _class and _file metadata. This means exported modules carry their class identity and source location, which module systems can use for diagnostics and class-checking (e.g., preventing a nixos module from being accidentally imported into a homeManager evaluation).