mirror of
https://github.com/RGBCube/hjem
synced 2025-10-13 13:12:16 +00:00
336 lines
10 KiB
Nix
336 lines
10 KiB
Nix
{
|
|
config,
|
|
pkgs,
|
|
lib,
|
|
...
|
|
}: let
|
|
inherit (lib.attrsets) filterAttrs mapAttrsToList;
|
|
inherit (lib.modules) mkIf mkMerge;
|
|
inherit (lib.options) literalExpression mkOption;
|
|
inherit (lib.strings) optionalString;
|
|
inherit (lib.trivial) pipe;
|
|
inherit (lib.types) attrs attrsOf bool listOf nullOr package raw submoduleWith either singleLineStr;
|
|
inherit (lib.meta) getExe;
|
|
inherit (builtins) filter attrNames attrValues mapAttrs getAttr concatLists concatStringsSep typeOf toJSON concatMap;
|
|
|
|
cfg = config.hjem;
|
|
|
|
enabledUsers = filterAttrs (_: u: u.enable) cfg.users;
|
|
disabledUsers = filterAttrs (_: u: !u.enable) cfg.users;
|
|
|
|
linker = getExe cfg.linker;
|
|
|
|
manifests = let
|
|
mapFiles = _: files:
|
|
lib.attrsets.foldlAttrs (
|
|
accum: _: value:
|
|
if value.enable -> value.source == null
|
|
then accum
|
|
else
|
|
accum
|
|
++ lib.singleton {
|
|
type = "symlink";
|
|
inherit (value) source target;
|
|
}
|
|
) []
|
|
files;
|
|
|
|
writeManifest = username: let
|
|
name = "manifest-${username}.json";
|
|
in
|
|
pkgs.writeTextFile {
|
|
inherit name;
|
|
destination = "/${name}";
|
|
text = builtins.toJSON {
|
|
clobber_by_default = cfg.users."${username}".clobberFiles;
|
|
version = 1;
|
|
files = mapFiles username cfg.users."${username}".files;
|
|
};
|
|
checkPhase = ''
|
|
set -e
|
|
CUE_CACHE_DIR=$(pwd)/.cache
|
|
CUE_CONFIG_DIR=$(pwd)/.config
|
|
|
|
${lib.getExe pkgs.cue} vet -c ${../../manifest/v1.cue} $target
|
|
'';
|
|
};
|
|
in
|
|
pkgs.symlinkJoin
|
|
{
|
|
name = "hjem-manifests";
|
|
paths = map writeManifest (builtins.attrNames enabledUsers);
|
|
};
|
|
|
|
hjemModule = submoduleWith {
|
|
description = "Hjem NixOS module";
|
|
class = "hjem";
|
|
specialArgs =
|
|
cfg.specialArgs
|
|
// {
|
|
inherit pkgs;
|
|
osConfig = config;
|
|
};
|
|
modules =
|
|
concatLists
|
|
[
|
|
[
|
|
../common/user.nix
|
|
({name, ...}: let
|
|
user = getAttr name config.users.users;
|
|
in {
|
|
user = user.name;
|
|
directory = user.home;
|
|
clobberFiles = cfg.clobberByDefault;
|
|
})
|
|
]
|
|
# Evaluate additional modules under 'hjem.users.<name>' so that
|
|
# module systems built on Hjem are more ergonomic.
|
|
cfg.extraModules
|
|
];
|
|
};
|
|
in {
|
|
options.hjem = {
|
|
clobberByDefault = mkOption {
|
|
type = bool;
|
|
default = false;
|
|
description = ''
|
|
The default override behaviour for files managed by Hjem.
|
|
|
|
While `true`, existing files will be overriden with new files on rebuild.
|
|
The behaviour may be modified per-user by setting {option}`hjem.users.<name>.clobberFiles`
|
|
to the desired value.
|
|
'';
|
|
};
|
|
|
|
users = mkOption {
|
|
default = {};
|
|
type = attrsOf hjemModule;
|
|
description = "Home configurations to be managed";
|
|
};
|
|
|
|
extraModules = mkOption {
|
|
type = listOf raw;
|
|
default = [];
|
|
description = ''
|
|
Additional modules to be evaluated as a part of the users module
|
|
inside {option}`config.hjem.users.<name>`. This can be used to
|
|
extend each user configuration with additional options.
|
|
'';
|
|
};
|
|
|
|
specialArgs = mkOption {
|
|
type = attrs;
|
|
default = {};
|
|
example = literalExpression "{ inherit inputs; }";
|
|
description = ''
|
|
Additional `specialArgs` are passed to Hjem, allowing extra arguments
|
|
to be passed down to to all imported modules.
|
|
'';
|
|
};
|
|
|
|
linker = mkOption {
|
|
default = null;
|
|
description = ''
|
|
Method to use to link files.
|
|
|
|
`null` will use `systemd-tmpfiles`, which is only supported on Linux.
|
|
|
|
This is the default file linker on Linux, as it is the more mature
|
|
linker, but it has the downside of leaving behind symlinks that may
|
|
not get invalidated until the next GC, if an entry is removed from
|
|
{option}`hjem.<user>.files`.
|
|
|
|
Specifying a package will use a custom file linker that uses an
|
|
internally-generated manifest. The custom file linker must use this
|
|
manifest to create or remove links as needed, by comparing the manifest
|
|
of the currently activated system with that of the new system.
|
|
This prevents dangling symlinks when an entry is removed from
|
|
{option}`hjem.<user>.files`.
|
|
|
|
:::{.note}
|
|
This linker is currently experimental; once it matures, it may become
|
|
the default in the future.
|
|
:::
|
|
'';
|
|
type = nullOr package;
|
|
};
|
|
|
|
linkerOptions = mkOption {
|
|
default = [];
|
|
description = ''
|
|
Additional arguments to pass to the linker.
|
|
|
|
This is for external linker modules to set, to allow extending the default set of hjem behaviours.
|
|
It accepts either a list of strings, which will be passed directly as arguments, or an attribute set, which will be
|
|
serialized to JSON and passed as `--linker-opts options.json`.
|
|
'';
|
|
type = either (listOf singleLineStr) attrs;
|
|
};
|
|
};
|
|
|
|
config = mkMerge [
|
|
{
|
|
users.users = (mapAttrs (_: v: {inherit (v) packages;})) enabledUsers;
|
|
assertions =
|
|
concatLists
|
|
(mapAttrsToList (user: config:
|
|
map ({
|
|
assertion,
|
|
message,
|
|
...
|
|
}: {
|
|
inherit assertion;
|
|
message = "${user} profile: ${message}";
|
|
})
|
|
config.assertions)
|
|
enabledUsers);
|
|
|
|
warnings =
|
|
concatLists
|
|
(mapAttrsToList (
|
|
user: v:
|
|
map (
|
|
warning: "${user} profile: ${warning}"
|
|
)
|
|
v.warnings
|
|
)
|
|
enabledUsers);
|
|
}
|
|
|
|
# Constructed rule string that consists of the type, target, and source
|
|
# of a tmpfile. Files with 'null' sources are filtered before the rule
|
|
# is constructed.
|
|
(mkIf (cfg.linker == null) {
|
|
assertions = [
|
|
{
|
|
assertion = pkgs.stdenv.hostPlatform.isLinux;
|
|
message = "The systemd-tmpfiles linker is only supported on Linux; on other platforms, use the manifest linker.";
|
|
}
|
|
];
|
|
|
|
systemd.user.tmpfiles.users =
|
|
mapAttrs (_: u: {
|
|
rules = pipe u.files [
|
|
attrValues
|
|
(filter (f: f.enable && f.source != null))
|
|
(map (
|
|
file:
|
|
# L+ will recreate, i.e., clobber existing files.
|
|
"L${optionalString file.clobber "+"} '${file.target}' - - - - ${file.source}"
|
|
))
|
|
];
|
|
})
|
|
enabledUsers;
|
|
})
|
|
|
|
(mkIf (cfg.linker != null) {
|
|
/*
|
|
The different Hjem services expect the manifest to be generated under `/var/lib/hjem/manifest-{user}.json`.
|
|
*/
|
|
systemd.targets.hjem = {
|
|
description = "Hjem File Management";
|
|
requiredBy = ["sysinit-reactivation.target"];
|
|
before = ["sysinit-reactivation.target"];
|
|
requires = let
|
|
requiredUserServices = name: [
|
|
"hjem-activate@${name}.service"
|
|
"hjem-copy@${name}.service"
|
|
];
|
|
in
|
|
concatMap requiredUserServices (attrNames enabledUsers)
|
|
++ ["hjem-cleanup.service"];
|
|
};
|
|
|
|
systemd.services = let
|
|
manifestsDir = "/var/lib/hjem";
|
|
checkEnabledUsers = ''
|
|
case "$1" in
|
|
${concatStringsSep "|" (attrNames enabledUsers)}) ;;
|
|
*) echo "User '%i' is not configured for Hjem" >&2; exit 1 ;;
|
|
esac
|
|
'';
|
|
in {
|
|
hjem-prepare = {
|
|
description = "Prepare Hjem manifests directory";
|
|
script = "mkdir -p ${manifestsDir}";
|
|
serviceConfig.Type = "oneshot";
|
|
unitConfig.RefuseManualStart = true;
|
|
};
|
|
|
|
"hjem-activate@" = {
|
|
description = "Link files for %i from their manifest";
|
|
serviceConfig = {
|
|
User = "%i";
|
|
Type = "oneshot";
|
|
};
|
|
requires = [
|
|
"hjem-prepare.service"
|
|
"hjem-copy@%i.service"
|
|
];
|
|
after = ["hjem-prepare.service"];
|
|
scriptArgs = "%i";
|
|
script = let
|
|
linkerOpts =
|
|
if (typeOf cfg.linkerOptions == "set")
|
|
then ''--linker-opts "${toJSON cfg.linkerOptions}"''
|
|
else concatStringsSep " " cfg.linkerOptions;
|
|
in ''
|
|
${checkEnabledUsers}
|
|
new_manifest=${manifests}/manifest-$1.json
|
|
|
|
if [ ! -f ${manifestsDir}/manifest-$1.json ]; then
|
|
${linker} ${linkerOpts} activate $new_manifest
|
|
exit 0
|
|
fi
|
|
|
|
${linker} ${linkerOpts} diff $new_manifest ${manifestsDir}/manifest-$1.json
|
|
'';
|
|
};
|
|
|
|
"hjem-copy@" = {
|
|
description = "Copy the manifest into Hjem's state directory for %i";
|
|
serviceConfig.Type = "oneshot";
|
|
after = ["hjem-activate@%i.service"];
|
|
scriptArgs = "%i";
|
|
/*
|
|
TODO: remove the if condition in a while, this is in place because the first iteration of the
|
|
manifest used to simply point /var/lib/hjem to the aggregate symlinkJoin directory. Since
|
|
per-user manifest services have now been implemented, trying to copy singular files into
|
|
/var/lib/hjem will fail if the user was using the previous manifest handling.
|
|
*/
|
|
script = ''
|
|
${checkEnabledUsers}
|
|
new_manifest=${manifests}/manifest-$1.json
|
|
|
|
if ! cp $new_manifest ${manifestsDir}; then
|
|
echo "Copying the manifest for $1 failed. This is likely due to using the previous\
|
|
version of the manifest handling. The manifest directory has been recreated and repopulated with\
|
|
%i's manifest. Please re-run the activation services for your other users, if you have ran this one manually."
|
|
|
|
rm -rf ${manifestsDir}
|
|
mkdir -p ${manifestsDir}
|
|
|
|
cp $new_manifest ${manifestsDir}
|
|
fi
|
|
'';
|
|
};
|
|
|
|
hjem-cleanup = {
|
|
description = "Cleanup disabled users' manifests";
|
|
serviceConfig.Type = "oneshot";
|
|
after = ["hjem.target"];
|
|
unitConfig.RefuseManualStart = false;
|
|
script = let
|
|
manifestsToDelete =
|
|
map
|
|
(user: "${manifestsDir}/manifest-${user}.json")
|
|
(attrNames disabledUsers);
|
|
in
|
|
if disabledUsers != {}
|
|
then "rm ${concatStringsSep " " manifestsToDelete}"
|
|
else "true";
|
|
};
|
|
};
|
|
})
|
|
];
|
|
}
|