caisson-core composes library overlays: identity, replacement, and deterministic order, implemented over plain Nix builtins.
caisson-core has zero flake inputs and its library code references
nothing but builtins. It is the foundation layer of the caisson
family; it is useful by itself to anyone who wants to compose an
extensible library out of overlay-shaped pieces without depending on
nixpkgs, flake-parts, or any other flake.
The unit of composition is an entry:
{
key = "example.base"; # stable identity: a string, or null
imports = [ ]; # entries this entry depends on
overlay = final: prev: { greet = name: "hello, ${name}"; };
}compose takes a list of entries and produces the composed library
plus composition metadata:
let
core = (builtins.getFlake "github:nix-caisson/caisson-core").lib.caisson-core;
base = {
key = "example.base";
imports = [ ];
overlay = final: prev: { greet = name: "hello, ${name}"; };
};
loud = {
key = "example.loud";
imports = [ base ];
overlay = final: prev: { greet = name: "${prev.greet name}!"; };
};
in
(core.compose { entries = [ loud ]; }).lib.greet "world"
# => "hello, world!"- Imports are reachability. Listing an entry pulls its transitive imports into the composition. Entries are collected by a depth-first, post-order walk, so an entry's imports precede it.
- The key is identity. A keyed entry appears once no matter how many entries import it. The first occurrence of a key fixes its position; the last occurrence supplies its value, so mentioning a key again replaces that entry wholesale. A replacement inherits the replaced entry's position: its imports are pulled into the composition, but they land later, guaranteeing reachability rather than precedence.
- Cycles terminate. The walk skips a key that is already on its path. Members of a cycle get no ordering guarantee relative to each other; everything else is unaffected.
- Keyless entries are a local tail. An entry with
key = nullcannot be imported. Keyless entries apply after the entire keyed world, in list order, and stack when listed repeatedly. They are the consumer's private patch layer: having no key, they can never be replaced by another entry. - Application is a classic overlay fold.
previs everything accumulated so far; references throughfinalsee the finished fixpoint. A law follows from the fixpoint itself: an overlay's output attribute names must not depend onfinal. composealso returnsmeta(key order, winning entries, tail length) so tooling can inspect and lint a composition;composeitself does not warn, because linting belongs to that tooling.
resolve implements layered lookup for handing ecosystem sources
(the tree a library or an evaluator is loaded from) to higher layers:
core.resolve {
name = "some-ecosystem";
explicit = null; # highest priority when non-null
defaults = { }; # the client repository's declared defaults
dependencies = { }; # the tree's dependencies, matched by exact name only
}Priority is explicit argument, then declared default, then the
dependency with exactly the declared name. A full miss returns null; resolve
can never throw or format an error message, because interpreting a
miss is deliberately the calling layer's job.
selectedSourceName names the dependency a selection returns:
core.selectedSourceName {
selection = dependencies: dependencies.some-pin; # a function of the dependencies
dependencies = { some-pin = …; };
}
# => "some-pin"It applies the selection to a copy of dependencies in which each
dependency carries its name, and reads the name from the result. A
selection that builds its value in another way, such as a directory
inside a dependency, gives null. The copy forces a dependency only
where the selection reads it.
mkLib builds a composed library from registered overlays and
modules over the empty seed, and injects the caisson-core namespace
(machinery, module registry, manifest) into the result:
core.mkLib {
# The tree's dependencies, which registered overlays and modules
# read from `closure-lib.caisson-core.libManifest.dependencies`, and
# the tree's root (see Dependencies and the root). Only
# `dependencies` is required.
dependencies = { };
root = null;
defaultEcosystemSrc = dependencies: { some-ecosystem = dependencies.some-pin; };
# the tree's default source per ecosystem, by
# exact name, selected from `dependencies`; read back
# with `ecosystemSrc`
modules = lib: { }; # class-keyed local registrations,
# given the bootstrap lib
configs = lib: { }; # class-keyed configurations
# (configs/<class>/<name>), given
# the bootstrap lib
libOverlays = lib: { }; # named overlay registrations, given
# the core lib; an entry is made with
# lib.caisson-core.mkLibOverlay
libOverlayImports = lib: [ lib.caisson-core.libManifest.libOverlays.my-overlay ];
# selection for this library, given
# the core lib; defaults to every
# project and local registration
extraLibOverlayImports = lib: [ ]; # entries added to that selection
pkgOverlays = lib: { }; # named package overlay registrations,
# given the bootstrap lib; an entry is
# made with lib.caisson-core.mkPkgOverlay
pkgSets = lib: { }; # package configs by config name, each
# an integration's mkConfiguration
# call, given the registered lib
projects = { }; # consumed upstream contributions,
# by project name
systems = [ "x86_64-linux" ]; # the platforms the tree builds on;
# null when absent
name = "my-project"; # the project's name, also the
# namespace it contributes to the
# composed library; null when absent
}The signature is the pattern of mkLib, with no ...: a missing or
unexpected argument is Nix's error at the call site, naming
mkLib and pointing at the pattern, whose comments say what each
argument is.
The rest of this section is reference, one topic per heading:
- how the library is built: Stages, Selecting lib overlays, Overlay registries, Registering from directories, Module classes, Libraries loaded from a source;
- what it holds: What the composed library carries, What a registered file receives, caisson-core composes itself, Projects, Package overlays;
- what it records: The manifest, History, Phase manifests;
- the tree of configurations: Configurations, Evaluating a configuration with and without its children, Configurations evaluated per system, What an evaluation gives the configurations inside it, Telling names apart, Finding a manifest.
The library is built in stages. Each is a new fixpoint over the
empty seed with a manifest in caisson-core.libManifest, and each
exists because some argument of mkLib is a function of it.
| Stage | What it holds | Arguments that receive it | Its manifest lacks |
|---|---|---|---|
| core | caisson-core's entries, with the lib overlay registry on its manifest | libOverlays, libOverlayImports, extraLibOverlayImports |
everything below |
| bootstrap | the core lib plus the selection, which is where the integrations are | modules, configs, pkgOverlays |
modules, moduleProjects, configs, pkgOverlays, pkgSets |
| registered | the same entries, with those registrations on the manifest | pkgSets |
pkgSets |
| full | the registered lib plus pkgSets |
none: mkLib returns it |
nothing |
pkgSetsreceives the registered lib because a package config selects from the registered modules and package overlays.- The core, bootstrap and registered manifests have
childless = true. - A registration made at an earlier stage still closes over the full
lib. The constructors those libs hold (
mkModuleand every class-boundmkModulemade from it,mkLibOverlay,mkPkgOverlay) giveclosure-libthe full lib, whose manifest holds the registry the entry joins. - A same-key registration replaces one of caisson-core's entries from the bootstrap stage on. The core lib keeps the original.
A libOverlays registration makes its entries with
lib.caisson-core.mkLibOverlay, and a selection refers to entries in
the registry of the manifest, as
lib.caisson-core.libManifest.libOverlays.<name>.
- The default selection is every registered overlay that is not a published entry.
libOverlayImportsreplaces the default selection.extraLibOverlayImports, of the same form, adds to the selection, whichever it is.
A library and a package set are built the same way: keyed overlays over an empty set, held in a registry that merges what consumed projects contribute with what the tree registers, from which a selection is applied. The lib overlay registry and the package overlay registry share the code for this, and the tests run every case below against both.
- Entry.
{ imports ? [ ]; overlay; }under a name. Its key is its name in the registry of the tree that composes it. - Projects. The entries of a project join as
<project>/<name>. The keys their imports carry are renamed with them: a key without a/is a name of that project and takes the project as prefix; a key with a/names an entry the project took from another project and is kept, so two projects that import the same entry import one entry. A local registration wins a name collision. - Imports. An import is a name or an entry.
- A name is looked up in the registry of the tree that registers
the importer:
imports = [ "base" ];for a neighbour,imports = [ "project/base" ];for an entry of a consumed project. A name nothing registers is an error that lists what is registered. - An entry under a key the registry holds is read from the registry. An entry under no registered key is composed as given; without a key it gets one derived from its importer.
- A name is looked up in the registry of the tree that registers
the importer:
- Replacement. Because imports are read from the registry, registering under a name replaces that entry for everything that imports it.
- Selection. Each entry is applied after the entries it imports, and each key once: where the key first occurs, with the value of its last occurrence.
- Clashes. Two entries built from different files under one key are refused. An entry built from a function records no file and is not told apart.
What differs between the two is who ties the fixpoint. mkLib
composes the library from its selection. Nothing in caisson-core ties
a package set: pkgOverlaysFor selection gives the overlays in order,
and the layer that builds package sets applies them.
A tree with this layout derives its registrations from its directories, using the readers of the lib each registry function is handed:
| Directory | Registration |
|---|---|
modules/<class>/<name>/default.nix |
modules = lib: lib.caisson-core.mkModules ./modules; |
configs/<class>/<name>/default.nix |
configs = lib: lib.caisson-core.mkModules ./configs; |
lib-overlays/<name>/default.nix |
libOverlays = lib: lib.caisson-core.mkLibOverlays ./lib-overlays; |
pkg-overlays/<name>/default.nix |
pkgOverlays = lib: lib.caisson-core.mkPkgOverlays ./pkg-overlays; |
- A reader belongs to the lib it is read from, so a composition that
registers another
caisson-core/readersentry reads its directories with that entry. - An entry is a directory holding a
default.nix, or a symlink to such a directory. Anything else in a directory being read is an error, so a stray file cannot silently vanish from a registry. mkLibOverlaysappliesmkLibOverlayto each entry, andmkPkgOverlaysappliesmkPkgOverlay.- A tree with another layout writes the registrations by hand.
The first level of a modules directory is the class, whatever its
name. Each entry registers through caisson-core.mkModule of the
library, given the name of that directory as the class. caisson-core
keeps no list of classes. The integration that evaluates a class
declares the class itself.
Nothing is composed over, and caisson-core ships no entry for any ecosystem. A library that exists outside the tree arrives as an entry that some project registers. Such an entry loads the library from a source and merges it in, so the names it adds come from the source.
- It reads the source from
prev.caisson-core.ecosystemSrc "<name>"(see below), and so loads the library of the composition it is composed into, whichever tree registered it. - An overlay that calls the functions of that library imports the entry by key, so composing the overlay composes the entry.
- An entry imported under a key the composing tree registers is read from the registry of that tree. Registering under the key replaces the entry wherever it is composed.
- A composition that supplies no source fails only where the entry is composed.
The caisson framework supplies such an entry for the library its integrations call.
Under caisson-core:
| Group | Names |
|---|---|
| Composition | mkLib, compose, resolve, importApply |
| Entry constructors | mkLibOverlay, mkPkgOverlay, mkModule (class-parameterized) |
| Directory readers | mkModules, mkLibOverlays, mkPkgOverlays |
| Package overlays | pkgOverlaysFor |
| Manifests | libManifest, pkgsManifest, evalManifest, manifestOf, namePathOf, definers |
| Configurations | mkConfiguration, finalizeChild, finalizeTop, elide, isManifest |
| Deferred configurations | mkDeferredConfiguration, deferChild, isDeferred, isDeferredConfiguration, mkArg, isArg |
| Ecosystem sources | ecosystemSrc |
| Lists | lists.unique, lists.zipListsWith, lists.init, lists.last |
| Attribute sets | attrsets.genAttrs, attrsets.filterAttrs |
| Strings | strings.hasInfix |
| Functions | functions.functionArgs, functions.setFunctionArgs |
The last four rows are what code written on caisson-core needs and
builtins lacks, grouped as the library of nixpkgs groups the
functions of the same names. With them such code can be composed in a
library that holds no other library.
The registries are in the manifest and nowhere else in the library:
libManifest.modules, libManifest.configs,
libManifest.libOverlays and libManifest.pkgOverlays. A
libOverlayImports selection refers into libManifest.libOverlays,
and a package set selects from libManifest.pkgOverlays.
ecosystemSrc <name> is the source the composition supplies for an
ecosystem, by exact name: the source its defaultEcosystemSrc
selects for that name, else the source it pins under that name, else
null. It is
fixed by the arguments of the mkLib call, so an overlay composed
after caisson-core's entries may read it from prev to decide what
it adds. An entry that a project contributes then gets the source of
the composition it is composed into.
A registered file takes a closure attribute set as its first argument list:
- a lib overlay file takes
{ closure-lib, mkLibOverlay, mkModule, ... }; - a package overlay file takes
{ closure-lib, mkPkgOverlay, ... }; - a module file takes
{ closure-lib, mkModule, ... }.
closure-lib is the composed library of the composition that
registered the file, bound lazily. So the functions of an overlay,
and a module, reach the registry of that composition under
caisson-core.libManifest.modules.<class> wherever they are later
composed or evaluated. The dependencies of the composition are in the
same manifest, at closure-lib.caisson-core.libManifest.dependencies.
lib/default.nix holds the primitive, compose, and composes the
overlays under lib-overlays/<name>/default.nix (compose,
resolve, lifecycle, readers, lists, attrsets, strings,
functions) over the empty
seed into the caisson-core namespace.
mkLib composes the same entries into the library of every consumer,
keyed caisson-core/<name>. So import caisson-core and
caisson-core inside a composed library are one definition, and each
part is a registered entry that a same-key entry replaces.
coreEntries { dependencies, defaultEcosystemSrc } returns those entries for a
composition assembled with compose directly.
A projects value is an attribute set with libOverlays,
class-keyed modules and pkgOverlays dictionaries: the outputs a
flake built on this machinery publishes. Its entries join the
registered dictionaries under <project>/<name>, so the existing
selections keep per-item choice and a local registration wins a name
collision.
Each registry records where an entry came from, so an export selector can keep the entries the composition registered:
- A lib overlay entry carries
project: null for a local registration, the name of the project for a contributed entry, andcaisson-corefor the entries caisson-core is made of. - For modules the manifest holds the same under
moduleProjects.<class>.<name>, beside the module dictionary, since a module value cannot carry a field without becoming a different module.
The package overlay registry holds package overlays in the shape of a
lib overlay entry. A file handed to mkPkgOverlay takes the closure
{ closure-lib, mkPkgOverlay, ... } and returns
{ imports ? [ ], overlay }, where overlay is an overlay of a package set (final: prev:).
Every registered entry carries:
| Field | Value |
|---|---|
key |
its registry name |
origin |
the file it was read from, null for an entry built from a function |
project |
null for a local registration, the name of the project for a contributed entry |
A selection of the local entries alone is a filter on project.
How entries are keyed, how imports are resolved and how a selection
is walked is in Overlay registries, and is the
same as for lib overlays. Nothing in caisson-core applies the
registry: pkgOverlaysFor selection turns a list of entries into the
list of overlays a package set applies.
By convention the entries named default (default,
<project>/default) are the default selection, as for modules. The
layer that builds package sets applies that default.
The manifest is the self-description of the composition, recorded at
caisson-core.libManifest.
| Field | What it holds |
|---|---|
type |
"lib" |
name |
the name of the project as declared on mkLib; absent when none is declared |
dependencies |
the tree's dependencies. The pin files a directory reader read are stated relative to the root when the directory lies in the tree of the root, and pin.dir is kept otherwise |
root |
the tree being built; null for a composition that is not a top |
defaultEcosystemSrc, systems |
as declared on mkLib |
defaultEcosystemSrcNames |
for each ecosystem defaultEcosystemSrc returns a source for, the name of that source in dependencies, or null when the selection builds the value in another way (selectedSourceName) |
projects |
the projects argument as given |
libOverlays, modules, pkgOverlays |
the registered dictionaries, with project entries prefixed and local entries winning |
moduleProjects |
where each module came from |
configs |
the configs registration |
pkgSets |
the package configs the pkgSets function declared, each finalized |
entries |
the selection in composition order |
history |
the events recorded on the way to the lib |
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.
name is the name the composition holds for itself, and the
namespace its overlays contribute to the composed library. It is not
passed anywhere: readers pull it back out of the composed library. A
layer above gives that name to a configuration no parent declares,
since a name is otherwise the attribute a parent declares a child
under.
entries lists each entry as { key, opaque }, with the entries
caisson-core forces first. An entry is opaque when its key names no
registry entry, which is an overlay imported by value and not
registered. A keyless entry gets a synthesized keyless/<n> key.
pkgSets entries are configurations (see Configurations below).
mkLib finalizes each with the name it is declared under and the
registered manifest as its parent, so anything else declared there is
refused where it is declared. The registered manifest lacks pkgSets,
so the full manifest lists the configs without containing itself.
caisson-core interprets nothing in them beyond the manifest shape.
history lists the events in stage order, and the history of each
stage begins with the history of the stage before it:
| Stage | Events it adds |
|---|---|
| core | one layer event per caisson-core entry, then the lib overlay registrations |
| bootstrap | 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 |
| registered | the modules, configs and pkgOverlays registrations |
| full | the pkgSets registrations |
Each event has:
manifest, the name path, empty for the root lib;type;operation,registryorlayer;key;index, its position within its operation;origin:project, andfilewhere the entry was built from a file. A lib overlay built from a file records it asorigin.
A layer event also carries, lazily, the sides of its overlay call
final: prev: result, as the stage that recorded it composed them:
result, the attribute set its overlay returned, and prev, the
accumulation it received.
definers manifest [ "my-project" "helper" ] reads them. It returns
the layers that define that path in order, the winner last, each with
its value after the layer and its binding position when that lies in
the file of the layer. A layer returning prev.x // { ... } carries
the names under x without defining them.
A lib carries a manifest per evaluation phase. libManifest is
filled in by mkLib. pkgsManifest and evalManifest are present
and null, for the layers that build package sets and module
evaluations to fill in on the libraries they hand out.
Every stage mkLib builds carries
caisson-core.withManifests { pkgsManifest = manifest; }, which
rebuilds that stage from its declaration with the given phase
manifests filled in: the same entries and libManifest, composed as
a new fixpoint, so everything that reads a phase manifest through the
fixpoint sees it.
- It is how a package set integration hands out
pkgs.lib: the lib the package config was declared under (the registered lib, for apkgSetsentry) withpkgsManifestfilled in. - Only
pkgsManifestandevalManifestare accepted, each a manifest or null. - A rebuilt lib carries
withManifeststoo, keeping what is already filled in.
A configuration is a module evaluation that sits in a tree: a NixOS
machine, a home, a flake. It learns its name and its parent from
where it is declared, so a configuration is a function
{ name, parent }: <manifest>.
caisson-core.mkConfiguration { type; evaluate; record ? { }; perSystem ? false; }
returns one:
| Argument | Meaning |
|---|---|
type |
the name of the integration |
evaluate |
the call of the evaluator. It takes { lib, manifest }, the lib the evaluation runs on and the manifest being built, and returns value, outputs and children, the finalized configurations declared inside it by integration and then name |
record |
plain data the integration adds to the manifest. It may not name a field mkConfiguration writes |
perSystem |
whether the configuration is evaluated once per system (below) |
Nothing is evaluated until the manifest the function returns has its
value, outputs or children read.
A configuration is called by finalizeChild or finalizeTop:
finalizeChild { name; parent; } childcalls it with them, after checking withbuiltins.functionArgsthat its pattern names exactlynameandparent, and requires a manifest back, or the evaluations by system of a per-system configuration.finalizeTop configurationfinalizes the configuration a top ends with. Its name is the name the composition declares onmkLib, absent when it declares none, and its parent is the manifest of the lib.
A configuration that declares children is evaluated with them and,
separately, without them. Each evaluation has its manifest, and each
runs on the declaring lib rebuilt with that manifest as
evalManifest.
- The full evaluation includes the configurations declared inside
it. Its manifest is the one
mkConfigurationreturns, and it carries the other aschildlessManifest. - The childless evaluation (
childless = true, nochildren) leaves them out. It is what the children are built against, so what a child reads of its parent does not depend on the children.
An integration finalizes each child against the childless manifest
(finalizeChild { inherit name; parent = manifest.childlessManifest; }),
so the parent of a child is the childless manifest of the
configuration that declares it. The childless evaluation runs only
when a child, or a reader of childlessManifest, reads its value, so
a configuration with no children is evaluated once.
Each of these manifests carries:
type,nameandparent;ancestors, the list of the parent with the parent appended;nearest, the attribute set of the parent with the parent under its integration, a lib excepted;inputs, the manifest of the lib, and on the full manifest the childless manifest and the children;- from the parent:
dependencies,root,systems,projects,defaultEcosystemSrc,pkgSetsand the registries; defaultPkgs, where a configuration above recorded one (below).
An integration whose configurations are built for one system at a
time, such as NixOS, passes perSystem = true. Such a configuration
has one evaluation for each system it is evaluated for, and
finalizing it returns those evaluations by system. There are as many
as there are systems, also for a single system, and none when there
is no system. Nothing is refused.
In the tree the system sits above the name:
- Each evaluation is a manifest as described above, evaluated with
and without its children, under the name it is declared by,
carrying its system as
system. - Its parent is the system, a manifest of type
systemnamed by the system, inside the parent that declares the configuration. - The full manifest of that parent holds each system under
children.system, with the evaluations declared at it by integration and then name. Configurations evaluated once stay underchildren.<integration>. - An evaluation sees the system above it without what is declared under it.
Which systems a per-system configuration is evaluated for depends on where it is declared:
- Declared at the top, or inside a configuration evaluated once: the
systems that configuration passes on, which start as the
systemsofmkLib. - Declared inside another per-system configuration: the system of its
parent, by default. A home declared inside a machine has, by
default, one evaluation per evaluation of the machine, for the same
system. The manifest of each machine evaluation holds that one
system as
systems. - A parent changes what its children get by returning
forChildren.systems(next section).
evaluate may return forChildren beside value, outputs and
children. It is read from the childless evaluation and recorded on
the manifest as forChildren.
| Field | What it gives the configurations inside |
|---|---|
extraDefaultModuleImports |
by class, a list of selections, each a function of a lib returning modules, added to the default selection of that class, those from the top first |
defaultPkgs |
a selection of the package set for the configurations inside, in place of the selection the evaluation runs on. Null when it makes none |
systems |
the systems its per-system children are evaluated for. Null when it states none |
How they are inherited:
- The manifest of a configuration holds the registry it sees as
modules, the selections added above it asdefaultModuleImports, and the package set selection asdefaultPkgs. - Every level on the way down extends or replaces them in turn. So what a level gives is inherited by the configurations inside it, nested ones included, until a level between replaces it.
- The level that gives them does not inherit them, and neither do the configurations beside it.
- An integration can also record a package set selection for a
configuration (
record.defaultPkgs). It applies to that configuration and is inherited the same way. systemshas to come from the systems allowed where the giving configuration is declared, and a system outside them is refused. A machine that holds an image for another architecture states that architecture there, and a configuration evaluated once can return a list to narrow what its children are evaluated for.
An integration passes class, the module class of its evaluations,
to mkConfiguration. It is recorded on the manifest as class, null
when the integration passes none. evaluate may then also return
forNearestChildren, with one field, extraDefaultModuleImports, of
the same shape as the field of forChildren. It is read from the
childless evaluation and recorded on the manifest as
forNearestChildren.
An addition for class C reaches the nearest configurations of class C beneath the evaluation:
- It passes through configurations of other classes, configurations with no class, and systems.
- At each configuration of class C it reaches, it is placed in
defaultModuleImports.Cafter the selections that configuration inherits, with itslevel, itsfileandnearest = true. The additions that reach the same configuration are in tree order, those from the top first. - The configurations beneath that configuration do not inherit it. A configuration of class C passes on its own additions for C, which connect at the next configurations of class C beneath it.
- The additions on their way down are carried on each manifest as
nearestPending, by class.
A level adds a module this way when the configurations directly beneath it live on what it describes, such as the deployment of a colmena node or the disk layout of a machine. A NixOS configuration declared inside such a machine does not get it.
evaluate may also return forDirectChildren, with the per-class
field extraDefaultModuleImports of the same shape. It is read from
the childless evaluation and recorded on the manifest as
forDirectChildren.
- An addition for class C reaches the configurations of class C declared directly beneath the evaluation, a configuration evaluated per system included, since a system is no level of its own.
- It is placed in
defaultModuleImports.Cafter the inherited selections and the nearest additions, with itslevel, itsfileanddirect = true. - It passes through nothing: a direct child of another class does not receive it, and the configurations beneath a direct child do not inherit it.
Each of the three containers may also hold
unsafeCrossChildReferences, by class a boolean. A configuration of
class C that a setting for C reaches reads the full evaluation of the
level that set it, with its children, as crossChildLevel on its
manifest, and the name path of that level as crossChildLevelPath.
Elsewhere both are null. Each container reaches as it does for
additions: forChildren every configuration of the class beneath,
forNearestChildren the nearest ones, forDirectChildren the direct
children.
- The view ends at the level that set it: the configuration reaches the level's children of every integration and what is beneath them, and nothing beside or above the level.
- When several settings reach a configuration,
forDirectChildrenbeatsforNearestChildren, which beatsforChildren, and within one container the nearest parent that set it wins. A setting throughforNearestChildrenconnects at the nearest configuration of its class whether or not it wins there. - The childless evaluation exists so that a loop between a child and
its parent cannot be written. A configuration that reads the full
evaluation of a level can close such a loop, which Nix reports as
infinite recursion; the
unsafeprefix says so. - The childless manifest of a level that sets the option for some
class carries its full manifest as
crossChildFull; on other manifests it is null.
A deferred configuration is a function of { name, parent, args },
where a configuration is a function of { name, parent }. Its args
are values a deployment tool supplies after the flake is evaluated,
such as the outputs of the resources it applies, and they reach the
configuration only through args.
mkDeferredConfiguration { type; class ? null; record ? { }; configuration; }
makes one. type and class are the integration and the module
class, known before any args are. record is plain data the entry
carries, such as how the entry is published. configuration is
refused unless it is a function of exactly { name, parent, args }
returning a manifest.
A parent cannot call a deferred configuration, because it has no args
to give. It holds an entry under children instead, which
deferChild { name; parent; } child makes. The entry holds the
record, the name, type, class and parent, and the function
finalize { args; argsSource; }, which calls the configuration and
returns its manifest. Nothing in the entry is evaluated at flake time.
The parent's history and inputs leave the entry out.
Every manifest says which args its evaluation takes, in argsNeeded:
an attrset whose leaves are made by
mkArg { cacheable ? false; type ? null; description ? null; }, and
empty for a configuration that takes none. cacheable says whether a
tool may keep a value it found in a cache only its user can read.
finalize fills the args against the argsNeeded of the manifest it
returns: a given arg is passed on unevaluated, and an arg the caller
did not give throws caisson-deferred: needs <path> when it is read.
Tools read that text to learn which arg an evaluation is blocked on.
Since the evaluation reads argsNeeded from its own result, the names
at each level of argsNeeded must not depend on the args beneath that
level.
The manifest finalize returns also carries argsSource, a function
from the path of an arg, a list of names, to a sentence saying where
its value came from. It never carries the values.
Every manifest also carries discoverArgs, a function of
{ system } returning a derivation whose program, built for and run
on system, prints the args it finds as JSON on its standard output;
or null, as it is for a configuration with no such program.
A parent that has args of its own, because it was finalized, gives
its deferred children their part of them by calling finalize on
their entries. It then holds the manifests finalize returns under
children, so a tool that walks the tree finds them as it finds any
other child.
caisson-core.elide paths gives, for each of a set of things in a
tree, the segments needed to tell it apart from the others.
- A path is the list of
{ type, name }segments from the top down to the thing, ending in the name of the thing. A system is a segment of typesystem. - The result has, for each path in order, the segments kept, as strings.
The rule 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, and does the same within each branch.
So a name that is alone stays bare, whatever sits above it, and names
that collide gain what tells them apart: hostname1 evaluated for a
single system stays hostname1, and hostname2 evaluated for
x86_64-linux and aarch64-linux keeps the system.
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.
Every manifest carries _type = "caisson-manifest", and manifestOf
finds it in whatever a file returns:
- a manifest;
- an attribute set carrying
caisson.manifest; - an evaluated configuration carrying
config.caisson.manifest; - a library, or a package set through
pkgs.lib, carrying the phase manifests, where the last manifest filled in is the manifest.
It returns null when the value carries none.
namePathOf gives the name path of a manifest: the { type; name; }
of every manifest beneath the top down to it, which is its address
under children of the top's manifest. The top is the first manifest
of the chain that is a configuration, neither a lib nor a system, and
the top and what is above it have the empty path. The events of
history name their manifest by this path.
Higher layers project the libOverlays and modules outputs of a
flake from the manifest, and the projects argument consumes those
projections one level down, which is how dictionaries populate across
flakes. The manifest carries no checks here: producers validate their
manifests, and consuming integrations type-check on the export side.
A tree is built from its dependencies. mkLib takes them as
dependencies, an attribute set by name, and takes root, the
identity of the tree being built. caisson-core reads no pin files: how
a flake's inputs, a flake.lock or another pin system's files become
dependencies and root is for a reader written on top of it (the
caisson framework has readers for flakes, flake-compat and npins).
- A dependency is a tree with an
outPath, as its pin system hands it over, optionally withpin, the record of how it is pinned. - When
pin.dirnames the directory that holds the pin files and that directory lies inside the root's tree, the manifest records the pin files relative to the root. rootis{ outPath; dirty; rev; … }, the source-info fields of the tree, or null for a composition that is not a top. Its out path is read only for a dependency with apin.dir, since inside a flake'soutputsthe out path ofselfcannot be read while the outputs are being computed.
The test suite is hermetic pure evaluation:
nix eval -f tests summaryPre-release. The contract described above is intended to freeze; until the first release it may still change. The caisson framework builds on this repository, and its pinned-world check tests the family against pinned upstreams.
MIT. See LICENSE.
Despite the org name, caisson-core is an independent project, not affiliated with or endorsed by the NixOS Foundation. Nix and NixOS are trademarks of the NixOS Foundation.