1{ config, lib, pkgs, utils }:
2
3let
4 inherit (lib)
5 all
6 attrByPath
7 attrNames
8 concatLists
9 concatMap
10 concatMapStrings
11 concatStrings
12 concatStringsSep
13 const
14 elem
15 filter
16 filterAttrs
17 flatten
18 flip
19 head
20 isInt
21 isFloat
22 isList
23 isPath
24 length
25 makeBinPath
26 makeSearchPathOutput
27 mapAttrs
28 mapAttrsToList
29 mkAfter
30 mkIf
31 optional
32 optionalAttrs
33 optionalString
34 pipe
35 range
36 replaceStrings
37 reverseList
38 splitString
39 stringLength
40 stringToCharacters
41 tail
42 toIntBase10
43 trace
44 types
45 ;
46
47 inherit (lib.strings) toJSON;
48
49 cfg = config.systemd;
50 lndir = "${pkgs.buildPackages.xorg.lndir}/bin/lndir";
51 systemd = cfg.package;
52in rec {
53
54 shellEscape = s: (replaceStrings [ "\\" ] [ "\\\\" ] s);
55
56 mkPathSafeName = replaceStrings ["@" ":" "\\" "[" "]"] ["-" "-" "-" "" ""];
57
58 # a type for options that take a unit name
59 unitNameType = types.strMatching "[a-zA-Z0-9@%:_.\\-]+[.](service|socket|device|mount|automount|swap|target|path|timer|scope|slice)";
60
61 makeUnit = name: unit:
62 if unit.enable then
63 pkgs.runCommand "unit-${mkPathSafeName name}"
64 { preferLocalBuild = true;
65 allowSubstitutes = false;
66 # unit.text can be null. But variables that are null listed in
67 # passAsFile are ignored by nix, resulting in no file being created,
68 # making the mv operation fail.
69 text = optionalString (unit.text != null) unit.text;
70 passAsFile = [ "text" ];
71 }
72 ''
73 name=${shellEscape name}
74 mkdir -p "$out/$(dirname -- "$name")"
75 mv "$textPath" "$out/$name"
76 ''
77 else
78 pkgs.runCommand "unit-${mkPathSafeName name}-disabled"
79 { preferLocalBuild = true;
80 allowSubstitutes = false;
81 }
82 ''
83 name=${shellEscape name}
84 mkdir -p "$out/$(dirname "$name")"
85 ln -s /dev/null "$out/$name"
86 '';
87
88 boolValues = [true false "yes" "no"];
89
90 digits = map toString (range 0 9);
91
92 isByteFormat = s:
93 let
94 l = reverseList (stringToCharacters s);
95 suffix = head l;
96 nums = tail l;
97 in elem suffix (["K" "M" "G" "T"] ++ digits)
98 && all (num: elem num digits) nums;
99
100 assertByteFormat = name: group: attr:
101 optional (attr ? ${name} && ! isByteFormat attr.${name})
102 "Systemd ${group} field `${name}' must be in byte format [0-9]+[KMGT].";
103
104 hexChars = stringToCharacters "0123456789abcdefABCDEF";
105
106 isMacAddress = s: stringLength s == 17
107 && flip all (splitString ":" s) (bytes:
108 all (byte: elem byte hexChars) (stringToCharacters bytes)
109 );
110
111 assertMacAddress = name: group: attr:
112 optional (attr ? ${name} && ! isMacAddress attr.${name})
113 "Systemd ${group} field `${name}' must be a valid MAC address.";
114
115 assertNetdevMacAddress = name: group: attr:
116 optional (attr ? ${name} && (! isMacAddress attr.${name} && attr.${name} != "none"))
117 "Systemd ${group} field `${name}` must be a valid MAC address or the special value `none`.";
118
119 isNumberOrRangeOf = check: v:
120 if isInt v
121 then check v
122 else let
123 parts = splitString "-" v;
124 lower = toIntBase10 (head parts);
125 upper = if tail parts != [] then toIntBase10 (head (tail parts)) else lower;
126 in
127 length parts <= 2 && lower <= upper && check lower && check upper;
128 isPort = i: i >= 0 && i <= 65535;
129 isPortOrPortRange = isNumberOrRangeOf isPort;
130
131 assertPort = name: group: attr:
132 optional (attr ? ${name} && ! isPort attr.${name})
133 "Error on the systemd ${group} field `${name}': ${attr.name} is not a valid port number.";
134
135 assertPortOrPortRange = name: group: attr:
136 optional (attr ? ${name} && ! isPortOrPortRange attr.${name})
137 "Error on the systemd ${group} field `${name}': ${attr.name} is not a valid port number or range of port numbers.";
138
139 assertValueOneOf = name: values: group: attr:
140 optional (attr ? ${name} && !elem attr.${name} values)
141 "Systemd ${group} field `${name}' cannot have value `${toString attr.${name}}'.";
142
143 assertValuesSomeOfOr = name: values: default: group: attr:
144 optional (attr ? ${name} && !(all (x: elem x values) (splitString " " attr.${name}) || attr.${name} == default))
145 "Systemd ${group} field `${name}' cannot have value `${toString attr.${name}}'.";
146
147 assertHasField = name: group: attr:
148 optional (!(attr ? ${name}))
149 "Systemd ${group} field `${name}' must exist.";
150
151 assertRange = name: min: max: group: attr:
152 optional (attr ? ${name} && !(min <= attr.${name} && max >= attr.${name}))
153 "Systemd ${group} field `${name}' is outside the range [${toString min},${toString max}]";
154
155 assertRangeOrOneOf = name: min: max: values: group: attr:
156 optional (attr ? ${name} && !(((isInt attr.${name} || isFloat attr.${name}) && min <= attr.${name} && max >= attr.${name}) || elem attr.${name} values))
157 "Systemd ${group} field `${name}' is not a value in range [${toString min},${toString max}], or one of ${toString values}";
158
159 assertMinimum = name: min: group: attr:
160 optional (attr ? ${name} && attr.${name} < min)
161 "Systemd ${group} field `${name}' must be greater than or equal to ${toString min}";
162
163 assertOnlyFields = fields: group: attr:
164 let badFields = filter (name: ! elem name fields) (attrNames attr); in
165 optional (badFields != [ ])
166 "Systemd ${group} has extra fields [${concatStringsSep " " badFields}].";
167
168 assertInt = name: group: attr:
169 optional (attr ? ${name} && !isInt attr.${name})
170 "Systemd ${group} field `${name}' is not an integer";
171
172 checkUnitConfig = group: checks: attrs: let
173 # We're applied at the top-level type (attrsOf unitOption), so the actual
174 # unit options might contain attributes from mkOverride and mkIf that we need to
175 # convert into single values before checking them.
176 defs = mapAttrs (const (v:
177 if v._type or "" == "override" then v.content
178 else if v._type or "" == "if" then v.content
179 else v
180 )) attrs;
181 errors = concatMap (c: c group defs) checks;
182 in if errors == [] then true
183 else trace (concatStringsSep "\n" errors) false;
184
185 toOption = x:
186 if x == true then "true"
187 else if x == false then "false"
188 else toString x;
189
190 attrsToSection = as:
191 concatStrings (concatLists (mapAttrsToList (name: value:
192 map (x: ''
193 ${name}=${toOption x}
194 '')
195 (if isList value then value else [value]))
196 as));
197
198 generateUnits = { allowCollisions ? true, type, units, upstreamUnits, upstreamWants, packages ? cfg.packages, package ? cfg.package }:
199 let
200 typeDir = ({
201 system = "system";
202 initrd = "system";
203 user = "user";
204 nspawn = "nspawn";
205 }).${type};
206 in pkgs.runCommand "${type}-units"
207 { preferLocalBuild = true;
208 allowSubstitutes = false;
209 } ''
210 mkdir -p $out
211
212 # Copy the upstream systemd units we're interested in.
213 for i in ${toString upstreamUnits}; do
214 fn=${package}/example/systemd/${typeDir}/$i
215 if ! [ -e $fn ]; then echo "missing $fn"; false; fi
216 if [ -L $fn ]; then
217 target="$(readlink "$fn")"
218 if [ ''${target:0:3} = ../ ]; then
219 ln -s "$(readlink -f "$fn")" $out/
220 else
221 cp -pd $fn $out/
222 fi
223 else
224 ln -s $fn $out/
225 fi
226 done
227
228 # Copy .wants links, but only those that point to units that
229 # we're interested in.
230 for i in ${toString upstreamWants}; do
231 fn=${package}/example/systemd/${typeDir}/$i
232 if ! [ -e $fn ]; then echo "missing $fn"; false; fi
233 x=$out/$(basename $fn)
234 mkdir $x
235 for i in $fn/*; do
236 y=$x/$(basename $i)
237 cp -pd $i $y
238 if ! [ -e $y ]; then rm $y; fi
239 done
240 done
241
242 # Symlink all units provided listed in systemd.packages.
243 packages="${toString packages}"
244
245 # Filter duplicate directories
246 declare -A unique_packages
247 for k in $packages ; do unique_packages[$k]=1 ; done
248
249 for i in ''${!unique_packages[@]}; do
250 for fn in $i/etc/systemd/${typeDir}/* $i/lib/systemd/${typeDir}/*; do
251 if ! [[ "$fn" =~ .wants$ ]]; then
252 if [[ -d "$fn" ]]; then
253 targetDir="$out/$(basename "$fn")"
254 mkdir -p "$targetDir"
255 ${lndir} "$fn" "$targetDir"
256 else
257 ln -s $fn $out/
258 fi
259 fi
260 done
261 done
262
263 # Symlink units defined by systemd.units where override strategy
264 # shall be automatically detected. If these are also provided by
265 # systemd or systemd.packages, then add them as
266 # <unit-name>.d/overrides.conf, which makes them extend the
267 # upstream unit.
268 for i in ${toString (mapAttrsToList
269 (n: v: v.unit)
270 (filterAttrs (n: v: (attrByPath [ "overrideStrategy" ] "asDropinIfExists" v) == "asDropinIfExists") units))}; do
271 fn=$(basename $i/*)
272 if [ -e $out/$fn ]; then
273 if [ "$(readlink -f $i/$fn)" = /dev/null ]; then
274 ln -sfn /dev/null $out/$fn
275 else
276 ${if allowCollisions then ''
277 mkdir -p $out/$fn.d
278 ln -s $i/$fn $out/$fn.d/overrides.conf
279 '' else ''
280 echo "Found multiple derivations configuring $fn!"
281 exit 1
282 ''}
283 fi
284 else
285 ln -fs $i/$fn $out/
286 fi
287 done
288
289 # Symlink units defined by systemd.units which shall be
290 # treated as drop-in file.
291 for i in ${toString (mapAttrsToList
292 (n: v: v.unit)
293 (filterAttrs (n: v: v ? overrideStrategy && v.overrideStrategy == "asDropin") units))}; do
294 fn=$(basename $i/*)
295 mkdir -p $out/$fn.d
296 ln -s $i/$fn $out/$fn.d/overrides.conf
297 done
298
299 # Create service aliases from aliases option.
300 ${concatStrings (mapAttrsToList (name: unit:
301 concatMapStrings (name2: ''
302 ln -sfn '${name}' $out/'${name2}'
303 '') (unit.aliases or [])) units)}
304
305 # Create .wants, .upholds and .requires symlinks from the wantedBy, upheldBy and
306 # requiredBy options.
307 ${concatStrings (mapAttrsToList (name: unit:
308 concatMapStrings (name2: ''
309 mkdir -p $out/'${name2}.wants'
310 ln -sfn '../${name}' $out/'${name2}.wants'/
311 '') (unit.wantedBy or [])) units)}
312
313 ${concatStrings (mapAttrsToList (name: unit:
314 concatMapStrings (name2: ''
315 mkdir -p $out/'${name2}.upholds'
316 ln -sfn '../${name}' $out/'${name2}.upholds'/
317 '') (unit.upheldBy or [])) units)}
318
319 ${concatStrings (mapAttrsToList (name: unit:
320 concatMapStrings (name2: ''
321 mkdir -p $out/'${name2}.requires'
322 ln -sfn '../${name}' $out/'${name2}.requires'/
323 '') (unit.requiredBy or [])) units)}
324
325 ${optionalString (type == "system") ''
326 # Stupid misc. symlinks.
327 ln -s ${cfg.defaultUnit} $out/default.target
328 ln -s ${cfg.ctrlAltDelUnit} $out/ctrl-alt-del.target
329 ln -s rescue.target $out/kbrequest.target
330
331 mkdir -p $out/getty.target.wants/
332 ln -s ../autovt@tty1.service $out/getty.target.wants/
333
334 ln -s ../remote-fs.target $out/multi-user.target.wants/
335 ''}
336 ''; # */
337
338 makeJobScript = name: text:
339 let
340 scriptName = replaceStrings [ "\\" "@" ] [ "-" "_" ] (shellEscape name);
341 out = (pkgs.writeShellScriptBin scriptName ''
342 set -e
343 ${text}
344 '').overrideAttrs (_: {
345 # The derivation name is different from the script file name
346 # to keep the script file name short to avoid cluttering logs.
347 name = "unit-script-${scriptName}";
348 });
349 in "${out}/bin/${scriptName}";
350
351 unitConfig = { config, name, options, ... }: {
352 config = {
353 unitConfig =
354 optionalAttrs (config.requires != [])
355 { Requires = toString config.requires; }
356 // optionalAttrs (config.wants != [])
357 { Wants = toString config.wants; }
358 // optionalAttrs (config.upholds != [])
359 { Upholds = toString config.upholds; }
360 // optionalAttrs (config.after != [])
361 { After = toString config.after; }
362 // optionalAttrs (config.before != [])
363 { Before = toString config.before; }
364 // optionalAttrs (config.bindsTo != [])
365 { BindsTo = toString config.bindsTo; }
366 // optionalAttrs (config.partOf != [])
367 { PartOf = toString config.partOf; }
368 // optionalAttrs (config.conflicts != [])
369 { Conflicts = toString config.conflicts; }
370 // optionalAttrs (config.requisite != [])
371 { Requisite = toString config.requisite; }
372 // optionalAttrs (config ? restartTriggers && config.restartTriggers != [])
373 { X-Restart-Triggers = "${pkgs.writeText "X-Restart-Triggers-${name}" (pipe config.restartTriggers [
374 flatten
375 (map (x: if isPath x then "${x}" else x))
376 toString
377 ])}"; }
378 // optionalAttrs (config ? reloadTriggers && config.reloadTriggers != [])
379 { X-Reload-Triggers = "${pkgs.writeText "X-Reload-Triggers-${name}" (pipe config.reloadTriggers [
380 flatten
381 (map (x: if isPath x then "${x}" else x))
382 toString
383 ])}"; }
384 // optionalAttrs (config.description != "") {
385 Description = config.description; }
386 // optionalAttrs (config.documentation != []) {
387 Documentation = toString config.documentation; }
388 // optionalAttrs (config.onFailure != []) {
389 OnFailure = toString config.onFailure; }
390 // optionalAttrs (config.onSuccess != []) {
391 OnSuccess = toString config.onSuccess; }
392 // optionalAttrs (options.startLimitIntervalSec.isDefined) {
393 StartLimitIntervalSec = toString config.startLimitIntervalSec;
394 } // optionalAttrs (options.startLimitBurst.isDefined) {
395 StartLimitBurst = toString config.startLimitBurst;
396 };
397 };
398 };
399
400 serviceConfig = { name, config, ... }: {
401 config = {
402 name = "${name}.service";
403 environment.PATH = mkIf (config.path != []) "${makeBinPath config.path}:${makeSearchPathOutput "bin" "sbin" config.path}";
404 };
405 };
406
407 pathConfig = { name, config, ... }: {
408 config = {
409 name = "${name}.path";
410 };
411 };
412
413 socketConfig = { name, config, ... }: {
414 config = {
415 name = "${name}.socket";
416 };
417 };
418
419 sliceConfig = { name, config, ... }: {
420 config = {
421 name = "${name}.slice";
422 };
423 };
424
425 targetConfig = { name, config, ... }: {
426 config = {
427 name = "${name}.target";
428 };
429 };
430
431 timerConfig = { name, config, ... }: {
432 config = {
433 name = "${name}.timer";
434 };
435 };
436
437 stage2ServiceConfig = {
438 imports = [ serviceConfig ];
439 # Default path for systemd services. Should be quite minimal.
440 config.path = mkAfter [
441 pkgs.coreutils
442 pkgs.findutils
443 pkgs.gnugrep
444 pkgs.gnused
445 systemd
446 ];
447 };
448
449 stage1ServiceConfig = serviceConfig;
450
451 mountConfig = { config, ... }: {
452 config = {
453 name = "${utils.escapeSystemdPath config.where}.mount";
454 mountConfig =
455 { What = config.what;
456 Where = config.where;
457 } // optionalAttrs (config.type != "") {
458 Type = config.type;
459 } // optionalAttrs (config.options != "") {
460 Options = config.options;
461 };
462 };
463 };
464
465 automountConfig = { config, ... }: {
466 config = {
467 name = "${utils.escapeSystemdPath config.where}.automount";
468 automountConfig =
469 { Where = config.where;
470 };
471 };
472 };
473
474 commonUnitText = def: lines: ''
475 [Unit]
476 ${attrsToSection def.unitConfig}
477 '' + lines + optionalString (def.wantedBy != [ ]) ''
478
479 [Install]
480 WantedBy=${concatStringsSep " " def.wantedBy}
481 '';
482
483 targetToUnit = def:
484 { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
485 text =
486 ''
487 [Unit]
488 ${attrsToSection def.unitConfig}
489 '';
490 };
491
492 serviceToUnit = def:
493 { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
494 text = commonUnitText def (''
495 [Service]
496 '' + (let env = cfg.globalEnvironment // def.environment;
497 in concatMapStrings (n:
498 let s = optionalString (env.${n} != null)
499 "Environment=${toJSON "${n}=${env.${n}}"}\n";
500 # systemd max line length is now 1MiB
501 # https://github.com/systemd/systemd/commit/e6dde451a51dc5aaa7f4d98d39b8fe735f73d2af
502 in if stringLength s >= 1048576 then throw "The value of the environment variable ‘${n}’ in systemd service ‘${def.name}.service’ is too long." else s) (attrNames env))
503 + (if def ? reloadIfChanged && def.reloadIfChanged then ''
504 X-ReloadIfChanged=true
505 '' else if (def ? restartIfChanged && !def.restartIfChanged) then ''
506 X-RestartIfChanged=false
507 '' else "")
508 + optionalString (def ? stopIfChanged && !def.stopIfChanged) ''
509 X-StopIfChanged=false
510 '' + attrsToSection def.serviceConfig);
511 };
512
513 socketToUnit = def:
514 { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
515 text = commonUnitText def ''
516 [Socket]
517 ${attrsToSection def.socketConfig}
518 ${concatStringsSep "\n" (map (s: "ListenStream=${s}") def.listenStreams)}
519 ${concatStringsSep "\n" (map (s: "ListenDatagram=${s}") def.listenDatagrams)}
520 '';
521 };
522
523 timerToUnit = def:
524 { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
525 text = commonUnitText def ''
526 [Timer]
527 ${attrsToSection def.timerConfig}
528 '';
529 };
530
531 pathToUnit = def:
532 { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
533 text = commonUnitText def ''
534 [Path]
535 ${attrsToSection def.pathConfig}
536 '';
537 };
538
539 mountToUnit = def:
540 { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
541 text = commonUnitText def ''
542 [Mount]
543 ${attrsToSection def.mountConfig}
544 '';
545 };
546
547 automountToUnit = def:
548 { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
549 text = commonUnitText def ''
550 [Automount]
551 ${attrsToSection def.automountConfig}
552 '';
553 };
554
555 sliceToUnit = def:
556 { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
557 text = commonUnitText def ''
558 [Slice]
559 ${attrsToSection def.sliceConfig}
560 '';
561 };
562
563 # Create a directory that contains systemd definition files from an attrset
564 # that contains the file names as keys and the content as values. The values
565 # in that attrset are determined by the supplied format.
566 definitions = directoryName: format: definitionAttrs:
567 let
568 listOfDefinitions = mapAttrsToList
569 (name: format.generate "${name}.conf")
570 definitionAttrs;
571 in
572 pkgs.runCommand directoryName { } ''
573 mkdir -p $out
574 ${(concatStringsSep "\n"
575 (map (pkg: "cp ${pkg} $out/${pkg.name}") listOfDefinitions)
576 )}
577 '';
578
579 # The maximum number of characters allowed in a GPT partition label. This
580 # limit is specified by UEFI and enforced by systemd-repart.
581 # Corresponds to GPT_LABEL_MAX from systemd's gpt.h.
582 GPTMaxLabelLength = 36;
583
584}