Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

KeyValue
closure-inputsThe 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-libThe 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
mkModuleA 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

FunctionPurpose
mkModuleCreates class-specific module normalizers with closed inputs
mkLibOverlayApplies the closure to registered library overlays
mkModules, mkLibOverlaysDerive the registrations from modules/<class>/<name> (also configs/) and lib-overlays/<name>, applying mkModule and mkLibOverlay
importApplyApplies static arguments to a module through the import chain
mkLibBootstraps a composed library with closed overlays
caisson.flake-parts.mkTopConfigurationCreates flake outputs with closed modules

Further Reading