1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-11-08 11:36:05 +01:00

tmpfiles: add option to purge rules' targets on change

This commit is contained in:
Benedikt Rips 2025-10-31 14:29:48 +01:00 committed by Austin Horstman
parent 090aa14e5d
commit b4350d54c2
7 changed files with 151 additions and 58 deletions

View file

@ -89,6 +89,46 @@ let
} }
); );
attrsWith' = placeholder: elemType: lib.types.attrsWith { inherit elemType placeholder; };
nonEmptyAttrsWith' =
placeholder: elemType:
let
attrs = lib.types.addCheck (attrsWith' placeholder elemType) (s: s != { });
in
attrs
// {
name = "nonEmptyAttrsOf";
description = "non-empty ${attrs.description}";
emptyValue = { }; # no .value attribute, meaning there is not empty value
substSubModules = m: nonEmptyAttrsWith' placeholder (elemType.substSubModules m);
};
configSubmodule = lib.types.submodule {
options.rules = lib.mkOption {
description = "The rules contained in this configuration.";
example = {
"%C".d = {
mode = "0755";
user = "alice";
group = "alice";
age = "4 weeks";
};
};
type = nonEmptyAttrsWith' "path" (nonEmptyAttrsWith' "tmpfiles-type" ruleSubmodule);
};
options.purgeOnChange = lib.mkOption {
description = ''
Whether the rules that are marked for purging, will automatically
be purged when the set of rules changes.
See {manpage}`systemd-tmpfiles(8)` for details about purging.
'';
type = lib.types.bool;
default = false;
};
};
modulePrefix = [ modulePrefix = [
"systemd" "systemd"
"user" "user"
@ -100,6 +140,9 @@ let
mkConfigFile = mkConfigFile =
name: rules: name: rules:
{
suffix ? [ name ],
}:
let let
escapeArgument = lib.strings.escapeC [ escapeArgument = lib.strings.escapeC [
"\t" "\t"
@ -115,7 +158,7 @@ let
{ {
text = '' text = ''
# This file was generated by Home Manager and should not be modified. # This file was generated by Home Manager and should not be modified.
# Please change the option '${lib.showAttrPath (modulePrefix ++ [ name ])}' instead. # Please change the option '${lib.showAttrPath (modulePrefix ++ suffix)}' instead.
${lib.pipe rules [ ${lib.pipe rules [
(lib.mapAttrs (_path: lib.attrValues)) (lib.mapAttrs (_path: lib.attrValues))
(lib.mapAttrsToList (path: map (mkRule path))) (lib.mapAttrsToList (path: map (mkRule path)))
@ -128,6 +171,13 @@ let
''; '';
}; };
nonPurgedConfigs = lib.filterAttrs (_name: config: !config.purgeOnChange) cfg.settings;
purgedConfigs = lib.filterAttrs (_name: config: config.purgeOnChange) cfg.settings;
# WARNING: When changing this path, the next home-manager generation will
# not find and the rules of the old generation that are subject to purging.
purgedRulesConfigName = "purge-on-change";
in in
{ {
meta.maintainers = [ lib.maintainers.dawidsowa ]; meta.maintainers = [ lib.maintainers.dawidsowa ];
@ -147,7 +197,7 @@ in
persistent files. persistent files.
''; '';
example = { example = {
cache."%C".d = { cache.rules."%C".d = {
mode = "0755"; mode = "0755";
user = "alice"; user = "alice";
group = "alice"; group = "alice";
@ -155,58 +205,70 @@ in
}; };
}; };
default = { }; default = { };
type = type = attrsWith' "config-name" configSubmodule;
let };
attrsWith' = placeholder: elemType: lib.types.attrsWith { inherit elemType placeholder; };
nonEmptyAttrsWith' = config = lib.mkMerge [
placeholder: elemType:
let (lib.mkIf pkgs.stdenv.hostPlatform.isLinux {
attrs = lib.types.addCheck (attrsWith' placeholder elemType) (s: s != { }); # The activation script must be enabled unconditionally in order to
in # guarantee that the old rules are purged even if the new set of rules
attrs # is empty, i.e. `cfg.rulesToPurgeOnChange == [ ]`.
// { home.activation.purgeTmpfiles = lib.hm.dag.entryAfter [ "writeBoundary" ] (
name = "nonEmptyAttrsOf"; let
description = "non-empty ${attrs.description}"; relativeXdgConfigHome = lib.strings.removePrefix "${config.home.homeDirectory}/" config.xdg.configHome;
emptyValue = { }; # no .value attribute, meaning there is not empty value configPath = "home-files/${relativeXdgConfigHome}/${purgedRulesConfigName}";
substSubModules = m: nonEmptyAttrsWith' placeholder (elemType.substSubModules m); in
}; ''
in if [[ -v oldGenPath && -f $oldGenPath/${configPath} ]] &&
attrsWith' "config-name" ( diff -q $oldGenPath/${configPath} $newGenPath/${configPath} &>/dev/null; then
nonEmptyAttrsWith' "path" (nonEmptyAttrsWith' "tmpfiles-type" ruleSubmodule) verboseEcho "Purge old tmpfiles"
run ${pkgs.systemd}/bin/systemd-tmpfiles --user --purge ''${DRY_RUN:+--dry-run} $oldGenPath/${configPath}
fi
''
); );
}; })
config = lib.mkIf (cfg.settings != { }) { (lib.mkIf (cfg.settings != { }) {
assertions = [ assertions = [
(lib.hm.assertions.assertPlatform "systemd.user.tmpfiles" pkgs lib.platforms.linux) (lib.hm.assertions.assertPlatform "systemd.user.tmpfiles" pkgs lib.platforms.linux)
]; ];
warnings = lib.flatten ( warnings = lib.flatten (
lib.mapAttrsToListRecursive ( lib.mapAttrsToListRecursive (
path: value: path: value:
lib.optional lib.optional
(lib.last path == "argument" && lib.match ''.*\\([nrt]|x[0-9A-Fa-f]{2}).*'' value != null) (lib.last path == "argument" && lib.match ''.*\\([nrt]|x[0-9A-Fa-f]{2}).*'' value != null)
'' ''
The '${lib.showAttrPath (modulePrefix ++ path)}' option The '${lib.showAttrPath (modulePrefix ++ path)}' option
appears to contain escape sequences, which will be escaped again. appears to contain escape sequences, which will be escaped again.
Unescape them if this is not intended. The assigned string is: Unescape them if this is not intended. The assigned string is:
"${value}" "${value}"
'' ''
) cfg.settings ) cfg.settings
); );
xdg.configFile = { xdg.configFile = {
"systemd/user/basic.target.wants/systemd-tmpfiles-setup.service".source = "systemd/user/basic.target.wants/systemd-tmpfiles-setup.service".source =
"${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-setup.service"; "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-setup.service";
"systemd/user/systemd-tmpfiles-setup.service".source = "systemd/user/systemd-tmpfiles-setup.service".source =
"${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-setup.service"; "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-setup.service";
"systemd/user/timers.target.wants/systemd-tmpfiles-clean.timer".source = "systemd/user/timers.target.wants/systemd-tmpfiles-clean.timer".source =
"${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.timer"; "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.timer";
"systemd/user/systemd-tmpfiles-clean.service".source = "systemd/user/systemd-tmpfiles-clean.service".source =
"${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.service"; "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.service";
} }
// lib.mapAttrs' ( // lib.mapAttrs' (
name: rules: lib.nameValuePair (mkFileName name) (mkConfigFile name rules) name: config: lib.nameValuePair (mkFileName name) (mkConfigFile name config.rules { })
) cfg.settings; ) nonPurgedConfigs
}; // lib.optionalAttrs (purgedConfigs != { }) {
${mkFileName purgedRulesConfigName} =
let
purgedConfigsMerged = lib.foldl' lib.recursiveUpdate { } (lib.attrValues purgedConfigs);
in
mkConfigFile purgedRulesConfigName purgedConfigsMerged.rules { suffix = [ ]; };
};
})
];
} }

View file

@ -37,7 +37,7 @@ in
# Use `systemd-tmpfiles` since glab requires its configuration file to have # Use `systemd-tmpfiles` since glab requires its configuration file to have
# mode 0600. # mode 0600.
systemd.user.tmpfiles.settings.glab = lib.mkIf (cfg.settings != { }) { systemd.user.tmpfiles.settings.glab = lib.mkIf (cfg.settings != { }) {
"${config.xdg.configHome}/glab-cli/config.yml" = { rules."${config.xdg.configHome}/glab-cli/config.yml" = {
"C+".argument = yaml.generate "glab-config" cfg.settings; "C+".argument = yaml.generate "glab-config" cfg.settings;
z.mode = "0600"; z.mode = "0600";
}; };

View file

@ -28,7 +28,7 @@ in
uid = 1000; uid = 1000;
}; };
systemd.tmpfiles.settings.age."/home/alice/age-key".f = { systemd.tmpfiles.settings.age.rules."/home/alice/age-key".f = {
mode = "400"; mode = "400";
user = "alice"; user = "alice";
group = "users"; group = "users";

View file

@ -2,8 +2,8 @@
imports = [ ./common-stubs.nix ]; imports = [ ./common-stubs.nix ];
systemd.user.tmpfiles.settings = { systemd.user.tmpfiles.settings = {
cache."%C".d.age = "4 weeks"; cache.rules."%C".d.age = "4 weeks";
myTool."%h/.config/myTool.conf"."f+" = { myTool.rules."%h/.config/myTool.conf"."f+" = {
mode = "0644"; mode = "0644";
user = "alice"; user = "alice";
group = "users"; group = "users";
@ -12,6 +12,8 @@
}; };
nmt.script = '' nmt.script = ''
assertPathNotExists home-files/.config/user-tmpfiles.d/home-manager-purge-on-change.conf
cacheRulesFile=home-files/.config/user-tmpfiles.d/home-manager-cache.conf cacheRulesFile=home-files/.config/user-tmpfiles.d/home-manager-cache.conf
assertFileExists $cacheRulesFile assertFileExists $cacheRulesFile
assertFileRegex $cacheRulesFile "^'d' '%C' '-' '-' '-' '4 weeks' $" assertFileRegex $cacheRulesFile "^'d' '%C' '-' '-' '-' '4 weeks' $"

View file

@ -1,6 +1,7 @@
{ {
tmpfiles-no-rules = ./no-rules.nix; tmpfiles-no-rules = ./no-rules.nix;
tmpfiles-basic-rules = ./basic-rules.nix; tmpfiles-basic-rules = ./basic-rules.nix;
tmpfiles-rules-with-purging = ./rules-with-purging.nix;
tmpfiles-escaped-argument-warning = ./escaped-argument-warning.nix; tmpfiles-escaped-argument-warning = ./escaped-argument-warning.nix;
} }

View file

@ -1,11 +1,11 @@
{ {
imports = [ ./common-stubs.nix ]; imports = [ ./common-stubs.nix ];
systemd.user.tmpfiles.settings.foo.path.f.argument = "my\\x20unescaped\\x20config"; systemd.user.tmpfiles.settings.foo.rules.path.f.argument = "my\\x20unescaped\\x20config";
test.asserts.warnings.expected = [ test.asserts.warnings.expected = [
'' ''
The 'systemd.user.tmpfiles.settings.foo.path.f.argument' option The 'systemd.user.tmpfiles.settings.foo.rules.path.f.argument' option
appears to contain escape sequences, which will be escaped again. appears to contain escape sequences, which will be escaped again.
Unescape them if this is not intended. The assigned string is: Unescape them if this is not intended. The assigned string is:
"my\x20unescaped\x20config" "my\x20unescaped\x20config"

View file

@ -0,0 +1,28 @@
{
imports = [ ./common-stubs.nix ];
systemd.user.tmpfiles.settings = {
cache.rules."%C".d.age = "4 weeks";
myTool = {
rules = {
"%h/.config/myTool.conf"."f+".argument = "my_config";
"%h/.config/myToolPurged.conf"."f+$".argument = "my_config_purged";
};
purgeOnChange = true;
};
};
nmt.script = ''
cacheRulesFile=home-files/.config/user-tmpfiles.d/home-manager-cache.conf
assertFileExists $cacheRulesFile
assertFileRegex $cacheRulesFile "^'d' '%C' '-' '-' '-' '4 weeks' $"
assertPathNotExists home-files/.config/user-tmpfiles.d/home-manager-myTool.conf
myToolRulesFile=home-files/.config/user-tmpfiles.d/home-manager-purge-on-change.conf
assertFileExists $myToolRulesFile
assertFileRegex $myToolRulesFile \
"^'f+' '%h/.config/myTool.conf' '-' '-' '-' '-' my_config$"
assertFileRegex $myToolRulesFile \
"^'f+$' '%h/.config/myToolPurged.conf' '-' '-' '-' '-' my_config_purged$"
'';
}