Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib-overlays/darwin/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@
perSystem = true;
defaultPkgs = args.defaultPkgs or null;
inherit exportsTo;
# A darwin configuration is activated on a machine; the
# configurations with an activation declared inside it check
# its marker.
activation = final.caisson.integrations.machineActivation;
evaluate = evaluate args;
};

Expand Down
16 changes: 16 additions & 0 deletions lib-overlays/hjem/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -292,9 +292,25 @@
perSystem = true;
defaultPkgs = args.defaultPkgs or null;
inherit exportsTo;
# A hjem configuration is activated for its user. Its
# identity is its manifest file; its marker is a file in the
# directory of the user, which the hjem module
# `activation-marker` adds to the files of the configuration.
# The configurations with an activation declared inside it
# check the marker.
activation = {
identity = manifest: manifest.outputs.manifestFile.outPath;
marker =
manifest:
"${manifest.value.config.directory}/${markerDirectory}/${final.caisson.integrations.markerFileName manifest}";
};
evaluate = evaluate args;
};

# The directory, relative to the directory of the user, that holds
# the marker of a hjem configuration.
markerDirectory = ".local/state/caisson/markers";

integration = final.caisson.integrations.mkIntegration {
name = "hjem";
class = "hjem";
Expand Down
11 changes: 11 additions & 0 deletions lib-overlays/home-manager/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,17 @@
perSystem = true;
defaultPkgs = args.defaultPkgs or null;
inherit exportsTo;
# A home is activated for its user. Its identity is its
# activation package; its marker is a file in the state
# directory of the user, which the home-manager module
# `activation-marker` writes. The configurations with an
# activation declared inside the home check it.
activation = {
identity = manifest: manifest.outputs.activationPackage.outPath;
marker =
manifest:
"\${XDG_STATE_HOME:-$HOME/.local/state}/caisson/markers/${selection.markerFileName manifest}";
};
evaluate = evaluation variant args;
};

Expand Down
164 changes: 163 additions & 1 deletion lib-overlays/integrations/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -265,19 +265,37 @@
# one output. It is recorded on the manifest, and applied where
# the configuration is passed up (`entriesOf`). Null for a
# configuration that is not published under a name.
#
# `activation` is given by an integration whose configurations
# are activated on a machine or for a user: NixOS, darwin and
# system-manager machines, homes, hjem configurations. It holds
# two functions of a manifest of the configuration. `identity`
# returns the store path that names what the activation applies,
# computed from the childless evaluation, or null where there is
# none. `standsAlone`, when true, says the configuration is
# applied on its own, as a machine is on its own host, so that it
# checks no configuration above it. `marker` returns the path of the file the activation
# writes that identity into, as shell text, which may name
# variables such as `$HOME`. It is recorded on the manifest. A
# configuration with an activation declared beneath one checks
# the marker of the nearest such configuration above it when it
# is activated (`driftCheckOf`). Null for a configuration that is
# not activated.
mkModuleConfiguration =
{
type,
perSystem ? false,
defaultPkgs ? null,
exportsTo ? null,
activation ? null,
evaluate,
}:
final.caisson-core.mkConfiguration {
inherit type perSystem;
record =
(if defaultPkgs == null then { } else { inherit defaultPkgs; })
// (if exportsTo == null then { } else { inherit exportsTo; });
// (if exportsTo == null then { } else { inherit exportsTo; })
// (if activation == null then { } else { inherit activation; });
evaluate =
view:
let
Expand Down Expand Up @@ -597,6 +615,144 @@
found
) null types;

# The nearest configuration above `manifest` that has an
# activation (`mkModuleConfiguration`'s `activation`), as its
# childless manifest, or null when there is none. A home inside a
# NixOS machine finds the machine; an nvf configuration inside
# that home finds the home.
activatingAncestorOf =
manifest:
let
found = builtins.filter (ancestor: (ancestor.activation or null) != null) (
manifest.ancestors or [ ]
);
in
if found == [ ] then null else last found;

# The `activation` of a machine: a NixOS, darwin or system-manager
# configuration. Its identity is its system, `toplevel`, null for
# a configuration that builds none, as the nixos-minimal
# integration can produce. Its marker is a file under `/etc`,
# which the module `activation-marker` of its class writes.
#
# A machine is activated on its own host. A machine declared
# inside another, such as an image, is applied on a different host
# than the machine above, and the marker under `/etc` that it
# finds there is its own, so it checks nothing above it:
# `standsAlone`.
machineActivation = {
standsAlone = true;
identity =
manifest: if (manifest.outputs or { }) ? toplevel then manifest.outputs.toplevel.outPath else null;
marker = _manifest: "/etc/caisson/marker.json";
};

# The file name of the marker of a configuration activated for a
# user: the integration and the name the configuration is declared
# under, with the characters the shell would read inside double
# quotes, and the path separator, replaced.
markerFileName =
manifest:
"${manifest.type}-${
builtins.replaceStrings [ "/" "\"" "$" "`" "\\" ] [ "_" "_" "_" "_" "_" ] (
manifest.name or "default"
)
}.json";

# The digest a marker holds for an identity: the SHA-256 of its
# store path. Computing the store path evaluates the childless
# evaluation and builds nothing, and the digest refers to no store
# path, so a marker that holds it depends on nothing. The digest
# is compared, never read as a path.
digestOf = identity: builtins.hashString "sha256" identity;

# What the activation of the configuration of `manifest` writes
# into its marker: the digest of the identity of its childless
# evaluation (`digestOf`). `path` is the shell text of the path of
# the marker. Null when the configuration has no activation or no
# identity.
markerOf =
manifest:
let
activation = manifest.activation or null;
childless = manifest.childlessManifest or manifest;
identity = if activation == null then null else activation.identity childless;
in
if identity == null then
null
else
{
path = activation.marker childless;
identity = digestOf identity;
text = builtins.toJSON { identity = digestOf identity; };
};

# True when `entry`, an entry of `caisson.exports.configurations`
# of the configuration of `manifest`, checks the marker of that
# configuration when it is activated: the entry has an activation,
# and the configuration is the nearest one above it that has an
# activation.
checksMarkerOf =
manifest: entry:
let
ancestor = activatingAncestorOf entry.manifest;
in
(entry.manifest.activation or null) != null
&& !(entry.manifest.activation.standsAlone or false)
&& ancestor != null
&& builtins.length (ancestor.ancestors or [ ]) == builtins.length (manifest.ancestors or [ ]);

# The shell text with which the activation of the configuration of
# `manifest` checks for drift: it compares the marker of the
# nearest configuration above with an activation against the
# identity of that configuration as this configuration was built
# for it, and prints a warning to standard error when the marker is
# missing or names another identity. It does not stop the
# activation. When that configuration activates this one, its
# marker already belongs to the generation being activated, and
# the check prints nothing. Empty when there is no such
# configuration, or it has no identity. `jq` is the path of a jq
# executable, and `optionText` names the option that turns the
# warning off, for the message.
driftCheckOf =
{ jq, optionText }:
manifest:
let
ancestor =
if (manifest.activation.standsAlone or false) then null else activatingAncestorOf manifest;
expected = if ancestor == null then null else ancestor.activation.identity ancestor;
marker = ancestor.activation.marker ancestor;
above = "the ${ancestor.type} configuration `${ancestor.name or "(unnamed)"}`";
self = "the ${manifest.type} configuration `${manifest.name or "(unnamed)"}`";
# The digest of the identity, quoted for the shell. A digest is
# hexadecimal, so quoting it whole is enough.
escaped = "'${digestOf expected}'";
in
if expected == null then
""
else
''
_caissonMarker="${marker}"
if [ ! -f "$_caissonMarker" ]; then
echo "WARNING: $_caissonMarker does not exist." >&2
echo " ${self} is declared inside ${above}," >&2
echo " and the activation of ${above} has not recorded what it applied." >&2
echo " Activate ${above} as this configuration was built with it," >&2
echo " or set ${optionText} to false in ${self}." >&2
else
_caissonApplied="$(${jq} -r '.identity // empty' < "$_caissonMarker")"
if [ "$_caissonApplied" != ${escaped} ]; then
echo "WARNING: ${self} was built for a version of ${above} that differs from the version that is applied." >&2
echo " digest of the version applied (configurations declared inside it left out):" >&2
echo " ''${_caissonApplied:-unknown}" >&2
echo " digest of the version built for (configurations declared inside it left out):" >&2
echo " "${escaped} >&2
echo " Activate ${above} as this configuration was built with it," >&2
echo " or set ${optionText} to false in ${self}." >&2
fi
fi
'';

isManifest = value: builtins.isAttrs value && (value._type or null) == "caisson-manifest";

# How a configuration is published: the exports its manifest
Expand Down Expand Up @@ -895,6 +1051,12 @@
integrationsIn
integrationsDeclaringIn
nearestOf
activatingAncestorOf
machineActivation
markerFileName
markerOf
checksMarkerOf
driftCheckOf
mkEvaluation
publish
topValue
Expand Down
4 changes: 4 additions & 0 deletions lib-overlays/nixos/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@
perSystem = true;
defaultPkgs = args.defaultPkgs or null;
inherit exportsTo;
# A NixOS configuration is activated on a machine; the
# configurations with an activation declared inside it check
# its marker.
activation = final.caisson.integrations.machineActivation;
evaluate = evaluate variant args;
};

Expand Down
4 changes: 4 additions & 0 deletions lib-overlays/system-manager/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,10 @@
perSystem = true;
defaultPkgs = args.defaultPkgs or null;
inherit exportsTo;
# A system-manager configuration is activated on a machine;
# the configurations with an activation declared inside it
# check its marker.
activation = final.caisson.integrations.machineActivation;
evaluate = evaluate args;
};

Expand Down
1 change: 1 addition & 0 deletions modules/darwin/activation-marker
1 change: 1 addition & 0 deletions modules/darwin/default/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
(mkModule ./caisson)
closure-lib.caisson.modules.darwin.home-manager-activation
closure-lib.caisson.modules.darwin.hjem-activation
closure-lib.caisson.modules.darwin.activation-marker
]
else
[ ];
Expand Down
36 changes: 36 additions & 0 deletions modules/hjem/activation-marker/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# SPDX-License-Identifier: MIT
#
# A hjem module with which a hjem configuration records what it
# applied, so that the configurations declared inside it can detect
# drift. It is the counterpart, for a hjem configuration, of the
# machine module `activation-marker`: see there for why.
#
# The module adds a file to the files of the configuration,
# `.local/state/caisson/markers/hjem-<name>.json` in the directory of
# the user, holding the identity of the childless evaluation of the
# configuration: its manifest file without the configurations declared
# inside it. hjem links the file into place when it applies the
# configuration.
#
# caisson's `default` hjem module imports this module. The file is
# added only when the configuration has a configuration declared inside
# it that checks it, and not in the childless evaluation.
{ ... }:
{
config,
lib,
...
}:
let
inherit (lib.caisson) evalManifest;
selection = lib.caisson.integrations;

checked = builtins.any (selection.checksMarkerOf evalManifest) config.caisson.exports.configurations;

marker = selection.markerOf evalManifest;
in
{
config = lib.mkIf (!evalManifest.childless && checked && marker != null) {
files.".local/state/caisson/markers/${selection.markerFileName evalManifest}".text = marker.text;
};
}
15 changes: 15 additions & 0 deletions modules/hjem/default/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# SPDX-License-Identifier: MIT
#
# The `default` hjem module caisson registers. A hjem configuration
# imports every registered module named `default` unless it passes
# `moduleImports`, so this is what it gets from caisson when it does not
# select its modules itself: `activation-marker`, which records what the
# hjem configuration applied, for the configurations declared inside it
# to check.
{ closure-lib, ... }:
{ ... }:
{
imports = [
closure-lib.caisson.modules.hjem.activation-marker
];
}
60 changes: 60 additions & 0 deletions modules/homeManager/activation-drift/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# SPDX-License-Identifier: MIT
#
# A home-manager module with which a home declared inside another
# configuration that has an activation, such as a NixOS, darwin or
# system-manager machine, detects drift when it is activated.
#
# The user can activate such a home by hand, with `home-manager switch`,
# built from the source the user has checked out. That source can
# describe a different version of the configuration above than the one
# that is applied. The configuration above records what it applied in a
# marker (the module `activation-marker` of its class). A step of the
# activation of the home compares that marker with the identity of the
# configuration above as the home was built for it, and prints a
# warning when they differ or when the marker is missing. The step does
# not stop the activation. When the configuration above activates the
# home, its marker belongs to the generation being activated, and the
# step prints nothing.
#
# The check is `lib.caisson.integrations.driftCheckOf`, the same check
# every configuration with an activation runs against the nearest
# configuration above it that has one. A home with no such
# configuration above it has nothing to check, and the module then
# defines no step.
#
# caisson's `default` home-manager module imports this module.
{ ... }:
{
config,
lib,
pkgs,
...
}:
let
check = lib.caisson.integrations.driftCheckOf {
jq = lib.getExe' pkgs.jq "jq";
optionText = "`caisson.drift.warn`";
} lib.caisson.evalManifest;
in
{
options.caisson.drift.warn = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether the activation of this home prints a warning when the home
was built for a version of the configuration it is declared inside
that differs from the version that is applied.

The configuration above, such as the machine, records what it
applied in a marker file. The activation of this home compares the
marker with the configuration above as it is in the source this
home was built from. The comparison leaves out the configurations
declared inside the configuration above, so a change to a home
does not cause a warning.
'';
};

config = lib.mkIf (config.caisson.drift.warn && check != "") {
home.activation.caissonDrift = lib.hm.dag.entryBefore [ "writeBoundary" ] check;
};
}
Loading
Loading