{ config, lib, pkgs, ... }: let cfg = config.systemd.user.tmpfiles; ruleSubmodule = lib.types.submodule ( { name, ... }: { options.type = lib.mkOption { type = lib.types.str; readOnly = true; default = name; defaultText = "‹tmpfiles-type›"; example = "d"; description = '' The type of operation to perform on the file. The type consists of a single letter and optionally one or more modifier characters. Please see the upstream documentation for the available types and more details: {manpage}`tmpfiles.d(5)` ''; }; options.mode = lib.mkOption { type = lib.types.str; default = "-"; example = "0755"; description = '' The file access mode to use when creating this file or directory. ''; }; options.user = lib.mkOption { type = lib.types.str; default = "-"; example = "root"; description = '' The user of the file. This may either be a numeric ID or a user/group name. If omitted or when set to `"-"`, the user and group of the user who invokes systemd-tmpfiles is used. ''; }; options.group = lib.mkOption { type = lib.types.str; default = "-"; example = "root"; description = '' The group of the file. This may either be a numeric ID or a user/group name. If omitted or when set to `"-"`, the user and group of the user who invokes systemd-tmpfiles is used. ''; }; options.age = lib.mkOption { type = lib.types.str; default = "-"; example = "10d"; description = '' Delete a file when it reaches a certain age. If a file or directory is older than the current time minus the age field, it is deleted. If set to `"-"`, no automatic clean-up is done. ''; }; options.argument = lib.mkOption { type = lib.types.str; default = ""; example = ""; description = '' An argument whose meaning depends on the type of operation. Please see the upstream documentation for the meaning of this parameter in different situations: {manpage}`tmpfiles.d(5)` ''; }; } ); 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 = [ "systemd" "user" "tmpfiles" "settings" ]; mkFileName = configName: "user-tmpfiles.d/home-manager-${configName}.conf"; mkConfigFile = name: rules: { suffix ? [ name ], }: let escapeArgument = lib.strings.escapeC [ "\t" "\n" "\r" " " "\\" ]; mkRule = path: rule: '' '${rule.type}' '${path}' '${rule.mode}' '${rule.user}' '${rule.group}' '${rule.age}' ${escapeArgument rule.argument} ''; in { text = '' # This file was generated by Home Manager and should not be modified. # Please change the option '${lib.showAttrPath (modulePrefix ++ suffix)}' instead. ${lib.pipe rules [ (lib.mapAttrs (_path: lib.attrValues)) (lib.mapAttrsToList (path: map (mkRule path))) lib.flatten lib.concatStrings ]} ''; onChange = '' run ${pkgs.systemd}/bin/systemd-tmpfiles --user --remove --create ''${DRY_RUN:+--dry-run} '${config.xdg.configHome}/${mkFileName name}' ''; }; 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 { meta.maintainers = with lib.maintainers; [ bmrips dawidsowa ]; imports = [ (lib.mkRemovedOptionModule [ "systemd" "user" "tmpfiles" "rules" ] '' It has been replaced by 'systemd.user.tmpfiles.settings'. '') ]; options.systemd.user.tmpfiles.settings = lib.mkOption { description = '' Declare systemd-tmpfiles rules to create, delete, and clean up volatile and temporary files and directories. Even though the service is called `*tmp*files` you can also create persistent files. ''; example = { cache.rules."%C".d = { mode = "0755"; user = "alice"; group = "alice"; age = "4 weeks"; }; }; default = { }; type = attrsWith' "config-name" configSubmodule; }; config = lib.mkMerge [ (lib.mkIf pkgs.stdenv.hostPlatform.isLinux { # The activation script must be enabled unconditionally in order to # guarantee that the old rules are purged even if the new set of rules # is empty, i.e. `cfg.rulesToPurgeOnChange == [ ]`. home.activation.purgeTmpfiles = lib.hm.dag.entryAfter [ "writeBoundary" ] ( let relativeXdgConfigHome = lib.strings.removePrefix "${config.home.homeDirectory}/" config.xdg.configHome; configPath = "home-files/${relativeXdgConfigHome}/${purgedRulesConfigName}"; in '' if [[ -v oldGenPath && -f $oldGenPath/${configPath} ]] && diff -q $oldGenPath/${configPath} $newGenPath/${configPath} &>/dev/null; then verboseEcho "Purge old tmpfiles" run ${pkgs.systemd}/bin/systemd-tmpfiles --user --purge ''${DRY_RUN:+--dry-run} $oldGenPath/${configPath} fi '' ); }) (lib.mkIf (cfg.settings != { }) { assertions = [ (lib.hm.assertions.assertPlatform "systemd.user.tmpfiles" pkgs lib.platforms.linux) ]; warnings = lib.flatten ( lib.mapAttrsToListRecursive ( path: value: lib.optional (lib.last path == "argument" && lib.match ''.*\\([nrt]|x[0-9A-Fa-f]{2}).*'' value != null) '' The '${lib.showAttrPath (modulePrefix ++ path)}' option appears to contain escape sequences, which will be escaped again. Unescape them if this is not intended. The assigned string is: "${value}" '' ) cfg.settings ); xdg.configFile = { "systemd/user/basic.target.wants/systemd-tmpfiles-setup.service".source = "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-setup.service"; "systemd/user/systemd-tmpfiles-setup.service".source = "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-setup.service"; "systemd/user/timers.target.wants/systemd-tmpfiles-clean.timer".source = "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.timer"; "systemd/user/systemd-tmpfiles-clean.service".source = "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.service"; } // lib.mapAttrs' ( name: config: lib.nameValuePair (mkFileName name) (mkConfigFile name config.rules { }) ) nonPurgedConfigs // lib.optionalAttrs (purgedConfigs != { }) { ${mkFileName purgedRulesConfigName} = let purgedConfigsMerged = lib.foldl' lib.recursiveUpdate { } (lib.attrValues purgedConfigs); in mkConfigFile purgedRulesConfigName purgedConfigsMerged.rules { suffix = [ ]; }; }; }) ]; }