Overview
This is the documentation for the framework’s design and API.
New here? Getting started walks through a first flake step by step; Choosing a flake framework places caisson relative to plain flake-parts, flakelight, and snowfall-lib; the FAQ answers the questions the concept pages tend to raise.
Reference documents the exported surface: the Library functions and the Options.
Conventions: Repository layout describes the directory conventions the documentation and examples assume.
Concepts explain the machinery and the reasoning behind it, in reading order:
- Closed inputs: how modules and library overlays close over the defining flake’s inputs, and the explicit closure convention every registered thing follows.
- Module classes: class-keyed module registration and export.
- Library overlays: namespaced,
dependency-declaring
libcomposition, and the patterns for writing overlays. - Ecosystem sources: why integrations pin nothing, and the places a source can come from.
Deep dives:
How lib is composed traces a
mkLib call from arguments to finished attrset, including the rules
that decide conflicts and composing through caisson-core directly.
How inputs are closed over
traces the closure attrset from mkLib to a registered file,
including what each kind of registration receives and which values
cross flake boundaries.
Guides: Testing covers unit and integration testing
(including callConsumerFlake), and
Evaluation weight covers measuring and gating
evaluation cost.
For a complete working flake with commentary, see
examples/literate-flake/ in the repository. The repository README
covers the quick start.
Despite the org name, caisson is an independent project and is not affiliated with, endorsed by, or sponsored by the NixOS Foundation. Nix and NixOS are trademarks of the NixOS Foundation.
Getting started
A step-by-step first flake: create it, add a library overlay, register
a module, use an integration, then consume your flake from a second
flake. The finished shape of each step also exists as a working flake
under examples/literate-flake/ in the repository, with commentary.
1. A minimal caisson flake
Create a directory with this flake.nix:
{
description = "my first caisson flake";
inputs = {
caisson.url = "github:nix-caisson/caisson";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";
};
outputs =
inputs@{ caisson, ... }:
let
lib = caisson.lib.caisson.mkLib {
inherit (caisson.lib.caisson.pins.flake inputs) sources root;
name = "my-flake";
systems = [ "x86_64-linux" ];
projects = {
inherit caisson;
};
configs = lib: lib.caisson.mkModules ./configs;
};
in
lib.caisson.flake-parts.mkTopConfiguration { };
}
lib.caisson.mkLib composes a library from the lib of nixpkgs, the
functions and registries of caisson under lib.caisson (where each
integration also has a namespace), and the overlays you register.
name is the
project’s name: the flake configuration takes it as its name, and
your overlays contribute to the composed library under it
(lib.my-flake). systems, the platforms the flake builds for, is
declared here too; flake-parts reads it from the composition.
Consuming
caisson as a project registers everything it exports, its
integrations included, which contributes lib.caisson (a namespace per integration
target); lib.caisson.flake-parts.mkTopConfiguration then evaluates
flake-parts with that library and your config module, the
configuration registered under the project’s name
(configs/flake/my-flake).
flake-parts
comes from the flake-parts input of the flake, like every ecosystem
caisson wraps: the integration calls that source with the composed
library, so the evaluation runs on the same nixpkgs lib the rest of
the composition does.
The config module is the top-level configuration of the flake, and it
is registered. lib.caisson.mkModules ./configs reads
configs/<class>/<name>/default.nix into the configs registration,
and the configuration is then lib.caisson.configs.flake.my-flake.
Create
configs/flake/my-flake/default.nix:
{ ... }:
{ pkgs, ... }:
{
perSystem =
{ pkgs, ... }:
{
packages.default = pkgs.hello;
};
}
Note the two argument lists: every registered file takes the closure
attrset ({ closure-inputs, ... }) first, then its ordinary module
arguments. That convention is the subject of
Closed inputs.
Check it:
nix flake check
nix build
2. Add a library overlay
An overlay contributes a namespace to the composed library. Create
lib-overlays/default/default.nix:
{ ... }:
{
imports = [ ];
overlay = final: prev: {
my-flake = (prev.my-flake or { }) // {
greet = name: "hello, ${name}";
};
};
}
Register it in flake.nix and export it, and turn on the lib export
in the config module. lib.caisson.mkLibOverlays ./lib-overlays reads
lib-overlays/<name>/default.nix into the libOverlays registration:
lib = caisson.lib.caisson.mkLib {
inherit (caisson.lib.caisson.pins.flake inputs) sources root;
name = "my-flake";
systems = [ "x86_64-linux" ];
projects = {
inherit caisson;
};
configs = lib: lib.caisson.mkModules ./configs;
libOverlays = lib: lib.caisson.mkLibOverlays ./lib-overlays;
};
caisson = {
libOverlays.exported = libOverlays: { inherit (libOverlays) default; };
};
Now lib.my-flake.greet is available everywhere the composed library
flows: in the config module, in registered modules, and to consumers
in the lib output of the flake, which is the composed library. Use
it in perSystem:
packages.default = pkgs.writeText "greeting" (lib.my-flake.greet "Nix");
3. Register a module
Modules are class-keyed: flake modules feed flake-parts, and
integration classes (nixos, homeManager, …) feed their module
systems. lib.caisson.mkModules ./modules reads
modules/<class>/<name>/default.nix into the registration, the
first directory level being the class. Register a flake-class module
by creating its directory:
lib = caisson.lib.caisson.mkLib {
inherit (caisson.lib.caisson.pins.flake inputs) sources root;
name = "my-flake";
systems = [ "x86_64-linux" ];
projects = {
inherit caisson;
};
modules = lib: lib.caisson.mkModules ./modules;
configs = lib: lib.caisson.mkModules ./configs;
libOverlays = lib: lib.caisson.mkLibOverlays ./lib-overlays;
};
modules/flake/default/default.nix:
{ ... }:
{ ... }:
{
perSystem =
{ pkgs, ... }:
{
devShells.default = pkgs.mkShell { packages = [ pkgs.nixfmt ]; };
};
}
mkConfiguration applies the selected flake-class modules alongside the
config module: moduleImports returns the list to apply, like
libOverlayImports, and when it is omitted every registered entry
named default applies, here this flake’s default and
caisson/default (the default module of caisson, which carries the
nixpkgs integration’s module layer). The entries named core apply
to every evaluation of the class regardless.
Module classes covers registration,
selection, and export.
4. Use an integration
Integrations bring the same conventions to other module ecosystems
and take their ecosystem as an explicit ecosystemSrc, a function of
the tree’s sources. A NixOS
configuration takes its systems and its package set from the
composition: it is evaluated per system, once for each system mkLib
declares, on the
package config named default, which the mkLib call declares with
pkgSets = lib: { default = lib.caisson.nixpkgs.mkConfiguration { }; };.
With the single system declared above, mkTopConfiguration returns
the evaluated configuration. At the top level of the config module:
flake.nixosConfigurations.example = lib.caisson.nixos.mkTopConfiguration {
ecosystemSrc = sources: sources.nixpkgs;
configModule =
{ ... }:
{
boot.loader.grub.enable = false;
fileSystems."/" = {
device = "none";
fsType = "tmpfs";
};
system.stateVersion = "25.05";
};
};
Instead of passing ecosystemSrc at every call, the mkLib call can
declare a default for the nixos key, the key a NixOS configuration
reads
(defaultEcosystemSrc = sources: { nixos = sources.nixpkgs; };), and
the argument can be dropped; an explicit argument still wins, and the
source named exactly nixos in the sources passed to mkLib is the
last fallback. See
Ecosystem sources.
With caisson consumed as a project, its integration overlays are
already registered and applied, so caisson.nixos is present. To
compose only some of them, keep the project registration and select
per item over the combined dictionary:
libOverlayImports =
lib: with lib.caisson; [
libOverlays."caisson/flake-parts"
libOverlays."caisson/nixos"
libOverlays.default
];
The library reference documents the integration namespaces.
5. Consume your flake from another flake
A consumer registers your exported overlay the same way:
{
inputs = {
caisson.url = "github:nix-caisson/caisson";
my-flake.url = "github:you/my-flake";
};
outputs =
inputs@{ caisson, my-flake, ... }:
let
lib = caisson.lib.caisson.mkLib {
inherit (caisson.lib.caisson.pins.flake inputs) sources root;
name = "consumer";
systems = [ "x86_64-linux" ];
projects = {
inherit caisson my-flake;
};
configs = lib: lib.caisson.mkModules ./configs;
};
in
lib.caisson.flake-parts.mkTopConfiguration { };
}
The consumer’s composed library now has lib.my-flake.greet: the
project registration brings in the overlays you exported, each
overlay’s imports chain guarantees anything it depends on composes
with it, and your exported modules land in the consumer’s registry
under my-flake/<name>, selectable at each use site. A consumer who
wants only part of your project selects with
libOverlayImports, as shown above; the
my-flake.modules.<class>.<name> flake outputs serve consumers who
import modules without composing anything.
Where next
- Closed inputs, the convention every registered file follows.
- How
libis composed: the whole composition pass, and caisson-core, the layerlib.caissonis built on. - Testing, including
callConsumerFlakefor testing consumer flakes without a push/lock cycle. - FAQ for the questions this page tends to raise.
Choosing a flake framework
Where caisson sits relative to plain flake-parts, flakelight, snowfall-lib, and the dendritic pattern, characterized from the documentation of those projects. The honest summary first: all of them produce working flakes, and the differences are about which conventions you want enforced by machinery rather than by discipline.
Plain flake-parts
flake-parts is a minimal module system mirroring the flake schema:
it splits configuration into modules, handles perSystem, and
deliberately avoids broader opinions, positioning itself as “a single
module that other repositories can build upon” with an ecosystem of
independent compatible modules.
caisson is built on flake-parts and keeps all of it. What it adds is
a set of enforced conventions on top: closed inputs (every registered
file takes an explicit closure argument list instead of reaching for
inputs ambiently), namespaced library overlays with declared
dependencies composed by caisson-core, class-keyed
module registration and export, integrations that take their
ecosystems as explicit ecosystemSrc arguments, and measured
evaluation-cost gates. Use plain flake-parts when you want the module
system and your own conventions; use caisson when you want these
conventions machine-enforced, particularly across several flakes that
consume each other’s libraries and modules.
flakelight
flakelight is a module-driven framework emphasizing automation: sensible defaults, automatic import of nix files from a directory, and auto-generated outputs (packages, overlays, formatters), with the stance that what can be done automatically, should be.
caisson leans the other way: registration is explicit, namespaces are explicit, dependencies between overlays are declared, and nothing is inferred from file layout. If you value minimal ceremony in a single project, flakelight gets a working flake with fewer lines. If you value being able to trace any attribute of a composed library to a declared registration, especially across a fleet of interdependent flakes, that explicitness is caisson’s point.
snowfall-lib
snowfall-lib generates systems, packages, modules, and shells from directory-structure conventions: predictable filesystem hierarchies in exchange for eliminated boilerplate, targeting multi-system NixOS and nix-darwin setups. Its repository currently describes it as seeking new maintainers.
The comparison is similar to flakelight but stronger: snowfall infers the most from layout, caisson infers nothing from layout. caisson’s integrations also differ structurally from a generator: they are thin adapters over each ecosystem’s evaluator, taking the ecosystem as an explicit source argument and pinning nothing.
The dendritic pattern
The dendritic pattern is an organizational discipline over
flake-parts rather than a framework: every Nix file except the entry
points is a module of the top-level configuration, each file
implements one feature across all the configurations it touches, and
lower-level modules (NixOS, home-manager, nix-darwin) live as
deferredModule values inside the top-level config, merged by name.
Files are commonly auto-imported with import-tree, and cross-cutting
values are read from the shared top-level config instead of
specialArgs threading.
caisson agrees with more of this than with the generators above:
both build on flake-parts, both eliminate ambient specialArgs
plumbing (dendritic through the shared top-level config, caisson
through closed inputs and the composed library), and both group
modules by the module system they belong to (dendritic by option
path, caisson by class key). The differences are scope and
mechanism. Dendritic organizes one repository’s configurations by
feature and, with import-tree, derives the import set from the file
tree; caisson registers modules and overlays explicitly and
infers nothing from layout. Dendritic keeps everything inside a
single module evaluation; caisson separates library composition from
module evaluation and adds export machinery so several repositories
can publish and consume each other’s overlays and modules. Use
dendritic to structure one flake’s configurations by aspect with
almost no machinery; use caisson when the unit of reuse is a
repository and the conventions need to hold across a fleet.
What is caisson-specific
Independently of the convention trade-offs above, these things are distinctive here rather than variations on a shared theme:
- Library composition with identity (deep dive): dedup, wholesale replacement, and reliable polyfills, implemented in caisson-core, a zero-dependency flake usable without caisson.
- Explicit ecosystem sources: integrations pin none of their ecosystems; the consumer hands each in, so a caisson revision works with any nixpkgs, home-manager, or colmena revision with a compatible evaluation contract.
- Evaluation-weight gates (guide): framework overhead is measured and held to committed ceilings in CI rather than described.
The relationship to flakes
Flakes today do acquisition (fetching, pinning, integrity) and
composition (deciding which copy of each dependency an evaluation
actually uses, via the follows pin bucket). caisson separates
them. Flakes keep acquisition. Composition moves to the evaluation
layer, with real semantics: deduplication is
key identity, override is wholesale replacement of a keyed entry,
local patches are the keyless tail, and ecosystems (nixpkgs,
home-manager, and the rest) are handed in as explicit
ecosystemSrc arguments instead of being re-pinned and re-wired
through the input graph.
Everything caisson adds is published through the output the
flake schema leaves freeform, lib: composed libraries, the module
registry, and the manifest (the composition’s self-description) all
live there, and the remaining flake outputs (modules.<class>,
libOverlays, per-system products) are projections from it that keep
the standard schema’s addresses. A flake built this way needs only a
small, regular subset of the flake schema; nothing about it requires
upstream changes to evaluate.
FAQ
What goes wrong without caisson that this fixes?
Caisson addresses major failure patterns seen in poly-flake setups:
First, input explosion: a flake-parts module that uses inputs.foo
works only if every downstream flake also pulls in foo. This ends
up meaning that each level of a dependency tree has to re-mention all
of its transitive dependencies, either directly or in the form of a
bunch of “follow” pins, or else you get an explosion of flake
versions.
Second, collisions: Overlays that write top-level attributes tend to fight over the same flat namespace.
What is closure-inputs, and who sets it?
closure-inputs is the sources your flake passes to mkLib: its
pinned inputs, under their input names and with no self.
inherit (caisson.lib.caisson.pins.flake inputs) sources root; in the
mkLib call passes them. The caisson framework threads it in:
mkLibOverlay and mkModule apply it to each registered file as the
file’s first argument list. A file always receives the sources of the
flake that registered it: an overlay or module consumed from another
flake sees the sources of the flake it came from, not those of the
flake consuming it.
Closed inputs is the full convention.
What happens when two overlays define the same thing?
Composition is done via an ordered pass across the specified overlays, traversing dependencies in a depth-first, postfix manner. For attrSets and lists, following the conventions results in a merge. For atomic attributes, contentions mean that the later overlay’s definition wins. I say “convention” because overlays are actually capable of addressing their predecessor directly, so they can technically implement whatever merging logic they deem appropriate.
Can I adopt this incrementally in an existing flake-parts flake?
Yes. lib.caisson.flake-parts.mkTopConfiguration wraps the
mkFlake of flake-parts, and plain
flake-parts modules work unchanged. It is recommended to start by
composing a library with lib.caisson.mkLib. You can hand your
existing top-level module to mkTopConfiguration and let the conventions
spread file by file from there.
What does lib.caisson.flake-parts.mkModule do to my module?
A few things:
- applies the closure argument list, so the module can use
closure-inputs - records the file’s path as
_file, for better error messages - gives path-registered modules a deduplication
key.
Do I pin nixpkgs, home-manager, and the rest myself?
Yes, you manage the ecosystem pins in your own flake, which is the
point: a caisson integration is glue over the ecosystem’s evaluator,
composing with whatever ecosystem source version you give it, so
caisson imposes no transitive pins and two consumers of the same
caisson revision can run different nixpkgs revisions. Version skew
within your ecosystems lives in your locks, where you can see and
manage it. A flake that uses one version of an ecosystem everywhere
can set a flake-level default at mkLib, a function of sources
(defaultEcosystemSrc = sources: { nixos = sources.nixpkgs; };). It
is also possible to pass ecosystemSrc, also a function of
sources, explicitly on each caisson call. The explicit argument
wins, and a source named exactly like the key is the final fallback
(handy for leaf nodes).
What does this cost at evaluation time?
Measurements show: relatively little. The eval-weight harness runs in CI and holds caisson’s overhead (a minimal caisson consumer minus a raw flake-parts flake) to ceilings on deterministic counters: thunks, values, allocations, and full nixpkgs, nixpkgs-lib, and module-system evaluation counts. The overhead is orders of magnitude below a single nixpkgs evaluation. Evaluation weight documents the harness and the current numbers.
How mature is this?
Pre-release. At the time of writing, it is used heavily by the author and by no one else.
Library Reference
A composed library carries the framework namespaces lib.caisson-core
and lib.caisson. lib.caisson-core holds the machinery, injected by
mkLib itself (its code lives in
caisson-core, which
caisson pins internally). lib.caisson
holds the integrations and the pkgs-dependent tooling, contributed by
the overlays this flake exports. The lib output of the caisson flake
is its composed library, so both namespaces are in it
(caisson.lib.caisson-core, caisson.lib.caisson).
Type notation used below:
lib: a composed nixpkgs-style library attrsetmodule: a module for some module class’s module systemoverlayFn:final: prev: attrs, the standard overlay functionlibOverlay:{ imports : listOf libOverlay; overlay : overlayFn }, the built overlay produced bymkLibOverlay(both keys always present)patharguments are imported before the rules below apply
The caisson-core namespace
- Source: caisson-core’s
lib/default.nix(thecomposeprimitive, and the composition of the entries below) andlib-overlays/<name>/default.nix(compose,resolve,lifecycle,readers,lists,attrsets,strings,functions). caisson-core is a composition of those entries.mkLibcomposes them into the library it builds under the keyscaisson-core/<name>, and an entry registered under the same key replaces the entry of that key.
mkLib
mkLib :
{ sources : attrs # the tree's pinned sources, as a pin reader returns them
, root ? null : root # the tree's identity; null for a composition that is not a top
, name ? null : str # the project's name, also the namespace it contributes
, modules ? (lib: { })
: lib -> attrsOf (attrsOf module) # class -> name -> module
, configs ? (lib: { })
: lib -> attrsOf (attrsOf module) # class -> name -> configuration
, libOverlays ? (lib: { })
: lib -> attrsOf libOverlay # given the core lib
, libOverlayImports ? (lib: <every project and local registration>)
: lib -> listOf libOverlay # given the core lib
, extraLibOverlayImports ? (lib: [ ])
: lib -> listOf libOverlay # given the core lib
, pkgOverlays ? (lib: { })
: lib -> attrsOf pkgOverlay # given the bootstrap lib
, pkgSets ? (lib: { })
: lib -> attrsOf configuration # package configs by name, given the registered lib
, defaultEcosystemSrc ? (sources: { })
: sources -> attrs # the tree's default source per ecosystem key, as a function of sources
, systems ? null : listOf str # the platforms the tree builds on
, projects ? { } : attrs # consumed upstream contributions, by project name
} -> lib
Builds a composed library over the seed, the empty attribute set: the
caisson-core entry (registered under that name, so a registration
under the same name replaces it), the selected registered overlays,
then the synthetic overlays: the local module registrations (so local
names win over overlay-borne contributions) and the manifest. In a
flake that uses caisson, the selected overlays include the
nixpkgs-lib entry, which is the lib of nixpkgs loaded from the
source ecosystemSrc "nixpkgs-lib" resolves: the source
defaultEcosystemSrc sources returns for the nixpkgs-lib key, else
the source pinned under exactly that name in sources. The caisson
integration overlays import it. Every source arrives as an argument
or a declaration; the exact-name fallback over sources applies to
ecosystem resolution only (see
Ecosystem sources).
The library is built in stages, each a new fixpoint over the
seed with a manifest in lib.caisson-core.libManifest. Each stage
exists because some argument is a function of it:
- The core lib holds the
caisson-coreentries and nothing else, with the lib overlay registry grafted onto its manifest.libOverlayImportsreceives it. - The bootstrap lib adds the selection, and with it the
nixpkgs-libentry and every integration namespace.modulesandconfigsreceive it; its manifest has nomodules,moduleProjects,configs,pkgOverlaysorpkgSets. - The registered lib is the same entries with the module
registrations, the configurations and the package overlay registry
grafted on.
pkgSetsreceives it, since a package config selects from those registries; its manifest has nopkgSets. It is built only when a package config is read. - The full lib adds
pkgSets.mkLibreturns it.
The core, bootstrap and registered manifests have childless = true.
A registration made at an earlier stage still closes over the full
lib. The constructors the earlier libs hold (mkModule,
the integrations’ mkModule, mkLibOverlay, mkPkgOverlay) give the
entry the full lib as closure-lib. The manifest of that lib holds
the registry the entry joins, which closure-lib.caisson.modules.<class>
reads, so a module imports a sibling by name. A registration under a
caisson-core/<name> key replaces that entry from the bootstrap stage
on; the core lib keeps the original.
sources and root come from a pin reader (see pins below); at a
flake top, inherit (caisson-core.lib.caisson-core.pins.flake inputs) sources root;. The registered overlays and modules close over
sources as closure-inputs, so the closure holds the pinned trees
under their input names and no self. The signature is the pattern
of mkLib, with no ...: a missing or unexpected argument, a
leftover inputs included, is Nix’s error at the call site,
naming mkLib and pointing at the pattern.
modulesreceives the bootstrap lib and returns the class-keyed registration, built with the integrations’mkModulehelpers (lib.caisson.nixos.mkModule,lib.caisson.flake-parts.mkModule, and so on).lib.caisson-core.mkModule "<class>"is for a class no integration covers.lib: lib.caisson-core.mkModules ./modulesreads the conventional layout.configsreceives the bootstrap lib the same way and returns the configurations of the tree, keyed by module class then name, the layoutconfigs/<class>/<name>on disk (colmena’s class iscaisson-colmena);lib: lib.caisson-core.mkModules ./configsreads that layout. They come back in the manifest, whichlib.caisson.configs.<class>.<name>reads, so a top and a configuration that evaluates another beneath itself reach them by name rather than by a path out of their directory.libOverlaysreceives the core lib and returns the registered overlays. An entry is made withlib.caisson-core.mkLibOverlay, which closes it over the sources of the composition;lib: lib.caisson-core.mkLibOverlays ./lib-overlaysreads the layout.pkgOverlaysreceives the bootstrap lib and returns the registered package overlays. An entry is made withlib.caisson-core.mkPkgOverlay;lib: lib.caisson-core.mkPkgOverlays ./pkg-overlaysreads the layout (seepkgOverlaysbelow). These four arguments take exactly the function shape shown; passing anything else is an error.pkgSetsreceives the registered lib and returns the package configs of the tree by name, each a configuration an integration’s constructor built:pkgSets = lib: lib.caisson.nixpkgs.mkConfigurations { };declares a package config for every configuration inconfigs/nixpkgsConfig(seecaisson.nixpkgsbelow).mkLibfinalizes each with the name it is declared under and the registered manifest as its parent, and records them in the manifest’spkgSets; the flake reads its package sets from there.libOverlayImportsselects which registered overlays apply to thelibof this flake. It receives the core lib and names entries from thelibOverlaysregistry described below.libOverlayImports = lib: [ lib.caisson.libOverlays.my-overlay ];selects the entry registered asmy-overlay. The default selects the project and local registrations, and a selection given here replaces it.extraLibOverlayImportshas the same form and adds to the selection, whichever it is: a flake that names a further entry with it keeps the default. Registration also feeds export, so what is registered and what is selected can differ.defaultEcosystemSrcis a function ofsourcesreturning the tree’s default source per ecosystem key, keyed by the exact names the integrations resolve (sources: { nixpkgs-lib = sources.nixpkgs; nixos = sources.nixpkgs; }gives the library and the NixOS module collection from a checkout pinned under thenixpkgskey). An attribute set given in place of the function is refused. mkLib records what the function selects in the manifest asdefaultEcosystemSrc, and the name of each selected source amongsourcesasdefaultEcosystemSrcNames, null for a value the function builds in another way. Thenixpkgs-libentry it composes reads its source throughecosystemSrc "nixpkgs-lib", the same resolution every ecosystem uses; the integrations interpret the rest.projectsconsumes whole upstream contributions ({ my-dep = inputs.my-dep; }). A project contributes the dictionarieslibOverlays,pkgOverlaysand class-keyedmodules. For a flake built with caisson, mkLib reads these dictionaries from the manifest of the flake, atlib.caisson-core.evalManifestin theliboutput, where they are underoutputs.exports. mkLib ignores the other outputs of that flake. A value with no manifest is read as an attribute set that holds these dictionaries itself. A project’s overlays and package overlays join their registries and its modules join the class registry under<project>/<name>, so the existing selections keep per-item choice:libOverlayImportsdecides which overlays apply, the registry selection at each use site decides which modules load, and a local registration beats a same-named project entry. Registering a single overlay by hand is the way to cherry-pick or rename it.
mkModules, mkLibOverlays, mkPkgOverlays
mkModules : path -> attrsOf (attrsOf module) # <dir>/<class>/<name>/default.nix
mkLibOverlays : path -> attrsOf libOverlay # <dir>/<name>/default.nix
mkPkgOverlays : path -> attrsOf pkgOverlay # <dir>/<name>/default.nix
The directory readers. A reader belongs to the lib it is read from:
it takes a directory and returns the registrations, built with the
entry constructors of that lib. Each registry
function of mkLib receives a lib, so a tree with the conventional
layout takes the reader from it and names the directory:
modules = lib: lib.caisson-core.mkModules ./modules;
configs = lib: lib.caisson-core.mkModules ./configs;
libOverlays = lib: lib.caisson-core.mkLibOverlays ./lib-overlays;
pkgOverlays = lib: lib.caisson-core.mkPkgOverlays ./pkg-overlays;
The readers are the registered entry caisson-core/readers, so a
composition that registers another entry under that name reads its
directories with that entry.
mkModules reads the first directory level as the class, whatever
its name, and registers each entry directory through mkModule of
the lib, given the name of that directory as the class. caisson keeps
no list of classes: the integration that evaluates a class names it
in its declaration. mkLibOverlays applies mkLibOverlay, and
mkPkgOverlays applies mkPkgOverlay. 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.
pkgOverlays, mkPkgOverlay, pkgOverlaysFor
mkPkgOverlay : freeformOverlay -> pkgOverlay
pkgOverlay : { key; imports : listOf pkgOverlay; overlay : final -> prev -> attrs; origin; project; }
pkgOverlaysFor : listOf pkgOverlay -> listOf (final -> prev -> attrs)
The package overlay registry, recorded as
caisson-core.libManifest.pkgOverlays. An entry has the lib overlay
entry’s shape with a nixpkgs overlay under overlay: a file handed to
mkPkgOverlay takes the closure
{ closure-inputs, closure-lib, mkPkgOverlay, ... } and returns
{ imports ? [ ], overlay }. An entry imports a sibling from the
registry of the composition that registered it,
closure-lib.caisson-core.libManifest.pkgOverlays.<name>:
# pkg-overlays/default/default.nix
{ closure-lib, ... }:
{
imports = [ closure-lib.caisson-core.libManifest.pkgOverlays.extra ];
overlay = final: prev: { my-project = prev.callPackage ./my-package.nix { }; };
}
Every registered entry carries its registry name as key, the file
it was read from as origin (null for an entry built from a
function), and project: null for a local registration, the
contributing project’s name for an entry from projects, so the
local entries alone are a filter on that field. A project’s entries
join under <project>/<name>; a key the project registered itself (a key
without a /) becomes <project>/<key> in its imports too, so an
import still meets its sibling, and a key naming another project’s
entry (a key with a /) is kept, so two projects importing the same
entry import one entry.
pkgOverlaysFor turns a selection, a list of registry entries, into
the list of nixpkgs overlays a package set applies: each entry after
the entries it imports, each key once where it first occurs. One key
reached through two paths is one entry when both carry the same
origin; two entries with different origins under one key are refused.
By convention the entries named default (default,
<project>/default) are the default selection, as for modules; the
layer that builds package sets applies it. caisson-core applies
nothing itself.
mkLibOverlay
mkLibOverlay : freeformOverlay -> libOverlay
freeformOverlay = path | (closure -> { imports ? listOf libOverlay
; overlay : overlayFn })
closure = { closure-inputs : attrs # the defining composition's pinned sources
; closure-lib : lib # the defining composition's lib, lazily bound
; mkLibOverlay : freeformOverlay -> libOverlay
; mkModule : string -> freeformModule -> module
}
Applies the closure attrset to an overlay given as a function or a path
to it, and normalizes the result: the built libOverlay always carries
both keys, with imports defaulted to [ ]. Already-built overlays are
registered directly rather than wrapped.
The closure’s closure-lib is the composed library of the composition
that registered the overlay, bound lazily: read it inside the
overlay function or a function it defines, never while the overlay
is being registered. An integration reaches the registry of its
composition through it (closure-lib.caisson.modules.<class>),
wherever it is later composed. The closure’s mkModule is bound to
the defining composition, so modules an overlay makes close over the
definer’s inputs and library. A lib overlay does not add to the
registry; see Module classes for the
ways modules enter it.
mkModule
mkModule : string -> freeformModule -> module
freeformModule = path | (closure -> module)
closure = { closure-inputs : attrs # the defining flake's pinned sources
; closure-lib : lib # the defining flake's composed lib
; mkModule : freeformModule -> module # bound to the class
}
Factory for class-specific module normalizers. Given a class name, returns a normalizer that applies the closure attrset to a module given as a function or a path to it; the module takes the closure as its first arg list ({ ... }: when unused). Plain modules are imported/registered directly rather than wrapped. Path modules gain _file and a path-based dedup key.
The mkModule closure member is bound to the same class, so nested module composition stays in that class. A module imports a sibling by name through closure-lib.caisson.modules.<class>.<name>, the registry of the composition that registered it.
modules
modules : attrsOf (attrsOf module) # class -> name -> module
The class-keyed module registry of this composition: the registrations
of the flake merged with every consumed project’s entries
(<project>/<name>), locals winning on name conflicts. It is read
from the manifest of the library (caisson-core.libManifest.modules).
Integration adapters read their class from here
(lib.caisson.modules.<class>): every entry named core (core,
<project>/core) is the framework module of the class, forced into
every evaluation, and every entry named default is the default
default, what an evaluation gets when it passes no moduleImports.
manifest
manifest : { _type : "caisson-manifest"; type : "lib";
name ? str;
entries : listOf { key : str; opaque : bool; };
history : listOf event;
sources : attrs; root : nullOr root;
modules : attrsOf (attrsOf module);
configs : attrsOf (attrsOf module);
libOverlays : attrsOf libOverlay;
pkgOverlays : attrsOf pkgOverlay;
defaultEcosystemSrc : sources -> attrs; systems : nullOr (listOf str);
projects : attrs;
childless : bool; inputs : list; parent : nullOr manifest;
ancestors : listOf manifest; nearest : attrsOf manifest;
children : attrs }
The composition’s self-description, injected as its final overlay.
name is the name argument, and is absent when the
composition declares none. entries lists the lib overlay selection
in composition order, caisson-core’s forced entries first. An entry
is opaque when its key names no registry entry, as with an overlay
imported by value; a keyless entry gets a synthesized keyless/<n>
key. The lib mkLib returns is the full lib of a root declaration,
so childless is false, parent is null, and ancestors, inputs,
nearest and children are empty. The manifests of the core,
bootstrap and registered libs have childless = true, and the core
manifest lists only the caisson-core entries in entries.
pkgSets holds the package configs declared in the pkgSets
argument, each the manifest its configuration returned, with the
registered manifest as its parent.
sources, root, defaultEcosystemSrc, systems,
projects and configs are the mkLib arguments as given, except
that a directory reader’s pin files are stated relative to the root
when the directory lies in the root’s tree (pin.dir is kept
otherwise); libOverlays, modules and pkgOverlays are the
registered dictionaries, so consumed projects’ entries appear under
<project>/<name> beside
the local registrations, with a local winning a name collision. Each
records where an entry came from: a lib overlay or package overlay
entry carries project (null for a local registration, the project’s
name for a contributed entry, caisson-core for the lib overlay entries
caisson-core publishes into every composition), and
moduleProjects.<class>.<name> holds the same for modules, beside the
module dictionary. The export selectors default to the entries whose
origin is null. An
mkLib composition self-describes: the composed library of a consumer
carries the manifest of that consumer. Checks live on the export side
only (the flake-parts integration type-checks it and projects the
exports and the flake.modules output from it, so an
exported selection can re-export a project-borne entry the same
way as a hand-registered entry); a producer validates the manifest it
publishes in its CI.
_type = "caisson-manifest" marks the attrset as a manifest, which
is how manifestOf recognizes a manifest.
history lists the events recorded on the way to the lib, in stage
order, and the history of each stage begins with the history of the
stage before it. The core stage records one layer event per
caisson-core entry, then the lib overlay registrations. The
bootstrap stage adds one layer event per entry it composes that the
core stage did not, in composition order: the selection, and a
registration replacing a caisson-core entry, which so comes after
the entry it replaces. The full stage adds the modules, configs
and pkgOverlays registrations. A layer event carries prev and
result as the stage that recorded it composed them.
event : { manifest : listOf { type : str; name : str; }; # [ ] for the root lib and the top
type : str; # "lib", or the integration
operation : "registry" | "layer" | "declaration";
key : str; index : int; # index within its operation
origin : { project : nullOr str; file : nullOr str; };
prev ? attrs; result ? attrs; } # layer events only, lazy
origin.project is the project that registered the entry (the
composition’s name for a local entry, caisson-core for its
entries) and origin.file the file the entry was built from, where
there is such a file; a lib overlay built from a file records it as its
origin. A layer event also keeps prev and result of its overlay
call final: prev: result: prev is the accumulation it received and
result the attrset it returned. They are the
values the lib was built from, and nothing reads them until
definers does.
The manifest of every configuration has a history too. The history
of its childless manifest is the history of its parent, then a
declaration event for each module its evaluator received, in that
order. The history of its full manifest is the history of the
childless manifest, then what each configuration beneath adds after
the history it began with, so the history of a child always begins
with the childless history of its parent. manifest is the name path
of the configuration, the { type, name } of each manifest beneath
the top, which gives its attribute path under children of the top’s
manifest; a configuration evaluated per system has its system as a
segment. The key of a declaration event says what the module is:
- the name of the ecosystem, such as
nixos, for the module list of the ecosystem, recorded as a single event, with the source and the file the list comes from (nixpkgs,nixos/modules/module-list.nix); caisson.<class>for the framework module of caisson, andcaisson.<class>.<purpose>for each other module an integration adds, such ascaisson.nixos.pkgSet;- the registry key, such as
modules.nixos.default, for a module selected from the registry, with the project that registered it; configs.<class>.<name>, such asconfigs.nixos.laptop, for the configuration module taken from the registry by the configuration’s name, with the project and the file that registered it;- the path, for a module given as a path;
inline/<index>for any other module, its position among the events of the evaluation, with the file the module names, where it names a file.
A module recorded as inline/<index> was given to the configuration
as a value, through configModule or a selection, rather than by a
name. We recommend registering such a module and selecting it by name,
so its history says where it came from.
A file inside a source in the Nix store is written relative to that source.
definers
definers : manifest -> listOf str
-> listOf { key; index; origin; value; position : nullOr { file; line; column; }; }
The layers that define an attribute path, in composition order: the
last is the winner and the rest are shadowed. value is the path’s
value after that layer. position is where the layer binds the name,
from unsafeGetAttrPos, kept only when it lies within the layer’s
file, so a name the layer computes (with mapAttrs, say) reports
none. A layer that returns prev.x // { ... } carries the names
already under x without defining them: a name counts as carried
when its binding position is the same in what the layer returned and
what it received, or, with no position on either side, its value is
equal.
definers lib.caisson-core.libManifest [ "my-project" "greet" ]
# [ { key = "default"; value = <function>; position = { file = ".../lib-overlays/default/default.nix"; line = 6; ... }; ... } ]
manifestOf
manifestOf : any -> nullOr manifest
Finds the manifest in whatever a file returns, so that a tool reading
a flakeless top needs only its default.nix. The value may be:
- a manifest, returned as it is;
- an attrset carrying a manifest at
caisson.manifest; - an evaluated configuration carrying a manifest at
config.caisson.manifest; - a composed library carrying the phase manifests (
libManifest,pkgsManifest,evalManifestundercaisson-core), or a package set carrying them inpkgs.lib. The last of them filled in is the manifest:evalManifestif set, elsepkgsManifest, elselibManifest.
A value carrying none of these returns null.
manifestOf (import ./.) # the manifest of a flakeless top
manifestOf lib # lib.caisson-core.libManifest
libOverlays, pkgOverlays
lib.caisson.libOverlays : attrsOf libOverlay
lib.caisson.pkgOverlays : attrsOf pkgOverlay
These attributes are the lib overlay registry and the package overlay
registry of the composition that built the library, keyed by registry
name. Both are read from the manifest of the library
(caisson-core.libManifest.libOverlays and
caisson-core.libManifest.pkgOverlays), which is the only place
caisson-core keeps them. Each has the name of the mkLib argument
that fills it.
libOverlaysholds the entriesmkLibregistered, the entries consumed projects contributed under<project>/<name>, and thecaisson-core/<name>entries. AlibOverlayImportsselection refers into it.pkgOverlaysholds the package overlay entries: thosemkLibregistered and those consumed projects contributed. A package config selects from it:caisson.nixpkgs.overlays = [ lib.caisson.pkgOverlays.default ];.
Both are empty in a library that no mkLib call built.
ecosystemSrc
lib.caisson-core.ecosystemSrc : name -> source | null
ecosystemSrc takes an ecosystem name and returns the source the
composition supplies for exactly that name. That is the source
defaultEcosystemSrc sources selects for that name, or else the
source it pins under that name, or else null. The result is fixed by
the arguments of the mkLib call.
An overlay whose added names come from a source calls it on prev.
prev.caisson-core.ecosystemSrc "nixpkgs-lib" returns the nixpkgs
library source of the flake whose mkLib call composes the overlay,
whichever flake registered the overlay. The nixpkgs-lib integration
of caisson finds its source this way.
withManifests
lib.caisson-core.withManifests :
{ pkgsManifest ? nullOr manifest; evalManifest ? nullOr manifest; } -> lib
Carried by every stage of a library mkLib builds. It rebuilds that
stage from its declaration with the given phase manifests filled in:
the same entries and the same libManifest, composed as a new
fixpoint, so code inside the library that reads
final.caisson-core.pkgsManifest sees the manifest, and manifestOf
returns it. Merging the attribute onto a built library would leave
those readers seeing null. The nixpkgs integration uses it to hand
out pkgs.lib, the bootstrap lib with pkgsManifest filled in, and a
module evaluation fills in evalManifest the same way. The rebuilt
library carries withManifests too, and a further call keeps what is
already filled in. Any other name, libManifest included, and any
value that is neither a manifest nor null is refused.
finalizeChild
lib.caisson-core.finalizeChild :
{ name : str; parent : manifest; what ? str; } -> configuration -> manifest
A configuration learns its name and its parent from where it is
declared, so what an integration’s mkConfiguration returns is a
function { name, parent }: manifest. finalizeChild calls it with
the name it is declared under and the parent’s childless manifest,
and requires a manifest back. It first reads the function’s pattern
with builtins.functionArgs and requires exactly name and
parent, so anything else declared where a configuration belongs is
refused there, with a message saying what a configuration is. what
names the declaration in those messages. mkLib finalizes each
pkgSets entry this way, and the caisson.<integration>.configurations
options finalize each entry this way.
mkConfiguration
lib.caisson-core.mkConfiguration :
{ type : str # the name of the integration
, evaluate : { lib, manifest } ->
{ value; outputs ? { }; children ? { }; forChildren ? { }; }
, record ? { } # plain data added to the manifest
} -> configuration
Builds the configuration of a module evaluation: the function of
{ name, parent } that finalizeChild calls. It is what an
integration’s mkConfiguration is written with. Nothing is evaluated
when it is called; the manifest the configuration returns reads
value, outputs and children from evaluate when they are read.
evaluate performs the evaluator’s call on lib, and returns the
evaluation as the evaluator returned it (value), the integration’s
references into it (outputs) and the finalized configurations
declared beneath it, by integration and then name (children).
The evaluation has a childless view and a full view. Each is a
manifest, and each runs on the declaring lib rebuilt with that manifest
as evalManifest (withManifests).
- The childless view is the evaluation without the configurations
declared beneath it:
childless = trueand nochildren. - The full view is the manifest returned. It carries the childless
manifest as
childlessManifest, and children are finalized against that, so a child’sparentandnearest.<integration>are the childless manifest of the configuration that declares it.
A configuration with children is evaluated in both views, and a configuration with none is evaluated once.
forChildren is what the evaluation gives the configurations
declared inside it. It has three fields:
extraDefaultModuleImports, by class a list of selections, each a function of a lib returning modules.defaultPkgs, which selects the package set its children run on by default. It is null when the configuration selects no set.systems, the systems its children are evaluated for. It is null when the configuration lists no systems.
They are read from the childless evaluation and recorded on the
manifest as forChildren. A configuration declared inside inherits
the selections of its parent, extended by them. Its manifest holds
the selections added above it as defaultModuleImports, those from
the top first, each a record { select; file; level; }: select is
the selection, file the file that defines it, and level the name
path of the level that added it. Every level extends them in turn, so a selection
reaches the configurations beneath the level that added it, nested
ones and ones beneath a system included.
Both views carry type, name, parent, ancestors (the parent’s
list with the parent appended), nearest (the parent’s attrset with
the parent under its integration; a lib is not among them), inputs,
and the parent’s sources, root, systems, projects,
defaultEcosystemSrc, pkgSets and registries. A record that names
a field mkConfiguration writes is refused.
An integration that evaluates a configuration per system passes
perSystem = true. A declared configuration is then an evaluation for
every system in force where it is declared, and the configuration
returns those evaluations by system. In the tree the system sits
above the name:
flake
└── children.system
├── x86_64-linux type system
│ └── children.nixos.hostname2 an evaluation, with a value
└── aarch64-linux type system
└── children.nixos.hostname2 an evaluation, with a value
There are as many evaluations as there are systems in force, also for
a single system, and none where no system is in force; nothing is
refused. Each evaluation is a manifest as described above, under the
name it is declared by, carrying its system as system, and its
parent is the system, a manifest of type system. The parent’s full
manifest holds each system under children.system, beside the
configurations evaluated once for every system, which stay under
children.<integration>.
Where a configuration that is evaluated per system is declared inside
another, the system of its parent is the default. A home-manager
configuration declared inside a NixOS configuration has, by default,
one evaluation per evaluation of the machine, for the same system. The
manifest of each evaluation of the machine holds that system as
systems.
A parent changes this by returning forChildren.systems, the systems
its per-system children are evaluated for: a machine that holds an
image for another architecture states that architecture there. A configuration
that is evaluated once can return a list the same way, to narrow what
its children are evaluated for. The list has to come from the systems
allowed where that parent is declared, and a system outside them is
refused.
finalizeChild accepts either result, a manifest or the evaluations
by system.
elide
lib.caisson-core.elide : listOf path -> listOf (listOf str)
path = listOf { type : str; name : str; }
The naming rule. A path is the list of segments from a top down to a
thing, ending in the name of the thing; a system is a segment of type
system. For each path, in order, elide returns the segments
needed to tell the thing apart from the others, as strings in path
order.
It keeps the last segment of every path, and beyond it only the segments where paths that end in the same name fork. Among those paths it drops the prefix they share, keeps the segment at which they first differ, the segment nearest the top that separates them, and does the same within each branch.
elide [
[ { type = "structural"; name = "a"; } { type = "system"; name = "x86_64-linux"; } { type = "nixos"; name = "host-1"; } ]
[ { type = "structural"; name = "b"; } { type = "system"; name = "aarch64-linux"; } { type = "nixos"; name = "host-1"; } ]
[ { type = "structural"; name = "a"; } { type = "system"; name = "x86_64-linux"; } { type = "nixos"; name = "host-2"; } ]
]
# [ [ "a" "host-1" ] [ "b" "host-1" ] [ "host-2" ] ]
A name that is alone stays bare, whatever sits above it, so the system
above a configuration with a single system in force drops out. A
segment is kept as its name, or as type/name where the branches of
that fork hold the same name under several types. How the kept
segments are written out as a name, and a clash between equal paths,
are for whoever publishes them (caisson.integrations.publish).
finalizeTop
lib.caisson-core.finalizeTop : configuration -> manifest
Finalizes the configuration a top ends with. A top has no parent that
declares it under an attribute, so its name is the name the composition
declares on mkLib, absent when it declares none, and its parent is
the lib’s manifest. An integration’s mkTopConfiguration is this
followed by the step that turns the result into what a tool reads.
importApply
importApply : freeformModule -> attrs -> module
Applies static arguments to a module through _file/imports wrappers while preserving wrapper metadata. Used for threading arguments through module import chains.
compose, resolve
compose composes entries by key. resolve resolves an ecosystem
source from explicit, defaults and sources, in that order. See
How lib is composed and
the documentation of caisson-core.
The caisson namespace
The functions, registries and manifests a flake uses are under
lib.caisson. It holds those listed in
Functions and registries,
a namespace per integration target (lib.caisson.flake-parts,
lib.caisson.nixos, and so on; see
Integration namespaces), and the
pkgs-dependent tooling documented at the end of this section.
caisson’s registered flake modules are listed here too.
Functions and registries under lib.caisson
lib = caisson.lib.caisson.mkLib {
inherit (caisson.lib.caisson.pins.flake inputs) sources root;
projects = { inherit caisson; };
modules = lib: lib.caisson.mkModules ./modules;
configs = lib: lib.caisson.mkModules ./configs;
libOverlays = lib: lib.caisson.mkLibOverlays ./lib-overlays;
};
| Group | Names under lib.caisson |
|---|---|
| composing | mkLib, pins, callFlake, callConsumerFlake, realizeInputs |
| making and reading entries | mkModule, mkModules, mkLibOverlay, mkLibOverlays, mkPkgOverlay, mkPkgOverlays, importApply |
| registries | modules, configs, libOverlays, pkgOverlays, pkgOverlaysFor |
| manifests | libManifest, pkgsManifest, evalManifest, manifestOf |
| for an integration written outside caisson | finalizeTop, elide, ecosystemSrc |
pins,callFlake,callConsumerFlakeandrealizeInputsare defined in caisson and documented below.modules,configs,libOverlaysandpkgOverlaysare read from the manifest of the library (lib.caisson-core.libManifest). They are empty in a library that nomkLibbuilt.- Every other name is the value of the same name under
lib.caisson-corein the same library, documented in The caisson-core namespace. caisson-core is the library caisson is built from. - Several arguments of
mkLibare functions of a library that is still being built. In themkLibof caisson-core,libOverlaysandlibOverlayImportsreceive a library that holds only the caisson-core names.lib.caisson.mkLibadds the names above, underlib.caisson, to the library each of those functions receives.
Overlay imports
An entry of libOverlays or pkgOverlays states what it imports as
names or as entries, and both registries treat them the same way.
- A name is looked up in the registry of the flake that registers the
importer.
imports = [ "base" ];imports an entry of the same flake, andimports = [ "caisson/nixpkgs-lib" ];imports an entry of a consumed project. - When a project is consumed, its entries become
<project>/<name>, and the names in their imports are renamed the same way. An entry that imports another entry of its project is therefore composed once. - Registering under a name replaces that entry for everything that imports it.
- Two entries built from different files under one key are refused.
pins
lib.caisson.pins.flake : inputs -> { sources; root; }
lib.caisson.pins.flake-compat : path -> { sources; }
lib.caisson.pins.npins : path -> { sources; }
lib.caisson.pins.gitRoot : path -> root
pins holds a reader per pin system. Each reads the files of its pin
system into sources: the pinned source trees, each as its pin system
hands it over (a flake input keeps its outputs), with pin, the
record of how it is pinned.
pin.systemis"flake"from either flake reader and"npins"frompins.npins. It says which tool moves the source.pin.filesis{ refs; revisions; }: the file that holds the ref and the file that holds the revision, relative topin.dirwhen present and to the root of the flake otherwise.pin.diris the directory holding the pin files, for a reader given a directory.pin.urlis the ref as the pin files write it.pin.rev,pin.narHashandpin.lastModifiedidentify the source tree, where the pin system records them.pin.followsis, for a flake input declared as afollows, the path of input names it follows. Its source tree is that of the input it lands on.pin.overriddenis set bypins.flakealone. It is true when an--override-inputreplaced the input, in which casepin.urldescribes the lock and not the source tree.
pins.flake inputs reads the inputs Nix’s flake evaluator resolved for
the flake being evaluated, the inputs its outputs receives, and
returns the root from self.
pins.flake-compat ./dir resolves the flake.lock beside a
flake.nix in that directory the way flake-compat does, stopping
before the flake’s outputs. Nix’s flake evaluator does not see that
pair of files, so no --override-input applies to it and it has no
root. It stays valid under read-only evaluation (nix flake check --no-build). A flake-parts partition takes the inputs of its
lockfile’d subflake from this reader.
pins.npins ./npins reads sources.json format 8 and fetches each
pin as npins’ generated default.nix does. It refuses Container
pins, which need nixpkgs.
A root is { outPath; dirty; rev; shortRev; dirtyRev; dirtyShortRev; lastModified; lastModifiedDate; narHash; }. It identifies the source
tree being built, with the source-info fields a flake’s self has,
each null where the reader has no value for it. The names are fixed
and the values lazy, since inside a flake’s outputs asking which
attributes self has forces the outputs being computed. The names of
a flake input’s pin are fixed for the same reason, and pin.url and
pin.follows are null where they do not apply. A flakeless top in a
git working tree reads its root with pins.gitRoot ./. under an
impure evaluation. The result has the revision of a clean working
tree, or dirty = true with dirtyRev for a dirty working tree.
# flake.nix outputs
inherit (caisson.lib.caisson.pins.flake inputs) sources root;
# test-only pins beside the pins of the flake
inherit (lib.caisson.pins.flake-compat ./tests/dependencies) sources;
# a flakeless top pinned with npins
inherit (lib.caisson.pins.npins ./npins) sources;
root = lib.caisson.pins.gitRoot ./.;
callFlake, callConsumerFlake
lib.caisson.callFlake : { src, inputs, sourceInfo ? { } } -> flakeOutputs
lib.caisson.callConsumerFlake :
{ path : path | string # directory containing flake.nix
, pool ? { } : attrs # inputs resolvable by name
, overrides ? { } : attrs # highest-precedence injections
, sourceInfo ? { } : attrs # extra self attrs (lastModified, rev, ...)
} -> flakeOutputs # self: inputs, outputs, outPath, _type
callFlake applies the outputs function of a flake to inputs given as
values and fetches nothing. The flake-parts integration instantiates
flake-parts with it.
callConsumerFlake evaluates a consumer-style flake from source with
inputs supplied by hand, which is how a flake checks the flakes under
its tests/. The declared inputs of that flake resolve by name:
overrides first, then follows chains through the other resolved
inputs, then pool. An input that does not resolve throws an error
naming it. callFlake handles the self fixpoint and its
decoration, and the eval-weight harness uses it too. Nothing is
fetched: locks are not read, and sourceInfo attributes appear only
if supplied. See Testing.
realizeInputs
lib.caisson.realizeInputs : inputs -> { "<name>/<name>/…" = storePath; }
realizeInputs takes flake inputs that Nix has already resolved. It
makes sure that each input is present in the Nix store, and it returns
the store path of each input. It does the same for the inputs of each
input, and for the inputs of those, to any depth. “Realize” is the
word Nix uses for making sure a store path is present, as in
nix-store --realise.
The name of each input in the result is the list of input names that
leads to it, joined with /. That is the form of name that the Nix
flag --override-input takes:
lib.caisson.realizeInputs { inherit (inputs) caisson; }
# => {
# "caisson" = "/nix/store/…-source";
# "caisson/caisson-core" = "/nix/store/…-source";
# "caisson/flake-parts" = "/nix/store/…-source";
# "caisson/flake-parts/nixpkgs-lib" = "/nix/store/…-source";
# "caisson/nixpkgs-lib" = "/nix/store/…-source";
# }
When two inputs both follow a third input, the third input appears in
the result twice, once under each name that leads to it. The function
leaves out self. An input that is a plain source tree has no inputs,
and neither has a store path that the caller writes directly as a
value.
The problem the function solves. Some tools evaluate a flake from inside a Nix build. nix-unit does this when it runs as a flake check. The check is a derivation. The builder of that derivation runs nix-unit, and nix-unit evaluates the flake that holds the tests.
Nix runs every builder in a sandbox. Inside the sandbox, the builder can see a path in the Nix store only when the derivation names that path as a dependency. The rest of the store is hidden from the builder. The builder also has no network access.
So when nix-unit evaluates the flake and the evaluation needs a flake
input, the evaluation fails. Nix looks for the input in the store and
does not see it, because the derivation did not name it. Nix then
tries to download the input and cannot, because the builder has no
network access. The check fails with the message unable to download.
The lock file of the flake does not change this. The lock file tells Nix which revision of the input to use. It does not put a copy of that revision where the builder can see it.
How the problem is solved by hand. nix-unit accepts the flag
--override-input <name> <store path> for each input. The flag
tells nix-unit to read the input from that store path
and not to fetch it. The store path is also written in the command
that the builder runs, so the derivation names the store path as a
dependency, and the builder can see it. The flake-parts module of
nix-unit writes these flags from the option nix-unit.inputs, which
is an attribute set of names and store paths.
The inputs of an input need the same flag. Suppose a test composes
caisson as a project. To do that, nix-unit has to evaluate the caisson
flake. The caisson flake has inputs too, and caisson-core is one of
them. Nix looks for caisson-core, does not see it, and the check fails
in the way described above. The flag for an input of an input names
both of them: --override-input caisson/caisson-core <store path>.
Writing these flags by hand means copying the list of inputs out of
the lock file of the caisson flake, and keeping that copy up to date.
If a name is missing from the copy, nothing reports it until the check
runs and fails inside the sandbox. realizeInputs builds the whole
set from the resolved inputs.
Which inputs to give the function. Give the function the inputs of the test flake:
nix-unit.inputs = lib.caisson.realizeInputs inputs;
The result then names each input of the test flake and each input of
those inputs, so the test flake needs no follows to point the inputs
of its inputs back at inputs it already lists.
To return the store path of an input, Nix has to fetch that input. The function therefore fetches every input it is given, and every input of those inputs, to any depth. It fetches an input even when no test reads it. For most test flakes that adds a few small sources.
The cost is large when one input exists to gather many others. The
unit test flake of caisson has an input of that kind, deps, which
pins everything any caisson test uses. Given all the inputs of that
test flake, the function fetches 24 sources where the unit tests read
6. In that case, give the function only the inputs that the tests
evaluate as flakes, and list the other inputs directly:
nix-unit.inputs = inputs // lib.caisson.realizeInputs { inherit (inputs) caisson; };
An input that the test flake declares with flake = false has no
inputs as far as the test flake is concerned. Give the function such
an input as a source tree, and not as an evaluated flake, or the
result will name inputs under it that nix-unit reports as matching
nothing. See also Testing.
modules.<class>."caisson/core", modules.flake."caisson/default"
- Source:
modules/generic/core/(the core module;caisson.*options undercaisson/),modules/structural/core,modules/nixos/coreandmodules/homeManager/core(symlinks to it),modules/flake/core/(imports it and adds the flake mechanics),modules/flake/default/
caisson’s core module, the caisson.* options every evaluation
carries (the manifest, the registry selectors, caisson.exports),
registered as core in the class-free generic class and in every
class whose integration forces it (flake, structural), and
exported so a consumer’s composition carries caisson/core there.
The registry selectors, caisson.libOverlays.exported,
caisson.modules.<class>.exported and caisson.pkgOverlays.exported,
each choose what of a
registry leaves the project, and each defaults to the entries the
project registers itself. An entry a consumed project contributed
(<project>/<name>), or an entry caisson-core publishes into every
composition, leaves only when a selector names it: a project
republishes an upstream’s entries only on purpose, and a consumer
takes them from that upstream through its projects. The exported
entries are in the manifest. The
outputs are modules (and flakeModules) and overlays.
caisson.flakeOutputs.modules selects which exported
modules the modules output holds. overlays holds the exported
package overlays as plain nixpkgs overlays. The exported lib overlays
have no output.
caisson/default for the flake class is caisson’s contribution to
the default default: it imports caisson/nixpkgs below.
modules.nixos."caisson/default", modules.homeManager."caisson/nixos-parent"
- Source:
modules/nixos/default/,modules/homeManager/nixos-parent/
These modules are what a NixOS configuration gives the home-manager configurations declared inside it.
home-manager has a NixOS module that embeds home-manager
configurations in a machine. For each home-manager configuration, that
module sets a few options of the configuration from the machine.
caisson does not use that NixOS module. A home-manager configuration
declared inside a NixOS configuration is a caisson configuration, and
it gets the same settings from the home-manager module
caisson/nixos-parent.
caisson/default is the default NixOS module caisson registers. A
NixOS configuration imports every registered module named default
unless it passes moduleImports. This module sets
caisson.forChildren.extraDefaultModuleImports.homeManager, which adds
caisson/nixos-parent to the modules that the home-manager
configurations declared inside the machine import by default. It also
imports
caisson/home-manager-activation,
which runs those home-manager configurations on the machine.
caisson/default also imports caisson/activation-marker, described
under
drift detection.
With it, a home-manager configuration prints a warning when it is
activated on a machine it was not built for.
caisson/default also imports
caisson/hjem-activation, which
activates the hjem configurations declared inside the machine, and adds
the hjem module
caisson/nixos-parent to the modules
those configurations import by default.
caisson/default also defines networking.hostName as the name the
NixOS configuration is declared under, at mkDefault priority. So the
machine declared as nas-1 names itself nas-1, and a host name the
machine’s modules set replaces it. A NixOS configuration with no
name, and an evaluation without NixOS’s networking module (the minimal
NixOS evaluator), keep NixOS’s default.
caisson/nixos-parent sets these options of the home-manager
configuration:
| Option of the home-manager configuration | Value |
|---|---|
home.homeDirectory | users.users.<user>.home of the machine, when the machine declares that account |
home.uid | users.users.<user>.uid of the machine, when the machine declares that account and its uid is set |
nix.enable | nix.enable of the machine |
nix.package | nix.package of the machine, when the machine has Nix enabled |
submoduleSupport.enable | true, which home-manager reads to tell that a machine activates the home-manager configuration |
home.packages | home-manager’s command-line program, when the home-manager configuration sets programs.home-manager.enable |
<user> is home.username of the home-manager configuration.
A machine whose user is managed by systemd-homed has no entry for
that user in users.users. For such a home-manager configuration,
nixos-parent leaves home.homeDirectory and home.uid alone, and
the home-manager configuration sets its home directory itself.
home-manager leaves its command-line program out of a home-manager
configuration that has submoduleSupport.enable set. nixos-parent puts it back, because
the program is how the user runs home-manager switch.
A NixOS configuration or a home-manager configuration can take
nixos-parent out:
- A NixOS configuration that passes
moduleImportsselects its modules itself. It does not importcaisson/default, and then no home-manager configuration inside it getsnixos-parentfrom caisson. - A home-manager configuration that passes
moduleImportsselects its modules itself, and then that home-manager configuration does not getnixos-parent.
Each can keep it while selecting its other modules. The NixOS
configuration lists caisson/default, which is a NixOS module, in its
selection. The home-manager configuration lists
caisson/nixos-parent, which is a home-manager module, in its
selection.
nixos-parent reads the machine through the manifest of the
home-manager configuration, so only a home-manager configuration with
a NixOS configuration above it can import it.
In a NixOS evaluation whose library is not a caisson composition,
caisson/default defines nothing, as the core module declares
nothing there.
modules.nixos."caisson/home-manager-activation"
- Source:
modules/nixos/home-manager-activation/
The NixOS module with which a machine activates the home-manager
configurations declared inside it. caisson/default for the nixos
class imports it, so a NixOS configuration has it unless it passes
moduleImports.
Activating a home-manager configuration means running the activate
script of its activation package as the user of the home-manager
configuration. The script links the files of the home-manager
configuration into the home directory and installs its packages. This
module writes a systemd unit for each home-manager configuration, so
that switching the machine to a new generation also applies its
home-manager configurations. caisson does not use home-manager’s NixOS
module for this.
Which home-manager configurations. The module activates the
home-manager configurations that the NixOS configuration passes up.
Those are the home-manager configurations declared inside it, also
through a structural configuration, that its
caisson.home-manager.exported selector selects. The module leaves
out:
- A home-manager configuration declared inside a NixOS configuration that is itself inside this one. That home-manager configuration belongs to the inner machine.
- A home-manager configuration evaluated for a system other than the system of this machine.
The account of the user. The module expects the machine to declare
the account of the user of each home-manager configuration in
users.users. A home-manager configuration whose user has no account
there is refused, with an assertion that names the home-manager
configuration and the user. On most machines such a home-manager
configuration is a mistake: a misspelled user name, or a home-manager
configuration declared on the wrong machine.
Some accounts are provided by something other than users.users. An
account that systemd-homed manages is one. systemd-homed keeps the
record of it, the machine must not declare it a second time, and its
home directory is mounted only at login. The option
caisson.home-manager.activation.usersDeclaredElsewhere
lists the users that have such an account on the machine. The
home-manager configuration of a listed user is not refused.
Which unit. The unit depends on what provides the account of the user.
Account in users.users | User listed as declared elsewhere | |
|---|---|---|
| Unit | system unit home-manager-<user> | user unit home-manager-<user> |
| Runs | during boot, as that user, before users can log in | when the service manager of that user starts, at login |
| Restricted by | User=<user> | ConditionUser=<user> |
| Waits for | the home directory, and the Nix daemon socket | the home directory |
<user> is home.username of the home-manager configuration.
The system unit is the unit home-manager’s NixOS module writes.
The profile of the home-manager configuration. Both units run
activate with no driver version. home-manager calls that the legacy
behavior: the script also updates the home-manager profile of the
user. The user can then run home-manager switch and home-manager generations on the same profile. home-manager’s NixOS module passes --driver-version 1,
which leaves the profile alone.
Two home-manager configurations for one user. A machine that would
activate two home-manager configurations with the same
home.username fails an assertion. The message names the user and
both home-manager configurations. Give one of the home-manager
configurations a different home.username, or leave one out with
caisson.home-manager.exported.
Where the module defines nothing. The module reads the home-manager configurations from the full evaluation of the NixOS configuration, so it defines nothing in the childless evaluation. It also defines nothing in a NixOS evaluation that declares no systemd options. The nixos-minimal integration produces such an evaluation, and a machine of that kind cannot run units.
modules.nixos."caisson/hjem-activation"
- Source:
modules/nixos/hjem-activation/
The NixOS module with which a machine activates the hjem configurations
declared inside it. caisson/default for the nixos class imports it,
so a NixOS configuration has it unless it passes moduleImports.
Activating a hjem configuration means applying its manifest with
hjem’s CLI. The CLI links the files of the user and removes the files of
the previous generation, whose manifest the machine keeps in
/var/lib/hjem/manifest-<user>.json. The module writes the units that
hjem’s NixOS module writes, under the same names:
| Unit | Runs |
|---|---|
hjem-prepare | creates /var/lib/hjem |
hjem-activate@<user> | as the user, links the files of the new manifest |
hjem-reload@<user> | as the user, reloads the systemd user units that the change touched, when the service manager of the user is running |
hjem-update-state@<user> | stores the new manifest in /var/lib/hjem |
hjem-cleanup | removes the stored manifests of users that are no longer activated |
The target hjem requires them all, and starts at boot and when the
machine switches to a new generation. Each instance runs the CLI of the
hjem configuration of its user. The units link with hjem’s CLI and
back up a file they replace under the prefix .backup-. hjem’s NixOS
options for another linker, for systemd-tmpfiles and for
clobberByDefault have no counterpart here; a configuration sets
clobberFiles itself.
The module also adds the packages of each user to
users.users.<user>.packages, and keeps the extraDependencies of each
user in the closure of the system.
Which configurations. The hjem configurations that the NixOS
configuration passes up and that are enabled, leaving out those
declared inside a NixOS configuration that is itself inside this one,
and those evaluated for another system. A machine that would activate
two hjem configurations with the same user fails an assertion naming
the user.
The module defines nothing in the childless evaluation, and nothing in a NixOS evaluation that declares no systemd options, such as one from the nixos-minimal integration.
modules.hjem."caisson/nixos-parent"
- Source:
modules/hjem/nixos-parent/
The hjem module that a NixOS configuration adds to the modules that the
hjem configurations declared inside it import by default, through
caisson/default. It sets directory to users.users.<user>.home of
the machine, at mkDefault priority. It refuses an enabled
configuration whose user has no enabled account on the machine, with an
assertion naming the user, as hjem’s NixOS module does.
A hjem configuration that passes moduleImports gets this module only
if its list names it. The module reads the machine through the manifest
of the configuration, so only a hjem configuration with a NixOS
configuration above it can import it.
Drift detection: caisson/activation-marker, modules.homeManager."caisson/activation-drift"
- Source:
modules/nixos/activation-marker/(registered also fordarwinandsystemManager),modules/homeManager/activation-marker/,modules/hjem/activation-marker/,modules/homeManager/activation-drift/, andactivatingAncestorOf,markerOf,checksMarkerOfanddriftCheckOfinlib-overlays/integrations/default.nix
A configuration with an activation, declared inside another configuration with an activation, warns when it is activated by hand after being built for a different version of the configuration above it. A home-manager configuration inside a machine is the common case.
The machine activates the home-manager configurations declared inside
it. The user can also activate such a home-manager configuration by
hand, with home-manager switch, which builds the home-manager
configuration from the source the user has checked out. That source
can describe a different machine than the machine that is applied.
This happens when the user changed the machine and has not switched it
yet, or when the checkout is older than the machine. A home-manager
configuration reads settings of its machine, so it was then built for
a machine other than the machine it is activated on.
Which configurations. An integration whose configurations are
activated passes activation to mkModuleConfiguration, and the
manifest records it. It names two things, each computed from a
manifest: the identity of a configuration and the path of its marker.
| Integration | Identity | Marker |
|---|---|---|
nixos, darwin, system-manager | the system, toplevel | /etc/caisson/marker.json |
home-manager | the activation package | $HOME/.local/state/caisson/markers/home-manager-<name>.json |
hjem | the manifest file | .local/state/caisson/markers/hjem-<name>.json in the directory of the user |
nvf, nixvim | the Neovim | beside its profile; nothing is declared inside one to read it |
A marker of a user is named by $HOME and not by $XDG_STATE_HOME,
because a unit of the machine and a shell of the user may see
$XDG_STATE_HOME differently, and both must find the same file.
The identity is that of the childless evaluation, which leaves out the configurations declared inside. A change to a home-manager configuration leaves the identity of its machine the same, and a change to anything else about the machine changes it.
Every configuration with an activation checks the nearest configuration above it that has one. A machine is the exception: it is applied on the host it describes, so a machine declared inside another, such as an image, checks nothing above it.
The marker. The default module of each of these classes imports
its caisson/activation-marker. When a configuration declared inside
checks it, the activation writes the SHA-256 digest of the store path
of the identity into the marker, as { "identity": "<digest>" }.
Computing the store path evaluates the childless evaluation and builds
nothing, and the digest names no store path, so the marker depends on
nothing and cannot be read as a path. A machine writes it with
environment.etc, a home-manager configuration with an activation
step after its files are linked, and a hjem configuration adds it to
its files. The marker is
not written in the childless evaluation, or in a NixOS evaluation
without environment.etc, which the nixos-minimal integration can
produce.
The check. driftCheckOf is the shell text of the check. It
compares the marker of the configuration above with the digest of the
identity of that configuration as this configuration was built for it, computed
from the same childless evaluation through the manifest. It prints a
warning to standard error when they differ, or when the marker does
not exist, and does not stop the activation. A home-manager
configuration runs it from the home-manager module
caisson/activation-drift, which the home-manager default module
imports, as an activation step before writeBoundary. The option
caisson.drift.warn of the
home-manager configuration removes the step.
When the configuration above activates it. The marker then already belongs to the generation being activated, so the two are equal, and the check prints nothing.
Cost. Computing the identity evaluates the configuration above a second time, without what is declared inside it. A configuration with nothing declared inside it that checks it writes no marker, and does not pay for the second evaluation.
modules.darwin."caisson/default"
- Source:
modules/darwin/default/,modules/homeManager/darwin-parent/,modules/hjem/darwin-parent/,modules/darwin/home-manager-activation/,modules/darwin/hjem-activation/
caisson/default is the default darwin module caisson registers. A
darwin configuration imports every registered module named default
unless it passes moduleImports. It holds what a macOS machine does
for the home-manager configurations and the hjem configurations
declared inside it.
It adds the home-manager module caisson/darwin-parent to the modules
that the home-manager configurations inside the machine import by
default, and the hjem
module caisson/darwin-parent to those of the hjem configurations.
They set what home-manager’s and hjem’s nix-darwin modules set from the
machine:
| Module | Option | Value |
|---|---|---|
home-manager darwin-parent | home.homeDirectory | users.users.<user>.home of the machine, when the machine declares that account and sets its home |
home.uid | users.users.<user>.uid of the machine, when it is set | |
nix.enable, nix.package | those of the machine | |
submoduleSupport.enable | true | |
home.packages | home-manager’s command-line program, when the home-manager configuration sets programs.home-manager.enable | |
hjem darwin-parent | directory | users.users.<user>.home of the machine, at mkDefault priority |
nix-darwin leaves users.users.<user>.home at null until the machine
sets it. When the machine declares no account for the user, or the
account sets no home, reading home.homeDirectory of the
home-manager configuration or directory of the hjem configuration
fails with a message that says which of the two the machine is
missing. The hjem module also refuses such a user with an assertion,
which covers a hjem configuration that sets directory itself. An
account without uid is accepted, and home.uid of the home-manager
configuration stays unset, as with home-manager’s nix-darwin module.
modules.darwin."caisson/home-manager-activation"
The darwin module with which a machine activates the home-manager
configurations declared inside it. caisson/default imports it. A
step of the machine’s postActivation script runs the activate
script of each home-manager configuration as its user, through
launchctl asuser and sudo, as home-manager’s nix-darwin module
does, so darwin-rebuild switch applies the home-manager
configurations. A home-manager configuration whose user has no account
in users.users is refused with an assertion, and so is a
home-manager configuration whose user’s account sets no home, which
matters for a home-manager configuration that sets
home.homeDirectory itself. Two home-manager configurations for one
user are refused as well.
The step runs activate with no driver version, as caisson’s NixOS
activation does. The script then also updates the home-manager profile
of the user, so the user can run home-manager switch on the same
profile. home-manager’s nix-darwin module passes --driver-version 1,
which leaves the profile alone.
modules.darwin."caisson/hjem-activation"
The darwin module with which a machine applies the hjem configurations
declared inside it. caisson/default imports it. It writes the two
launchd agents of hjem’s nix-darwin module, with the same labels and
scripts:
| Agent | Runs |
|---|---|
org.hjem.activate | at login and when the machine is activated, as the user: applies the manifest of the user, with the state in ~/Library/Application Support/Hjem |
org.nix.link-nix-apps | the same: links the applications of the packages of the user into ~/Applications/Nix User Apps |
Each agent exits for a user that no hjem configuration of the machine
names, and runs the CLI of the hjem configuration of the user it runs
for. The module also adds each user’s packages to
users.users.<user>.packages, and lists each user’s
extraDependencies in /etc/hjem/extra-dependencies, which keeps them
in the system.
The module differs from hjem’s nix-darwin module in where it declares the agents and in how it applies the files at an activation:
- It declares the agents in
launchd.agents. nix-darwin installs those in/Library/LaunchAgents, where launchd loads them for every user who logs in. hjem’s nix-darwin module declares them inlaunchd.user.agents, which nix-darwin installs forsystem.primaryUseralone, and it requires the machine to setsystem.primaryUser. - It applies the files of each hjem user who is logged in from
postActivation, which nix-darwin runs after it installs the agents of the new generation. nix-darwin loads the agents of/Library/LaunchAgentsas root, which loads them into no session, so a user who is logged in when the machine is activated has the agents of the generation that was current at the login, or none. For each such user, the step removes the agents from the session, runs the program of each agent of the new generation as the user, so the files are applied before the activation returns, and loads the agents of the new generation into the session. A user who is not logged in gets the agents at the next login. hjem’s nix-darwin module restarts the activation agent from an activation step that nix-darwin does not run, and the applications agent from a step that runs before the agents are installed.
Profiles: caisson/profile-activation
- Source:
modules/nixos/profile-activation/(registered also forsystemManager),modules/darwin/profile-activation/,modules/homeManager/profile-activation/,modules/hjem/profile-activation/, andprofileOf,profileShimsOf,profilesActivatedByandprofileChoicesOfinlib-overlays/integrations/default.nix
An nvf or nixvim configuration is a program a person runs. It relates to the configuration it is declared inside as a home-manager configuration relates to its machine: it has a separate activation, which the configuration above runs each time it is activated itself.
The profile. The configuration has the outputs activationPackage
and profile. activate checks for
drift
against the nearest configuration above with an activation, and points
a Nix profile at the Neovim with nix-env --profile. That also makes
the profile a garbage-collector root. When the profile already points
at the Neovim, activate changes nothing, so activating the same
generation again adds no generation. The profile keeps its generations,
so it can be rolled back with nix-env --profile <path> --rollback.
| Declared inside | Profile | Activated by |
|---|---|---|
| a NixOS or system-manager machine | /nix/var/nix/profiles/caisson/<kind>/<name> | the unit caisson-profile-<kind>-<name>, as root, wanted by multi-user.target |
| a darwin machine | the same | a step of postActivation, as root |
| a home-manager configuration | $HOME/.local/state/caisson/profiles/<kind>/<name> | a step of the activation of the home-manager configuration, after its files are linked |
| a hjem configuration inside a NixOS machine | the same | the unit caisson-profile-<user>-<kind>-<name> of the machine, as the user, after hjem-activate@<user>, wanted by hjem.target and part of it |
| a hjem configuration inside a system-manager machine | the same | the unit hjem-activate@<user> of the machine, as the user, after it applies the files of the user |
| a hjem configuration inside a darwin machine | the same | a step of postActivation, as the user, after the hjem agents are restarted |
| a hjem configuration with no machine above it, on Linux | the same | the systemd user service caisson-profiles in the files of the hjem configuration, when the systemd of the user starts and at each hjem standalone switch |
| a hjem configuration with no machine above it, on macOS | the same | the launchd agent org.caisson.profiles in the files of the hjem configuration, when launchd loads it at login and at each hjem standalone switch |
| nothing (a top) | the same | running activate by hand |
<kind> is nvf or nixvim. A hjem configuration has no activation
step that a configuration can add to. A machine activates the profiles
of a hjem configuration declared inside it. On a NixOS machine,
hjem.target starts the units that apply the files of each user, at
boot and again at every switch, and the unit of each profile starts
with it.
system-manager reloads a changed unit by its name, which fails for a
template, restarts each changed unit in a separate request, so the order
between units does not hold, and does not stop hjem.target at an
activation. On a system-manager machine, caisson therefore writes a
single unit for each hjem user, hjem-activate@<user>, wanted by
multi-user.target, which links the files, reloads the units of the
user, stores the state and activates the profiles inside the hjem
configuration, in that order. The unit names the manifests and the
profiles of its generation, so it changes, and system-manager restarts
it, when the files or the profiles of the user change. Activating the
same generation again starts no unit, so a profile changed by hand is
put back at the next activation that changes it, or at a boot.
A hjem configuration with no machine above it is applied with
hjem standalone switch. On Linux, its files hold the systemd user
units that activate its profiles. The service caisson-profiles runs
~/.local/state/caisson/activate-profiles, which the files link to the
script that runs the activate of each profile, and is wanted by
default.target. The unit files are the same in every generation, and
only the link changes, since the systemd of the user keeps the units it
has read until it reloads, which a switch does not make it do. The
path unit caisson-profiles starts the service when
$XDG_STATE_HOME/hjem/standalone/current/manifest.json changes, which
hjem standalone switch writes at every switch. The systemd of the
user reads the units when it starts, so the switch that first adds them
activates the profiles at the next login. To activate them before then,
run systemctl --user daemon-reload and
systemctl --user start caisson-profiles.path caisson-profiles.service.
On macOS, the files of such a hjem configuration hold the launchd agent
org.caisson.profiles, as
~/Library/LaunchAgents/org.caisson.profiles.plist. The agent runs the
same script from the same link when launchd loads it, which it does at
login, and when the manifest of the current generation changes. Like
the units, the agent is the same in every generation. launchd
expands no variables in the paths an agent watches, so the agent
watches ~/.local/state/hjem/standalone/current/manifest.json, where
hjem keeps the manifest when XDG_STATE_HOME is unset. To activate the
profiles before the next login, run
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/org.caisson.profiles.plist.
Reset. The configuration above runs activate each time it is
activated. A profile changed by hand is put back to what the
configuration above declares at the next activation: home-manager switch for a home-manager configuration, a switch of the machine, a
boot, since the machine activates its home-manager configurations and
its profiles at boot, and hjem standalone switch or hjem standalone rollback for a hjem configuration with no machine above it.
Choosing a profile. Each class installs, with
profileShimsOf, a program for each program the Neovims have in
bin: nvim, and vi and vim where they have them. The program
runs the program of the profile that $CAISSON_NVIM_PROFILE names,
read each time it runs, and of the default of the configuration that
installed it when the variable is unset. A home-manager configuration
and a hjem configuration set the variable to their profile, with
home.sessionVariables and environment.sessionVariables, so the
programs the machine installs choose the same profile. A shell can
export another value.
The default is the only profile of the configuration for that variable.
Where several share it,
caisson.profiles.selected
names the default, and an evaluation without it fails with a message
listing them.
modules.systemManager."caisson/default"
- Source:
modules/systemManager/default/; the modulesmodules/systemManager/home-manager-activation,modules/systemManager/hjem-activation,modules/systemManager/profile-activationandmodules/systemManager/activation-markerare the NixOS modules of the same names, andmodules/homeManager/system-manager-parentandmodules/hjem/system-manager-parentare thenixos-parentmodules
caisson/default is the default system-manager module caisson
registers. A system-manager configuration imports every registered
module named default unless it passes moduleImports. It holds what
a machine running another Linux distribution does for the
configurations declared inside it, as caisson/default of the nixos
class does for a NixOS machine:
| Part | What it does |
|---|---|
caisson/home-manager-activation | writes the system unit home-manager-<user> that activates each home-manager configuration declared inside the machine |
caisson/hjem-activation | writes the units of hjem that apply each hjem configuration declared inside the machine, and adds the packages of each user to users.users.<user>.packages |
caisson/profile-activation | activates the profile of each nvf or nixvim configuration declared inside the machine, and installs the programs that choose among them; see profiles |
caisson/activation-marker | records what the machine applied; see drift detection |
caisson.forChildren | gives each home-manager configuration the home-manager module caisson/system-manager-parent and each hjem configuration the hjem module caisson/system-manager-parent |
The two activation modules are those of a NixOS machine. system-manager
declares users.users and the systemd options, and starts the units
that ask for multi-user.target with system-manager.target,
so system-manager switch applies the home-manager configurations and
the hjem configurations. The units and their checks are described at
caisson/home-manager-activation
and caisson/hjem-activation.
system-manager has no system.extraDependencies, so the hjem module
keeps no extra dependencies in the system there.
The two system-manager-parent modules are the nixos-parent modules,
which read the nearest NixOS or system-manager configuration above the
child. They set home.homeDirectory and directory from the account
the machine declares in users.users. A home-manager configuration or
a hjem configuration inside a system-manager machine is refused when
the machine declares no account for its user, unless, for a
home-manager configuration, the user is listed in
caisson.home-manager.activation.usersDeclaredElsewhere.
A home-manager configuration declared inside a system-manager machine
is published as homeConfigurations."<user>@<machine>", as a
home-manager configuration inside a NixOS machine is.
modules.flake."caisson/nixpkgs"
- Source:
modules/flake/nixpkgs/
The nixpkgs integration’s flake module. It hands perSystem the
package sets of the package configs declared in mkLib’s pkgSets
(see caisson.nixpkgs below), and exports from them:
- The
perSystempkgSetsargument is every package config’s set for that system, keyed by config name, andpkgsis thedefaultconfig’s set. A flake that declares nodefaultconfig keeps thepkgsflake-parts provides. - The flake’s
overlaysoutput carries the entries ofcaisson.pkgOverlays.exportedas plain overlays, each with the entries it imports composed in, so a consumer that is not caisson gets a working overlay. pkgs.export.enabled,packages.export.enabled: whether to exportlegacyPackages, and the package scope of the flake (pkgs.<name>, after thenamedeclared onmkLib) aspackages.
integrations
- Source:
lib-overlays/integrations/default.nix
This namespace holds the functions that every integration is written
from. Each integration overlay imports this overlay by key, so
composing any integration composes this overlay as well, and the
integration reads these functions through final.
mkEvaluation
mkEvaluation : { compose, evaluate } -> args -> result
mkEvaluation is the body of both entry points of an integration. It
passes the arguments the entry point’s pattern admitted to compose,
which returns an attribute set holding ecosystemArgs, the arguments
of the evaluator’s call as the integration composed them, beside
anything evaluate needs, such as the resolved source. It then calls
evaluate with that set and the call to make. When the arguments
carry ecosystemArgs, which only the pattern of the
WithEcosystemArgs twin admits, they are merged over the composed
call verbatim, last.
The pattern is the whole check. Every entry point is a function of an
attribute set pattern with no ...,
so Nix matches the call against the pattern before the body runs: a
missing or unexpected argument is Nix’s error, named after the entry
point and raised at the call site, with no frame of caisson above it.
Required arguments are bare in the pattern and optional arguments
default to null; the composition supplies the value of an omitted
argument. builtins.functionArgs reads a signature back as data.
selectedEcosystemSrc
lib.caisson.integrations.selectedEcosystemSrc : context -> manifest -> explicit -> src | null
selectedEcosystemSrc applies a configuration’s ecosystemSrc
argument, a function of sources or null, to the sources of
manifest. It returns null for a null explicit, and refuses an
explicit that is not a function. context names the caller in that
refusal’s message.
resolveEcosystemSrc
resolveEcosystemSrc : { name, context } -> { explicit ? null, manifest ? libManifest } -> src
resolveEcosystemSrc finds the source of an ecosystem in the layered
order that Ecosystem sources
describes: selectedEcosystemSrc applied to the explicit argument
first, then the source defaultEcosystemSrc sources returns for the
name, then the pinned source named exactly <name>. The first call
names the ecosystem and the caller; the second supplies the explicit
argument and the manifest to read the sources and the declarations
from.
When nothing provides a source, it throws a message that names the
caller and those places.
coreModules, defaultModuleImports
coreModules : attrsOf module -> listOf module
defaultModuleImports : attrsOf module -> listOf module
Both take a class registry (lib.caisson.modules.<class>) and
select entries by name. coreModules returns every entry named core,
which includes the <project>/core entries of consumed projects;
these form the framework module of the class, and every integration
applies them to every evaluation. defaultModuleImports returns every
entry named default, in the same way; this list is the default
default, the selection an evaluation gets when it passes no
moduleImports.
integrationsIn, childrenOf, configurationsModuleFor, nearestOf
integrationsIn : options -> listOf str
childrenOf : { options; config } -> attrsOf (attrsOf manifest)
configurationsModuleFor : listOf str -> module
nearestOf : listOf str -> manifest -> manifest or null
integrationsDeclaringIn : options -> listOf str
integrationsDeclaringIn is the part of integrationsIn whose
caisson.<integration>.configurations a module of the evaluation
defines. childrenOf and re-export read only those integrations, so an
evaluation does not merge the options of an integration it holds no
configurations of.
nearestOf takes a list of integration names and a manifest, and
returns the nearest configuration above the manifest whose integration
is one of them, null when there is none. The home-manager and hjem
integrations find the machine of a configuration with
nearestOf [ "nixos" "darwin" ].
integrationsIn takes the options of an evaluation and lists the
integrations whose configurations it can hold beneath it: those whose
configurationsModule the evaluation imports, which declares
caisson.<integration>.configurations and
caisson.<integration>.exported. configurationsModuleFor takes a
list of integration names and returns one module that declares those
options for all of them. The core module of caisson imports it once,
for every integration of caisson, which costs less to evaluate than a
module per integration. childrenOf takes the
options and the config of an evaluated configuration and returns
the configurations declared beneath it, by integration and then name,
leaving out an integration with none: the children
mkModuleConfiguration hands to lib.caisson-core.mkConfiguration.
It reads only the options that a module of the evaluation defines.
frameworkModules, mkModuleConfiguration, ecosystemUnit, inEcosystem
frameworkModules : str -> attrsOf module -> listOf module
mkModuleConfiguration : { type : str; perSystem ? false; defaultPkgs ? null; exportsTo ? null; selection ? null; evaluate : view -> evaluated; } -> configuration
evaluated = { value; outputs ? { }; config ? value.config; modules ? listOf (module | unit); }
ecosystemUnit : { key : str; project : str; file : nullOr str; } -> unit
inEcosystem : { name : str; src : path; } -> listOf module -> listOf (module | unit)
exportsTo = export | listOf export
export = { attrset : str; value : manifest -> value; single ? false : bool;
bySystem ? false : bool; only ? : manifest -> bool;
name ? : { name, manifest } -> { value : str; description ? : str; }; }
mkModuleConfiguration builds a configuration that is a module
evaluation. type and perSystem are what
lib.caisson-core.mkConfiguration takes. evaluate is the evaluator
step of the integration: from the view being evaluated,
{ lib, manifest }, it returns value, the evaluation as the
evaluator returned it, and outputs, the references into it that the
integration declares (config says where the evaluated options are
when that is not value.config). defaultPkgs is the package set
selection the configuration was constructed with, which the builder
records on the manifest (pkgSetOf below). selection is
{ class; args; }, the class of the evaluation and the arguments the
configuration was constructed with, from which the builder records
how the evaluation selected its modules as moduleImports
(selectionAccountOf below). Every integration of caisson passes it.
evaluate also returns modules, the list of modules the evaluator
received, in that order, and the builder records a declaration
event in the history of the manifest for each element (manifest
above). Where the evaluator receives the module list of the ecosystem,
the list holds an ecosystemUnit instead of those modules, so the
module list is a single event: key names the ecosystem, project
the source, and file the file of the list inside the source, null
when the list has no single file. NixOS records
ecosystemUnit { key = "nixos"; project = "nixpkgs"; file = "nixos/modules/module-list.nix"; }
before the modules of the composition, since eval-config adds its
module list first. inEcosystem makes those units for an integration
whose list names modules of the ecosystem by their files: each module
of the list inside the source src becomes an ecosystemUnit of the
source name, with its file inside the source.
exportsTo says how the configuration is published, and the builder
records it on the manifest too. It is one export, or a list of them for
an integration whose configurations are published in more than one
output. attrset is the output attribute set; a NixOS configuration
records nixosConfigurations. value takes the manifest of the
configuration to what is published.
only takes the manifest and says whether the configuration is
published in that output at all. A configuration it leaves out is still
passed up. The treefmt integration
uses it to publish only the configuration named default.
single says the output holds a single value rather than values by
name, as colmenaHive does. The configuration declared under the name
default is then the output’s value, and a configuration declared
under another name is refused there.
bySystem says the output is keyed by system first, as flakes key
packages and formatter. Each configuration is published under the
system of its evaluation, <attrset>.<system>.<name>, named by the
usual rules over its path without the systems on it. With single
too, the output holds the configuration named default for each
system, as formatter.<system> does. A configuration with no system,
one not evaluated per system, is refused in such an output.
name is for an integration whose configurations are known by a name
other than the name they are declared under. It takes the name the
configuration is passed up under and its manifest. It returns value,
the name to publish the configuration under, and optionally
description, a sentence describing how the name was formed. The
home-manager integration uses it to name a home-manager configuration
declared inside a NixOS configuration: the name of the home-manager
configuration, @, and the name of the NixOS configuration.
A structural or flake-parts configuration is built with no exportsTo
and is not published under a name. The configurations declared inside
it are published, with its segment on their paths.
The configuration holds configurations of any integration beneath
it: those its modules declare under
caisson.<integration>.configurations are its children, and
caisson.exports, which carries what they pass up, is its exports
output. What its modules define under caisson.forChildren is what
it registers for the configurations beneath it
(lib.caisson-core.mkConfiguration).
frameworkModules takes a class and its registry and returns the
framework module of the class, which every evaluation of the class
imports: caisson’s core module for the class, read from the closure of
caisson, followed by coreModules of the registry. The core module
declares caisson.manifest, caisson.<integration>.configurations,
caisson.<integration>.exported and caisson.exports.
Structural, flake-parts, nixos and nixos-minimal configurations are built with both.
moduleImportsOf
moduleImportsOf : str -> { lib, manifest } -> args -> registry -> listOf module
The selection of an evaluation over the registry of its class. It takes the class, the view being evaluated and the arguments of the configuration, and reads two of them:
moduleImportsreplaces the default of the class. With none given, the selection is that default: every entry nameddefault, followed by what the levels above the evaluation added withcaisson.forChildren.extraDefaultModuleImports, those from the top first, each applied to the lib of the evaluation.extraModuleImportsis appended to the selection, whichever it is. A configuration that adds a module with it keeps the default of its class.
Both are functions of the registry returning modules, and every constructor of every integration takes both.
selectionAccountOf
selectionAccountOf : { class : str; args } -> { lib, manifest } -> account
account = { act : "default" | "appended" | "reset";
position : nullOr { file; line; };
default : listOf { key : nullOr str; level : nullOr namePath; file : nullOr str; };
selected : listOf (nullOr str); }
How an evaluation of class, constructed with args, selected its
modules. The manifest of every configuration records it as
moduleImports, through the selection argument of
mkModuleConfiguration. act is default when the configuration
took the default of its class, appended when it gave
extraModuleImports alone, and reset when it gave moduleImports.
position is the file and line number where the configuration gave
the argument, and null for default. default is the default in
force, whatever the act: the entries of the registry named default,
with a null level and file, then each entry a level above added,
with the name path of the level and the file that defines it.
selected holds the registry keys of the modules selected, null for
a module that is not from the registry. For a NixOS configuration
with a home-manager configuration inside it, the home-manager
configuration records:
{
act = "default";
position = null;
default = [
{ key = "modules.homeManager.caisson/default"; level = null; file = null; }
{ key = "modules.homeManager.nixos-parent";
level = [ { type = "system"; name = "x86_64-linux"; } { type = "nixos"; name = "laptop"; } ];
file = "modules/nixos/default/caisson"; }
];
selected = [ "modules.homeManager.caisson/default" "modules.homeManager.nixos-parent" ];
}
ecosystemAccountOf
ecosystemAccountOf : { name : str; context : str; } -> { explicit ? null; manifest ? libManifest; } -> account
account = { from : "ecosystemSrc" | "defaultEcosystemSrc" | "sources";
source : nullOr str; }
How an evaluation came by the source of the ecosystem name. It
takes the arguments resolveEcosystemSrc takes and follows the same
order. from names the declaration that supplied the source:
ecosystemSrc for the selection of the configuration,
defaultEcosystemSrc for the selection of the tree, and sources for
the source the tree pins under exactly that name. source is the name
of that source among the pinned sources of the tree, where
sources.<name>.pin records its revision. It is null where a selection
builds the value in another way, such as a directory inside a source.
The name is read from the selection applied to the sources tagged with
their names (caisson-core.selectedSourceName), so reading the
account forces no source the selection does not read.
The manifest of every configuration records ecosystems, an account
for each part of the stack the evaluation used, keyed as the parts are
resolved. nixpkgs-lib is always there, since every evaluation runs on
the lib. An integration adds the ecosystems it resolves, and its
evaluate returns them as ecosystems. For a NixOS configuration
that selects its module collection, in a tree whose
defaultEcosystemSrc names its nixpkgs input for the library:
{
nixpkgs-lib = { from = "defaultEcosystemSrc"; source = "nixpkgs"; };
nixos = { from = "ecosystemSrc"; source = "nixpkgs"; };
}
A package config records nixpkgs-lib and nixpkgs. Its nixpkgs
entry also holds libraries, the top-level names the lib of its sets
takes from the source of the package set: the package-set libraries,
their tables, and the functions nixpkgs publishes from them at the top
level of its library.
pkgSetsForSystem, pkgSetOf
pkgSetsForSystem : { context, what } -> manifest -> system -> attrsOf pkgs
pkgSetOf : { context, what } -> manifest -> attrsOf pkgs -> pkgs
selection = attrsOf pkgs -> pkgs
How a configuration gets the package set it runs on. It is the same for every integration whose configurations run on a package set.
A configuration selects its set when it is constructed, with the
defaultPkgs argument: a function of the package sets available where
the configuration is declared, by package config name, each for the
system of the evaluation.
lib.caisson.nixos.mkConfiguration {
defaultPkgs = pkgSets: pkgSets.stable;
}
The function receives the available sets as an attribute set and
returns the one to run on, here the set of the package config named
stable.
A selection holds for the subtree: the configuration that makes it
and every configuration beneath it that selects none, through levels
of any integration. A configuration that selects none runs on what
the nearest configuration above it selected. A module of a
configuration selects for the configurations beneath it, and not for
the configuration itself, with the caisson.forChildren.defaultPkgs
option. Where no configuration from the top down selects, a configuration runs on the set named
default, and a composition that declares no default and selects
nothing is told so, with the sets it declares.
The modules of a configuration can also use any available set by
name, through the pkgSets argument.
pkgSetsForSystem gives the sets available to an evaluation: the package
configs its manifest holds, each projected to its set for the system.
pkgSetOf applies the selection in force at the manifest (defaultPkgs,
which mkModuleConfiguration records from the argument and
lib.caisson-core carries to everything beneath) to those sets.
context and what are used only in the errors the two functions
raise. context names the entry point that was called, and what
names the evaluation being built. A package config that builds no set
for the evaluation’s system fails only when the evaluation selects it
or reads it by name. With context = "lib.caisson.nixos.mkConfiguration"
and what = "the NixOS configuration `builder-arm` for aarch64-linux",
the message reads:
lib.caisson.nixos.mkConfiguration: the package config `cuda` builds no set for aarch64-linux,
which the NixOS configuration `builder-arm` for aarch64-linux needs. Add the system to
`caisson.nixpkgs.systems` in the config's module.
The argument is on the nixos, nixos-minimal, home-manager,
home-manager-minimal, flake-parts, structural and colmena
constructors. In a
flake it selects the pkgs of perSystem. A structural configuration
runs on no package set and selects for what is beneath it.
pkgSetAccountOf
pkgSetAccountOf : { own : bool; perSystem : bool; } -> manifest -> attrsOf pkgs -> account
account = { name : nullOr str; level : nullOr namePath; }
Which package config a configuration runs on, and which configuration
selected it. The manifest of every configuration records it as
pkgSet. name is the name of the package config whose set the
selection in force returns. It is read from the selection applied to
the available sets tagged with their names, so it is null where the
selection returns a set it builds in another way. level is the name
path of the configuration that made the selection: the configuration
itself when it was constructed with defaultPkgs, else the nearest
configuration above that selected for the configurations beneath it,
with its own defaultPkgs or with caisson.forChildren.defaultPkgs.
The top has the empty name path, [ ]. Where no configuration
selects, name is default and level is null.
A configuration evaluated once for every system, such as a structural
configuration or a flake, runs on no single set. Its name is null,
and its level still says who selected for the configurations beneath
it. For a NixOS configuration beneath a structural configuration
group that selects pkgSets: pkgSets.other, the NixOS configuration
records:
{
name = "other";
level = [ { type = "structural"; name = "group"; } ];
}
namespacesOf
namespacesOf : lib -> { caisson : attrsOf (nullOr (listOf str)); caisson-core : listOf str; }
The names of the namespaces of a lib. The manifest of every
configuration records them for the lib its evaluation runs on as
namespaces. caisson holds, for each entry of lib.caisson, the
names of its attributes where it is an attribute set, and null where it
is not. caisson-core holds the names of lib.caisson-core. They are
names only, so recording them forces nothing past the keys of the
namespaces. namespaces.caisson.nixos lists mkConfiguration,
mkConfigurationWithEcosystemArgs, mkModule and the rest of the
nixos integration.
entriesOf, publish, recordOf, displayName, topValue
entriesOf : attrsOf level -> attrsOf (attrsOf finalized) -> listOf entry
publish : listOf entry -> attrsOf (attrsOf value)
recordOf : listOf entry -> attrsOf (attrsOf recordEntry)
entry = { path; namePath; notes; code; manifest; attrset; value; description ?; }
level = { notes : listOf str; code : listOf position; }
recordEntry = { from.namePath; notes; code; }
position = { file; line : int | null; }
displayName : listOf str -> str
topValue : attrsOf manifest -> value | attrsOf value
How the configurations in a tree are named and published.
entriesOf gives the entries a configuration passes up: an entry for
each configuration declared inside it, directly or deeper, whose
manifest records exportsTo. Its second argument is what each
integration’s exported selected, by integration and then name. Its
first argument is what the configuration adds, for each integration,
to the entries of that integration’s configurations it passes up:
notes, its caisson.<integration>.exportNote as a list, and code,
the files that define its caisson.<integration>.exported. A
selector left at its default adds no file.
The configuration that passes a child up makes the entry, because it
knows the name the child is declared under. The entry holds the
output attribute set and the value the manifest records, and the path
from that configuration to the child. The last segment of the path is
the name exportsTo.name returns, when the integration defines
name. A name that contains / is refused. The structural,
flake-parts and system levels on the way are segments of the path.
caisson’s core module defines caisson.exports.configurations with
entriesOf.
publish is what a top does with the entries. It groups them by the
output attribute set named in each entry, computes the names in each
group from lib.caisson-core.elide over the paths, and returns the
values by attribute set and name. Entries that still share a name are
refused, with their paths. The flake-parts top writes the
result into the flake outputs, and the structural top into what it
returns.
recordOf is the export record of the same entries, keyed as
publish keys the values. For each name published, from.namePath
is the path from the top to the configuration by the names the
configurations are declared under, before any exported renamed
them, so it is the address of the configuration’s manifest under the
children of the top’s manifest. notes holds the sentence the
integration’s exportsTo.name wrote, when it wrote a sentence, and
then the exportNote of each level the configuration passed through.
code holds the files that define each level’s exported, and the
file and line number where the integration gives its exportsTo, in
the same order, from the configuration up to the top. A position is
{ file; line; }, with the line number in line. The module system
records the file of each definition of an option but not its line
number, so the position of an exported has a line of null. Both
tops publish their manifest as caisson.manifest,
carrying the record as exportRecord.
displayName writes the kept segments out as a name: in path order,
separated by /. Every name caisson publishes is written by it.
topValue is what a tool reads from a top that is a configuration
evaluated per system, given its evaluations by system. They are
named as anything published is, and the name of the top, which the
file that returns it stands for, is left out of each: a single
evaluation is its value, and several are the values by system.
mkIntegration
mkIntegration :
{ name : string # the namespace, lib.caisson.<name>
, class : string # the module class this integration owns
, mkConfiguration : pattern -> result # the entry point, a pattern function
, mkConfigurationWithEcosystemArgs : pattern -> result # the same pattern plus ecosystemArgs
, top ? null : manifest -> value # what a configuration with no parent returns
, extra ? { } : attrs # further members of the namespace
} -> { namespace : attrs }
mkIntegration builds an integration that owns a module class from a
declaration. The result has the part namespace.
namespace is the value to publish as lib.caisson.<name>. It holds
mkConfiguration, mkConfigurationWithEcosystemArgs, mkModule,
configurationsModule, mkTopConfiguration when the declaration
gives top, and everything in extra. The entry points come from the declaration,
written as pattern functions whose body is
mkEvaluation { compose, evaluate } applied to the admitted
arguments. The pattern of mkConfiguration names the arguments
every entry point takes (ecosystemSrc, pkgSets, configModule,
moduleImports, specialArgs) and the few of its target, with the
required arguments bare and the rest defaulting to null; a comment
beside each argument says what it is, and the at line of Nix’s
argument error points at that block. The twin’s pattern repeats the
entry point’s plus ecosystemArgs, merged over the composed call
before evaluating, so the caller can set or replace anything the
evaluator takes. mkModule is lib.caisson-core.mkModule bound to
class. extra is for the members a declaration cannot generate,
such as a variant entry point, an adapter, or the composition an alt
over this class builds on.
configurationsModule is the module that lets a configuration hold
configurations of this integration beneath it. It declares
caisson.<name>.configurations and caisson.<name>.exported. An
integration written outside caisson registers a module that imports
it where its configurations may be declared. The integrations of
caisson have their options declared by the core module of caisson,
through configurationsModuleFor, so importing the
configurationsModule of one of them into a caisson evaluation
declares its options a second time.
top is what a configuration of the integration returns when it has
no parent, as a function of its finalized manifest. When the
declaration gives it, the namespace has mkTopConfiguration, which
takes the arguments of mkConfiguration, finalizes the configuration
with no parent, and applies top to the manifest. Its argument
pattern is the pattern of mkConfiguration.
The integration’s compose receives the admitted arguments and
returns an attribute set. That set must hold ecosystemArgs, the
arguments of the evaluator’s call as the integration composed them; it
may hold anything else evaluate needs, such as the resolved source.
evaluate receives that set and the call arguments to use, and
returns the evaluation.
The constructor returns values rather than an overlay output, because
the attribute names an overlay produces must not depend on final.
The overlay file writes the key itself:
overlay = final: prev:
let
integration = final.caisson.integrations.mkIntegration { ... };
in
{
caisson = (prev.caisson or { }) // {
nixos = integration.namespace;
};
};
mkAltIntegration
mkAltIntegration :
{ over : attrs # the integration that owns the class, e.g. final.caisson.nixos
, mkConfiguration : pattern -> result
, mkConfigurationWithEcosystemArgs : pattern -> result
, top ? null : manifest -> value # as for mkIntegration
, extra ? { } : attrs
} -> attrs # the value of lib.caisson.<name>
mkAltIntegration builds an integration that evaluates a class
another integration owns. over is that owning integration, reached
through the lib. The result is the value to publish as
lib.caisson.<name>: the entry points of the declaration, pattern
functions as for an owner, mkTopConfiguration when the declaration
gives top, and extra. It has no mkModule, because modules of the
class are registered through the owner, and no
configurationsModule, because its configurations are configurations
of the owner. The composition its entry points evaluate over is expected
to build on the composition the owner publishes, so that the evaluators
cannot produce different configurations from the same arguments;
caisson.nixos-minimal composes through caisson.nixos.compose and
caisson.home-manager-minimal through
caisson.home-manager.configuration, which builds on
caisson.home-manager.compose.
Every integration caisson ships is declared with these
constructors: mkIntegration for the owners of a class (nixos,
flake-parts, structural, home-manager, colmena, terranix,
system-manager) and mkAltIntegration for caisson.nixos-minimal
and caisson.home-manager-minimal. Each overlay file holds the
composition, the evaluator step, the patterns and the extras of
its integration, and nothing else.
eval-weight
- Source:
lib-overlays/tooling/eval-weight/
The evaluation-cost measurement harness: eval-weight.mkCheck builds a
check derivation that measures eval scenarios in a sandbox and gates
deterministic metrics against a committed baseline. Documented in
Evaluation weight.
mkMemoizedDerivationRead
- Source:
lib-overlays/tooling/mk-memoized-derivation-read.nix
Builds memoized derivation-content readers; see the source header.
Integration namespaces
Each integration is a library overlay exported by this flake
(libOverlays.<ecosystem>). Composing an integration contributes its
lib.caisson.<ecosystem> namespace, documented below (the flake-parts
integration also contributes the lib.flake-parts mirror of
the flake-parts library, instantiated over the composed lib). Each
entry point takes its ecosystem as an ecosystemSrc argument, and
every ecosystem, flake-parts included, is resolved from the
composition of the consumer.
An adapter’s ecosystem source resolves in layers: the explicit
ecosystemSrc argument first, applied to sources through
selectedEcosystemSrc; then the source defaultEcosystemSrc sources
returns for <name> (defaultEcosystemSrc is an mkLib argument,
carried by the manifest); then the source named exactly <name> in
the sources passed to mkLib. The name is the integration’s
ecosystem, and a single ecosystem may serve several integrations:
nixos for the nixos and nixos-minimal integrations, nixpkgs for
the nixpkgs integration, colmena for the caisson-colmena and colmena
integrations, disko for the caisson-disko and disko integrations,
nix-darwin for the darwin integration, then home-manager, hjem,
terranix, system-manager and flake-parts for the integration of
the same name
(the table in
Ecosystem sources). A full
miss throws at the adapter, naming those places; a composition
built without mkLib (no manifest) accepts only the explicit argument.
Common conventions:
- Every integration exports
mkConfiguration(evaluate the target’s module system with the selected class modules) and, where it has a module class,mkModule, the form a tree registers that class’s modules with (caisson-core.mkModulebound to the class). Target-specific variants and helpers sit beside them under the same namespace. configModule: the top-level module of the configuration, always a single module (compose several withimports); the framework’s selected class modules are applied beside it.specialArgs: extra module arguments; the same name on every entry point, translated to the evaluator’s spelling where it differs (home-manager’sextraSpecialArgs, terranix’sextraArgs).defaultPkgsis accepted by the nixos, nixos-minimal, darwin, hjem, home-manager, home-manager-minimal, terranix, flake-parts, structural, caisson-colmena, colmena, caisson-disko and disko entry points. It selects the package set the configuration runs on from the package configs the composition declares (seecaisson.integrations.pkgSetOf). In a configuration evaluated per system, the modules get the package sets by config name as thepkgSetsspecial argument.pkgSets: on the colmena and disko entry points, an attrset of package sets passed through as thepkgSetsspecial argument, wherepkgSets.pkgsis the package set ofcolmena eval. flake-parts and structural forward it to their modules.- The signature is the whole surface. An entry point takes exactly
the arguments listed for it and composes the evaluator’s call from
them; nothing else is forwarded, and an unknown or missing argument
is Nix’s function-argument error, raised at the call site and
pointing at the entry point’s pattern, whose comments name the
caisson argument to use where such an argument exists. That
keeps an evaluator argument from being silently overwritten
(
modules), silently dropped (anything the minimal evaluator does not take), or surfacing as a conflict inside the evaluator (pkgsbeside the framework’snixpkgs.pkgs). - The full surface of the evaluator is reachable, deliberately, through
the
mkConfigurationWithEcosystemArgstwin of each entry point. It takes the same arguments plusecosystemArgs, an attrset merged over the composed evaluator call verbatim, last: anything the evaluator accepts can be set or replaced there, including what caisson composed (modules,specialArgs, the package set). The name is the contract: from there on the evaluator’s semantics are the caller’s to know. moduleImports: a selection function over the corresponding class registry (lib.caisson.modules.<class>), returning the list of modules to apply; the default is the default default, every entry nameddefault(default,<project>/default). The framework module, every entry namedcore, applies before the selection whatever it is. The list shape matcheslibOverlayImports; for order-sensitive list-typed options, prefermkOrderover selection position.- Framework-provided special arguments compose first; the caller’s win on conflict.
caisson.structural (module class structural)
- Source:
lib-overlays/structural/default.nix
The empty integration: it wraps no ecosystem, and its class carries
nothing but caisson’s core module: the manifest, the configurations
declared beneath, the registry selectors and caisson.exports. A
structural configuration is the top of a repository whose point is
what it exports (the default.nix of caisson is such a top), and a
layer at any depth, declared beneath another configuration under
caisson.structural.configurations.
mkModule : freeformModule -> module: the registration form for structural modules.mkConfiguration:
mkConfiguration :
{ configModule ? (the configuration registered under the name)
: module # structural class
, moduleImports ? (every entry named default)
: attrsOf module -> listOf module # selection from the structural class registry
, specialArgs ? { }
, pkgSets ? null : attrs # the pkgSets special argument
} -> configuration
Returns a configuration, a function of { name, parent }, built with
lib.caisson-core.mkConfiguration. The parent that declares it under
caisson.structural.configurations.<name> finalizes it, and
mkTopConfiguration finalizes it at a top. Nothing is evaluated
until the manifest is read.
The evaluation is the framework module (every core of the class,
the core of caisson, read through the integration’s closure), the
selected structural modules and the config module, with evalModules
over the lib of the view being evaluated. When configModule is
absent, the config module is the configuration registered under the
configuration’s name, lib.caisson.configs.structural.<name>,
and there is none when nothing is registered under it. The manifest’s
value is the evaluation as evalModules returned it (config,
options); outputs.exports is caisson.exports, the lib,
libOverlays, modules and pkgOverlays the selectors chose; and
children holds the configurations declared beneath it. The
integration wraps no ecosystem, so the signature has no ecosystemSrc.
mkConfigurations: a configuration for every configuration registered in the class, each asmkConfigurationbuilds it withoutconfigModule.mkConfigurationWithEcosystemArgs: the twin;ecosystemArgsis merged over theevalModulescall (class,modules,specialArgs).mkTopConfiguration: the same arguments. It finalizes the configuration withlib.caisson-core.finalizeTopand returnsoutputs.exportswith the configuration’s manifest beside it ascaisson.manifest, which is whatdefault.nixreturns for a reader that indexes attributes of the file’s value.
caisson.flake-parts (module class flake)
- Source:
lib-overlays/flake-parts/default.nix mkModule : freeformModule -> module: the registration form for flake-parts modules (caisson-core.mkModulebound to theflakeclass).mkConfiguration:
mkConfiguration :
{ configModule ? (the configuration registered under the name)
: module # flake class
, pkgSets ? null # the `pkgSets` special argument
, ecosystemSrc ? null : sources -> source # the flake-parts source
, moduleImports ? (every entry named default)
: attrsOf module -> listOf module # selection from the flake class registry
, specialArgs ? { } # beside `lib`, the composed library
} -> configuration
Returns a configuration, a function of { name, parent }, built
with lib.caisson-core.mkConfiguration. A parent that declares it
under caisson.flake-parts.configurations.<name> finalizes it, and
mkTopConfiguration finalizes it at a top. The evaluator’s call is
flake-parts’ evalFlakeModule over the lib of the view being
evaluated: the manifest’s value is the evaluation (config,
options), outputs.flake is what flake-parts’ mkFlake returns
from it, outputs.exports is caisson.exports, and children
holds the configurations declared beneath it. When configModule is
absent, the config module is the configuration registered under the
configuration’s name, lib.caisson.configs.flake.<name>.
flake-parts’ inputs are the manifest’s pinned sources, and the
integration ties self the way Nix does for a flake: the
evaluation’s outputs with the root’s source info (out path,
revision, last-modified) beside them and self.inputs the sources,
so flake-parts modules receive self, self' and inputs as they
would under Nix’s flake evaluation. moduleImports selects over
the flake class of lib.caisson.modules, the same registry
every adapter selects from, so modules arriving by local
registration, overlay contribution, or consumed project are all
selectable. flake-parts itself resolves like every ecosystem, from
ecosystemSrc, the source defaultEcosystemSrc sources returns for
flake-parts, or the pinned source named flake-parts, and is
instantiated over the composed library. The name of the configuration sets flake-parts’
moduleLocation, so exported modules deduplicate across revisions.
mkTopConfiguration: the same arguments. It finalizes the configuration withlib.caisson-core.finalizeTop, under the name the composition declares onmkLib, and returns its flake outputs (outputs.flake). It is what aflake.nixreturns fromoutputs, and what a flakeless top that keeps flake-parts returns fromdefault.nix.mkConfigurations: a configuration for every configuration registered in the class, each asmkConfigurationbuilds it withoutconfigModule.types.libOverlay: a module-system option type for built library overlays. Itscheckverifies the structure recursively: an attrset with anoverlayfunction and a (possibly absent)importslist whose entries are themselves validlibOverlays. Used by options that carry overlays, such ascaisson.libOverlays.exported.types.manifest: a structural option type for the caisson-core lib manifest ({ inputs, modules, libOverlays, ecosystems, projects, systems }). The export-side check: the core flake-parts module reads the manifest of the evaluation through an option of this type before projecting the exports and theflake.modulesoutput.
caisson.nixos (module class nixos)
- Source:
lib-overlays/nixos/default.nix mkModule : freeformModule -> module: class-boundmkModule.mkConfiguration:
mkConfiguration :
{ configModule ? (the configuration registered under the name)
: module # nixos class
, ecosystemSrc ? null : sources -> source # the source under the `nixos` key
, moduleImports ? (every entry named default)
: attrsOf module -> listOf module # selection from the nixos class registry
, specialArgs ? { }
} -> configuration
Returns a configuration, a function of { name, parent }, built
with lib.caisson-core.mkConfiguration. A parent that declares it
under caisson.nixos.configurations.<name> finalizes it, and
mkTopConfiguration finalizes it at a top. ecosystemSrc resolves
under the nixos key (see
Ecosystem sources); the
evaluation is <ecosystemSrc>/nixos/lib/eval-config.nix, a nixpkgs
source tree, over the selected class modules and the config module.
When
configModule is absent, the config module is the configuration
registered under the configuration’s name,
lib.caisson.configs.nixos.<name>.
A NixOS configuration is evaluated per system, so the integration
passes perSystem: a declared configuration is an evaluation for
every system in force where it is declared (systems on mkLib),
each under its name beneath its system in the tree. There are as
many as there are systems, also for a single system, and none where
no system is in force. An evaluation’s value is the evaluated
NixOS configuration, and its outputs are toplevel, vm,
vmWithBootLoader and images, each a reference into
config.system.build, and exports, what the configuration passes
up to its parent.
A module of the configuration declares configurations beneath it
under caisson.<integration>.configurations. They see the
evaluation as nearest.nixos.
The package set comes from the composition, through the manifest.
An evaluation runs on the set the defaultPkgs argument selects
from the package sets available for the system of the evaluation
(see caisson.integrations.pkgSetOf), defined as nixpkgs.pkgs. A
configuration that passes none runs on what the nearest
configuration above it selected, and on the set named default
where none above selects. The modules can also use any set for that
system by config name, through the pkgSets special argument. A
module reads its name as
lib.caisson-core.evalManifest.name and its system as
lib.caisson-core.evalManifest.system.
A NixOS configuration records exportsTo as nixosConfigurations,
the value being the evaluated configuration: a flake-parts or
structural top
publishes the NixOS configurations declared beneath it there, under
names from their paths.
mkTopConfiguration: the same arguments. It finalizes the configuration withlib.caisson-core.finalizeTopand returns what a tool reads (caisson.integrations.topValue): the evaluated NixOS configuration where there is a single evaluation, which is whatnixos-rebuild --filereads and what a test evaluates, and the evaluated configurations by system where there are several.mkConfigurationFull: asmkConfiguration, additionally passing nixpkgs’module-list.nixasbaseModules.mkConfigurationWithEcosystemArgs: the twin withecosystemArgs(see the conventions above); eval-config’sbaseModulesis among the arguments reachable that way.mkConfigurations: a configuration for every configuration registered in the class, each asmkConfigurationbuilds it withoutconfigModule.compose : { context?, nixpkgsModule? } -> { lib, manifest } -> args -> { modules, specialArgs, system, pkgSets, src }: the composition of the class on the view being evaluated, from the caisson arguments (the framework and selected modules, the config module, the package-set module, the system, the resolved nixpkgs source), which every entry point here builds on and which an integration evaluating thenixosclass with another evaluator reads, so two evaluators cannot express different machines from the same arguments.nixpkgsModule = falsedelivers the package set as thepkgsmodule argument for an evaluation without NixOS’ nixpkgs module.
caisson.nixos-minimal (module class nixos, owned by caisson.nixos)
- Source:
lib-overlays/nixos-minimal/default.nix
A second integration over the nixos class: the minimal evaluator,
evalModules from <ecosystemSrc>/nixos/lib, with no NixOS base
modules, so the config module declares any options it uses and the
package set arrives as the pkgs module argument rather than through
nixpkgs.pkgs. It carries constructors only. The class, its
registration form (caisson.nixos.mkModule), its framework module and
its default default belong to the nixos integration, and the module
list comes from caisson.nixos.compose; composing it needs the nixos
integration composed beside it. In the tree its configurations are
nixos configurations: the manifest’s type is nixos, a
configuration is declared under caisson.nixos.configurations and
published under nixosConfigurations, and it is nearest.nixos for
what is declared beneath it.
mkConfiguration : { configModule?, ecosystemSrc?, defaultPkgs?, moduleImports?, extraModuleImports?, specialArgs?, prefix? } -> configuration: the arguments ofcaisson.nixos.mkConfigurationplusprefix, and the same result, a configuration whose manifest’svalueis the evaluation. The system and the package set come from the composition as they do there.mkTopConfiguration: the same arguments; finalizes the configuration at a top and returns the evaluation.mkConfigurationWithEcosystemArgs: the twin withecosystemArgs(prefix,modules,specialArgs).
caisson.darwin (module class darwin)
- Source:
lib-overlays/darwin/default.nix
A darwin configuration is a macOS machine, evaluated with nix-darwin’s
eval-config.nix over the composed library, the way nix-darwin’s
darwinSystem calls it. caisson adds to the modules what
darwinSystem adds: nixpkgs.source from the nixpkgs the
composition declares, no check of NIX_PATH, and the revision of the
nix-darwin source in the version of the system.
Like a NixOS configuration, a darwin configuration is evaluated per
system, for every system in force where it is declared, and runs on the package
set selected for it (defaultPkgs, else the set named default),
which caisson hands to nixpkgs.pkgs. nixpkgs.hostPlatform
defaults to the system of the evaluation. nix-darwin records no
nixpkgs revision for a package set it did not build itself, so the
system carries none, as with darwinSystem given pkgs.
A tree registers the machine as configs/darwin/macbook-1/default.nix
and declares it with
caisson.darwin.configurations.macbook-1 = lib.caisson.darwin.mkConfiguration { };,
which finds the configuration by its name. It is published as
darwinConfigurations.macbook-1, the evaluated configuration with
system beside config, which is what darwin-rebuild reads.
darwin-rebuild without --flake '.#<name>' looks the machine up by
its local host name, so we recommend declaring a machine under that
name.
Every configuration evaluated per system has an evaluation for each
system in force where it is declared. A tree whose systems hold both
Linux and darwin systems therefore declares its darwin machines under a
configuration that narrows the systems beneath it, such as a structural
configuration with caisson.forChildren.systems = [ "aarch64-darwin" ];,
and its NixOS machines under one narrowed to its Linux systems.
A darwin machine activates the home-manager configurations and the
hjem configurations declared inside it, with the modules of
modules.darwin."caisson/default".
A home-manager configuration inside a darwin machine is published as
<user>@<machine>, as a home-manager configuration inside a NixOS
machine is, and gets osClass = "darwin" and the machine as
darwinConfig, as home-manager’s nix-darwin module gives them.
caisson’s tests evaluate a darwin machine on Linux and compare its
system with what darwinSystem builds from the same module and package
set. They also evaluate a machine with a home-manager configuration and
a hjem configuration inside it, and compare the manifest of the hjem configuration with the
manifest hjem’s nix-darwin module makes. caisson’s CI builds the systems
of both machines on a macOS runner. Activation on a running machine is
not tested.
mkModule : freeformModule -> module: class-boundmkModulefor darwin modules.mkConfiguration : { configModule?, ecosystemSrc?, defaultPkgs?, moduleImports?, extraModuleImports?, specialArgs? } -> configuration: the arguments ofcaisson.nixos.mkConfiguration.configModuledefaults to the configuration registered under its name inconfigs/darwin.ecosystemSrcis the nix-darwin flake or source tree, resolved from the composition under the namenix-darwinwhen absent.mkTopConfiguration: the same arguments; finalizes the configuration at a top and returns the evaluation, or the evaluations by system where several systems are in force.mkConfigurationWithEcosystemArgs: the twin withecosystemArgs, merged over the call ofeval-config.nix(modules,specialArgs).
caisson.home-manager (module class homeManager)
-
Source:
lib-overlays/home-manager/default.nix -
mkModule : freeformModule -> module. -
mkConfiguration : { configModule?, ecosystemSrc?, defaultPkgs?, moduleImports?, extraModuleImports?, specialArgs?, osConfig?, check? } -> configurationmkConfigurationbuilds a home-manager configuration: a configuration evaluated with home-manager’s evaluator, the modules under<ecosystemSrc>/modules. A home-manager configuration is declared undercaisson.home-manager.configurations.<name>in a module of any configuration, or finalized as a top.mkConfigurationWithEcosystemArgstakesecosystemArgsas well, which is how to pass home-manager’slibargument.A home-manager configuration is evaluated per system. A home-manager configuration declared inside a NixOS configuration is evaluated for the system of that configuration by default, and the NixOS configuration can list other systems with
caisson.forChildren.systems. A home-manager configuration declared anywhere else is evaluated for each system the composition declares onmkLib, unless a configuration it is declared inside lists others the same way.configModuledefaults to the configuration registered asconfigs/homeManager/<name>, where<name>is the name of the home-manager configuration.home.usernamedefaults to the name the home-manager configuration is declared under, and a module of the home-manager configuration may set a different username.The home-manager configuration runs on the package set
defaultPkgsselects (seecaisson.integrations.pkgSetOf). A home-manager configuration constructed with nodefaultPkgsand declared inside a NixOS configuration runs on the set of that NixOS configuration, unless the NixOS configuration setscaisson.forChildren.defaultPkgs.pkgsin the home-manager configuration is that set. The evaluation leaves out home-manager’s nixpkgs module, as home-manager’s NixOS module does underuseGlobalPkgs, sonixpkgs.configandnixpkgs.overlaysbelong in the package config. The modules can also use any package set for that system by config name, through thepkgSetsspecial argument.A home-manager configuration declared inside a NixOS configuration is given that machine. The integration hands the home-manager configuration the configuration of the machine as the module argument
osConfig, and the class of the machine asosClass. These are the arguments home-manager’s NixOS module passes to a home-manager configuration it embeds. The configuration of the machine is the childless evaluation of the NixOS configuration, which leaves out the configurations declared inside it, because the home-manager configuration is one of them. A home-manager configuration with no NixOS configuration above it getsosConfig = null, as a home-manager configuration evaluated by home-manager alone does. AnosConfigpassed tomkConfigurationtakes the place of the configuration of the machine.Such a home-manager configuration also imports the home-manager module
nixos-parentby default, which the NixOS configuration adds, and which sets the options of the home-manager configuration that come from the machine. The home-managerdefaultmodule gives every home-manager configurationactivation-drift, which makes a home-manager configuration print a warning when it is activated on a machine it was not built for. The machine activates the home-manager configuration with the NixOS modulehome-manager-activation. Seemodules.nixos."caisson/default".A home-manager configuration that passes
moduleImportsselects all of its modules itself. Such a home-manager configuration getsmodules."caisson/nixos-parent"andmodules."caisson/activation-drift"only if its list names them.extraModuleImportsadds to the default selection and keeps both.home-manager’s legacy argument
nixosConfigstays null. Its fontconfig module readsnixosConfig.home-manager.useUserPackages, an option that only home-manager’s NixOS module declares, and caisson does not use that module. -
mkTopConfigurationtakes the same arguments. It finalizes the home-manager configuration at a top and returns the evaluated home-manager configuration with itsactivationPackage, which is what the home-manager CLI reads. -
A top publishes home-manager configurations as
homeConfigurations.<name>. A home-manager configuration declared inside a NixOS configuration is published under the name the home-manager configuration is declared under,@, and the name the NixOS configuration is declared under. The home-manager CLI looks up$USER@$(hostname), so that is the name it finds when the two names are the user and the hostname. A home-manager configuration with no NixOS configuration above it is published under its name.
caisson.hjem (module class hjem)
- Source:
lib-overlays/hjem/default.nix
A hjem configuration is the files and packages of one user, written
with the options of hjem’s user module (files, xdg.config.files,
packages, directory). hjem evaluates a user only inside its NixOS
and nix-darwin modules, as an entry of hjem.users. caisson evaluates
the user module itself, with the arguments those modules pass: hjem’s
library as hjem-lib, the name of the user as name, the package set
as pkgs, and the machine above the configuration as osConfig, null
when there is none.
A hjem configuration is evaluated per system, once for each system in
force where it is declared, and runs on the package set selected for it (defaultPkgs,
else the selection of the nearest configuration above, else the set
named default). user defaults to the name the configuration is
declared under, and clobberFiles defaults to false, as in hjem’s NixOS
module.
A tree registers the user as configs/hjem/alice/default.nix and
declares it with
caisson.hjem.configurations.alice = lib.caisson.hjem.mkConfiguration { };,
which finds the configuration by its name. Its outputs are what hjem’s
tools read:
| Output | Value |
|---|---|
manifest | the version 3 manifest of the files of the user, as hjem’s OS modules write it |
manifestFile | that manifest as a JSON file, checked against hjem’s schema with cue vet when it is built |
packages | the packages of the user |
cli | hjem’s CLI, built from the hjem source the configuration is evaluated with |
A failed assertion of the user fails manifest, manifestFile and
packages with its message.
A top publishes the configuration as hjemConfigurations.alice, with
manifest and packages. hjem standalone switch --flake . reads
hjemConfigurations."$USER" from the flake in the current directory, so
it finds the configuration of the user running it when the two names
are the same; --flake-attr names another attribute. The value of a hjem
configuration that is a top has manifest and packages too, which is
what hjem standalone switch --config reads from a file.
Inside a NixOS machine. A NixOS configuration activates the hjem
configurations declared inside it with
caisson/hjem-activation, and
gives each of them the hjem module
caisson/nixos-parent, which sets
directory from the account of the user on the machine. Such a
configuration also imports hjem’s modules/nixos/systemd.nix with the
utils and options of the machine, as a user that hjem’s NixOS module
embeds does, so it can declare systemd user units under
systemd.services and the other unit types. hjem’s NixOS module is not
used.
Inside a darwin machine. A darwin configuration applies the hjem
configurations declared inside it with the launchd agent of
caisson/hjem-activation, and
gives each of them the hjem module caisson/darwin-parent, which sets
directory from the account of the user on the machine.
Inside a system-manager machine. A system-manager configuration
activates the hjem configurations declared inside it with the same
caisson/hjem-activation module and units as a NixOS machine, and
gives each of them the hjem module caisson/system-manager-parent, the
same module as caisson/nixos-parent under a second name. See
modules.systemManager."caisson/default".
caisson’s tests compare the manifest of a hjem configuration inside a NixOS machine with the manifest hjem’s NixOS module makes from the same user module, and build its manifest file and the units that activate it. Activation on a running machine is not tested.
mkModule : freeformModule -> module: class-boundmkModulefor hjem modules.mkConfiguration : { configModule?, ecosystemSrc?, defaultPkgs?, moduleImports?, extraModuleImports?, specialArgs? } -> configuration.configModuledefaults to the configuration registered under its name inconfigs/hjem.ecosystemSrcis the hjem flake or source tree, resolved from the composition under the namehjemwhen absent.mkTopConfiguration: the same arguments; finalizes the configuration at a top and returns its value, or the values by system where several systems are in force.mkConfigurationWithEcosystemArgs: the twin withecosystemArgs, merged over the call ofevalModules(modules,specialArgs).manifestOf : hjemLib -> config -> manifest: the manifest of an evaluated user, from hjem’s library and theconfigof the user.
caisson.home-manager-minimal (module class homeManager, owned by caisson.home-manager)
- Source:
lib-overlays/home-manager-minimal/default.nix
A second integration over the homeManager class: home-manager’s
minimal evaluation, the module list that omits what a standalone
activation needs, which home-manager selects with its minimal flag.
It carries constructors only. The class, its registration form
(caisson.home-manager.mkModule), its framework module and its
default default belong to the home-manager integration, and the
configuration it builds comes from caisson.home-manager.configuration;
composing it needs the home-manager integration composed beside it.
mkConfigurationtakes the arguments ofcaisson.home-manager.mkConfigurationand returns the same result. The configuration is a home-manager configuration: it is declared undercaisson.home-manager.configurationsand published underhomeConfigurations.mkTopConfigurationtakes the same arguments and returns the evaluated home-manager configuration.mkConfigurationWithEcosystemArgs: the twin withecosystemArgs.
caisson.nixpkgs (module class nixpkgsConfig)
- Source:
lib-overlays/nixpkgs/default.nix
The nixpkgs integration builds package sets from package configs. A
package config is a module evaluation of the class nixpkgs evaluates
pkgs/top-level/config.nix under: nixpkgs’ options (allowUnfree
and the rest) sit at the top level, as upstream declares them, and
caisson’s sit under caisson.nixpkgs. Its module lives at
configs/nixpkgsConfig/<name>/default.nix, and further modules of
the class are registered under modules/nixpkgsConfig/<name>.
mkConfiguration
lib.caisson.nixpkgs.mkConfiguration :
{ configModule ? null : module # the config's module
, moduleImports ? null : registry -> listOf module
, ecosystemSrc ? null : sources -> path # the nixpkgs source tree for this config
} -> configuration
Declares a package config, in mkLib’s pkgSets. configModule
defaults to the configuration registered under the name the config is
declared under (lib.caisson.configs.nixpkgsConfig.<name>), and
a config with neither evaluates with no module of the tree.
moduleImports selects from the nixpkgsConfig module registry and
defaults to every entry named default. ecosystemSrc is a function
of sources returning the nixpkgs tree for this config, replacing
the nixpkgs key for this evaluation and nothing else; a value given
in place of the function is refused. A config on another nixpkgs
revision writes
ecosystemSrc = sources: sources.nixpkgs-stable;. When
ecosystemSrc is absent, the tree resolves from the composition’s
declarations (see
Ecosystem sources).
It returns a configuration: a function { name, parent }: manifest
that mkLib calls. The manifest has type = "nixpkgs", the config’s
name, its systems, the evaluation as value, the config handed
to nixpkgs as config, and one package set per system under
children.nixpkgs.<system>, whose value is the set.
These options under caisson.nixpkgs declare the sets:
caisson.nixpkgs.systems: the systems to build a set for. It defaults to thesystemsdeclared onmkLib; a config with no systems in force is an error saying to declare them.caisson.nixpkgs.overlays: the package overlay registry entries the sets apply, each after the entries it imports and each key once. It defaults to every entry nameddefaultor<project>/default, and a definition replaces that default.caisson.nixpkgs.extraOverlays: entries applied in addition tooverlays, whichever selection that is. A config that adds an entry with it keeps the default:caisson.nixpkgs.extraOverlays = [ lib.caisson.pkgOverlays.<name> ];.
caisson instantiates the sets itself, without going through
pkgs/top-level/default.nix: it boots the stdenv stages and
stage.nix on the composed library with pkgsManifest filled in, so
pkgs.lib is the library the config is declared under and
pkgs.lib.caisson-core.pkgsManifest is the set’s manifest. The
derivations are those import nixpkgs { system; config; overlays; }
produces. mkConfigurationWithEcosystemArgs takes ecosystemArgs as
well, merged into every instantiation (crossSystem,
crossOverlays, stdenvStages).
mkConfigurations
lib.caisson.nixpkgs.mkConfigurations :
{ moduleImports ? null; ecosystemSrc ? null; } -> attrsOf configuration
Declares a package config for every configuration registered in
configs/nixpkgsConfig, by its name, each as mkConfiguration
builds it without configModule. The arguments apply to all of them.
pkgSets = lib: lib.caisson.nixpkgs.mkConfigurations { }; is the
usual declaration; a tree that wants only some configs, or a config that
differs, declares them with mkConfiguration. mkIntegration
generates this function for every integration whose
mkConfiguration finds its module by name.
mkTopPkgSet
lib.caisson.nixpkgs.mkTopPkgSet : manifest -> pkgs
Turns a package config into what the legacy readers take, for a
repository that is a package set. Its default.nix ends with
lib.caisson.nixpkgs.mkTopPkgSet lib.caisson.nixpkgs.pkgSets.default.
The result is the set for the current system, with a __functor
that takes the legacy arguments and returns the set for the system
asked for. Nix auto-calls a lambda and not a functor, so
nix-build -A hello indexes the set, and
import ./. { system = "aarch64-linux"; } calls it.
systemdefaults tobuiltins.currentSystem. Pure evaluation has none: the result is then the functor alone, and a call has to passsystem. A system the config builds no set for is refused, with the systems it does build.overlaysare applied over the config’s selection, through the set’sappendOverlays.configmust be empty. The nixpkgs config is the package config, declared inconfigs/nixpkgsConfig/<name>.
The set carries its manifest in pkgs.lib.caisson-core.pkgsManifest,
whose parent is the config, so nothing else is added to it.
traced, traceChainOf
<package config manifest>.traced.children.nixpkgs.<system>.value : pkgs
lib.caisson.nixpkgs.traceChainOf : drv -> nullOr (listOf step)
step = { step : "callPackage" | "override" | "overrideAttrs" | "overrideDerivation";
drvPath : str; from : nullOr str;
touched : nullOr (listOf str); position : nullOr { file; line; };
# on the callPackage step:
file : nullOr str; resolved : listOf str; set : { config : str; system : str; }; }
How each package of a package set was made. nixpkgs keeps no link
from the result of an override to what it overrode, so the trace is
recorded while a set is built. The manifest of a package config holds
traced: the same sets, built on the same lib with callPackageWith
replaced by a version that records each call. Only an inspection reads
it, so an ordinary evaluation builds no traced set.
In a traced set, every derivation a callPackage returns carries
caissonTrace, and so does every derivation its override,
overrideAttrs and overrideDerivation return. Each record names the
step, the derivation it was applied to, the names the step was given,
and the position of the argument set the caller wrote. The
callPackage at the bottom also names the file of the package
function, the arguments it took from the scope, and the set. The trace
is attributes beside the derivation, so a traced package has the
drvPath of the same package untraced.
traceChainOf reads a trace as plain data, from the last step down to
the callPackage, with each derivation given by its drvPath. It
gives null for a value that carries no trace. For a hello made by
(hello.override { }).overrideAttrs (_: { pname = "hello-traced"; })
in the traced set of the default config, the steps are
overrideAttrs, override and callPackage, and the last names
pkgs/by-name/he/hello/package.nix and { config = "default"; system = "x86_64-linux"; }.
pkgSets
lib.caisson.nixpkgs.pkgSets : attrsOf manifest # the package configs, by name
pkgSets is a view of the manifest. It holds the package configs
mkLib recorded, by name.
Overlay constructors and types
mkScope : pkgs -> (callPackage -> attrs) -> scope: amakeScopewrapper handing the scope function itscallPackage.mkPackagesOverlay : pkgsFn -> name -> overlayFn: turns a scope function (or path; optionally context-taking{ callPackage, inputs, lib }) into an overlay that merges the scope under attributename.mkPolyfillOverlay : overlayFn -> name -> overlayFn: wraps an overlay (or path; optionally context-taking) for registration alongside package overlays; the name is ignored.types.nixpkgsOverlay,types.nixpkgs: option types.
caisson.caisson-colmena (module class caisson-colmena)
- Source:
lib-overlays/caisson-colmena/default.nix
caisson-colmena is the native colmena integration, and the one we
recommend for writing new configurations. Its machines are NixOS
configurations declared like any other, so they share modules through
the registry and through structural groups, and they take their
package sets from caisson’s package configs. The integration named
colmena, below, exists for porting a hive written for colmena as it
is.
A colmena configuration is a module of the caisson-colmena class, a
class caisson defines itself, since colmena’s hive format is not a
module class; the class carries the caisson- prefix because the bare
name is not caisson’s to claim. The integration is not compatible with
colmena’s hive modules (makeHive, defaults, meta.nixpkgs). The
word hive names only the output colmena’s CLI reads.
A colmena configuration declares meta (name, description,
machinesFile, allowApplyAll), and the machines colmena deploys as
NixOS configurations, under caisson.nixos.configurations.<name>, the
way any configuration declares them:
caisson.caisson-colmena.configurations.default = lib.caisson.caisson-colmena.mkConfiguration {
configModule = { lib, ... }: {
meta.allowApplyAll = false;
caisson.nixos.configurations.nas-1 = lib.caisson.nixos.mkConfiguration {
configModule = { deployment.targetHost = "nas-1.lan"; /* ... */ };
};
};
};
The nodes are the NixOS configurations the colmena configuration passes
up, each under the name it is published under. The default module of
the class adds caisson’s NixOS module colmena-deployment to the
default selection of the NixOS configurations declared inside it. That
module declares colmena’s deployment options and imports colmena’s
modules for assertions and keys, from the colmena source the
composition declares or pins under the name colmena. A NixOS
configuration that passes moduleImports lists the module itself, as
caisson/colmena-deployment in a flake that lists caisson in
projects.
The integration reads deployment back from each node and builds the
hive: __schema, nodes, toplevel, deploymentConfig,
evalSelected and the other attributes colmena’s CLI reads. A
deployment.targetHost the node leaves at null is the node’s name in
the hive. The hive refuses a node that declares no deployment
options, and a node whose name colmena’s --on filter could never
select: a name containing a comma, starting with @, or empty. A node
is one evaluation, so the hive also refuses a NixOS configuration
evaluated per system for more than one system, such as one inside a colmena
configuration that sets caisson.forChildren.systems to two systems.
The message names the configuration and its systems. Names
colmena’s hive format reserves (meta, defaults, network) are
ordinary names. The schema version is asserted against the makeHive
of the colmena source, so a colmena revision that moves it fails at
evaluation.
A colmena configuration is published as colmenaHive, an output that
holds a single value, so the colmena configuration declared under the
name default fills it, and another name is refused there. Its nodes
are published as nixosConfigurations.<name> as well, from the same
evaluation, so nixos-rebuild and colmena apply build the same
machine.
mkModule : freeformModule -> module: class-boundmkModulefor colmena modules.mkConfiguration : { configModule?, pkgSets?, defaultPkgs?, ecosystemSrc?, moduleImports?, extraModuleImports?, specialArgs? }: a colmena configuration, a function of{ name, parent }, finalized by the configuration that declares it undercaisson.caisson-colmena.configurations.<name>.configModuledefaults to the configuration registered under its name inconfigs/caisson-colmena.ecosystemSrcis the colmena flake, resolved from the composition when absent.pkgSets.pkgsservescolmena eval(introspect) and nothing else.mkConfigurationWithEcosystemArgs: the twin withecosystemArgs, merged over theevalModulescall (class,modules,specialArgs).metaOptions,hiveOf,exportsTo,ecosystemSrcOf: themetaoptions, the hive built from an evaluated colmena configuration, how a colmena configuration is published, and the resolution of the colmena source. The upstream-compatiblecolmenaintegration builds on them.
caisson.colmena (module class colmena)
- Source:
lib-overlays/colmena/default.nix
colmena is the upstream-compatible colmena integration. It is there
so that a hive written for colmena can move to caisson as it is, and so
that modules written for colmena’s evaluator can be used with caisson:
a project with many of them can port without rewriting them. For a new
configuration, we recommend caisson-colmena, which works like the
other caisson integrations. It gives up compatibility with Nix code
written for colmena to do that: a hive file or a module written for
colmena’s evaluator does not work with it. It keeps compatibility with
colmena’s CLI, which deploys from colmenaHive either way.
It takes a hive in the format colmena reads: meta (or network, its
older name), defaults, and a module or list of modules per node. Each node
becomes a caisson NixOS configuration declared beneath the colmena
configuration, and is published as nixosConfigurations.<node>:
caisson.colmena.configurations.default = lib.caisson.colmena.mkConfiguration {
hive = {
meta.nixpkgs = pkgSets.default;
defaults = { name, ... }: { networking.hostName = name; };
nas-1 = ./hosts/nas-1.nix;
};
};
A node is built as colmena builds it: from colmena’s deployment,
assertion and key modules, the defaults of the hive and the modules
of the node, with the special arguments name, nodes,
meta.specialArgs and meta.nodeSpecialArgs.<node>. With name
given, deployment.targetHost defaults to the name of the node, as in
colmena. nodes holds every node evaluated again with type checking
off, as colmena provides it, made only when a module reads it.
A node runs on meta.nodeNixpkgs.<node>, else meta.nixpkgs, else the
package set caisson selects for it. Those values are package sets
already built, such as caisson’s package sets; a path to nixpkgs, a
nixpkgs function or an uninitialized flake input is refused. The set is
handed to NixOS whole, so its overlays and its config are those it was
built with. As a caisson NixOS configuration, a node also gets the
framework modules and the default selection of caisson’s NixOS
integration.
For a hive with nothing caisson adds to it, each node builds the same
system as colmena builds from the same hive. The colmena configuration
builds its hive from the NixOS configurations it passes up, as
caisson-colmena does, and is published as colmenaHive.
mkModule : freeformModule -> module: class-boundmkModulefor modules of the colmena class.mkConfiguration : { hive, pkgSets?, defaultPkgs?, ecosystemSrc?, moduleImports?, extraModuleImports?, specialArgs? }: a colmena configuration, a function of{ name, parent }, finalized by the configuration that declares it undercaisson.colmena.configurations.<name>.hiveis an attribute set, or a function returning one, as colmena accepts.defaultPkgsselects the package set of a node the hive names none for.pkgSets.pkgsservescolmena eval, elsemeta.nixpkgs.mkConfigurationWithEcosystemArgs: the twin withecosystemArgs, merged over theevalModulescall of the colmena configuration.
caisson.caisson-disko (module class caisson-disko)
- Source:
lib-overlays/caisson-disko/default.nix
caisson-disko is the native disko integration, and the one we
recommend for new configurations. A disko configuration is a disk
layout, and the NixOS configurations installed onto it are declared
inside it. Those NixOS configurations get the file systems, boot
settings and swap the layout produces. The scripts that format and
destroy the disks are outputs of the layout. The layout and each
machine installed onto it are separate configurations, and one layout
can hold several machines. The integration named disko, below, is
disko’s two forms, where a machine and its layout are one
configuration or the layout stands alone.
A disko configuration is a module of the caisson-disko class. It sets
disko.devices, disko’s grammar for the disks, typed with disko’s type
library from the disko source the composition declares or pins under
the name disko. It declares the NixOS configurations installed onto
the layout under caisson.nixos.configurations.<name>, the way any
configuration declares them, and it may declare none, as for a data
array:
A tree registers the layout as configs/caisson-disko/nas-1/default.nix:
{ ... }:
{ lib, ... }:
{
disko.devices.disk.main = {
device = "/dev/disk/by-id/nvme-Samsung_SSD_980_PRO_1TB_S5GXNX0T123456";
type = "disk";
content = { type = "gpt"; partitions = { /* ... */ }; };
};
# The NixOS configuration registered as `configs/nixos/nas-1`.
caisson.nixos.configurations.nas-1 = lib.caisson.nixos.mkConfiguration { };
}
and declares it where it belongs, here at the top of its flake.
mkConfiguration without configModule takes the configuration
registered under the name it is declared under:
caisson.caisson-disko.configurations.nas-1 = lib.caisson.caisson-disko.mkConfiguration { };
The layout is evaluated with an evalModules call in caisson over
disko.devices, the framework modules, the selection and the
configuration’s module. The class also declares assertions, a list of
{ assertion, message } as in NixOS. A module of the class refuses a
layout with an assertion that is false, and every output of the layout
then fails with the messages of those assertions.
The outputs are the layout product:
serializableLayout: the device tree as data, with disko’s internal attributes and functions left out.layoutHash: the sha256 ofserializableLayoutas JSON, the identity of the disks the layout describes.nixosConfig: the NixOS settings the devices produce,fileSystems,bootandswapDevices, each a definition for a NixOS option.meta: disko’s record of which device depends on which.scripts: a function of a package set returning the scripts disko makes from the layout:format,mount,unmount,destroy,formatMount,destroyFormatMount, and each of them without its dependencies (formatNoDepsand so on).packages: a function of a package set returning the packages the scripts run.rootMountPoint: the directory the mount script mounts the layout under.
The default module of the class adds caisson’s NixOS module
caisson-disko-layout to the default selection of the NixOS
configurations declared inside the layout. That module defines
fileSystems, boot and swapDevices from the layout’s
nixosConfig, at default priority, and refuses a layout that produces
settings under any other key. A NixOS configuration that passes
moduleImports lists the module itself, as
caisson/caisson-disko-layout in a flake that lists caisson in
projects. A module of a NixOS configuration that needs more of its
layout reads it as lib.caisson-core.evalManifest.nearest.caisson-disko.
A layout passes up its NixOS configurations under the names they are
declared with. So the machine declared as nas-1 inside the layout
nas-1 is published as nixosConfigurations.nas-1, and is the node
nas-1 of a colmena configuration that declares the layout.
Where a layout holds one machine, we recommend naming the machine after the layout. The layout’s module declares it under the layout’s name, read from the manifest, and caisson’s default NixOS module names the host after the name the machine is declared under:
{ lib, ... }: {
caisson.nixos.configurations.${lib.caisson-core.evalManifest.name} =
lib.caisson.nixos.mkConfiguration { };
}
mkConfiguration without configModule takes the NixOS configuration
registered under the name the machine is declared under. So the layout
nas-1 holds the machine registered as configs/nixos/nas-1, and
publishes it as nixosConfigurations.nas-1, whose host name is nas-1
unless its modules set another.
A disko configuration is published as diskoConfigurations.<name>, in
the form disko’s CLI reads: { disko.devices = <serializableLayout>; }.
disko’s CLI evaluates it again with disko’s evaluator, so
disko --mode format --flake '.#nas-1' formats the disks the layout
describes.
mkModule : freeformModule -> module: class-boundmkModulefor modules of the caisson-disko class.mkConfiguration : { configModule?, pkgSets?, defaultPkgs?, ecosystemSrc?, moduleImports?, extraModuleImports?, specialArgs?, rootMountPoint?, checked? }: a disko configuration, a function of{ name, parent }, finalized by the configuration that declares it undercaisson.caisson-disko.configurations.<name>.configModuledefaults to the configuration registered under its name inconfigs/caisson-disko.ecosystemSrcis the disko flake or source tree, resolved from the composition when absent.rootMountPointdefaults to/mnt.checkedsays whether the scripts are checked with shellcheck when they are built, and defaults to false.mkConfigurationWithEcosystemArgs: the twin withecosystemArgs, merged over theevalModulescall.ecosystemSrcOf,diskoLibOf,devicesOptions,layoutOf,exportsTo: the resolution of the disko source, disko’s type library from a source, the module declaringdisko.devices, the layout product of an evaluated layout, and how a layout is published. The upstream-compatiblediskointegration builds on them.
caisson.disko (module class disko)
- Source:
lib-overlays/disko/default.nix
disko is the upstream-compatible disko integration. disko has two
forms, and caisson supports each as disko has it, so a layout or a
machine written for disko is used with caisson as it is.
- A standalone layout. A disko configuration is a module of the
diskoclass: what a file written for disko’sdiskoConfigurationssets,disko.devices, is its module as it is. It is published asdiskoConfigurations.<name>, which disko’s CLI formats and mounts withdisko --flake .#<name>. - A machine that carries its layout. A NixOS configuration that
selects caisson’s NixOS module
diskoand writes its layout asdisko.devicesis disko’s NixOS form, with the machine and its layout in one configuration.disko-install, the image builder and the install test read this form.
For a machine installed onto a layout that is a separate
configuration, we recommend caisson-disko, above.
A tree registers a layout as configs/disko/data-array/default.nix:
{ ... }:
{ ... }:
{
# As written for disko.
disko.devices.disk.main = { /* ... */ };
}
and declares it with
caisson.disko.configurations.data-array = lib.caisson.disko.mkConfiguration { };.
The layout is evaluated as disko evaluates a standalone layout:
evalModules over disko.devices, typed with disko’s type library,
with the framework modules, the selection and the configuration’s
module beside it. The outputs are the layout product of
caisson-disko, without assertions.
The NixOS module disko imports disko’s NixOS module, from the disko
source the composition declares or pins under the name disko. A
NixOS configuration selects it with
extraModuleImports = modules: [ modules.disko ];, or
modules."caisson/disko" in a flake that lists caisson in projects,
and sets disko.devices and the other options of disko’s module, such
as disko.imageBuilder, in its modules. For a layout and a machine
module with nothing caisson adds to them, it builds the same system,
with the same format script, as NixOS builds from disko’s module and
the same modules.
mkModule : freeformModule -> module: class-boundmkModulefor modules of the disko class.mkConfiguration : { configModule?, pkgSets?, defaultPkgs?, ecosystemSrc?, moduleImports?, extraModuleImports?, specialArgs?, rootMountPoint?, checked? }: a disko configuration, a function of{ name, parent }, finalized by the configuration that declares it undercaisson.disko.configurations.<name>.configModuledefaults to the configuration registered under its name inconfigs/disko. The other arguments are those ofcaisson-disko.mkConfigurationWithEcosystemArgs: the twin withecosystemArgs, merged over theevalModulescall.
caisson.caisson-facter (module class caisson-facter)
- Source:
lib-overlays/caisson-facter/default.nix
A caisson-facter configuration is the hardware of a machine: a module
of the class with facter.report, the hardware report nixos-facter
writes, as data, or facter.reportPath, the facter.json it writes,
which facter.report defaults to. It declares the NixOS configurations
that run on the hardware under caisson.nixos.configurations. A
project can extend the class with further options about the
hardware.
caisson evaluates the configuration with evalModules, on the
composed library, over the report options, assertions, the framework
modules, the selection and the configuration’s module. Its outputs are:
| Output | Value |
|---|---|
report | the report, refused unless its version is one of supportedReportVersions (today [ 1 ]) or the report is empty |
reportFile | the report as a JSON file |
serializableConfig | the configuration as data, without caisson’s options and without functions or attributes whose names begin with _ |
hash | the hash of serializableConfig |
Every output fails with the messages of the assertions that do not
hold. The NixOS configurations declared inside it get caisson’s NixOS
module caisson-facter-hardware by default. That module sets
hardware.facter.report to the report, at mkDefault priority, and
nixpkgs’ hardware.facter module turns the report into the settings
of the hardware. A module of such a NixOS configuration that needs more
of the hardware reads it through
lib.caisson-core.evalManifest.nearest.caisson-facter.
A tree registers the hardware as
configs/caisson-facter/box-1/default.nix, with facter.reportPath = ./facter.json; and the machine
caisson.nixos.configurations.${lib.caisson-core.evalManifest.name} = lib.caisson.nixos.mkConfiguration { };. It is not published under an
output attribute set; its outputs are on its manifest.
mkModule : freeformModule -> module.mkConfiguration : { configModule?, defaultPkgs?, moduleImports?, extraModuleImports?, specialArgs? } -> configuration.configModuledefaults to the configuration registered under its name inconfigs/caisson-facter.mkConfigurationWithEcosystemArgs: the twin withecosystemArgs, merged over theevalModulescall.supportedReportVersions: the report versions the integration accepts.
caisson.facter (module class facter)
- Source:
lib-overlays/facter/default.nix
A facter configuration is a facter.json as nixos-facter writes it,
named by reportPath, the one option of the class, with the NixOS
configurations that run on that hardware declared inside it. The
NixOS configurations get caisson’s NixOS module facter-hardware by
default, which sets hardware.facter.reportPath to that file at
mkDefault priority, the line a NixOS configuration written for
nixos-facter carries. The outputs are reportPath and report, the
content of the file. For a report checked against the versions caisson
knows or extended with options of a project, see
caisson.caisson-facter.
mkModule : freeformModule -> module.mkConfiguration : { configModule?, defaultPkgs?, moduleImports?, extraModuleImports?, specialArgs? } -> configuration.configModuledefaults to the configuration registered under its name inconfigs/facter.mkConfigurationWithEcosystemArgs: the twin withecosystemArgs, merged over theevalModulescall.
caisson’s tests declare a machine inside each kind of facter
configuration, from the report of nixpkgs’ facter test. Each
machine gets the report and the kernel modules of its initrd that a
NixOS configuration setting hardware.facter.reportPath itself gets.
caisson.terranix (module class terranix)
- Source:
lib-overlays/terranix/default.nix
A terranix configuration is a Terraform configuration written as
terranix modules, evaluated with terranix’s terranixConfiguration for
every system in force where it is declared, on the package set
selected for it (defaultPkgs, else the set named default). A tree
registers it as configs/terranix/infra/default.nix and declares it
with
caisson.terranix.configurations.infra = lib.caisson.terranix.mkConfiguration { };,
which finds the configuration by its name.
Its value and its output json are the config.tf.json file that
Terraform reads, with the evaluated configuration under
passthru.config. A top publishes it as terranixConfigurations.infra,
the name terranix’s flake-parts module gives its configurations, and a
terranix configuration that is a top is that file.
terranix evaluates its modules itself, on the
library of the package set, and caisson cannot hand it the composed
library as the library of the evaluation. The modules get the composed
library as the lib argument, and the package sets of the composition
as pkgSets; terranix merges and checks the options with that
library. terranix returns only the configuration Terraform reads and
its _meta, so caisson reads the caisson options back through
_meta.caisson, and a terranix configuration holds no configurations
beneath it.
mkModule : freeformModule -> module.mkConfiguration : { configModule?, ecosystemSrc?, defaultPkgs?, moduleImports?, extraModuleImports?, specialArgs? } -> configuration.configModuledefaults to the configuration registered under its name inconfigs/terranix.specialArgsbecomes terranix’sextraArgs.mkTopConfiguration: the same arguments; finalizes the configuration at a top and returns itsconfig.tf.json, or the files by system where several systems are in force.mkConfigurationWithEcosystemArgs: the twin withecosystemArgs, merged over the call ofterranixConfiguration(pkgs,modules,extraArgs,strip_nulls).
caisson.treefmt (module class treefmt)
- Source:
lib-overlays/treefmt/default.nix
A treefmt configuration is the formatters of a tree, written as
treefmt-nix modules (projectRootFile, programs.<name>.enable,
settings). It is evaluated per system, once for each system in force
where it is declared, on the package set selected for it (defaultPkgs, else the
set named default).
treefmt-nix evaluates its modules on the library of a nixpkgs. caisson
evaluates them with evalModules, on the composed library, over
submodule-modules, the modules treefmt-nix publishes for a
configuration embedded in another, with pkgs set to the selected
package set. The special arguments those modules need are read from the
type treefmt-nix’s submoduleWith returns.
| Output | Value |
|---|---|
wrapper | treefmt with its configuration, the program nix fmt runs |
configFile | the treefmt.toml |
programs | the formatters by name |
check | a function from a source tree to a derivation that fails when the tree is not formatted |
A tree registers the configuration as configs/treefmt/default/default.nix
and declares it with
caisson.treefmt.configurations.default = lib.caisson.treefmt.mkConfiguration { };.
A top publishes only the configuration named default:
| Output | Value |
|---|---|
formatter.<system> | its wrapper, which nix fmt runs |
checks.<system>.treefmt | its check over the root of the tree, root on mkLib, which nix flake check builds; it fails when the tree is not formatted |
The check is published where mkLib is given a root. caisson’s tests check that the wrapper is the derivation treefmt-nix’s
mkWrapper builds from the same module on the same package set, and
that the published check is the configuration’s check over the root.
mkModule : freeformModule -> module.mkConfiguration : { configModule?, ecosystemSrc?, defaultPkgs?, moduleImports?, extraModuleImports?, specialArgs? } -> configuration.ecosystemSrcis the treefmt-nix flake or source tree, resolved from the composition under the nametreefmt-nixwhen absent.mkTopConfiguration: the same arguments; finalizes the configuration at a top and returns its evaluation, or the evaluations by system where several systems are in force.mkConfigurationWithEcosystemArgs: the twin withecosystemArgs, merged over theevalModulescall.
caisson.nvf (module class nvf)
- Source:
lib-overlays/nvf/default.nix
An nvf configuration is a Neovim, written as nvf modules (vim.*). It
is evaluated per system, once for each system in force where it is
declared, on the package set selected for it (defaultPkgs, else the
set named default).
nvf evaluates its modules with neovimConfiguration, on the library of
nixpkgs extended with nvf’s lib.nvim. caisson evaluates the same
module list, modules/modules.nix, with the special arguments
neovimConfiguration passes, on the composed library extended with
lib.nvim from the same nvf source. nvf’s modules read the inputs of
the nvf flake, so ecosystemSrc is the nvf flake, resolved from the
composition under the name nvf when absent.
The output is neovim, the wrapped Neovim that neovimConfiguration
returns. A top publishes the configuration as
nvfConfigurations.<system>.<name>, in the shape neovimConfiguration
returns: neovim, config and options. caisson’s tests check that
the Neovim is the derivation neovimConfiguration builds from the same
module on the same package set.
An nvf configuration is kept in a separate Nix profile, and the
configuration it is declared inside activates it. See
profiles. Its outputs
activationPackage and profile are that profile.
mkModule : freeformModule -> module.mkConfiguration : { configModule?, ecosystemSrc?, defaultPkgs?, moduleImports?, extraModuleImports?, specialArgs? } -> configuration.configModuledefaults to the configuration registered under its name inconfigs/nvf.mkTopConfiguration: the same arguments; finalizes the configuration at a top and returns its evaluation, or the evaluations by system where several systems are in force.mkConfigurationWithEcosystemArgs: the twin withecosystemArgs, merged over theevalModulescall.
caisson.nixvim (module class nixvim)
- Source:
lib-overlays/nixvim/default.nix
A nixvim configuration is a Neovim, written as nixvim modules
(plugins.*, opts, keymaps). It is evaluated per system, once for
each system in force where it is declared, on the package set selected for it
(defaultPkgs, else the set named default).
nixvim evaluates its modules with evalNixvim, evalModules over
modules/top-level on a library extended with the overlay the nixvim
flake publishes as lib.overlay. caisson makes the same evaluation on
the composed library extended with that overlay, with nixpkgs.pkgs
set to the selected package set and the flake as flake. So
ecosystemSrc is the nixvim flake, resolved from the composition under
the name nixvim when absent.
The output is package, the Neovim (config.build.package). A top
publishes the evaluated configuration as
nixvimConfigurations.<system>.<name>, the name the nixvim flake
publishes configurations under. caisson’s tests check that the
package is the derivation evalNixvim builds from the same module on
the same package set.
A nixvim configuration is kept in a separate Nix profile, as an nvf configuration is, and the configuration it is declared inside activates it. See profiles.
mkModule : freeformModule -> module.mkConfiguration : { configModule?, ecosystemSrc?, defaultPkgs?, moduleImports?, extraModuleImports?, specialArgs? } -> configuration.configModuledefaults to the configuration registered under its name inconfigs/nixvim.mkTopConfiguration: the same arguments; finalizes the configuration at a top and returns its evaluation, or the evaluations by system where several systems are in force.mkConfigurationWithEcosystemArgs: the twin withecosystemArgs, merged over theevalModulescall.
caisson.topology (module class topology)
- Source:
lib-overlays/topology/default.nix
A topology configuration is the diagram of a network: the NixOS
configurations declared inside it under caisson.nixos.configurations,
whose topology options nix-topology reads, and modules of the class
that add nodes and networks. It is evaluated per system,
once for each system in force where it is declared, on the package set selected for
it, extended with the packages of nix-topology.
caisson evaluates nix-topology’s topology modules with
evalModules on the composed library, and sets nixosConfigurations
to the NixOS configurations declared inside it, evaluated for its
system. Those NixOS configurations get nix-topology’s NixOS module by
default, through caisson.forChildren. ecosystemSrc is the
nix-topology flake or source tree, resolved under the name
nix-topology.
The output is output, the rendered diagram. A top publishes the
configuration named default as topology.<system>, the evaluation,
where nix-topology’s flake module publishes it, so
nix build '.#topology.x86_64-linux.config.output' builds the diagram.
Its machines are published as nixosConfigurations like any NixOS
configuration. caisson’s tests check that two machines declared inside
give the nodes and the diagram nix-topology gives the same machines
evaluated with its NixOS module.
caisson.kubenix (module class kubenix)
- Source:
lib-overlays/kubenix/default.nix
A kubenix configuration is Kubernetes resources written as kubenix
modules, which import the kubenix modules they use from the kubenix
argument (imports = [ kubenix.modules.k8s ];). caisson makes the
evaluation kubenix’s evalModules makes, on the composed library
extended with kubenix’s upstreamables.nix, with the same kubenix
argument, for every system in force on the selected package set.
ecosystemSrc is the kubenix flake or source tree, resolved under the
name kubenix.
The output is result, the resources as the JSON file
kubectl apply -f reads, the kubernetes.result of kubenix’s k8s
module. A top publishes the configuration as
kubenixConfigurations.<system>.<name>, the evaluation kubenix’s
evalModules returns, whose config.kubernetes.result is the result.
caisson’s tests check that the result is the one kubenix’s
evalModules gives the same module.
caisson.nixidy (module class nixidy)
- Source:
lib-overlays/nixidy/default.nix
A nixidy configuration is an Argo CD environment written as nixidy
modules (nixidy.target, applications.*). caisson evaluates it with
nixidy’s mkEnv, giving it the composed library as the library of
the evaluation and, through mkEnv’s libOverlay, the namespaces of
the composed library that nixidy’s library lacks, such as caisson.
mkEnv returns no options, so a nixidy configuration holds no
configurations beneath it. ecosystemSrc is the nixidy flake,
resolved under the name nixidy.
The outputs are environmentPackage, the rendered manifests,
activationPackage and bootstrapPackage. A top publishes the
configuration as nixidyEnvs.<system>.<name>, which the nixidy CLI
reads. caisson’s tests check that the environment is the one mkEnv
renders from the same module.
Each of the three takes the arguments of caisson.treefmt:
mkModule : freeformModule -> module.mkConfiguration : { configModule?, ecosystemSrc?, defaultPkgs?, moduleImports?, extraModuleImports?, specialArgs? } -> configuration,configModuledefaulting to the configuration registered under its name inconfigs/<class>.mkTopConfiguration, with the same arguments.mkConfigurationWithEcosystemArgs, the twin withecosystemArgs, merged over the evaluator’s call (mkEnv’s arguments for nixidy).
caisson.system-manager (module class systemManager)
- Source:
lib-overlays/system-manager/default.nix
A system-manager configuration is the system of a machine running
another Linux distribution, evaluated with system-manager’s
makeSystemConfig for every system in force where it is declared. A
tree registers it as configs/systemManager/default/default.nix and
declares it with
caisson.system-manager.configurations.default = lib.caisson.system-manager.mkConfiguration { };.
Its value and its output toplevel are the system, with the evaluated
configuration under passthru.config. A top publishes it as
systemConfigs.default, which system-manager switch --flake . reads
when it is given no name. A system-manager configuration activates the
home-manager configurations and the hjem configurations declared
inside it, and the profiles
of the nvf and nixvim configurations declared inside it, with
modules.systemManager."caisson/default".
A system-manager configuration runs on the package set selected for it
(defaultPkgs, else the set named default), as every other
integration does, and on the source resolved under the nixos key,
as a NixOS configuration does (see
Ecosystem sources).
system-manager’s flake publishes makeSystemConfig from its
nix/lib.nix called with the nixpkgs the flake pins. caisson calls
the same file with the source resolved under the nixos key and the
composed library, as the NixOS integration calls
nixos/lib/eval-config.nix. The library and the evaluation come from
the composed library; the NixOS modules system-manager imports come
from the source resolved under the nixos key.
makeSystemConfig makes pkgs itself and declares nixpkgs.pkgs
read-only. caisson passes the selected set as the pkgs special
argument, which the module system takes before _module.args, so the
selected set is the pkgs of every module and nixpkgs.pkgs.
userborn, the user manager system-manager pins, is built from the same
source on the selected set. caisson defaults nixpkgs.hostPlatform to
the platform of the selected set.
The selected set makes nixpkgs.overlays, nixpkgs.config and the
overlays argument of makeSystemConfig ineffective, so a
configuration that sets one is refused, as one whose
nixpkgs.hostPlatform differs from the platform of the selected set
is. Package configs and package overlays are declared on mkLib. The
modules get the composed library as lib and the package sets of the
composition as pkgSets. caisson adds a compatibility bridge for the
current nixos-unstable restructuring of the NixOS nix module (each half
self-retires; see the source comments).
mkModule : freeformModule -> module.mkConfiguration : { configModule?, ecosystemSrc?, defaultPkgs?, moduleImports?, extraModuleImports?, specialArgs? } -> configuration.configModuledefaults to the configuration registered under its name inconfigs/systemManager.mkTopConfiguration: the same arguments; finalizes the configuration at a top and returns its system, or the systems by system where several systems are in force.mkConfigurationWithEcosystemArgs: the twin withecosystemArgs, merged over the call ofmakeSystemConfig(modules,specialArgs,allowUnsupportedNixpkgs;overlaysis refused).
Module Options Reference
This reference documents the caisson framework’s module options. All descriptions are sourced from the Nix-native description fields in the module code.
For lib.caisson functions, see Library Reference.
The options are declared by caisson’s core module, which is imported into an evaluation of an integrated class. In an evaluation whose library is not a caisson composition, such as a home-manager configuration that home-manager’s NixOS module embeds, the core module declares nothing.
Options
caisson.manifest
- Type:
caisson.flake-parts.types.manifest(read-only) - Default: the composed library’s
caisson-core.evalManifest, or itscaisson-core.libManifestin an evaluation that carries none - Source:
modules/generic/core/caisson/manifest.nix
The manifest of this evaluation: its type, name, parent and children, and the registries and declared facts of the composition it is declared under, which are sources, defaultEcosystemSrc, systems and projects as given to mkLib, plus the registered libOverlays and modules dictionaries (project entries under <project>/<name>, locals winning). Structural, flake-parts and NixOS configurations carry a manifest. Reading it type-checks the manifest; the flake.modules and flake.libOverlays projections are drawn from it, and flake-parts’ systems defaults to the manifest’s systems, the empty list when the composition declares none, so such a flake has no per-system outputs (modules/flake/core/caisson/systems.nix).
caisson.<integration>.configurations
- Type: lazy attribute set of configurations, read back as manifests
- Default:
{ } - Source:
lib.caisson.<integration>.configurationsModule, generated bymkIntegrationinlib-overlays/integrations/default.nix
The configurations of an integration declared beneath this configuration, by name. The option is declared by the module of the integration, lib.caisson.<integration>.configurationsModule. For the integrations of caisson, the core module of caisson declares it instead, through one module for all of them (lib.caisson.integrations.configurationsModuleFor). An entry takes what that integration’s mkConfiguration returns, a function of { name, parent }, and reads back as the finished manifest:
{ config, lib, ... }:
{
caisson.structural.configurations.inner = lib.caisson.structural.mkConfiguration { };
}
Each entry is finalized when it is read, with the attribute it is declared under as its name and the childless manifest of this evaluation as its parent. An entry of an integration that evaluates a configuration per system (nixos) reads back as its evaluations by system, config.caisson.nixos.configurations.laptop.x86_64-linux, a manifest for every system in force. The names are known without finalizing anything, so builtins.attrNames config.caisson.structural.configurations evaluates no configuration.
A configuration declared inside this configuration sees its childless evaluation, which leaves out the configurations declared inside it. In the childless evaluation an entry’s result is not readable, and reading it fails with a message saying so. A definition that reads an entry is therefore written under !lib.caisson.evalManifest.childless:
# an option of this configuration, defined from a configuration beneath it
innerGreeting =
if lib.caisson.evalManifest.childless then
null
else
config.caisson.structural.configurations.inner.value.config.greeting;
A module that only declares configurations needs no such test. What the configurations beneath export is passed up without that test, through caisson.<integration>.exported below.
An entry that is not a configuration, or is a configuration of another integration, is refused. Structural, flake-parts and nixos configurations can be declared. A configuration of an integration that evaluates a class another integration owns is a configuration of the owner in the tree: lib.caisson.nixos-minimal.mkConfiguration returns a nixos configuration, declared under caisson.nixos.configurations and published with the others. The constructors of the remaining integrations return an evaluated value. A configuration holds configurations of any integration beneath it.
caisson.<integration>.exported
- Type: function from the configurations declared to an attribute set of them
- Default:
configurations: configurations, every configuration declared - Source:
lib.caisson.<integration>.configurationsModule, generated bymkIntegrationinlib-overlays/integrations/default.nix
Function that selects which of the configurations declared under caisson.<integration>.configurations this configuration passes up. What each selected configuration exports (its outputs.exports: lib, libOverlays, modules, pkgOverlays) is merged into this configuration’s caisson.exports, beside what the registry selectors here choose. A configuration beneath passes up what lies beneath it in turn, so the exports of a tree of structural configurations reach its top with nothing written:
{ lib, ... }:
{
caisson.structural.configurations.impl = lib.caisson.structural.mkConfiguration { };
}
The configurations themselves are passed up as well, each with its path, and the top publishes them under the output attribute set their integration declares, named from the paths:
{ lib, ... }:
{
caisson.nixos.configurations.laptop = lib.caisson.nixos.mkConfiguration { };
}
publishes nixosConfigurations.laptop from a flake-parts or structural top. A name that is alone stays bare. Names that collide gain the segments that tell them apart, from the segment nearest the top: host-1 declared in the structural configurations a and b is published as a/host-1 and b/host-1, and a configuration evaluated for several systems as x86_64-linux/laptop and aarch64-linux/laptop. Configurations that still share a name are refused, with their paths. An integration may give its configurations a different name first: a home-manager configuration declared as chris inside the NixOS configuration laptop is published as homeConfigurations."chris@laptop".
caisson.structural.exported = _: { }; passes up none, and a selector returning a subset passes up those. The merge is made in the evaluation that holds the configurations: it is absent from the childless view, and from an evaluation that carries no manifest.
The top records what it published in the export record, exportRecord of the manifest it publishes as caisson.manifest, keyed as the outputs are. The entry for a name holds the path to the configuration by the names declared, the sentences that say how the name came about, and the positions of the code that made it, among them the file that defines this selector when it is not left at its default.
caisson.<integration>.exportNote
- Type: null or string
- Default:
null - Source:
lib.caisson.<integration>.configurationsModule, generated bymkIntegrationinlib-overlays/integrations/default.nix
A sentence saying what caisson.<integration>.exported does to the configurations it passes up, for a selector that does more than pass every configuration up unchanged. The top adds it to the notes of the export record entry of each name published from those configurations, after the notes of the levels beneath:
{ lib, ... }:
{
caisson.nixos.configurations = {
nas-1 = lib.caisson.nixos.mkConfiguration { };
lab-3 = lib.caisson.nixos.mkConfiguration { };
};
caisson.nixos.exported =
configurations:
lib.mapAttrs' (name: lib.nameValuePair "office-${name}") (
lib.filterAttrs (name: _: !lib.hasPrefix "lab-" name) configurations
);
caisson.nixos.exportNote = "office machines carry the site prefix; lab machines stay unpublished";
}
publishes nixosConfigurations.office-nas-1, and its entry in the export record holds the note.
caisson.home-manager.activation.usersDeclaredElsewhere
- Type:
listOf str - Default:
[ ] - Source:
modules/nixos/home-manager-activation/ - Declared in: a NixOS configuration that imports
caisson/home-manager-activation, whichcaisson/defaultfor thenixosclass does
The users that have an account on this machine although users.users does not declare it.
A NixOS configuration activates the home-manager configurations declared inside it. It expects the account of the user of each home-manager configuration in users.users, and it refuses a home-manager configuration whose user has no account there. Such a home-manager configuration is usually a mistake: a misspelled user name, or a home-manager configuration declared on the wrong machine.
An account that systemd-homed manages is not in users.users. systemd-homed keeps the record of the account, and the machine must not declare it a second time. List the user here:
{ lib, ... }:
{
services.homed.enable = true;
caisson.home-manager.activation.usersDeclaredElsewhere = [ "chris" ];
caisson.home-manager.configurations.chris = lib.caisson.home-manager.mkConfiguration {
configModule = {
home.homeDirectory = "/home/chris";
};
};
}
The home-manager configuration of a listed user is not refused. It is activated by a user unit that runs when the user logs in, which is when systemd-homed mounts the home directory. The home-manager configuration sets its home directory itself, because the machine has no account to read it from.
A user that is listed here and also declared in users.users is refused, because the two statements contradict each other.
See modules.nixos."caisson/home-manager-activation" for the units.
caisson.drift.warn
- Type:
bool - Default:
true - Source:
modules/homeManager/activation-drift/,modules/nvf/default/ - Declared in: a home-manager configuration that imports
caisson/activation-drift, which the home-managerdefaultmodule imports; an nvf or nixvim configuration that imports thedefaultmodule of its class
Whether the activation of this home-manager configuration, or activate of this nvf or nixvim configuration, prints a warning when it was built for a version of the configuration it is declared inside, such as its machine or its home-manager configuration, that differs from the version that is applied.
The configuration above records what it applied in a marker file, /etc/caisson/marker.json on a machine. The activation of the home-manager configuration compares the marker with the configuration above as it is in the source the home-manager configuration was built from.
The comparison leaves out the configurations declared inside the configuration above. A change to a home-manager configuration does not cause a warning. The warning does not stop the activation.
{ ... }:
{
caisson.drift.warn = false;
}
Here the home-manager configuration is activated with no comparison.
See drift detection for what the marker holds and what is compared.
caisson.profiles.selected
- Type:
attrsOf str - Default:
{ } - Source:
modules/*/profile-activation/ - Declared in: a NixOS, darwin or system-manager machine, a home-manager configuration or a hjem configuration that imports
caisson/profile-activation, which thedefaultmodule of each of these classes imports
By the variable that chooses among them, the profile a shell runs when it sets no other, as <kind>/<name>. nvf/editor is the nvf configuration declared as editor.
A configuration installs a program for each program of the profiles declared inside it, such as nvim, which runs the profile the variable names, and the default profile when the variable is unset. With one profile for a variable, that profile is the default. With several, this option names it, and the evaluation fails until it does.
{ ... }:
{
caisson.profiles.selected.CAISSON_NVIM_PROFILE = "nvf/editor";
}
Here a shell that sets no CAISSON_NVIM_PROFILE runs the Neovim of the nvf configuration editor.
See profiles.
caisson.forChildren.extraDefaultModuleImports
- Type:
attrsOf (functionTo (listOf module)), by class - Default:
{ } - Source:
modules/generic/core/caisson/forChildren.nix
Additions to the default selection of a class for the configurations beneath this configuration: a function of the lib of such a configuration returning registered modules. A module listed here is registered in the composition, on mkLib or by a project.
{ ... }:
{
caisson.forChildren.extraDefaultModuleImports.nixos = lib: [ lib.caisson.modules.nixos.gaming ];
}
A configuration that passes no moduleImports gets every entry named default followed by these, those of the levels above it first. A configuration that passes moduleImports gets what it selects in place of that default, and one that passes extraModuleImports gets what that selects in addition. Definitions from several modules are concatenated.
The name says that the option adds to what is in force, as extraModuleImports does. The options defaultPkgs and systems below replace what is in force.
A configuration with a configuration beneath it is evaluated in both views, since what it gives is read from the childless view.
caisson.forChildren.defaultPkgs
- Type:
nullOr (functionTo pkgs) - Default:
null - Source:
modules/generic/core/caisson/forChildren.nix
The package set the configurations beneath this configuration get by default: a function that receives the package sets available to such a configuration, as an attribute set by package config name, and returns the set to run on.
{ ... }:
{
caisson.forChildren.defaultPkgs = pkgSets: pkgSets.stable;
}
Here the default beneath this configuration is the set of the package config named stable. A configuration beneath runs on it unless that configuration, or a configuration between the two, is constructed with defaultPkgs or sets this option. This configuration runs on the set it was constructed with. When the option is null, the default beneath is the selection in force at this configuration.
caisson.forChildren.systems
- Type:
nullOr (listOf str) - Default:
null - Source:
modules/generic/core/caisson/forChildren.nix
The systems the configurations declared inside this configuration are evaluated for. It applies to configurations that are evaluated per system, such as a NixOS configuration or a home-manager configuration: each gets one evaluation per system listed.
{ ... }:
{
caisson.forChildren.systems = [ "aarch64-linux" ];
}
Set on a NixOS configuration for x86_64-linux, this evaluates the configurations declared inside it for aarch64-linux. A machine that builds an image for an aarch64-linux device is an example. Set on a structural or flake level, it limits the systems the configurations inside that level are evaluated for. The systems have to be among those allowed where this configuration is declared, and a system that is not among them is refused.
When the option is null, a configuration that is itself evaluated per system has its children evaluated for its system only, so a home-manager configuration inside a NixOS configuration for x86_64-linux is evaluated for x86_64-linux. Any other configuration passes on the list it was given.
caisson.modules
- Source:
modules/generic/core/caisson/modules.nix
Export settings for each module class of the composition. caisson declares these options for each class:
export.enabled(bool, defaulttrue)exported(function -> attrsOf deferredModule, default: the modules of the class that the composition registers itself)
The classes of the composition are the classes that the integrations declare and the classes that have a registered module. A setting for another class is refused as an option that does not exist, and so is a setting other than export.enabled and exported. A misspelled class name or setting name is therefore an error.
The selected modules are the exported modules. They are in the manifest, and
mkLib reads them from there for each entry of projects.
The exported modules are also published under flake.modules.<class>, and
caisson.flakeOutputs.modules selects which of
them. For the "flake" class, the published modules are also mirrored to
flake.flakeModules.
caisson.modules.<class>.export.enabled
- Type:
bool - Default:
true - Source:
modules/generic/core/caisson/modules.nix
Whether to export modules for a given class.
caisson.modules.<class>.exported
- Type: a function from the registered modules to the modules to export
- Default: the modules of the class that the composition registers itself
- Source:
modules/generic/core/caisson/modules.nix
Function that selects which registered modules of a class to export. Receives the modules registered for the class and returns the modules to export, which are exported as the function returns them. A module that came from an entry of projects, registered as <project>/<name>, is exported only when the function names it.
The two settings of every class are held in one option, caisson.modules, an attribute set by class of lazyAttrsOf raw values, so a class that no module sets costs nothing to evaluate. Each setting is a plain value: a priority such as mkDefault on exported takes effect, and two definitions of exported for one class conflict rather than merge. A class with no registered module, or a key other than export and exported, is refused when the exports are read.
caisson.flakeOutputs.modules
- Type:
function -> attrsOf (attrsOf deferredModule) - Default:
modules: modules - Source:
modules/flake/core/caisson/exports.nix
Function that selects which exported modules are also published in the modules output. It receives the exported modules, as an attribute set of classes that each hold modules by name, and returns the modules to publish in the same shape. The option exists in a flake-parts configuration.
The exported modules are the modules that the caisson.modules.<class>.exported selectors chose. They are in the manifest, and mkLib reads them from there for each entry of projects. mkLib reads every exported module, whatever this function returns.
The modules output serves an import by hand, as inputs.<input>.modules.<class>.<name>. Leave a module out of the output when it only works inside an evaluation that caisson makes, because it reads the options under caisson or the library under lib.caisson.
caisson.flakeOutputs.modules =
modules: modules // { nixos = removeAttrs modules.nixos [ "needs-caisson" ]; };
caisson sets the option to _: { }. Each module that caisson exports works only inside an evaluation that caisson makes. The modules output of caisson holds the empty default entry of the flake class and nothing else.
caisson.libOverlays.export.enabled
- Type:
bool - Default:
true - Source:
modules/generic/core/caisson/libOverlays.nix
Whether to enable lib overlay export. When enabled, the lib overlays selected by caisson.libOverlays.exported are exported. They are in the manifest. mkLib reads them from there for each entry of projects, and registers each as <project>/<name>. A lib overlay is an entry in the shape caisson registers, so there is no libOverlays output.
caisson.libOverlays.exported
- Type:
function -> libOverlays, an attribute set of lib overlays merged as one attribute set, each entry checked when it is read - Default: the lib overlays that the composition registers itself
- Source:
modules/generic/core/caisson/libOverlays.nix
Function that selects which registered library overlays to export. Receives the set of overlays registered via mkLib and returns the overlays to export. A lib overlay that came from an entry of projects, registered as <project>/<name>, is exported only when the function names it.
Repository Layout
caisson mandates nothing about layout: registration takes paths, and
any arrangement evaluates. We recommend the conventions below because
they have proven to work for us, they resolve ambiguity about where a
thing belongs, and they make it easier for someone new to a repository
to come up to speed. A repository that keeps them registers by naming
the directories, as in modules = lib: lib.caisson.mkModules ./modules;.
A repository with another layout writes its registrations by hand. This repository and its
integrations use them, and the documentation and the
examples/literate-flake example assume them.
flake.nix
Wiring only: mkLib and lib.caisson.flake-parts.mkTopConfiguration. The substance lives in the
directories below.
configs/
configs/<class>/<config>/
Configurations, grouped by module class and named for what they
configure. A flake-parts config configures the flake itself, so there
is typically exactly one, named after the flake (this repository uses
configs/flake/caisson/) or default. In other classes,
a config is named for the thing it describes: a machine, a
home-manager configuration, a deployment.
lib-overlays/
lib-overlays/<overlay>/
Library overlays by overlay name; most flakes start with a single
overlay called default. Each is a file taking the closure arg list
and returning { imports ? [ ], overlay }; see
Library Overlays.
modules/
modules/<class>/<module>/default.nix
modules/<class>/<module>/<flake-name>/*.nix
Reusable modules, keyed first by module class (directory names are the
class names: flake, nixos, homeManager, and generic for a
module any class may import), then by the module’s name. The names
core and default carry meaning in every class: core is forced
into every evaluation of the class, and default is the default
default (see
Module classes); the conventional
exported module is modules/<class>/default/. default.nix is the
module’s entry point, and its implementation files sit under a
directory named for the defining flake, grouped by the option
namespace they declare, in this repository,
modules/generic/core/caisson/lib.nix declares the caisson.lib.*
options.
pkgs/
pkgs/<package>/ # or, in flakes with many packages:
pkgs/<flake-name>/<package>/
Package definitions. How package sets and package overlays are composed
and surfaced as outputs is the domain of caisson-nixpkgs (in active
use, not yet published); until its documentation is available, pkgs/
is best read as the conventional home for package expressions.
Package overlays
Package overlays follow the same safety ideas as library overlays
(namespacing, input closure) but their tooling belongs to
caisson-nixpkgs, not to caisson itself. See
Library Overlays for the
shared principles.
tests/
tests/unit/ # pure evaluation tests, wired into checks
tests/integration/ # nested flakes that consume this flake
tests/dependencies/ # a small flake whose lock pins test-only inputs
Unit tests are pure Nix expressions evaluated as a check. Integration
tests are nested flakes that take the project as an input and assert
that composition behaves as documented: consumption tested from the
outside, the way a consumer would experience it. tests/dependencies/
is a lock-bearing flake that pins inputs used only by the test and
formatter machinery, so the main flake.lock stays free of
test-only pins (it feeds the checks partition via
pins.flake-compat). The Testing page covers how the
nested flakes are evaluated.
Other directories
examples/ (worked examples; examples/literate-flake/ here) and
docs/ (these pages) appear where a repository has use for them.
Closed Inputs
Overview
Nix flake modules often need access to the defining flake’s inputs, but flake-parts does not provide a built-in mechanism for closing over them. This means every module and library overlay would need inputs threaded explicitly through its call site, a tedious and error-prone pattern.
caisson solves this with an explicit closure convention: everything registered through mkModule or mkLibOverlay takes a closure attrset as its first arg list. Plain modules and already-built overlays are registered directly instead.
The Problem
In a standard flake-parts setup, accessing inputs from a module requires either:
- Passing them via
specialArgs(fragile, global) - Using
config._module.args(implicit, hard to trace) - Threading them manually through every import
None of these compose well when modules are re-exported for downstream consumption.
How caisson Handles It
mkModule
A flake registers a module with the mkModule of the integration that
owns its class (lib.caisson.nixos.mkModule,
lib.caisson.flake-parts.mkModule, and so on). Each is
lib.caisson.mkModule, a function of the class, applied to that
integration’s class:
mkModule = class: freeformModule: ...
caisson.flake-parts.mkModule = mkModule "flake"
Everything passed to the class-specific normalizer takes the closure attrset as its first arg list, followed by an ordinary module:
# Uses closure values
{ closure-inputs, closure-lib, mkModule, ... }:
{ config, lib, ... }:
{ ... }
# Ignores the closure but still takes the arg list
{ ... }:
{ config, lib, ... }:
{ ... }
The closure attrset contains:
| Key | Value |
|---|---|
closure-inputs | The defining flake’s pinned sources, the sources it passed to mkLib: its inputs under their names, with no self (distinct from the flake-parts inputs module arg, which belongs to the consuming flake) |
closure-lib | The defining flake’s composed lib (distinct from the lib module arg); its registry, closure-lib.caisson.modules.<class>, is how a module imports a sibling by name |
mkModule | A normalizer bound to the same class, for nested composition |
Path modules are wrapped with _file for error locations and key = toString path, so a file passed through mkModule at two sites deduplicates exactly like importing the same path twice.
Passing a non-function (attrset, path to a plain module, null) is an error: plain modules are imported or registered directly rather than wrapped in mkModule.
mkLibOverlay
mkLibOverlay follows the same convention. A registered overlay takes { closure-inputs, closure-lib, mkLibOverlay, ... } as its first arg list and returns an { imports ? [ ], overlay } attrset: the final: prev: function under overlay, and the overlays it depends on under imports. closure-lib is the composed library of the composition that registered the overlay, bound lazily (read it inside the overlay function, never while the overlay is being registered); an integration reaches the registry of the composition that registered it through it:
{ closure-inputs, ... }:
{
imports = [ "some-flake/default" ];
overlay = final: prev: { ... };
}
The string names an entry of the registry. some-flake/default is the lib overlay default that the project some-flake exports, which is in the registry when projects lists some-flake.
Key Functions
| Function | Purpose |
|---|---|
mkModule | Creates class-specific module normalizers with closed inputs |
mkLibOverlay | Applies the closure to registered library overlays |
mkModules, mkLibOverlays | Derive the registrations from modules/<class>/<name> (also configs/) and lib-overlays/<name>, applying mkModule and mkLibOverlay |
importApply | Applies static arguments to a module through the import chain |
mkLib | Bootstraps a composed library with closed overlays |
caisson.flake-parts.mkTopConfiguration | Creates flake outputs with closed modules |
Further Reading
- How inputs are closed over: the mechanism behind this convention
- How
libis composed: the whole composition pass - Module Classes: class-keyed module registration and export
examples/literate-flake/: a working example demonstrating closed input wiring
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).
Library Overlays
The Problem People Actually Had
Nix overlays have a reputation problem. Package overlays in nixpkgs caused real pain (attribute collisions, silent shadowing, unpredictable evaluation order), and the community learned to be cautious. That caution has grown into a broader skepticism that prevented the community from really embracing library overlays, out of fear that a worse version of the same problems might occur.
The skepticism is understandable but misdirected. The problems were never inherent to overlays as a mechanism. They came from how overlays were used: global modifications to shared namespaces, no convention for scoping additions, no way to declare dependencies between overlays, and no isolation between unrelated consumers. Fix those problems and overlays become a safe, composable extension mechanism.
caisson fixes those problems for library overlays. The result is something the Nix ecosystem has been missing: composed, layered lib extensions that multiple flakes can contribute to without stepping on each other.
What Makes Overlays Safe
The following properties, applied together, address the collision and ordering risks that gave overlays a bad name:
Namespacing
Every overlay adds its functions under a dedicated attribute path rather than mixing into the top-level lib. A project called myProject puts its functions at lib.myProject.*:
overlay = final: prev: {
myProject = (prev.myProject or {}) // {
helper = x: x + 1;
};
};
This means two independent projects do not collide unless the project names do: lib.projectA.helper and lib.projectB.helper coexist without interference, as they would in any language with a module system. caisson’s convention helps here: a project declares its name as name on mkLib, and contributes to the composed library under that name, so if every project uses its canonical name, collisions are unlikely in practice. Choose a distinctive name for your flake; generic names like utils or helpers invite collisions, while project-specific names like caisson or acme-infra make them vanishingly rare. This is a convention, not an enforcement mechanism: if two upstream flakes happen to choose the same name, their lib contributions will merge into the same namespace.
prev-Based Merging
The (prev.myProject or {}) // { ... } pattern ensures that if multiple overlays contribute to the same namespace (e.g., a base overlay and an extension overlay within the same project), their contributions are merged rather than one silently replacing the other. This is the standard Nix overlay contract, and it matters: it means overlays compose additively.
Input Closure
caisson’s mkLibOverlay automatically closes over the flake’s inputs when registering an overlay. This binds each overlay to the specific set of inputs it was written against, rather than relying on the leaf flake to do the right thing. The result is that overlays from different upstream flakes don’t interfere with each other’s inputs, even when composed into the same final lib.
See Closed Inputs for the full mechanism.
Dependency Tracking
Library overlays sometimes need to call functions defined by other library overlays. Without explicit dependency management, this requires manually ensuring that overlays are applied in the right order, a fragile arrangement that breaks when overlays are reorganized or new overlays are added.
caisson solves this. A registered overlay is an { imports ? [ ], overlay } attrset, and imports is where its dependencies go. Entries are built overlays; the closure’s mkLibOverlay member exists exactly so a dependency can be built in place:
{ mkLibOverlay, ... }:
{
overlay = final: prev: {
myProject = (prev.myProject or {}) // {
fullName = person: "${prev.myProject.greet person} (${person})";
};
};
imports = [ (mkLibOverlay ./greet-overlay.nix) ];
}
When mkExtendedLib encounters this structure, it recursively applies the imports before applying the overlay itself. This guarantees that prev.myProject.greet exists by the time fullName is evaluated, regardless of the order overlays were listed in libOverlays.
The resolution is recursive: imported overlays can themselves declare imports, and the framework handles the full dependency graph.
Package Overlays and Library Overlays
The safety techniques described here (namespacing, input closure, dependency tracking) apply equally to package overlays and library overlays. The underlying mechanism is the same: both are functions of final: prev: that extend an attribute set.
caisson provides the tooling for safe library overlays. The same principles apply to package overlays, but package overlay tooling is the domain of caisson-nixpkgs (not yet published), which builds on caisson’s foundation.
Tradeoffs
The safety mechanisms described here aren’t free. Import chains are flattened depth-first with duplicates preserved (an overlay’s imports are applied before it, every time it appears) and folded through the caisson-core, so evaluation cost grows with the number of overlays and the depth of the dependency graph. A contract worth knowing: the base library is contributed as an opaque attribute set, so overriding one of its attributes changes what readers of the composed library see, without re-tying the base’s internal references.
For most flakes the overhead is negligible, but it’s worth being aware of, especially if you’re composing a large number of upstream library overlays. The eval-weight harness (see the guides) is the tool for holding it to a measured ceiling.
Practical Patterns
A Simple Library Overlay
The most common case: adding namespaced functions to lib.
# lib-overlays/default/default.nix
{ closure-inputs, ... }:
{
imports = [ ];
overlay = final: prev: {
myProject = (prev.myProject or {}) // {
greet = name: "Hello, ${name}!";
double = x: x * 2;
upstreamVersion = closure-inputs.some-flake.lib.version;
};
};
}
The closure arg list always comes first: closure-inputs here is the
defining flake’s pinned sources, so some-flake resolves against the
pins this overlay was written with, no matter which downstream flake
eventually composes it. An overlay that needs nothing from the closure
still takes the arg list, as { ... }:.
Register it in your flake’s mkLib call:
libOverlays = lib: {
default = lib.caisson.mkLibOverlay ./lib-overlays/default;
};
After composition, lib.myProject.greet "world" returns "Hello, world!".
An Overlay With Dependencies
When one overlay needs functions from another, declare the dependency:
# lib-overlays/extended/default.nix
{ mkLibOverlay, ... }:
{
overlay = final: prev: {
myProject = (prev.myProject or {}) // {
greetLoud = name: final.toUpper (prev.myProject.greet name);
};
};
imports = [ (mkLibOverlay ../default) ];
}
The imports list ensures default is applied first, so prev.myProject.greet is available. Register the extended overlay normally:
libOverlays = lib: {
default = lib.caisson.mkLibOverlay ./lib-overlays/default;
extended = lib.caisson.mkLibOverlay ./lib-overlays/extended;
};
Common Mistakes to Avoid
- Top-level additions. Don’t add attributes directly to
lib(e.g.,{ helper = ...; }). Always namespace under a project-specific attribute. - Forgetting
prevmerge. WritingmyProject = { helper = ...; }instead ofmyProject = (prev.myProject or {}) // { helper = ...; }will silently discard any functions added tomyProjectby earlier overlays. - Implicit ordering assumptions. If overlay B uses a function from overlay A, declare the dependency with
{ overlay = ...; imports = [...]; }rather than hoping the registration order is correct.
Further Reading
- How
libis composed: the whole composition pass - Closed Inputs: how inputs are closed over in overlays and modules
examples/literate-flake/: a working example with a custom library overlay
Ecosystem sources
Each integration takes its ecosystem as an argument, the ecosystem source, and calls the evaluator inside that source; it pins nothing itself. A caisson revision therefore works with any nixpkgs, home-manager, or colmena revision with a compatible evaluation contract, and two consumers of that same caisson revision can pin different revisions of each ecosystem.
What a source is
An ecosystem is a community library outside of caisson at the center
of an extensible Nix abstraction framework. Most ecosystems are built
on NixOS modules, but some use other abstractions
(a package set, a lib ecosystem). The ecosystem source is that
project’s source tree or
flake, in whatever shape its evaluator expects. Each integration
documents the shape it takes; in practice:
caisson.nixostakes the NixOS module collection under thenixoskey (it evaluatesnixos/lib/eval-config.nixfrom it);caisson.nixos-minimaltakes the same key and evaluatesnixos/lib’sevalModulesfrom it.caisson.nixpkgstakes a nixpkgs source tree under thenixpkgskey, for the package set and its configuration.caisson.darwintakes the nix-darwin flake or source tree (it evaluateseval-config.nixfrom it).caisson.home-managertakes a home-manager source tree.caisson.hjemtakes the hjem flake or source tree (it evaluates the user modulemodules/common/user.nixfrom it, with hjem’s library fromlib.nix).caisson.caisson-colmenaandcaisson.colmenatake the colmena flake: they read the hive schema from itslib.makeHiveand the deployment options from itsnixosModules.caisson.caisson-diskoandcaisson.diskotake the disko flake or source tree: they build disko’s type library from itslib/default.nix, andcaisson.diskogives its machines the NixOS module in itsmodule.nix.caisson.terranixandcaisson.system-managertake their project’s flake (they calllib.terranixConfigurationandlib.makeSystemConfigon it).caisson.system-manageralso takes thenixoskey, for the NixOS module collection it mixes into the evaluation.caisson.flake-partstakes a flake-parts source tree and calls itsflake.nixwith the composed library asnixpkgs-lib.
The three parts of a nixpkgs checkout
A nixpkgs checkout holds three separate parts, and each is resolved from a separate key, with no fallback between keys:
nixpkgs-lib: the library (lib/: the module systemlib.evalModules,lib.types,lib.mkOption, and the rest), which thenixpkgs-libentry composes into the lib.nixpkgs: the package set and its configuration (pkgs/top-level).nixos: the NixOS module collection (nixos/modules,nixos/lib/eval-config.nix), read by NixOS configurations and by system-manager for its NixOS modules. It is the module collection, not the Linux distribution; the distribution is one use of the collection.
A key a tree does not declare resolves to the source pinned under
exactly that name in sources. A tree that takes several parts from
one checkout names that checkout under each key. Every tree uses
nixpkgs-lib; a tree with package sets uses nixpkgs; a tree with
NixOS or system-manager configurations uses nixos. The parts may
come from different sources on purpose: the nixpkgs.lib mirror
supplies the library, and one revision of the module collection runs
on the library of another revision.
A tree with a single nixpkgs checkout, pinned under the input name
nixpkgs, names that checkout under the other two keys with
defaultEcosystemSrc:
caisson.lib.caisson.mkLib {
inherit sources root;
defaultEcosystemSrc = sources: {
nixpkgs-lib = sources.nixpkgs;
nixos = sources.nixpkgs;
};
};
The nixpkgs key needs no entry here: the source pinned under the
input name nixpkgs already resolves there, through the exact-name
fallback below.
The library of a project that other flakes list
We recommend that a project other flakes list in projects take its
library from the nixpkgs.lib mirror, a repository that holds only the
lib/ directory of nixpkgs:
inputs.nixpkgs-lib.url = "github:nix-community/nixpkgs.lib";
A flake that lists a project reads the outputs of that project, and reading them builds the lib of the project. Building that lib fetches the library source the project pins. The mirror is about 1.4 MB in the store, and a nixpkgs checkout is about 210 MB. A flake that lists several projects fetches the library source of each of them, once per distinct revision.
The mirror follows the master branch of nixpkgs and is updated about
once a week. Its revision is therefore rarely the revision of the
nixpkgs a package set comes from. A project that only publishes
modules and overlays runs only its registrations and its top
configuration on its library. The flakes that list it evaluate its
modules and overlays on their own lib, so the difference does not
reach them. A project that also builds packages or configurations runs
them on general utilities and a module system of the mirror revision.
nixpkgs’ library holds two kinds of library. The general utilities
(strings, lists, attrsets and the rest) and the module system
(modules, options, types) serve every evaluation, and a tree
supplies them from its nixpkgs-lib key. The package-set libraries
describe and build packages:
systemsandplatforms, the platform model;meta,licenses,sourceTypes,maintainersandteams, the package metadata;customisation, which holdscallPackageWithandmakeOverridable;derivations,fetchersandversions.
Inside a package set, the nixpkgs integration takes the package-set
libraries from the source of that package set, together with the
names nixpkgs publishes from them at the top level of its library,
such as lib.getExe and lib.callPackageWith. The packages of a set
are written against those libraries at the revision of the set. The
general utilities still come from the mirror, which follows the
unstable branch, so a package set of a stable release meets the
general utilities of a later revision wherever its packages call them. nixpkgs reads maintainers and teams
from its maintainers directory beside lib/, so a package set
needs them from the source of the package set in any case.
The module system still comes from nixpkgs-lib. A NixOS
configuration whose nixos key names a stable release runs that
module collection on the module system of the mirror.
Which integration uses which ecosystem
One ecosystem may be wrapped by several integrations, each with a different evaluator over the same source. The name in the second column is what the integration resolves its source by, in the places listed below.
| Integration | Ecosystem name | Source shape |
|---|---|---|
caisson.flake-parts | flake-parts | flake-parts source tree |
caisson.nixpkgs | nixpkgs | nixpkgs source tree (package set and configuration) |
caisson.nixos | nixos | nixpkgs source tree (the NixOS module collection) |
caisson.nixos-minimal | nixos | nixpkgs source tree (the nixos class under the minimal evaluator) |
caisson.darwin | nix-darwin | nix-darwin flake or source tree |
caisson.home-manager | home-manager | home-manager source tree |
caisson.hjem | hjem | hjem flake or source tree |
caisson.caisson-colmena | colmena | colmena flake |
caisson.colmena | colmena | colmena flake |
caisson.caisson-disko | disko | disko flake or source tree |
caisson.disko | disko | disko flake or source tree |
caisson.terranix | terranix | terranix flake |
caisson.system-manager | system-manager, and nixos for its NixOS modules | system-manager flake |
caisson.treefmt | treefmt-nix | treefmt-nix flake or source tree |
caisson.nvf | nvf | nvf flake |
caisson.nixvim | nixvim | nixvim flake |
caisson.topology | nix-topology | nix-topology flake or source tree |
caisson.kubenix | kubenix | kubenix flake or source tree |
caisson.nixidy | nixidy | nixidy flake |
caisson.structural | none | no source: the empty integration evaluates caisson’s core module alone |
The nixpkgs library that the integrations call is loaded by the
nixpkgs-lib integration of caisson, from the source resolved under
the nixpkgs-lib key.
How does caisson get access to ecosystem sources?
A source comes from one of these places, in priority order:
- Explicit argument.
ecosystemSrcat the call site always wins. It is a function of the tree’ssources, returning the source for that call; a value given in place of the function is refused.ecosystemSrc = sources: sources.nixpkgs;names an input callednixpkgsexplicitly. It replaces the source for that evaluation and nothing else: a level cannot set sources for the levels beneath it. A package config on a different nixpkgs revision writesecosystemSrc = sources: sources.nixpkgs-stable;. - Composition default.
defaultEcosystemSrconmkLibis a function ofsources, returning the source of each ecosystem by its exact key:defaultEcosystemSrc = sources: { nixpkgs-lib = sources.nixpkgs; nixos = sources.nixpkgs; };. An attribute set given in place of the function is refused. - Exact-name source. As a final fallback, the source named
exactly like the key (
nixpkgs,home-manager, and the rest) in thesourcespassed tomkLibis used. For a flake that declares that input anyway, this is the common case.
If none of these places can provide a needed ecosystem source, it triggers an evaluation error.
A NixOS configuration that selects another module collection with
ecosystemSrc still runs its evaluation on the library of the tree;
only the source under the nixos key changes.
Where each source came from
The manifest of every configuration records ecosystems: for each
part of the stack its evaluation used, which of the three places
supplied the source and which pinned source it is.
{
nixpkgs-lib = { from = "defaultEcosystemSrc"; source = "nixpkgs"; };
nixos = { from = "ecosystemSrc"; source = "nixpkgs"; };
}
from is ecosystemSrc, defaultEcosystemSrc or sources, in the
order above. source is the name of the source among the pinned
sources, where the manifest records its revision under
sources.<name>.pin. It is null when a selection builds the value in
another way, such as a directory inside a source. A package config
also lists, under nixpkgs.libraries, the names its sets take from
the source of the package set
(ecosystemAccountOf has the
details).
How lib is composed
A caisson flake’s lib is canonically built by a call to
caisson-core.mkLib. This page traces what happens between that
call and the finished attrset: what goes in, the order things apply,
the rules that decide conflicts, and how to drive the same machinery
without mkLib when you want to.
What goes in
mkLib is given a set of declarations, and nothing to compose over:
the seed is the empty attribute set, and nixpkgs’ library arrives as
an entry like everything else.
defaultEcosystemSrc: a function of the tree’ssources, returning the tree’s default source per ecosystem key (see Ecosystem sources). An attribute set given in place of the function is refused. A tree declaresnixpkgs-libunder this key for a mirror such asnix-community/nixpkgs.lib, for a flake that carries no nixpkgs, or for a nixpkgs checkout pinned under another input name. Thenixpkgs-libintegration of caisson, atlib-overlays/nixpkgs-libin the caisson source tree, is the entry that imports thelibdirectory of the source resolved under thenixpkgs-libkey, with the fixed point upstream computes. It loads the source declared by the flake whosemkLibcall composes it, including when that flake consumes caisson as a project. The caisson integrations import the entry. An overlay outside caisson that calls upstream’s functions imports it by key from the caisson source tree:(mkLibOverlay (closure-inputs.caisson.outPath + "/lib-overlays/nixpkgs-lib")) // { key = "nixpkgs-lib"; }. A flake that declares no source fails only where that entry is composed, with a message naming the declaration.libOverlays: the flake’s overlay registrations, built withlib.caisson-core.mkLibOverlayof the core lib the function receives, or registered directly when already built (another flake’s export, for example).modules: the flake’s class-keyed module registrations.projects: whole upstream contributions, each carrying exported overlays and modules that register under<project>/<name>.
The registry also holds the entries caisson-core publishes,
caisson-core (the machinery) and nixpkgs-lib, under those names.
Neither is selectable, and a registration under either name replaces
the entry for every overlay that imports it, so a polyfill over
upstream’s library or a replacement of the core is an ordinary
registration.
Registration and application are separate steps: libOverlayImports
selects which of the registered overlays apply to this flake’s
lib (default: all of them), and registration also feeds export, so
a flake can register overlays for downstream consumers that it does
not apply to itself.
The stages
mkLib builds the library in stages, each a new fixpoint over
the seed, because some of its arguments are functions of a library
and can only see what is built before them:
- The core lib is the
caisson-coreentries with the lib overlay registry recorded on its manifest.libOverlayImportsreceives it and picks entries from that registry. - The bootstrap lib is the core entries plus the selection, the
sequence below without the module registrations.
modulesandconfigsreceive it, so they can use every integration’s constructors, and its manifest lacks what they register. - The registered lib is the whole sequence below, with the module,
configuration and package overlay registries on its manifest.
pkgSetsreceives it, because a package config selects from those registries, and its manifest lackspkgSets. - The full lib adds the package configs to the manifest, and
mkLibreturns it.
Each stage records a manifest at
lib.caisson-core.libManifest, and the history in each manifest
begins with the history of the stage before it. Entries registered at
the earlier stages still close over the full lib, so a module reaches
its siblings through closure-lib.caisson.modules, which reads the
registry from the manifest of that lib.
The sequence
The selected overlays are applied in one pass, wrapped by entries caisson adds itself:
{ }
-> caisson-core namespace injection (the `caisson-core` entry)
-> selected overlays (flattened, imports first; the `nixpkgs-lib`
entry wherever an overlay imports it)
-> consumed projects' modules
-> the flake's module registrations
-> the manifest
The first added overlay injects the machinery and the empty module
registry under caisson-core. The last records the manifest,
the composition’s self-description, at
lib.caisson-core.libManifest (beside pkgsManifest and
evalManifest, the manifests of the other evaluation phases,
null until a package set or a module evaluation fills them in). Module registrations apply after every
selected overlay so that a local name always beats a same-named
contribution from an overlay or a consumed project.
Before application, each selected overlay is flattened: a built
overlay is an { imports, overlay } value, and the flattening walks
depth-first with imports before the overlay itself. This guarantees
that anything an overlay depends on has already applied when its
overlay function runs, regardless of registration order. Within
mkLib, imports guarantee order, not uniqueness: an overlay that two
registrations both import is applied once per appearance, which is
harmless for overlays that follow the merge conventions.
The fold
Application is the standard Nix overlay contract, folded over the
sequence above. Each overlay = final: prev: { ... } receives
prev, everything accumulated so far, and final, the finished
fixpoint. For an attribute defined by two overlays, the later
definition wins, and prev gives it the earlier definition to build on,
which is what the my-flake = (prev.my-flake or { }) // { ... }
merge convention relies on.
These consequences of the fixpoint are worth knowing:
- An overlay’s output attribute names must not depend on
final; a fixpoint whose shape depends on itself diverges. - The base library is contributed as an opaque value. Overriding one of its attributes changes what readers of the composed library see, and does not change what the base’s internals call; a function patched for everything downstream of the base has to be patched in the base source you pass in.
Composing without mkLib
mkLib is a convenience over caisson-core.compose, which works on
entries, overlay-shaped pieces with identity:
{
key = "example.base"; # stable identity: a string, or null
imports = [ ]; # entries this entry depends on
overlay = final: prev: { greet = name: "hello, ${name}"; };
}
Keys change the rules. A keyed entry applies once no matter how many
entries import it; the first occurrence of a key fixes its position
and the last occurrence supplies its value, so mentioning a key again
replaces that entry wholesale; replacement is the only override
mechanism. An entry with key = null
cannot be imported, applies after the whole keyed world in list
order, and can never be replaced by another entry, because
replacement addresses keys and it has none; keyless entries are a
consumer’s private patch layer. Import cycles terminate (a key
already on the walk’s path is skipped) and grant the cycle’s
members no ordering relative to each other.
Identity is what makes patching a dependency reliable. A polyfill
imports the entry it patches, which guarantees the target is present
and already applied when the polyfill reads prev:
polyfill = {
key = "example.backport";
imports = [ base ];
overlay = final: prev: {
concatLines = prev.concatLines or (lines: prev.concatStringsSep "\n" lines + "\n");
};
};
The prev.concatLines or ... shape adds the function only where the
base does not already provide it, so the same entry composes
correctly over old and new bases.
The exact contract (walk order, the position a replacement takes, metadata) is specified in caisson-core, where the code lives.
caisson exports its contributions in entry form through
lib.composition.entriesFor:
caisson.lib.composition.entriesFor {
# a directory importable as nixpkgs' lib, e.g. "${nixpkgs}/lib"
# or "${nixpkgs-lib}/lib" for the nixpkgs.lib mirror
ecosystemSrc = "${inputs.nixpkgs-lib}/lib";
}
# => { base, caisson-lib,
# flake-parts, tooling,
# nixpkgs, nixos, nixos-minimal, home-manager,
# colmena, terranix, system-manager }
base contributes the nixpkgs library the composition builds on;
caisson-lib imports it and contributes the caisson-core
machinery, the same injection mkLib performs; the rest are the
integrations and tooling, each importing caisson-lib.
Where the composed lib goes
lib.caisson.flake-parts.mkTopConfiguration passes the composed library to flake-parts as
specialArgs.lib, so modules receive it as their ordinary lib
argument.
The lib output of the flake is the composed library. By convention,
the lib overlays of a flake named my-flake define their values under
my-flake, and the lib overlays of other flakes do not define values
there (see Library overlays).
The values of my-flake are in the output at lib.my-flake.
The manifest of the flake is in the output too, at
lib.caisson-core.evalManifest. The manifest is published in lib
because the other flake outputs each have a fixed shape. The manifest
holds what the flake exports, as outputs.exports: the lib overlays,
package overlays and modules that the exported selectors of the
flake chose.
producer is a flake that exports a lib overlay. consumer is a
flake that lists producer in projects.
consumer reads what producer exports from the manifest of
producer, and ignores the flake outputs of producer. consumer
adds the lib overlay to the library that consumer composes.
The lib overlay was built with mkLibOverlay, in the flake whose
source holds the overlay. mkLibOverlay gave the overlay
closure-inputs and closure-lib: the pinned sources and the composed
library of the flake that called mkLibOverlay. The overlay keeps
both in the library of consumer.
By default, producer exports the lib overlays that producer
registers itself. For those, the flake that called mkLibOverlay is
producer.
producer can also list a flake upstream in projects, and name a
lib overlay of upstream in the exported selector of producer.
For that lib overlay, the flake that called mkLibOverlay is
upstream. In the library of consumer, the overlay keeps the pinned
sources and the composed library of upstream.
Inspecting the result
nix repl .
# :p lib.my-flake -- your namespaced additions
# :p lib.my-flake.helper 41 -- call a function
nix eval .#lib --apply builtins.attrNames
How inputs are closed over
Every file registered through caisson takes a closure attrset as its first argument list. This page traces the mechanism behind that convention: where the closure comes from, when it is applied, what it contains for each kind of registration, and what it means when a registered file is evaluated inside another flake’s composition.
The convention
A registered file is a function of two argument lists: the closure attrset first, then whatever the file ordinarily takes.
{ closure-inputs, ... }: # the closure arg list
{ config, lib, pkgs, ... }: # the ordinary module arg list
{
services.foo.package = closure-inputs.foo-flake.packages.x86_64-linux.default;
}
A file that needs nothing from the closure still takes the arg list
({ ... }:), so every registered file has the same shape and a
reader always knows what the first line is.
Where the closure comes from
The flake defines the closure by calling mkLib:
lib = caisson.lib.caisson.mkLib {
inherit (caisson.lib.caisson.pins.flake inputs) sources root;
libOverlays = lib: { ... };
modules = lib: { ... };
};
mkLib builds registration helpers closed over the sources it was
given, the flake’s pinned inputs as the flake pin reader returns them,
and hands them to the registration arguments inside the lib each
receives. libOverlays receives the core lib, which contains the
closed lib.caisson.mkLibOverlay, and modules receives the
bootstrap lib,
whose helpers (lib.caisson.flake-parts.mkModule,
lib.caisson.nixos.mkModule, and the lib.caisson.mkModule they
are bound from) are closed the same way. There is no ambient lookup
anywhere in this chain: the only sources a registration can see are
those the registering flake passed to mkLib. They do not include
self. A registration reads the identity of its flake from
closure-lib.caisson.libManifest.root and a sibling module from
closure-lib.caisson.modules.
When the closure is applied
At registration time, not at evaluation time. mkLibOverlay and
mkModule call the registered function with the closure attrset
immediately and keep the result, so what sits in the registry, and
what the flake exports, is an ordinary overlay or module with the
closure values already baked in.
This timing is what makes export work. A consumer who imports
my-flake.modules.nixos.my-service receives a plain NixOS module;
the closure was applied when my-flake registered the file, so the
module references my-flake’s inputs without the consumer declaring,
follows-pinning, or even knowing about them.
What the closure contains
The contents differ by registration kind, because the kinds are evaluated at different times.
A library overlay’s closure contains registration helpers, sources and the library of the defining composition:
closure-inputs: the pinned sources of the defining flake.closure-lib: the composed library of the defining flake, bound lazily. Inside theoverlayfunction,finalandprevare the composition being built, which may belong to a consumer;closure-libis the composition of the definer, so an integration reaches the registry of the composition that registered it (itscoremodule, for example) wherever it is composed. It is read inside the overlay function or a function the overlay defines, never while the overlay is being registered.mkLibOverlay: the same helper, for building nested overlays.mkModule: the module normalizer bound to the defining composition, so modules the overlay makes close over the inputs and library of the definer.
A module’s closure contains the definer’s finished world:
closure-inputs: the pinned sources of the defining flake.closure-lib: the composed library of the defining flake. This is not thelibmodule argument; see the next section. Its registry,closure-lib.caisson.modules.<class>, is how a module imports a sibling by name.mkModule: a normalizer bound to the same class, so nested module composition stays in that class.
Plain values bypass the mechanism: an already-built overlay (another flake’s export) and a plain module are registered directly, because their closures were applied by whoever built them.
Two worlds in one file
Inside a registered module, two sets of similar-looking values are in scope, and they answer different questions:
| Value | Whose world |
|---|---|
closure-inputs | The flake that registered the file |
closure-lib | The flake that registered the file |
inputs module arg | The flake being evaluated |
lib module arg | The flake being evaluated |
While a flake consumes its registrations the distinction is
invisible, because both worlds are the same flake. It starts to
matter the moment a module is exported: a consumer evaluates the
module inside the consuming composition, so the ordinary lib argument
is the consumer’s composed library, while closure-lib remains the
definer’s. A module that formats a string with a helper from the
namespace of the flake that registered it wants closure-lib; a module that inspects the
configuration it is being evaluated into wants the ordinary
arguments.
The same split governs version skew. An exported module built against
closure-inputs.nixpkgs uses the definer’s nixpkgs pin even when the
consumer runs a different pin. Each flake’s files run against the
pins that flake tested with, the pins are visible in each flake’s
lock, and nothing forces every repository to upgrade in lockstep. Where a consumer
does want to override a definer’s pin, flake-level follows on the
definer’s input still works, because closure-inputs holds the
definer’s inputs as its flake resolved them and follows rewrites
what that resolution contains.
importApply
Closure application composes with module imports through
lib.caisson.importApply, which applies static arguments to a
module without losing its file identity: a path is imported and
wrapped with its _file, wrapper modules produced by registration
are walked rather than replaced, and the arguments are applied to
the innermost function.
imports = [
(closure-lib.caisson.importApply ./listener.nix { port = 8080; })
];
Here listener.nix begins { port }: and receives the arguments as
its first arg list. Wrapping the module in a plain lambda instead
would work, but the module system would see an anonymous function:
error messages would no longer point at the file.
Seeing it in the example
examples/literate-flake/ wires all of this in a working flake: its
overlay reads closure-inputs, and its modules take the two argument
lists. Step 5 of Getting started shows the
consuming side, a second flake that imports the exports without
declaring the definer’s dependencies.
Testing
The conventions and tools for testing a caisson flake, as used by the
test suite of this repository (tests/ here is a worked example of all
of it).
Unit tests
tests/unit/ holds pure evaluation tests, wired into checks; this
repository runs them with nix-unit. Anything that can be asserted by
evaluating lib belongs here.
When nix-unit runs as a flake check, it runs inside a Nix build. The check is a derivation. The builder of that derivation runs nix-unit, and nix-unit evaluates the flake that holds the tests.
Nix runs every builder in a sandbox. Inside the sandbox, the builder
can see a path in the Nix store only when the derivation names that
path as a dependency, and the builder has no network access. So Nix
cannot find a flake input that the tests need, and it cannot download
the input either. The check fails with the message unable to download.
The option nix-unit.inputs prevents that. It is an attribute set of
input names and store paths. nix-unit reads each input it lists from
the store path given, and the builder can see those store paths. Every
flake input that the tests read has to be in the set.
A flake that the tests compose as a project has inputs too. Those
inputs also have to be in the set, under names such as
caisson/caisson-core. lib.caisson.realizeInputs builds the whole
set from the inputs of the test flake:
perSystem.nix-unit.inputs = lib.caisson.realizeInputs inputs;
With this, the test flake does not need follows to point the inputs
of its inputs back at inputs it already lists. The
reference entry says when to give
the function fewer inputs.
Integration tests: consumption from the outside
The strongest test of a flake framework is what a consumer experiences,
so integration tests are nested flakes under tests/integration/
that take the project as an input and assert that composition behaves
as documented. They are written as completely normal flakes:
{
inputs = {
# Standalone equivalent (without shared deps infrastructure):
# nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
# flake-parts.url = "github:hercules-ci/flake-parts";
# parent.url = "github:example/my-flake";
deps.url = "path:../../dependencies";
parent.url = "path:../../..";
nixpkgs.follows = "deps/nixpkgs";
flake-parts.follows = "deps/flake-parts";
};
outputs = inputs@{ parent, ... }: {
# consume `parent` the way any downstream flake would
};
}
tests/dependencies
A small lock-bearing flake whose only job is to pin the inputs that
tests (and formatters) need, so the main flake.lock stays free of
test-only pins. The checks partition takes its inputs as
(lib.caisson.pins.flake-compat ./tests/dependencies).sources,
the flake-compat pin reader, which resolves the lock without
evaluating the flake.
Evaluating the nested flakes
Hand-threading a nested flake’s inputs (the recursive self fixpoint,
the resolved input graph) is the genuinely hard part, and
callConsumerFlake owns it. In the parent’s checks:
let
consumerPool = {
inherit (inputs) flake-parts nixpkgs;
deps = inputs.self; # the tests/dependencies flake
parent = self; # the flake under test
};
minimalConsumer = lib.my-flake.callConsumerFlake {
path = self.outPath + "/tests/integration/minimal-consumer";
pool = consumerPool;
};
in
{
checks = minimalConsumer.checks.${system};
}
The nested flake’s declared inputs resolve by name (overrides first,
then follows chains, then the pool), and an unresolvable input throws
an error naming it and what to do. The contract is deliberately
explicit, in the same spirit as closed inputs: nothing is fetched, no
lock is read, and you supply exactly the input graph you mean the test
to see. One consumer’s outputs can feed another’s overrides, so
chains of consumers (a flake consuming a flake that consumes yours) are
plain data flow.
See callConsumerFlake for the
full signature.
Gating evaluation cost
Beyond correctness, checks can gate what evaluation costs; see
Evaluation weight.
Evaluation Weight
lib.caisson.eval-weight measures what an evaluation costs and can
gate that cost in checks, so a framework regression is caught by CI
rather than noticed as slowness later. This repository uses it to gate
its overhead; the numbers quoted in these docs come from it.
How it measures
A scenario runs a pinned Nix evaluator inside a derivation sandbox against explicitly wired inputs and captures the evaluator’s statistics. The deterministic counters (thunks, values, environments, function and primop calls, total allocations) are reproducible for a fixed lock set and Nix version, so they can be gated. CPU and wall-clock time are machine-dependent, so they are always reported but not gated.
Semantic counters are derived from the same run, keyed to stable anchors in the evaluated source rather than line numbers:
nixpkgsEvals: full nixpkgs instantiationsnixpkgsLibEvals: nixpkgs-lib bootstraps (distinct lib sources)moduleSystemEvals:evalModulesruns, including submodules
These are gated exactly, with no growth allowance: one extra nixpkgs instantiation is the regression.
mkCheck
checks.eval-weight = lib.caisson.eval-weight.mkCheck {
inherit pkgs;
name = "my-flake";
scenarios = {
raw-flake-parts = {
entry = self.outPath + "/tests/eval-weight/raw-flake-parts.nix";
args = { /* store paths and system for the entry */ };
};
minimal-consumer = {
entry = self.outPath + "/tests/eval-weight/minimal-consumer.nix";
args = { /* ... */ };
};
};
gates = [
# framework overhead, isolated from ecosystem churn
{
name = "my-flake-overhead";
minuend = "minimal-consumer";
subtrahend = "raw-flake-parts";
}
# loose ceiling on the whole consumer
{
name = "minimal-consumer-total";
scenario = "minimal-consumer";
maxGrowth = 0.25;
}
];
baseline =
builtins.fromJSON (builtins.readFile ./tests/eval-weight/baseline.json);
};
- scenarios:
entryis a self-contained file imported inside the sandbox and applied toargs(store paths arrive as absolute-path strings); the resulting value is forced strictly, so the entry decides exactly what evaluation gets measured. Entries that need to evaluate a flake import the sharedcall-flake.nixkernel from a store path inargs, the same kernel undercallConsumerFlake. - gates: one per scenario by default. A subtraction gate measures the difference between two scenarios, which is the important trick: a 2× regression in framework machinery is invisible in a whole-nixpkgs total but obvious in the delta.
- baseline:
nullruns in measure-only bootstrap mode: metrics are printed, including a paste-ready baseline, and the check passes. Commit the pasted baseline (this repository keeps it attests/eval-weight/baseline.json) and subsequent runs gate against it: deterministic metrics may grow up tomaxGrowth(10% by default; exact metrics not at all), and marked shrinkage logs a note suggesting the baseline be tightened.
The workflow
- Write an entry per scenario under
tests/eval-weight/. - Run once with
baseline = null; paste the printed baseline intotests/eval-weight/baseline.json. - Wire
mkCheckintocheckswith the committed baseline. - When a gate fails, the report shows which metric moved and by how much; either fix the regression or, for intended changes, update the baseline in the same change, where review can see both.