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/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";
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"
];