1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11 cfg = config.systemd.tmpfiles;
12 initrdCfg = config.boot.initrd.systemd.tmpfiles;
13 systemd = config.systemd.package;
14
15 attrsWith' =
16 placeholder: elemType:
17 types.attrsWith {
18 inherit elemType placeholder;
19 };
20
21 escapeArgument = lib.strings.escapeC [
22 "\t"
23 "\n"
24 "\r"
25 " "
26 "\\"
27 ];
28
29 settingsOption = {
30 description = ''
31 Declare systemd-tmpfiles rules to create, delete, and clean up volatile
32 and temporary files and directories.
33
34 Even though the service is called `*tmp*files` you can also create
35 persistent files.
36 '';
37 example = {
38 "10-mypackage" = {
39 "/var/lib/my-service/statefolder".d = {
40 mode = "0755";
41 user = "root";
42 group = "root";
43 };
44 };
45 };
46 default = { };
47 type = attrsWith' "config-name" (
48 attrsWith' "path" (
49 attrsWith' "tmpfiles-type" (
50 types.submodule (
51 { name, config, ... }:
52 {
53 options.type = mkOption {
54 type = types.str;
55 default = name;
56 defaultText = "‹tmpfiles-type›";
57 example = "d";
58 description = ''
59 The type of operation to perform on the file.
60
61 The type consists of a single letter and optionally one or more
62 modifier characters.
63
64 Please see the upstream documentation for the available types and
65 more details:
66 {manpage}`tmpfiles.d(5)`
67 '';
68 };
69 options.mode = mkOption {
70 type = types.str;
71 default = "-";
72 example = "0755";
73 description = ''
74 The file access mode to use when creating this file or directory.
75 '';
76 };
77 options.user = mkOption {
78 type = types.str;
79 default = "-";
80 example = "root";
81 description = ''
82 The user of the file.
83
84 This may either be a numeric ID or a user/group name.
85
86 If omitted or when set to `"-"`, the user and group of the user who
87 invokes systemd-tmpfiles is used.
88 '';
89 };
90 options.group = mkOption {
91 type = types.str;
92 default = "-";
93 example = "root";
94 description = ''
95 The group of the file.
96
97 This may either be a numeric ID or a user/group name.
98
99 If omitted or when set to `"-"`, the user and group of the user who
100 invokes systemd-tmpfiles is used.
101 '';
102 };
103 options.age = mkOption {
104 type = types.str;
105 default = "-";
106 example = "10d";
107 description = ''
108 Delete a file when it reaches a certain age.
109
110 If a file or directory is older than the current time minus the age
111 field, it is deleted.
112
113 If set to `"-"` no automatic clean-up is done.
114 '';
115 };
116 options.argument = mkOption {
117 type = types.str;
118 default = "";
119 example = "";
120 description = ''
121 An argument whose meaning depends on the type of operation.
122
123 Please see the upstream documentation for the meaning of this
124 parameter in different situations:
125 {manpage}`tmpfiles.d(5)`
126 '';
127 };
128 }
129 )
130 )
131 )
132 );
133 };
134
135 # generates a single entry for a tmpfiles.d rule
136 settingsEntryToRule = path: entry: ''
137 '${entry.type}' '${path}' '${entry.mode}' '${entry.user}' '${entry.group}' '${entry.age}' ${escapeArgument entry.argument}
138 '';
139
140 # generates a list of tmpfiles.d rules from the attrs (paths) under tmpfiles.settings.<name>
141 pathsToRules = mapAttrsToList (
142 path: types: concatStrings (mapAttrsToList (_type: settingsEntryToRule path) types)
143 );
144
145 mkRuleFileContent = paths: concatStrings (pathsToRules paths);
146in
147{
148 options = {
149 systemd.tmpfiles.rules = mkOption {
150 type = types.listOf types.str;
151 default = [ ];
152 example = [ "d /tmp 1777 root root 10d" ];
153 description = ''
154 Rules for creation, deletion and cleaning of volatile and temporary files
155 automatically. See
156 {manpage}`tmpfiles.d(5)`
157 for the exact format.
158 '';
159 };
160
161 systemd.tmpfiles.settings = mkOption settingsOption;
162
163 boot.initrd.systemd.tmpfiles.settings = mkOption (
164 settingsOption
165 // {
166 description = ''
167 Similar to {option}`systemd.tmpfiles.settings` but the rules are
168 only applied by systemd-tmpfiles before `initrd-switch-root.target`.
169
170 See {manpage}`bootup(7)`.
171 '';
172 }
173 );
174
175 systemd.tmpfiles.packages = mkOption {
176 type = types.listOf types.package;
177 default = [ ];
178 example = literalExpression "[ pkgs.lvm2 ]";
179 apply = map getLib;
180 description = ''
181 List of packages containing {command}`systemd-tmpfiles` rules.
182
183 All files ending in .conf found in
184 {file}`«pkg»/lib/tmpfiles.d`
185 will be included.
186 If this folder does not exist or does not contain any files an error will be returned instead.
187
188 If a {file}`lib` output is available, rules are searched there and only there.
189 If there is no {file}`lib` output it will fall back to {file}`out`
190 and if that does not exist either, the default output will be used.
191 '';
192 };
193 };
194
195 config = {
196 warnings =
197 let
198 paths = lib.filter (path: path != null && lib.hasPrefix "/etc/tmpfiles.d/" path) (
199 map (path: path.target) config.boot.initrd.systemd.storePaths
200 );
201 in
202 lib.optional (lib.length paths > 0) (
203 lib.concatStringsSep " " [
204 "Files inside /etc/tmpfiles.d in the initrd need to be created with"
205 "boot.initrd.systemd.tmpfiles.settings."
206 "Creating them by hand using boot.initrd.systemd.contents or"
207 "boot.initrd.systemd.storePaths will lead to errors in the future."
208 "Found these problematic files: ${lib.concatStringsSep ", " paths}"
209 ]
210 )
211 ++ (lib.flatten (
212 lib.mapAttrsToList (
213 name: paths:
214 lib.mapAttrsToList (
215 path: entries:
216 lib.mapAttrsToList (
217 type': entry:
218 lib.optional (lib.match ''.*\\([nrt]|x[0-9A-Fa-f]{2}).*'' entry.argument != null) (
219 lib.concatStringsSep " " [
220 "The argument option of ${name}.${type'}.${path} appears to"
221 "contain escape sequences, which will be escaped again."
222 "Unescape them if this is not intended: \"${entry.argument}\""
223 ]
224 )
225 ) entries
226 ) paths
227 ) cfg.settings
228 ));
229
230 systemd.additionalUpstreamSystemUnits = [
231 "systemd-tmpfiles-clean.service"
232 "systemd-tmpfiles-clean.timer"
233 "systemd-tmpfiles-setup-dev-early.service"
234 "systemd-tmpfiles-setup-dev.service"
235 "systemd-tmpfiles-setup.service"
236 ];
237
238 systemd.additionalUpstreamUserUnits = [
239 "systemd-tmpfiles-clean.service"
240 "systemd-tmpfiles-clean.timer"
241 "systemd-tmpfiles-setup.service"
242 ];
243
244 # Allow systemd-tmpfiles to be restarted by switch-to-configuration. This
245 # service is not pulled into the normal boot process. It only exists for
246 # switch-to-configuration.
247 #
248 # This needs to be a separate unit because it does not execute
249 # systemd-tmpfiles with `--boot` as that is supposed to only be executed
250 # once at boot time.
251 #
252 # Keep this aligned with the upstream `systemd-tmpfiles-setup.service` unit.
253 systemd.services."systemd-tmpfiles-resetup" = {
254 description = "Re-setup tmpfiles on a system that is already running.";
255
256 requiredBy = [ "sysinit-reactivation.target" ];
257 after = [
258 "local-fs.target"
259 "systemd-sysusers.service"
260 "systemd-journald.service"
261 ];
262 before = [
263 "sysinit-reactivation.target"
264 "shutdown.target"
265 ];
266 conflicts = [ "shutdown.target" ];
267 restartTriggers = [ config.environment.etc."tmpfiles.d".source ];
268
269 unitConfig.DefaultDependencies = false;
270
271 serviceConfig = {
272 Type = "oneshot";
273 RemainAfterExit = true;
274 ExecStart = "systemd-tmpfiles --create --remove --exclude-prefix=/dev";
275 SuccessExitStatus = "DATAERR CANTCREAT";
276 ImportCredential = [
277 "tmpfiles.*"
278 "loging.motd"
279 "login.issue"
280 "network.hosts"
281 "ssh.authorized_keys.root"
282 ];
283 RestrictSUIDSGID = false;
284 };
285 };
286
287 environment.etc = {
288 "tmpfiles.d".source =
289 (pkgs.symlinkJoin {
290 name = "tmpfiles.d";
291 paths = map (p: p + "/lib/tmpfiles.d") cfg.packages;
292 postBuild = ''
293 for i in $(cat $pathsPath); do
294 (test -d "$i" && test $(ls "$i"/*.conf | wc -l) -ge 1) || (
295 echo "ERROR: The path '$i' from systemd.tmpfiles.packages contains no *.conf files."
296 exit 1
297 )
298 done
299 ''
300 + concatMapStrings (
301 name:
302 optionalString (hasPrefix "tmpfiles.d/" name) ''
303 rm -f $out/${removePrefix "tmpfiles.d/" name}
304 ''
305 ) config.system.build.etc.passthru.targets;
306 })
307 + "/*";
308 "mtab" = {
309 mode = "direct-symlink";
310 source = "/proc/mounts";
311 };
312 };
313
314 systemd.tmpfiles.packages = [
315 # Default tmpfiles rules provided by systemd
316 (pkgs.runCommand "systemd-default-tmpfiles" { } ''
317 mkdir -p $out/lib/tmpfiles.d
318 cd $out/lib/tmpfiles.d
319
320 ln -s "${systemd}/example/tmpfiles.d/home.conf"
321 ln -s "${systemd}/example/tmpfiles.d/journal-nocow.conf"
322 ln -s "${systemd}/example/tmpfiles.d/portables.conf"
323 ln -s "${systemd}/example/tmpfiles.d/static-nodes-permissions.conf"
324 ln -s "${systemd}/example/tmpfiles.d/systemd.conf"
325 ln -s "${systemd}/example/tmpfiles.d/systemd-nologin.conf"
326 ln -s "${systemd}/example/tmpfiles.d/systemd-nspawn.conf"
327 ln -s "${systemd}/example/tmpfiles.d/systemd-tmp.conf"
328 ln -s "${systemd}/example/tmpfiles.d/tmp.conf"
329 ln -s "${systemd}/example/tmpfiles.d/var.conf"
330 ln -s "${systemd}/example/tmpfiles.d/x11.conf"
331 '')
332 # User-specified tmpfiles rules
333 (pkgs.writeTextFile {
334 name = "nixos-tmpfiles.d";
335 destination = "/lib/tmpfiles.d/00-nixos.conf";
336 text = ''
337 # This file is created automatically and should not be modified.
338 # Please change the option ‘systemd.tmpfiles.rules’ instead.
339
340 ${concatStringsSep "\n" cfg.rules}
341 '';
342 })
343 ]
344 ++ (mapAttrsToList (
345 name: paths: pkgs.writeTextDir "lib/tmpfiles.d/${name}.conf" (mkRuleFileContent paths)
346 ) cfg.settings);
347
348 systemd.tmpfiles.rules = [
349 "d /run/lock 0755 root root - -"
350 "d /var/db 0755 root root - -"
351 "L /var/lock - - - - ../run/lock"
352 ]
353 ++ lib.optionals config.nix.enable [
354 "d /nix/var 0755 root root - -"
355 "L+ /nix/var/nix/gcroots/booted-system 0755 root root - /run/booted-system"
356 ]
357 # Boot-time cleanup
358 ++ [
359 "R! /etc/group.lock - - - - -"
360 "R! /etc/passwd.lock - - - - -"
361 "R! /etc/shadow.lock - - - - -"
362 ]
363 ++ lib.optionals config.nix.enable [
364 "R! /nix/var/nix/gcroots/tmp - - - - -"
365 "R! /nix/var/nix/temproots - - - - -"
366 ];
367
368 boot.initrd.systemd = {
369 additionalUpstreamUnits = [
370 "systemd-tmpfiles-setup-dev-early.service"
371 "systemd-tmpfiles-setup-dev.service"
372 "systemd-tmpfiles-setup.service"
373 ];
374
375 # override to exclude the prefix /sysroot, because it is not necessarily set up when the unit starts
376 services.systemd-tmpfiles-setup.serviceConfig = {
377 ExecStart = [
378 ""
379 "systemd-tmpfiles --create --remove --boot --exclude-prefix=/dev --exclude-prefix=/sysroot"
380 ];
381 };
382
383 # sets up files under the prefix /sysroot, after the hierarchy is available and before nixos activation
384 services.systemd-tmpfiles-setup-sysroot = {
385 description = "Create Volatile Files and Directories in the Real Root";
386 after = [ "initrd-fs.target" ];
387 before = [
388 "initrd.target"
389 "shutdown.target"
390 "initrd-switch-root.target"
391 ];
392 conflicts = [
393 "shutdown.target"
394 "initrd-switch-root.target"
395 ];
396 wantedBy = [ "initrd.target" ];
397 serviceConfig = {
398 Type = "oneshot";
399 RemainAfterExit = true;
400 ExecStart = "systemd-tmpfiles --create --remove --boot --exclude-prefix=/dev --prefix=/sysroot";
401 SuccessExitStatus = [ "DATAERR CANTCREAT" ];
402 ImportCredential = [
403 "tmpfiles.*"
404 "login.motd"
405 "login.issue"
406 "network.hosts"
407 "ssh.authorized_keys.root"
408 ];
409 };
410 unitConfig = {
411 DefaultDependencies = false;
412 RefuseManualStop = true;
413 };
414
415 };
416
417 contents."/etc/tmpfiles.d" = mkIf (initrdCfg.settings != { }) {
418 source = pkgs.linkFarm "initrd-tmpfiles.d" (
419 mapAttrsToList (name: paths: {
420 name = "${name}.conf";
421 path = pkgs.writeText "${name}.conf" (mkRuleFileContent paths);
422 }) initrdCfg.settings
423 );
424 };
425 };
426 };
427}