From b3ce09dd1c750adf5c0ef4cfa27553e8553af6e4 Mon Sep 17 00:00:00 2001
From: Chris Hodapp
Date: Sat, 17 Oct 2026 00:34:42 -0700
Subject: [PATCH 1/2] perf(integrations): read only the configurations options
a module defines
Every evaluation declares `caisson..configurations` for
every integration caisson ships, and `childrenOf` read each of them,
merging an option that only holds its default. It now reads an option
only when a module defines it, and the types of the options are built
once for all integrations.
Co-Authored-By: Claude Opus 5.5
---
lib-overlays/integrations/default.nix | 41 ++++++++++++++++++++++-----
1 file changed, 34 insertions(+), 7 deletions(-)
diff --git a/lib-overlays/integrations/default.nix b/lib-overlays/integrations/default.nix
index 77843291..15f40754 100644
--- a/lib-overlays/integrations/default.nix
+++ b/lib-overlays/integrations/default.nix
@@ -410,23 +410,41 @@
# The module that declares the options `configurationsModuleOf`
# declares, for several integrations at once. The core module of
# caisson imports it once, for all the integrations of caisson.
+ # The types of the options are built once for all of them.
configurationsModuleFor = names: {
key = "caisson-configurations:${builtins.concatStringsSep "," names}";
imports = [
(
{ lib, options, ... }:
+ let
+ types = configurationsTypesOf lib;
+ in
{
- options.caisson = genAttrs names (name: configurationsOptionsOf { inherit lib options; } name);
+ options.caisson = genAttrs names (
+ name: configurationsOptionsOf { inherit lib options types; } name
+ );
}
)
];
};
+ # The types of `caisson..configurations` and
+ # `caisson..exported`, the same for every integration.
+ configurationsTypesOf = lib: {
+ configurations = lib.types.lazyAttrsOf lib.types.raw;
+ exported = lib.types.functionTo (lib.types.lazyAttrsOf lib.types.raw);
+ };
+
# The options `caisson..configurations` and
# `caisson..exported` of the integration `name`, in the
- # evaluation whose `lib` and `options` are given.
+ # evaluation whose `lib` and `options` are given, with `types` from
+ # `configurationsTypesOf`.
configurationsOptionsOf =
- { lib, options }:
+ {
+ lib,
+ options,
+ types ? configurationsTypesOf lib,
+ }:
name:
let
manifest = lib.caisson-core.evalManifest;
@@ -483,7 +501,7 @@
in
{
exported = lib.mkOption {
- type = lib.types.functionTo (lib.types.lazyAttrsOf lib.types.raw);
+ type = types.exported;
default = configurations: configurations;
defaultText = lib.literalMD "every configuration declared";
description = ''
@@ -495,7 +513,7 @@
'';
};
configurations = lib.mkOption {
- type = lib.types.lazyAttrsOf lib.types.raw;
+ type = types.configurations;
default = { };
apply = builtins.mapAttrs finalize;
description = ''
@@ -531,11 +549,20 @@
# integration's `evaluate` returns: the values of its
# `caisson..configurations` options, which finalize
# each entry when read. An integration with none declared is left
- # out.
+ # out. An option with no definition but its default, whose highest
+ # priority is the priority of an option default, is not read at
+ # all, which spares merging it in every evaluation for every
+ # integration the evaluation could hold.
childrenOf =
{ options, config }:
+ let
+ optionDefaultPriority = 1500;
+ declaring = builtins.filter (
+ name: options.caisson.${name}.configurations.highestPrio < optionDefaultPriority
+ ) (integrationsIn options);
+ in
filterAttrs (_: declared: declared != { }) (
- genAttrs (integrationsIn options) (name: config.caisson.${name}.configurations)
+ genAttrs declaring (name: config.caisson.${name}.configurations)
);
isManifest = value: builtins.isAttrs value && (value._type or null) == "caisson-manifest";
From 2129fdbd6dae8d28a9b46439eb4927fba8a87e5c Mon Sep 17 00:00:00 2001
From: Chris Hodapp
Date: Sat, 17 Oct 2026 00:41:59 -0700
Subject: [PATCH 2/2] feat(hjem): add the hjem integration
An hjem configuration is the files and packages of a user, evaluated
with hjem's user module at every system in force where it is declared.
Its outputs are the version 3 manifest hjem's OS modules write, that
manifest as a file checked with hjem's schema, the packages and hjem's
CLI. It is published as `hjemConfigurations.`, which
`hjem standalone switch --flake` reads, and a top projects to what
`hjem standalone switch --config` reads.
A NixOS configuration activates the hjem configurations declared inside
it with the NixOS module `hjem-activation`. It writes the template
units hjem's NixOS module writes, under the same names, and each
instance runs the CLI of the configuration of its user. caisson's
default NixOS module imports it, and gives each hjem configuration the
hjem module `nixos-parent`, which takes the home directory from the
machine's account for the user. An hjem configuration inside a NixOS
machine also imports hjem's `modules/nixos/systemd.nix` with the
`utils` and options of the machine, so it declares systemd user units
as a user hjem's NixOS module embeds does.
The `hjem-system` check builds the units and the manifest file.
Co-Authored-By: Claude Opus 5.5
---
README.md | 3 +-
configs/flake/caisson/partitions/checks.nix | 37 ++
configs/structural/impl/default.nix | 1 +
docs/development/repository-structure.md | 4 +-
docs/development/testing-architecture.md | 3 +-
docs/development/testing.md | 16 +-
lib-overlays/hjem/default.nix | 341 ++++++++++++++++++
modules/generic/core/default.nix | 1 +
modules/hjem/core | 1 +
modules/hjem/nixos-parent/default.nix | 45 +++
modules/nixos/default/caisson/forChildren.nix | 16 +-
modules/nixos/default/default.nix | 7 +
modules/nixos/hjem-activation/default.nix | 265 ++++++++++++++
tests/argument-errors/check.sh | 13 +-
tests/dependencies/flake.lock | 21 ++
tests/dependencies/flake.nix | 2 +
tests/hjem-system/default.nix | 121 +++++++
tests/pinned-world/default.nix | 74 ++++
tests/unit/lib-overlays.nix | 3 +
19 files changed, 954 insertions(+), 20 deletions(-)
create mode 100644 lib-overlays/hjem/default.nix
create mode 120000 modules/hjem/core
create mode 100644 modules/hjem/nixos-parent/default.nix
create mode 100644 modules/nixos/hjem-activation/default.nix
create mode 100644 tests/hjem-system/default.nix
diff --git a/README.md b/README.md
index e9da8d3d..c22104a2 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@
-The drift badge reports the daily run of the pinned-world suite against the current upstreams (nixpkgs, home-manager, colmena, terranix, system-manager); red means an upstream moved and broke an expectation caisson holds.
+The drift badge reports the daily run of the pinned-world suite against the current upstreams (nixpkgs, home-manager, colmena, disko, nix-darwin, hjem, terranix, system-manager); red means an upstream moved and broke an expectation caisson holds.
---
@@ -124,6 +124,7 @@ source by.
| `nixos-minimal` | `nixpkgs` | the `nixos` class under the minimal evaluator (no NixOS base modules); a second integration over the class `nixos` owns |
| `home-manager` | `home-manager` | Home Manager configurations |
| `home-manager-minimal` | `home-manager` | the `homeManager` class with Home Manager's necessary modules alone, the configuration importing what it uses from `modulesPath`; a second integration over the class `home-manager` owns |
+| `hjem` | `hjem` | the files and packages of a user, as hjem manifests; activated by the NixOS configuration they are declared inside, or applied with `hjem standalone` |
| `terranix` | `terranix` | Terranix and Terraform configurations |
| `caisson-colmena` | `colmena` | colmena configurations whose machines are NixOS configurations declared inside them, published as the hive colmena's command reads; the colmena integration we recommend for new configurations |
| `colmena` | `colmena` | a hive written for colmena, taken as it is, with each node a NixOS configuration; for porting an existing hive and the modules written for it |
diff --git a/configs/flake/caisson/partitions/checks.nix b/configs/flake/caisson/partitions/checks.nix
index 6299fa11..36cc9239 100644
--- a/configs/flake/caisson/partitions/checks.nix
+++ b/configs/flake/caisson/partitions/checks.nix
@@ -178,6 +178,43 @@
};
in
builtins.seq suite.summary (pkgs.runCommand "pinned-world" { } "touch $out");
+ # The manifest file of an hjem configuration inside a NixOS
+ # machine and the units that activate it, built. Building
+ # the manifest file checks it against hjem's own schema
+ # with `cue vet`, so the file the machine activates is one
+ # hjem's CLI accepts. Building the units runs shellcheck
+ # on their scripts.
+ hjem-system =
+ let
+ probe = import (self.outPath + "/tests/hjem-system") {
+ inputs = inputs // {
+ caisson = self;
+ };
+ };
+ units = probe.top.nixosConfigurations.probe-machine.config.systemd.units;
+ in
+ pkgs.linkFarm "hjem-system" (
+ [
+ {
+ name = "manifest-probe.json";
+ path = probe.inner.outputs.manifestFile;
+ }
+ ]
+ ++
+ builtins.map
+ (name: {
+ inherit name;
+ path = units.${name}.unit;
+ })
+ [
+ "hjem.target"
+ "hjem-prepare.service"
+ "hjem-activate@.service"
+ "hjem-reload@.service"
+ "hjem-update-state@.service"
+ "hjem-cleanup.service"
+ ]
+ );
# The argument error of every entry point comes from Nix
# itself, raised at the call site with no frame of caisson
# above it: the real evaluator runs each wrong call and the
diff --git a/configs/structural/impl/default.nix b/configs/structural/impl/default.nix
index 985d7ae9..9f7d8b4e 100644
--- a/configs/structural/impl/default.nix
+++ b/configs/structural/impl/default.nix
@@ -24,6 +24,7 @@
nixos-minimal
home-manager
home-manager-minimal
+ hjem
caisson-colmena
colmena
caisson-disko
diff --git a/docs/development/repository-structure.md b/docs/development/repository-structure.md
index 5d0d4d8d..59d72bf5 100644
--- a/docs/development/repository-structure.md
+++ b/docs/development/repository-structure.md
@@ -26,7 +26,7 @@ without nixpkgs can depend on it directly.
The layer users reach for: the integrations (`structural`,
`flake-parts`, `nixpkgs`, `nixos`, `nixos-minimal`, `home-manager`,
-`home-manager-minimal`, `darwin`, `caisson-colmena`, `colmena`,
+`home-manager-minimal`, `darwin`, `hjem`, `caisson-colmena`, `colmena`,
`caisson-disko`, `disko`, `terranix`, `system-manager`), each
a library overlay
contributing its `lib.caisson` namespace, registering its module
@@ -50,7 +50,7 @@ inject the same names beside `self`.
## The pinned world
The upstream world caisson is tested against (nixpkgs, home-manager,
-colmena, disko, nix-darwin, terranix, system-manager, and the test
+colmena, disko, nix-darwin, hjem, terranix, system-manager, and the test
tooling) is pinned in
`tests/dependencies/flake.lock`, a lock only the checks and formatter
partitions read, so caisson's `flake.nix` carries no churning pin. The
diff --git a/docs/development/testing-architecture.md b/docs/development/testing-architecture.md
index 7b34980c..e44f741e 100644
--- a/docs/development/testing-architecture.md
+++ b/docs/development/testing-architecture.md
@@ -311,7 +311,8 @@ The checks partition merges outputs from all test and example flakes into the
| `literate-flake-default` | `examples/literate-flake/` | Example default package builds |
| `literate-flake-greeting` | `examples/literate-flake/` | Example greeting package builds |
| `debug-disabled` | `checks.nix` (inline) | `self.debug` is not exposed in production |
-| `pinned-world` | `checks.nix` + `tests/pinned-world/` | caisson composed with the pinned upstream world (nixpkgs, home-manager, colmena, disko, nix-darwin, terranix, system-manager) evaluates end to end; the `drift` workflow runs the same check against advanced pins |
+| `pinned-world` | `checks.nix` + `tests/pinned-world/` | caisson composed with the pinned upstream world (nixpkgs, home-manager, colmena, disko, nix-darwin, hjem, terranix, system-manager) evaluates end to end; the `drift` workflow runs the same check against advanced pins |
+| `hjem-system` | `checks.nix` + `tests/hjem-system/` | The manifest file of an hjem configuration inside a NixOS machine builds, which checks it against hjem's schema with `cue vet`, and so do the units that activate it, which runs shellcheck on their scripts |
| `darwin-system` (aarch64-darwin) | `checks.nix` + `tests/darwin-system/` | The macOS machine of the pinned-world suite, declared through the darwin integration, builds; the `darwin-build` job of the check workflow builds it on a macOS runner |
| `argument-errors` | `checks.nix` + `tests/argument-errors/` | The argument error of every entry point comes from Nix itself, raised at the call site with no frame of caisson |
| `eval-weight` | `checks.nix` + `tests/eval-weight/` | Framework evaluation cost held to committed ceilings ([guide](https://nix-caisson.github.io/docs/eval-weight.html)) |
diff --git a/docs/development/testing.md b/docs/development/testing.md
index b3e571a0..7d416c7c 100644
--- a/docs/development/testing.md
+++ b/docs/development/testing.md
@@ -23,13 +23,17 @@ plumbing works.
## Pinned-World Suite
- **Scope:** caisson composed with concrete pinned versions of the upstream
- world (nixpkgs, home-manager, colmena, disko, nix-darwin, terranix,
- system-manager) and exercised end to end: a home configuration, a
- NixOS system with homes declared inside it, a colmena hive, disk
- layouts with the machines installed onto them, a darwin machine
- evaluated at aarch64-darwin, a terranix and a system-manager
+ world (nixpkgs, home-manager, colmena, disko, nix-darwin, hjem,
+ terranix, system-manager) and exercised end to end: a home
+ configuration, a NixOS system with homes declared inside it, a
+ colmena hive, disk layouts with the machines installed onto them, a
+ darwin machine evaluated at aarch64-darwin, hjem configurations on
+ their own and inside a NixOS machine, a terranix and a system-manager
configuration, and the composition guarantees of the real exported
- overlays. The darwin machine of the suite is also the one check at
+ overlays. The `hjem-system` check builds the manifest file of the hjem
+ configuration inside the machine, which checks it against hjem's
+ schema, and the units that activate it, which runs shellcheck on
+ their scripts. The darwin machine of the suite is also the one check at
aarch64-darwin, `checks.aarch64-darwin.darwin-system`, which the
`darwin-build` job of the check workflow builds on a macOS runner. Every upstream expectation caisson relies on is a probe here.
- **Location:** `tests/pinned-world/default.nix`; the pins in
diff --git a/lib-overlays/hjem/default.nix b/lib-overlays/hjem/default.nix
new file mode 100644
index 00000000..6e936afb
--- /dev/null
+++ b/lib-overlays/hjem/default.nix
@@ -0,0 +1,341 @@
+# SPDX-License-Identifier: MIT
+#
+# The hjem integration: it owns the `hjem` class, hjem's class for the
+# files and packages of a user. An hjem configuration is the home of a
+# user, evaluated at every system in force where it is declared.
+#
+# hjem itself evaluates a user only inside its NixOS and nix-darwin
+# modules, as a submodule of `hjem.users`; its standalone CLI evaluates
+# no modules and reads a manifest. caisson evaluates the user with its
+# own `evalModules` over hjem's user module, `modules/common/user.nix`,
+# with the arguments hjem's OS modules give it: hjem's library as
+# `hjem-lib`, the user's name as `name`, the package set as `pkgs`, and
+# the machine above it, if any, as `osConfig`. The user's name is the
+# name the configuration is declared under, unless a module of the
+# configuration sets `user`.
+#
+# The outputs are what hjem's tools read: `manifest`, the version 3
+# manifest of the user's files as hjem's OS modules write it,
+# `manifestFile`, that manifest as a JSON file checked against hjem's
+# schema, and `packages`. The configuration is published as
+# `hjemConfigurations.` with `manifest` and `packages`, which
+# `hjem standalone switch --flake` reads. A failed assertion of the
+# user fails every output.
+#
+# A NixOS configuration activates the hjem configurations declared
+# inside it with caisson's `hjem-activation` NixOS module, and gives
+# them the home directory of the user's account with the hjem module
+# `nixos-parent`.
+{
+ mkLibOverlay,
+ ...
+}:
+{
+
+ imports = [
+ # The library of nixpkgs, which this overlay calls through the
+ # composed library, imported by key so it is composed wherever
+ # this is.
+ ((mkLibOverlay ../nixpkgs-lib) // { key = "nixpkgs-lib"; })
+ # What an integration is written from, imported by key so it is
+ # composed wherever this integration is.
+ ((mkLibOverlay ../integrations) // { key = "integrations"; })
+ ];
+
+ overlay =
+ final: prev:
+ let
+ context = "lib.caisson.hjem.mkConfiguration";
+
+ resolveEcosystemSrc = final.caisson.integrations.resolveEcosystemSrc {
+ name = "hjem";
+ context = "caisson.hjem";
+ };
+
+ # An hjem source: the hjem flake or source tree, which holds the
+ # user module and hjem's library.
+ assertHjemSrc =
+ ecosystemSrc:
+ let
+ path = if builtins.isAttrs ecosystemSrc then ecosystemSrc.outPath or null else ecosystemSrc;
+ in
+ if
+ path != null
+ && builtins.pathExists "${path}/modules/common/user.nix"
+ && builtins.pathExists "${path}/lib.nix"
+ then
+ ecosystemSrc
+ else
+ throw "${context} requires `ecosystemSrc` to be an hjem flake or source tree (holding modules/common/user.nix and lib.nix).";
+
+ # The manifest of an evaluated user, as hjem's OS modules write
+ # it: version 3, with an entry for every enabled file of the
+ # user's file sets.
+ manifestOf =
+ hjemLib: config:
+ let
+ fileSets = [
+ config.files
+ config.xdg.cache.files
+ config.xdg.config.files
+ config.xdg.data.files
+ config.xdg.state.files
+ ];
+ in
+ {
+ version = 3;
+ files = builtins.concatMap (
+ files:
+ builtins.map hjemLib.fileToJson (builtins.filter (file: file.enable) (builtins.attrValues files))
+ ) fileSets;
+ };
+
+ evaluate =
+ args:
+ { lib, manifest }:
+ let
+ selection = lib.caisson.integrations;
+ src = assertHjemSrc (resolveEcosystemSrc {
+ explicit = args.ecosystemSrc or null;
+ manifest = lib.caisson-core.libManifest or { };
+ });
+
+ name = manifest.name or null;
+ inherit (manifest) system;
+ what =
+ if name != null then
+ "the hjem configuration `${name}` at ${system}"
+ else
+ "this hjem configuration at ${system}";
+
+ pkgSets = selection.pkgSetsAt { inherit context what; } manifest system;
+ pkgs = selection.pkgSetOf { inherit context what; } manifest pkgSets;
+
+ hjemLib = import "${src}/lib.nix" { inherit lib pkgs; };
+
+ registry = lib.caisson.modules.hjem or { };
+ frameworkModules = selection.frameworkModules "hjem" registry;
+ moduleImports = selection.moduleImportsOf "hjem" { inherit lib manifest; } args;
+
+ # The configuration's module: the module passed, else the
+ # configuration registered under the configuration's name
+ # (`configs/hjem/`), else none.
+ registered = lib.caisson.configs.hjem or { };
+ configModule =
+ if (args.configModule or null) != null then
+ args.configModule
+ else if name != null then
+ registered.${name} or null
+ else
+ null;
+
+ # The machine the configuration is declared inside, when there
+ # is one: the childless evaluation of the nearest NixOS
+ # configuration above it, which hjem's modules receive as
+ # `osConfig`, null when there is none.
+ machine = manifest.nearest.nixos or null;
+ osConfig = if machine != null then machine.value.config else null;
+
+ # A user hjem's NixOS module embeds also imports hjem's
+ # `modules/nixos/systemd.nix`, which declares the user's
+ # systemd units and links them into the user's files, with the
+ # `utils` and the options of the machine. A configuration inside
+ # a NixOS machine that has `utils` gets the same; the minimal
+ # NixOS evaluator gives none.
+ machineArgs = if machine != null then (machine.value._module or { }).args or { } else { };
+ nixosUser = machineArgs ? utils;
+
+ # What hjem's OS modules set on a user that caisson sets too:
+ # the user's name from the name the configuration is declared
+ # under, and hjem's default of not clobbering existing files.
+ userModule =
+ { lib, ... }:
+ {
+ _file = "caisson-hjem:user";
+ user = lib.mkIf (name != null) (lib.mkDefault name);
+ clobberFiles = lib.mkDefault false;
+ };
+
+ value = lib.evalModules (
+ {
+ class = "hjem";
+ specialArgs = {
+ inherit
+ lib
+ pkgs
+ pkgSets
+ osConfig
+ ;
+ hjem-lib = hjemLib;
+ name = if name != null then name else "hjem";
+ }
+ // lib.optionalAttrs nixosUser {
+ inherit (machineArgs) utils;
+ osOptions = machine.value.options;
+ }
+ // (if (args.specialArgs or null) != null then args.specialArgs else { });
+ modules = [
+ "${src}/modules/common/user.nix"
+ userModule
+ ]
+ ++ lib.optional nixosUser "${src}/modules/nixos/systemd.nix"
+ ++ frameworkModules
+ ++ moduleImports registry
+ ++ (if configModule == null then [ ] else [ configModule ]);
+ }
+ // (if (args.ecosystemArgs or null) != null then args.ecosystemArgs else { })
+ );
+ inherit (value) config;
+
+ # Every output fails with the messages of the user's failed
+ # assertions, and carries its warnings.
+ failed = builtins.map (assertion: assertion.message) (
+ builtins.filter (assertion: !assertion.assertion) config.assertions
+ );
+ checked =
+ output:
+ if failed != [ ] then
+ throw ''
+ ${what}:
+ ${lib.concatMapStringsSep "\n" (message: "- ${message}") failed}
+ ''
+ else
+ lib.foldr (warning: lib.warn "${what}: ${warning}") output config.warnings;
+
+ manifestValue = checked (manifestOf hjemLib config);
+ in
+ {
+ inherit value;
+ outputs = {
+ manifest = manifestValue;
+ manifestFile = pkgs.writeTextFile {
+ name = "manifest-${config.user}.json";
+ text = builtins.toJSON manifestValue;
+ checkPhase = ''
+ set -e
+ export CUE_CACHE_DIR=$(pwd)/.cache
+ export CUE_CONFIG_DIR=$(pwd)/.config
+ ${lib.getExe pkgs.cue} vet -c ${src + "/manifest/v3.cue"} $target
+ '';
+ };
+ packages = checked config.packages;
+ # hjem's CLI, which applies a manifest, built from the hjem
+ # source the configuration was evaluated with.
+ cli = pkgs.callPackage "${src}/cli/package.nix" { };
+ };
+ };
+
+ # An hjem configuration is published as
+ # `hjemConfigurations.`, with `manifest` and `packages`,
+ # which is what `hjem standalone switch --flake` reads.
+ exportsTo = {
+ attrset = "hjemConfigurations";
+ value = manifest: {
+ inherit (manifest.outputs) manifest packages;
+ };
+ };
+
+ # What a reader of a file gets from an hjem configuration that is
+ # a top: `manifest` and `packages`, which `hjem standalone switch
+ # --config` reads, and the manifest of the evaluation under
+ # `caisson.manifest`. hjem's CLI serializes the whole value with
+ # `nix eval --json`, and an attribute set with `outPath` is
+ # serialized as that string, so the live manifest stays out of
+ # what the CLI reads.
+ projection = manifest: {
+ inherit (manifest.outputs) manifest packages;
+ caisson = {
+ outPath = "caisson manifest: read caisson.manifest";
+ inherit manifest;
+ };
+ };
+
+ # A top evaluated at a single system is the projection of that
+ # evaluation; at several, the projections by system.
+ top =
+ evaluations:
+ let
+ systems = builtins.attrNames evaluations;
+ in
+ if builtins.length systems == 1 then
+ projection evaluations.${builtins.head systems}
+ else
+ builtins.mapAttrs (_: projection) evaluations;
+
+ configuration =
+ args:
+ final.caisson.integrations.mkModuleConfiguration {
+ type = "hjem";
+ # An hjem configuration is evaluated at a system: it has an
+ # evaluation for every system in force where it is declared.
+ perSystem = true;
+ defaultPkgs = args.defaultPkgs or null;
+ inherit exportsTo;
+ evaluate = evaluate args;
+ };
+
+ integration = final.caisson.integrations.mkIntegration {
+ name = "hjem";
+ class = "hjem";
+ inherit top;
+ # What these return is a configuration, a function of
+ # `{ name, parent }`: a parent that declares it under
+ # `caisson.hjem.configurations.` finalizes it, and
+ # `mkTopConfiguration` finalizes it at a top.
+ mkConfiguration =
+ {
+ # The configuration's module. When absent, the configuration
+ # registered under the configuration's name
+ # (`lib.caisson.configs.hjem.`), if any. Further
+ # modules of the class are selected with `moduleImports`,
+ # from the registry.
+ configModule ? null,
+ # The hjem flake or source tree; resolved from the
+ # composition's declarations when absent.
+ ecosystemSrc ? null,
+ # The package set the configuration runs on: a function
+ # that receives the package sets available where it is
+ # declared, by package config name, each at the system of
+ # the evaluation, and returns the set to run on. When
+ # absent, the selection of the nearest configuration above,
+ # and the set named `default` where none above selects.
+ defaultPkgs ? null,
+ # The selection over the hjem class of the registry. It
+ # replaces the default of the class, which is every entry
+ # named `default` followed by what the configurations
+ # above added.
+ moduleImports ? null,
+ # A selection added to that selection, whichever it is.
+ extraModuleImports ? null,
+ # Extra module arguments, merged over those the framework
+ # supplies.
+ specialArgs ? null,
+ }@args:
+ configuration args;
+ # The same arguments and `ecosystemArgs`, the evaluator's
+ # arguments merged over the composed call last.
+ mkConfigurationWithEcosystemArgs =
+ {
+ configModule ? null,
+ ecosystemSrc ? null,
+ defaultPkgs ? null,
+ moduleImports ? null,
+ extraModuleImports ? null,
+ specialArgs ? null,
+ ecosystemArgs ? null,
+ }@args:
+ configuration args;
+ extra = {
+ # The manifest of an evaluated user, from hjem's library and
+ # the user's `config`.
+ inherit manifestOf;
+ };
+ };
+ in
+ {
+ caisson = (prev.caisson or { }) // {
+ hjem = integration.namespace;
+ };
+ };
+
+}
diff --git a/modules/generic/core/default.nix b/modules/generic/core/default.nix
index 428fa24e..ffc7bf5c 100644
--- a/modules/generic/core/default.nix
+++ b/modules/generic/core/default.nix
@@ -34,6 +34,7 @@
"darwin"
"disko"
"flake-parts"
+ "hjem"
"home-manager"
"nixos"
"nixpkgs"
diff --git a/modules/hjem/core b/modules/hjem/core
new file mode 120000
index 00000000..3715354a
--- /dev/null
+++ b/modules/hjem/core
@@ -0,0 +1 @@
+../generic/core
\ No newline at end of file
diff --git a/modules/hjem/nixos-parent/default.nix b/modules/hjem/nixos-parent/default.nix
new file mode 100644
index 00000000..18235e8a
--- /dev/null
+++ b/modules/hjem/nixos-parent/default.nix
@@ -0,0 +1,45 @@
+# SPDX-License-Identifier: MIT
+#
+# An hjem module for an hjem configuration declared inside a NixOS
+# configuration. It sets the home directory of the user from the
+# account the machine declares, as hjem's NixOS module does for the
+# users it embeds, and refuses a user the machine has no account for.
+#
+# caisson does not use hjem's NixOS module. A NixOS configuration adds
+# this module to the modules that the hjem configurations declared
+# inside it import by default; caisson's `default` NixOS module does
+# that, through `caisson.forChildren`. An hjem configuration that
+# passes `moduleImports` selects its modules itself, and gets this
+# module only if it lists it.
+#
+# The module reads the machine through the manifest of the hjem
+# configuration, so it can only be imported by an hjem configuration
+# that has a NixOS configuration above it.
+{ ... }:
+# `lib` is the library of the hjem configuration, and `config` is its
+# configuration.
+{ lib, config, ... }:
+let
+ # The configuration of the NixOS configuration the hjem configuration
+ # is declared inside, in its childless evaluation, which leaves out
+ # the configurations declared inside it.
+ machine = lib.caisson.evalManifest.nearest.nixos.value.config;
+
+ # The account of the user, when the machine declares one.
+ account = (machine.users.users or { }).${config.user} or null;
+in
+{
+ config = lib.mkMerge [
+ {
+ assertions = [
+ {
+ assertion = config.enable -> account != null && (account.enable or true);
+ message = "The hjem user `${config.user}` has no enabled account in `users.users` of the NixOS configuration it is declared inside.";
+ }
+ ];
+ }
+ (lib.mkIf (account != null) {
+ directory = lib.mkDefault account.home;
+ })
+ ];
+}
diff --git a/modules/nixos/default/caisson/forChildren.nix b/modules/nixos/default/caisson/forChildren.nix
index 9b282789..3e50c996 100644
--- a/modules/nixos/default/caisson/forChildren.nix
+++ b/modules/nixos/default/caisson/forChildren.nix
@@ -3,10 +3,11 @@
# What a NixOS configuration gives the homes declared inside it.
#
# A configuration gives things to the configurations declared inside
-# it through the options under `caisson.forChildren`. This file sets
-# one of them: it adds two home-manager modules, `nixos-parent` and
-# `nixos-source-marker`, to the modules that a home declared inside
-# this NixOS configuration imports by default.
+# it through the options under `caisson.forChildren`. This file adds
+# two home-manager modules, `nixos-parent` and `nixos-source-marker`,
+# to the modules that a home declared inside this NixOS configuration
+# imports by default, and the hjem module `nixos-parent` to those of an
+# hjem configuration declared inside it.
#
# `nixos-source-marker` is the module caisson registers as
# `modules/homeManager/nixos-source-marker`. It adds a step to the
@@ -36,4 +37,11 @@
closure-lib.caisson.modules.homeManager.nixos-parent
closure-lib.caisson.modules.homeManager.nixos-source-marker
];
+
+ # The hjem module `nixos-parent` sets the home directory of an hjem
+ # configuration declared inside this machine from the account of its
+ # user, and refuses a user the machine has no account for.
+ caisson.forChildren.extraDefaultModuleImports.hjem = _lib: [
+ closure-lib.caisson.modules.hjem.nixos-parent
+ ];
}
diff --git a/modules/nixos/default/default.nix b/modules/nixos/default/default.nix
index 329b0dfb..ed0fa4c7 100644
--- a/modules/nixos/default/default.nix
+++ b/modules/nixos/default/default.nix
@@ -8,6 +8,12 @@
# `./networking` defaults the host name of the NixOS configuration to
# the name it is declared under, at `mkDefault` priority.
#
+# The registered module `hjem-activation` activates the hjem
+# configurations declared inside the machine, and
+# `./caisson/forChildren.nix` gives each of them the hjem module
+# `nixos-parent`, which sets the user's home directory from the
+# machine's account.
+#
# It also holds what a NixOS configuration does for the homes declared
# inside it, in three parts:
#
@@ -35,6 +41,7 @@
(mkModule ./networking)
(mkModule ./caisson)
closure-lib.caisson.modules.nixos.home-manager-activation
+ closure-lib.caisson.modules.nixos.hjem-activation
closure-lib.caisson.modules.nixos.home-manager-source-marker
]
else
diff --git a/modules/nixos/hjem-activation/default.nix b/modules/nixos/hjem-activation/default.nix
new file mode 100644
index 00000000..0eaea2f1
--- /dev/null
+++ b/modules/nixos/hjem-activation/default.nix
@@ -0,0 +1,265 @@
+# SPDX-License-Identifier: MIT
+#
+# A NixOS module with which a machine activates the hjem configurations
+# declared inside it.
+#
+# Activating an hjem configuration means applying its manifest with
+# hjem's CLI: `hjem internal activate` links the user's files and
+# replaces those of the previous generation, whose manifest is kept in
+# `/var/lib/hjem/manifest-.json`. hjem has a NixOS module that
+# does this for the users it embeds. caisson does not use that NixOS
+# module. An hjem configuration declared inside a NixOS configuration
+# is a caisson configuration, and this module writes the units hjem's
+# NixOS module writes, under the same names. Each template unit has an
+# instance for each user:
+#
+# - `hjem-activate@` runs as the user and links the files of the
+# new manifest, given the stored manifest of the previous one;
+# - `hjem-reload@` runs as the user after it and reloads the
+# user's systemd units the change touched;
+# - `hjem-update-state@` stores the new manifest as the state;
+#
+# and `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. The units
+# link with hjem's own CLI and the backup prefix of hjem's NixOS
+# module; hjem's options for another linker are not offered here.
+#
+# The module also installs each user's `packages` for that user and
+# keeps each user's `extraDependencies` in the closure of the system,
+# as hjem's NixOS module does.
+#
+# Which configurations: the hjem configurations this NixOS
+# configuration passes up, leaving out those declared inside a NixOS
+# configuration that is itself inside this one, and those evaluated for
+# another system. caisson's `default` NixOS module imports this module.
+# A NixOS configuration that passes `moduleImports` activates its hjem
+# configurations only if it lists it.
+#
+# The module reads the hjem configurations from the full evaluation of
+# the NixOS configuration; in the childless evaluation, and in an
+# evaluation without systemd options, it defines nothing.
+{ closure-lib, ... }:
+{
+ config,
+ lib,
+ options,
+ pkgs,
+ ...
+}:
+let
+ inherit (lib.caisson) evalManifest;
+
+ # True when this evaluation declares the options the module defines.
+ hasUnits =
+ options ? systemd
+ && options.systemd ? services
+ && options.systemd ? targets
+ && options ? users
+ && options ? system
+ && options.system ? extraDependencies;
+
+ isActivatedHere =
+ entry:
+ entry.manifest.type == "hjem"
+ && !(builtins.any (segment: segment.type == "nixos") (lib.lists.init entry.path))
+ && entry.manifest.system == evalManifest.system;
+
+ # What the units need of each enabled hjem configuration.
+ users =
+ builtins.map
+ (entry: {
+ inherit (entry.manifest.value.config)
+ user
+ directory
+ packages
+ extraDependencies
+ ;
+ inherit (entry.manifest.outputs) manifestFile cli;
+ })
+ (
+ builtins.filter (entry: isActivatedHere entry && entry.manifest.value.config.enable) (
+ config.caisson.exports.configurations
+ )
+ );
+
+ # The manifests of the previous generations, which the units read and
+ # write, and the manifests of this generation, one file per user.
+ oldManifests = "/var/lib/hjem";
+ newManifests = pkgs.linkFarm "hjem-manifests" (
+ builtins.map (user: {
+ name = "manifest-${user.user}.json";
+ path = user.manifestFile;
+ }) users
+ );
+ homeDirectories = builtins.map (user: user.directory) users;
+
+ # The prefix hjem's CLI gives the backup of a file it replaces, the
+ # default of hjem's NixOS module.
+ prefix = ".backup-";
+
+ # The start of each template unit: it stops for a user no hjem
+ # configuration here names, and sets `hjem` to the CLI of the
+ # configuration of the user. Each hjem configuration builds the CLI
+ # from the hjem source it is evaluated with.
+ selectUser = ''
+ case "$1" in
+ ${lib.concatMapStrings (user: ''
+ ${lib.escapeShellArg user.user}) hjem=${lib.getExe user.cli} ;;
+ '') users}
+ *) echo "User '$1' is not configured for hjem" >&2; exit 0 ;;
+ esac
+ '';
+
+ # The users that more than one hjem configuration names, which would
+ # both write the files of that user.
+ names = builtins.map (user: user.user) users;
+ repeated = lib.lists.unique (
+ builtins.filter (name: lib.lists.count (other: other == name) names > 1) names
+ );
+in
+{
+ # The module reads `config.caisson.exports.configurations`. The core
+ # module declares that option.
+ imports = [ closure-lib.caisson.modules.nixos.core ];
+
+ # `optionalAttrs` and not `mkIf` decides whether the options are
+ # defined at all: NixOS refuses a definition, even under `mkIf false`,
+ # of an option that no module declares.
+ config = lib.optionalAttrs hasUnits (
+ lib.mkIf (!evalManifest.childless && users != [ ]) {
+ assertions = [
+ {
+ assertion = repeated == [ ];
+ message = "caisson: this NixOS configuration activates more than one hjem configuration for ${lib.concatStringsSep ", " repeated}.";
+ }
+ ];
+
+ users.users = builtins.listToAttrs (
+ builtins.map (user: {
+ name = user.user;
+ value.packages = user.packages;
+ }) users
+ );
+
+ system.extraDependencies = builtins.concatMap (user: user.extraDependencies) users;
+
+ systemd.targets.hjem = {
+ description = "hjem file management";
+ unitConfig.X-StopOnReconfiguration = true;
+ after = [ "local-fs.target" ];
+ wantedBy = [
+ "sysinit-reactivation.target"
+ "multi-user.target"
+ ];
+ before = [ "sysinit-reactivation.target" ];
+ requires =
+ builtins.concatMap (user: [
+ "hjem-activate@${user.user}.service"
+ "hjem-reload@${user.user}.service"
+ "hjem-update-state@${user.user}.service"
+ ]) users
+ ++ [ "hjem-cleanup.service" ];
+ };
+
+ systemd.services = {
+ hjem-prepare = {
+ description = "Prepare Hjem manifests directory";
+ enableStrictShellChecks = true;
+ script = "mkdir -p ${oldManifests}";
+ serviceConfig.Type = "oneshot";
+ unitConfig.RefuseManualStart = true;
+ };
+
+ "hjem-activate@" = {
+ description = "Link files for %i from their manifest";
+ enableStrictShellChecks = true;
+ unitConfig.RequiresMountsFor = homeDirectories;
+ serviceConfig = {
+ User = "%i";
+ Type = "oneshot";
+ };
+ requires = [ "hjem-prepare.service" ];
+ after = [ "hjem-prepare.service" ];
+ scriptArgs = "%i";
+ script = ''
+ ${selectUser}
+ "$hjem" internal activate \
+ --manifest "${newManifests}/manifest-$1.json" \
+ --state "${oldManifests}/manifest-$1.json" \
+ --skip-state-update \
+ --prefix "${prefix}" \
+ --json
+ '';
+ };
+
+ "hjem-reload@" = {
+ description = "Reload systemd user units for %i after Hjem file activation";
+ enableStrictShellChecks = true;
+ serviceConfig = {
+ User = "%i";
+ Type = "oneshot";
+ };
+ unitConfig.RequiresMountsFor = homeDirectories;
+ requires = [ "hjem-activate@%i.service" ];
+ after = [ "hjem-activate@%i.service" ];
+ path = [
+ config.systemd.package
+ pkgs.coreutils-full
+ ];
+ scriptArgs = "%i";
+ script = ''
+ ${selectUser}
+ XDG_RUNTIME_DIR=$(loginctl show-user "$1" --property=RuntimePath --value 2>/dev/null || true)
+ if [ -z "$XDG_RUNTIME_DIR" ]; then
+ echo "Could not determine XDG runtime directory for $1. Skipping."
+ exit 0
+ fi
+ export XDG_RUNTIME_DIR
+
+ systemd_status=$(systemctl --user is-system-running 2>&1 || true)
+ if [ "$systemd_status" != "running" ] && [ "$systemd_status" != "degraded" ]; then
+ echo "User systemd for $1 is not running (status: $systemd_status). Skipping."
+ exit 0
+ fi
+
+ "$hjem" internal reload-actions \
+ --old-manifest "${oldManifests}/manifest-$1.json" \
+ --new-manifest "${newManifests}/manifest-$1.json" \
+ --user "$1" \
+ --require-running-systemd
+ '';
+ };
+
+ "hjem-update-state@" = {
+ description = "Update Hjem state manifest for %i";
+ enableStrictShellChecks = true;
+ serviceConfig.Type = "oneshot";
+ requires = [ "hjem-reload@%i.service" ];
+ unitConfig.RequiresMountsFor = homeDirectories;
+ after = [ "hjem-reload@%i.service" ];
+ scriptArgs = "%i";
+ script = ''
+ ${selectUser}
+ "$hjem" internal update-state \
+ --manifest "${newManifests}/manifest-$1.json" \
+ --state "${oldManifests}/manifest-$1.json"
+ '';
+ };
+
+ hjem-cleanup = {
+ description = "Cleanup disabled users' manifests";
+ enableStrictShellChecks = true;
+ serviceConfig.Type = "oneshot";
+ after = [ "hjem.target" ];
+ unitConfig.RefuseManualStart = false;
+ script = ''
+ ${lib.getExe (builtins.head users).cli} internal cleanup-state \
+ --state-dir ${oldManifests} \
+ ${lib.concatMapStringsSep " " (user: "--enabled-user ${lib.escapeShellArg user.user}") users}
+ '';
+ };
+ };
+ }
+ );
+}
diff --git a/tests/argument-errors/check.sh b/tests/argument-errors/check.sh
index 3b99d8e1..1a0be246 100755
--- a/tests/argument-errors/check.sh
+++ b/tests/argument-errors/check.sh
@@ -21,17 +21,18 @@ shift
flags=("$@")
# Every entry point takes `configModule`. The structural, caisson-colmena,
-# caisson-disko, disko, flake-parts, nixos, nixos-minimal, darwin,
+# caisson-disko, disko, flake-parts, nixos, nixos-minimal, darwin, hjem,
# home-manager and home-manager-minimal entry points require nothing:
# each finds the configuration registered under the name of the
# configuration. The others require `configModule`, and take `pkgSets`,
-# which the nixos, darwin and home-manager entry points do not: a NixOS
-# configuration, a darwin configuration and a home take their package
-# sets from the composition.
+# which the nixos, darwin, hjem and home-manager entry points do not: a
+# NixOS configuration, a darwin configuration, an hjem configuration and
+# a home take their package sets from the composition.
namespaces=(
nixos
nixos-minimal
darwin
+ hjem
home-manager
home-manager-minimal
flake-parts
@@ -91,13 +92,13 @@ missing="function '[^']*%s' called without required argument '(configModule|pkgS
# The arguments a call passes beside the wrong argument.
given_for() {
case "$1" in
- nixos | nixos-minimal | darwin | home-manager | home-manager-minimal) printf 'configModule = { };' ;;
+ nixos | nixos-minimal | darwin | hjem | home-manager | home-manager-minimal) printf 'configModule = { };' ;;
*) printf 'configModule = { }; pkgSets.pkgs = { };' ;;
esac
}
requires_nothing() {
case "$1" in
- structural | caisson-colmena | caisson-disko | disko | flake-parts | nixos | nixos-minimal | darwin | home-manager | home-manager-minimal) return 0 ;;
+ structural | caisson-colmena | caisson-disko | disko | flake-parts | nixos | nixos-minimal | darwin | hjem | home-manager | home-manager-minimal) return 0 ;;
*) return 1 ;;
esac
}
diff --git a/tests/dependencies/flake.lock b/tests/dependencies/flake.lock
index 36213121..c64eaf79 100644
--- a/tests/dependencies/flake.lock
+++ b/tests/dependencies/flake.lock
@@ -195,6 +195,26 @@
"type": "github"
}
},
+ "hjem": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1791807571,
+ "narHash": "sha256-Q5hGcKBaqz1L0neSRDfQgQPBKmSCkV1N5YBRwp+RtUc=",
+ "owner": "feel-co",
+ "repo": "hjem",
+ "rev": "6b2cc57463bbbac3d7c61206ab365a2c928861a4",
+ "type": "github"
+ },
+ "original": {
+ "owner": "feel-co",
+ "repo": "hjem",
+ "type": "github"
+ }
+ },
"home-manager": {
"inputs": {
"nixpkgs": [
@@ -379,6 +399,7 @@
"colmena": "colmena",
"disko": "disko",
"flake-parts": "flake-parts",
+ "hjem": "hjem",
"home-manager": "home-manager",
"nix-darwin": "nix-darwin",
"nix-unit": "nix-unit",
diff --git a/tests/dependencies/flake.nix b/tests/dependencies/flake.nix
index b2df2947..b09d16b2 100644
--- a/tests/dependencies/flake.nix
+++ b/tests/dependencies/flake.nix
@@ -21,6 +21,8 @@
inputs.system-manager.inputs.nixpkgs.follows = "nixpkgs";
inputs.disko.url = "github:nix-community/disko";
inputs.disko.inputs.nixpkgs.follows = "nixpkgs";
+ inputs.hjem.url = "github:feel-co/hjem";
+ inputs.hjem.inputs.nixpkgs.follows = "nixpkgs";
inputs.nix-darwin.url = "github:nix-darwin/nix-darwin";
inputs.nix-darwin.inputs.nixpkgs.follows = "nixpkgs";
outputs = { ... }: { };
diff --git a/tests/hjem-system/default.nix b/tests/hjem-system/default.nix
new file mode 100644
index 00000000..1194fa41
--- /dev/null
+++ b/tests/hjem-system/default.nix
@@ -0,0 +1,121 @@
+# SPDX-License-Identifier: MIT
+#
+# hjem configurations declared through caisson's hjem integration, for
+# the pinned-world suite and the hjem manifest check. `inputs` is the
+# dependency pool of the checks partition with `caisson` set to the
+# tree itself.
+#
+# The composition is at x86_64-linux, with a package config whose set
+# there is nixpkgs, and with nixpkgs and hjem declared as its
+# ecosystems. `user` is an hjem module with a file and a package.
+# `standalone` is an hjem configuration that is a top, for the user
+# `probe` with the home directory it sets itself. `top` is a structural
+# top holding a NixOS machine `probe-machine`, which declares the
+# account of `probe` and an hjem configuration `probe` from
+# `machineUser`, which is `user` with a systemd user unit.
+{ inputs }:
+let
+ core = inputs.caisson.lib.caisson-core;
+
+ system = "x86_64-linux";
+
+ pkgs = import inputs.nixpkgs { inherit system; };
+
+ lib = core.mkLib {
+ sources = { };
+ systems = [ system ];
+ projects.caisson = inputs.caisson;
+ defaultEcosystemSrc = {
+ inherit (inputs) nixpkgs hjem;
+ };
+ pkgSets = _lib: {
+ default =
+ { name, parent }:
+ {
+ _type = "caisson-manifest";
+ type = "nixpkgs";
+ inherit name parent;
+ children.nixpkgs.${system} = {
+ _type = "caisson-manifest";
+ type = "nixpkgs";
+ name = system;
+ value = pkgs;
+ };
+ };
+ };
+ };
+
+ user = {
+ files.".config/probe/greeting".text = "hello from caisson";
+ xdg.config.files."probe/settings".source = pkgs.writeText "settings" "probe";
+ packages = [ pkgs.hello ];
+ };
+
+ # The user inside the machine also declares a systemd user unit, which
+ # only a user inside a NixOS machine can declare.
+ machineUser = {
+ imports = [ user ];
+ systemd.services.probe = {
+ description = "probe";
+ script = "true";
+ wantedBy = [ "default.target" ];
+ };
+ };
+
+ standalone = lib.caisson.hjem.mkTopConfiguration {
+ configModule = {
+ imports = [ user ];
+ user = "probe";
+ directory = "/home/probe";
+ };
+ };
+
+ machine = {
+ boot.loader.grub.enable = false;
+ fileSystems."/" = {
+ device = "none";
+ fsType = "tmpfs";
+ };
+ system.stateVersion = "25.05";
+ users.users.probe = {
+ isNormalUser = true;
+ home = "/home/probe";
+ };
+ };
+
+ top = lib.caisson.structural.mkTopConfiguration {
+ moduleImports = _modules: [ ];
+ configModule =
+ { lib, ... }:
+ {
+ caisson.nixos.configurations.probe-machine = lib.caisson.nixos.mkConfiguration {
+ configModule =
+ { lib, ... }:
+ {
+ imports = [ machine ];
+ caisson.hjem.configurations.probe = lib.caisson.hjem.mkConfiguration {
+ configModule = machineUser;
+ };
+ };
+ };
+ };
+ };
+in
+{
+ inherit
+ lib
+ machine
+ machineUser
+ pkgs
+ standalone
+ system
+ top
+ user
+ ;
+ # The machine and the hjem configuration `probe` inside it, as their
+ # manifests. A configuration evaluated at a system sits under that
+ # system in the manifest of its parent.
+ machineManifest = top.caisson.manifest.children.system.${system}.children.nixos.probe-machine;
+ inner =
+ top.caisson.manifest.children.system.${system}.children.nixos.probe-machine.children.system.${system}.children.hjem.probe;
+}
diff --git a/tests/pinned-world/default.nix b/tests/pinned-world/default.nix
index 843f9575..d0dab88f 100644
--- a/tests/pinned-world/default.nix
+++ b/tests/pinned-world/default.nix
@@ -61,6 +61,7 @@ let
registered."caisson/nixos"
registered."caisson/nixos-minimal"
registered."caisson/home-manager"
+ registered."caisson/hjem"
registered."caisson/caisson-colmena"
registered."caisson/colmena"
registered."caisson/caisson-disko"
@@ -93,6 +94,7 @@ let
"evalManifest"
"finalizeTop"
"flake-parts"
+ "hjem"
"home-manager"
"importApply"
"integrations"
@@ -464,6 +466,7 @@ let
"caisson/nixos"
"caisson/nixos-minimal"
"caisson/home-manager"
+ "caisson/hjem"
"caisson/caisson-colmena"
"caisson/colmena"
"caisson/caisson-disko"
@@ -560,6 +563,7 @@ let
"nixos"
"nixos-minimal"
"home-manager"
+ "hjem"
"caisson-colmena"
"colmena"
"caisson-disko"
@@ -884,6 +888,7 @@ let
hiveLib.caisson.nixos ? mkTopConfiguration
&& hiveLib.caisson.darwin ? mkTopConfiguration
&& hiveLib.caisson.home-manager ? mkTopConfiguration
+ && hiveLib.caisson.hjem ? mkTopConfiguration
&& hiveLib.caisson.structural ? mkTopConfiguration
&& hiveLib.caisson.flake-parts ? mkTopConfiguration
&& !(hiveLib.caisson.caisson-colmena ? mkTopConfiguration)
@@ -1403,6 +1408,75 @@ let
&& probe.configuration.config.nixpkgs.hostPlatform.system == "aarch64-darwin"
&& probe.configuration.pkgs.hello.drvPath == probe.pkgs.hello.drvPath;
+ # An hjem configuration that is a top gives what hjem's standalone
+ # CLI reads from a file: the version 3 manifest of the user's files,
+ # with targets in the user's home directory, and the packages. The
+ # manifest of the evaluation sits under `caisson.manifest`, behind an
+ # `outPath`, so the JSON the CLI reads holds a string in its place.
+ hjemTopIsWhatHjemsCliReads =
+ let
+ probe = import ../hjem-system { inherit inputs; };
+ top = probe.standalone;
+ targets = builtins.map (file: file.target) top.manifest.files;
+ in
+ top.manifest.version == 3
+ &&
+ builtins.sort builtins.lessThan targets == [
+ "/home/probe/.config/probe/greeting"
+ "/home/probe/.config/probe/settings"
+ ]
+ && builtins.all (file: file.type == "symlink") top.manifest.files
+ && builtins.map (package: package.pname) top.packages == [ "hello" ]
+ && builtins.toJSON top.caisson == builtins.toJSON "caisson manifest: read caisson.manifest"
+ && top.caisson.manifest.type == "hjem";
+
+ # An hjem configuration inside a NixOS machine takes the home
+ # directory of the account the machine declares, and its manifest is
+ # the one hjem's own NixOS module makes from the same user module,
+ # the systemd user unit of the user included. The machine activates
+ # it with the template units hjem's NixOS module writes, which read
+ # the manifest file from the directory of this generation, installs
+ # the user's packages, and the top publishes it as
+ # `hjemConfigurations.probe`.
+ hjemInsideNixosIsActivated =
+ let
+ probe = import ../hjem-system { inherit inputs; };
+ inner = probe.inner;
+ machine = probe.top.nixosConfigurations.probe-machine;
+ services = machine.config.systemd.services;
+ upstream = import "${inputs.nixpkgs}/nixos/lib/eval-config.nix" {
+ inherit (probe) system;
+ modules = [
+ inputs.hjem.nixosModules.default
+ probe.machine
+ { hjem.users.probe = probe.machineUser; }
+ ];
+ };
+ hjemLib = import "${inputs.hjem}/lib.nix" {
+ inherit (probe.pkgs) lib;
+ inherit (probe) pkgs;
+ };
+ upstreamManifest = probe.lib.caisson.hjem.manifestOf hjemLib upstream.config.hjem.users.probe;
+ targets = builtins.map (file: file.target) inner.outputs.manifest.files;
+ target = machine.config.systemd.targets.hjem;
+ in
+ inner.value.config.directory == "/home/probe"
+ && builtins.toJSON inner.outputs.manifest == builtins.toJSON upstreamManifest
+ && builtins.elem "/home/probe/.config/systemd/user/probe.service" targets
+ && builtins.all (name: services ? ${name}) [
+ "hjem-prepare"
+ "hjem-activate@"
+ "hjem-reload@"
+ "hjem-update-state@"
+ "hjem-cleanup"
+ ]
+ && builtins.elem "hjem-activate@probe.service" target.requires
+ && probe.pkgs.lib.strings.hasInfix "hjem-manifests" services."hjem-activate@".script
+ && builtins.elem "hello" (
+ builtins.map (package: package.pname or "") machine.config.users.users.probe.packages
+ )
+ && builtins.attrNames probe.top.hjemConfigurations == [ "probe" ];
+
terranixConfigurationEvaluatesEndToEnd =
let
terraform = composed.lib.caisson.terranix.mkConfiguration {
diff --git a/tests/unit/lib-overlays.nix b/tests/unit/lib-overlays.nix
index 35aa58d1..f5eef058 100644
--- a/tests/unit/lib-overlays.nix
+++ b/tests/unit/lib-overlays.nix
@@ -114,6 +114,7 @@ let
"nixos"
"nixos-minimal"
"darwin"
+ "hjem"
"home-manager"
"home-manager-minimal"
"flake-parts"
@@ -5264,6 +5265,7 @@ in
"nixos"
"nixos-minimal"
"darwin"
+ "hjem"
"home-manager"
"home-manager-minimal"
];
@@ -5271,6 +5273,7 @@ in
"nixos"
"nixos-minimal"
"darwin"
+ "hjem"
"home-manager"
"home-manager-minimal"
];