1{ config, lib, pkgs }:
2
3with lib;
4
5let
6 cfg = config.systemd;
7 lndir = "${pkgs.buildPackages.xorg.lndir}/bin/lndir";
8 systemd = cfg.package;
9in rec {
10
11 shellEscape = s: (replaceChars [ "\\" ] [ "\\\\" ] s);
12
13 mkPathSafeName = lib.replaceChars ["@" ":" "\\" "[" "]"] ["-" "-" "-" "" ""];
14
15 # a type for options that take a unit name
16 unitNameType = types.strMatching "[a-zA-Z0-9@%:_.\\-]+[.](service|socket|device|mount|automount|swap|target|path|timer|scope|slice)";
17
18 makeUnit = name: unit:
19 if unit.enable then
20 pkgs.runCommand "unit-${mkPathSafeName name}"
21 { preferLocalBuild = true;
22 allowSubstitutes = false;
23 inherit (unit) text;
24 }
25 ''
26 name=${shellEscape name}
27 mkdir -p "$out/$(dirname "$name")"
28 echo -n "$text" > "$out/$name"
29 ''
30 else
31 pkgs.runCommand "unit-${mkPathSafeName name}-disabled"
32 { preferLocalBuild = true;
33 allowSubstitutes = false;
34 }
35 ''
36 name=${shellEscape name}
37 mkdir -p "$out/$(dirname "$name")"
38 ln -s /dev/null "$out/$name"
39 '';
40
41 boolValues = [true false "yes" "no"];
42
43 digits = map toString (range 0 9);
44
45 isByteFormat = s:
46 let
47 l = reverseList (stringToCharacters s);
48 suffix = head l;
49 nums = tail l;
50 in elem suffix (["K" "M" "G" "T"] ++ digits)
51 && all (num: elem num digits) nums;
52
53 assertByteFormat = name: group: attr:
54 optional (attr ? ${name} && ! isByteFormat attr.${name})
55 "Systemd ${group} field `${name}' must be in byte format [0-9]+[KMGT].";
56
57 hexChars = stringToCharacters "0123456789abcdefABCDEF";
58
59 isMacAddress = s: stringLength s == 17
60 && flip all (splitString ":" s) (bytes:
61 all (byte: elem byte hexChars) (stringToCharacters bytes)
62 );
63
64 assertMacAddress = name: group: attr:
65 optional (attr ? ${name} && ! isMacAddress attr.${name})
66 "Systemd ${group} field `${name}' must be a valid mac address.";
67
68 isPort = i: i >= 0 && i <= 65535;
69
70 assertPort = name: group: attr:
71 optional (attr ? ${name} && ! isPort attr.${name})
72 "Error on the systemd ${group} field `${name}': ${attr.name} is not a valid port number.";
73
74 assertValueOneOf = name: values: group: attr:
75 optional (attr ? ${name} && !elem attr.${name} values)
76 "Systemd ${group} field `${name}' cannot have value `${toString attr.${name}}'.";
77
78 assertHasField = name: group: attr:
79 optional (!(attr ? ${name}))
80 "Systemd ${group} field `${name}' must exist.";
81
82 assertRange = name: min: max: group: attr:
83 optional (attr ? ${name} && !(min <= attr.${name} && max >= attr.${name}))
84 "Systemd ${group} field `${name}' is outside the range [${toString min},${toString max}]";
85
86 assertMinimum = name: min: group: attr:
87 optional (attr ? ${name} && attr.${name} < min)
88 "Systemd ${group} field `${name}' must be greater than or equal to ${toString min}";
89
90 assertOnlyFields = fields: group: attr:
91 let badFields = filter (name: ! elem name fields) (attrNames attr); in
92 optional (badFields != [ ])
93 "Systemd ${group} has extra fields [${concatStringsSep " " badFields}].";
94
95 assertInt = name: group: attr:
96 optional (attr ? ${name} && !isInt attr.${name})
97 "Systemd ${group} field `${name}' is not an integer";
98
99 checkUnitConfig = group: checks: attrs: let
100 # We're applied at the top-level type (attrsOf unitOption), so the actual
101 # unit options might contain attributes from mkOverride and mkIf that we need to
102 # convert into single values before checking them.
103 defs = mapAttrs (const (v:
104 if v._type or "" == "override" then v.content
105 else if v._type or "" == "if" then v.content
106 else v
107 )) attrs;
108 errors = concatMap (c: c group defs) checks;
109 in if errors == [] then true
110 else builtins.trace (concatStringsSep "\n" errors) false;
111
112 toOption = x:
113 if x == true then "true"
114 else if x == false then "false"
115 else toString x;
116
117 attrsToSection = as:
118 concatStrings (concatLists (mapAttrsToList (name: value:
119 map (x: ''
120 ${name}=${toOption x}
121 '')
122 (if isList value then value else [value]))
123 as));
124
125 generateUnits = { allowCollisions ? true, type, units, upstreamUnits, upstreamWants, packages ? cfg.packages, package ? cfg.package }:
126 let
127 typeDir = ({
128 system = "system";
129 initrd = "system";
130 user = "user";
131 nspawn = "nspawn";
132 }).${type};
133 in pkgs.runCommand "${type}-units"
134 { preferLocalBuild = true;
135 allowSubstitutes = false;
136 } ''
137 mkdir -p $out
138
139 # Copy the upstream systemd units we're interested in.
140 for i in ${toString upstreamUnits}; do
141 fn=${package}/example/systemd/${typeDir}/$i
142 if ! [ -e $fn ]; then echo "missing $fn"; false; fi
143 if [ -L $fn ]; then
144 target="$(readlink "$fn")"
145 if [ ''${target:0:3} = ../ ]; then
146 ln -s "$(readlink -f "$fn")" $out/
147 else
148 cp -pd $fn $out/
149 fi
150 else
151 ln -s $fn $out/
152 fi
153 done
154
155 # Copy .wants links, but only those that point to units that
156 # we're interested in.
157 for i in ${toString upstreamWants}; do
158 fn=${package}/example/systemd/${typeDir}/$i
159 if ! [ -e $fn ]; then echo "missing $fn"; false; fi
160 x=$out/$(basename $fn)
161 mkdir $x
162 for i in $fn/*; do
163 y=$x/$(basename $i)
164 cp -pd $i $y
165 if ! [ -e $y ]; then rm $y; fi
166 done
167 done
168
169 # Symlink all units provided listed in systemd.packages.
170 packages="${toString packages}"
171
172 # Filter duplicate directories
173 declare -A unique_packages
174 for k in $packages ; do unique_packages[$k]=1 ; done
175
176 for i in ''${!unique_packages[@]}; do
177 for fn in $i/etc/systemd/${typeDir}/* $i/lib/systemd/${typeDir}/*; do
178 if ! [[ "$fn" =~ .wants$ ]]; then
179 if [[ -d "$fn" ]]; then
180 targetDir="$out/$(basename "$fn")"
181 mkdir -p "$targetDir"
182 ${lndir} "$fn" "$targetDir"
183 else
184 ln -s $fn $out/
185 fi
186 fi
187 done
188 done
189
190 # Symlink units defined by systemd.units where override strategy
191 # shall be automatically detected. If these are also provided by
192 # systemd or systemd.packages, then add them as
193 # <unit-name>.d/overrides.conf, which makes them extend the
194 # upstream unit.
195 for i in ${toString (mapAttrsToList
196 (n: v: v.unit)
197 (lib.filterAttrs (n: v: (attrByPath [ "overrideStrategy" ] "asDropinIfExists" v) == "asDropinIfExists") units))}; do
198 fn=$(basename $i/*)
199 if [ -e $out/$fn ]; then
200 if [ "$(readlink -f $i/$fn)" = /dev/null ]; then
201 ln -sfn /dev/null $out/$fn
202 else
203 ${if allowCollisions then ''
204 mkdir -p $out/$fn.d
205 ln -s $i/$fn $out/$fn.d/overrides.conf
206 '' else ''
207 echo "Found multiple derivations configuring $fn!"
208 exit 1
209 ''}
210 fi
211 else
212 ln -fs $i/$fn $out/
213 fi
214 done
215
216 # Symlink units defined by systemd.units which shall be
217 # treated as drop-in file.
218 for i in ${toString (mapAttrsToList
219 (n: v: v.unit)
220 (lib.filterAttrs (n: v: v ? overrideStrategy && v.overrideStrategy == "asDropin") units))}; do
221 fn=$(basename $i/*)
222 mkdir -p $out/$fn.d
223 ln -s $i/$fn $out/$fn.d/overrides.conf
224 done
225
226 # Create service aliases from aliases option.
227 ${concatStrings (mapAttrsToList (name: unit:
228 concatMapStrings (name2: ''
229 ln -sfn '${name}' $out/'${name2}'
230 '') (unit.aliases or [])) units)}
231
232 # Create .wants and .requires symlinks from the wantedBy and
233 # requiredBy options.
234 ${concatStrings (mapAttrsToList (name: unit:
235 concatMapStrings (name2: ''
236 mkdir -p $out/'${name2}.wants'
237 ln -sfn '../${name}' $out/'${name2}.wants'/
238 '') (unit.wantedBy or [])) units)}
239
240 ${concatStrings (mapAttrsToList (name: unit:
241 concatMapStrings (name2: ''
242 mkdir -p $out/'${name2}.requires'
243 ln -sfn '../${name}' $out/'${name2}.requires'/
244 '') (unit.requiredBy or [])) units)}
245
246 ${optionalString (type == "system") ''
247 # Stupid misc. symlinks.
248 ln -s ${cfg.defaultUnit} $out/default.target
249 ln -s ${cfg.ctrlAltDelUnit} $out/ctrl-alt-del.target
250 ln -s rescue.target $out/kbrequest.target
251
252 mkdir -p $out/getty.target.wants/
253 ln -s ../autovt@tty1.service $out/getty.target.wants/
254
255 ln -s ../remote-fs.target $out/multi-user.target.wants/
256 ''}
257 ''; # */
258
259 makeJobScript = name: text:
260 let
261 scriptName = replaceChars [ "\\" "@" ] [ "-" "_" ] (shellEscape name);
262 out = (pkgs.writeShellScriptBin scriptName ''
263 set -e
264 ${text}
265 '').overrideAttrs (_: {
266 # The derivation name is different from the script file name
267 # to keep the script file name short to avoid cluttering logs.
268 name = "unit-script-${scriptName}";
269 });
270 in "${out}/bin/${scriptName}";
271
272 unitConfig = { config, options, ... }: {
273 config = {
274 unitConfig =
275 optionalAttrs (config.requires != [])
276 { Requires = toString config.requires; }
277 // optionalAttrs (config.wants != [])
278 { Wants = toString config.wants; }
279 // optionalAttrs (config.after != [])
280 { After = toString config.after; }
281 // optionalAttrs (config.before != [])
282 { Before = toString config.before; }
283 // optionalAttrs (config.bindsTo != [])
284 { BindsTo = toString config.bindsTo; }
285 // optionalAttrs (config.partOf != [])
286 { PartOf = toString config.partOf; }
287 // optionalAttrs (config.conflicts != [])
288 { Conflicts = toString config.conflicts; }
289 // optionalAttrs (config.requisite != [])
290 { Requisite = toString config.requisite; }
291 // optionalAttrs (config ? restartTriggers && config.restartTriggers != [])
292 { X-Restart-Triggers = toString config.restartTriggers; }
293 // optionalAttrs (config ? reloadTriggers && config.reloadTriggers != [])
294 { X-Reload-Triggers = toString config.reloadTriggers; }
295 // optionalAttrs (config.description != "") {
296 Description = config.description; }
297 // optionalAttrs (config.documentation != []) {
298 Documentation = toString config.documentation; }
299 // optionalAttrs (config.onFailure != []) {
300 OnFailure = toString config.onFailure; }
301 // optionalAttrs (config.onSuccess != []) {
302 OnSuccess = toString config.onSuccess; }
303 // optionalAttrs (options.startLimitIntervalSec.isDefined) {
304 StartLimitIntervalSec = toString config.startLimitIntervalSec;
305 } // optionalAttrs (options.startLimitBurst.isDefined) {
306 StartLimitBurst = toString config.startLimitBurst;
307 };
308 };
309 };
310
311 serviceConfig = { config, ... }: {
312 config.environment.PATH = mkIf (config.path != []) "${makeBinPath config.path}:${makeSearchPathOutput "bin" "sbin" config.path}";
313 };
314
315 stage2ServiceConfig = {
316 imports = [ serviceConfig ];
317 # Default path for systemd services. Should be quite minimal.
318 config.path = mkAfter [
319 pkgs.coreutils
320 pkgs.findutils
321 pkgs.gnugrep
322 pkgs.gnused
323 systemd
324 ];
325 };
326
327 stage1ServiceConfig = serviceConfig;
328
329 mountConfig = { config, ... }: {
330 config = {
331 mountConfig =
332 { What = config.what;
333 Where = config.where;
334 } // optionalAttrs (config.type != "") {
335 Type = config.type;
336 } // optionalAttrs (config.options != "") {
337 Options = config.options;
338 };
339 };
340 };
341
342 automountConfig = { config, ... }: {
343 config = {
344 automountConfig =
345 { Where = config.where;
346 };
347 };
348 };
349
350 commonUnitText = def: ''
351 [Unit]
352 ${attrsToSection def.unitConfig}
353 '';
354
355 targetToUnit = name: def:
356 { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
357 text =
358 ''
359 [Unit]
360 ${attrsToSection def.unitConfig}
361 '';
362 };
363
364 serviceToUnit = name: def:
365 { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
366 text = commonUnitText def +
367 ''
368 [Service]
369 ${let env = cfg.globalEnvironment // def.environment;
370 in concatMapStrings (n:
371 let s = optionalString (env.${n} != null)
372 "Environment=${builtins.toJSON "${n}=${env.${n}}"}\n";
373 # systemd max line length is now 1MiB
374 # https://github.com/systemd/systemd/commit/e6dde451a51dc5aaa7f4d98d39b8fe735f73d2af
375 in if stringLength s >= 1048576 then throw "The value of the environment variable ‘${n}’ in systemd service ‘${name}.service’ is too long." else s) (attrNames env)}
376 ${if def ? reloadIfChanged && def.reloadIfChanged then ''
377 X-ReloadIfChanged=true
378 '' else if (def ? restartIfChanged && !def.restartIfChanged) then ''
379 X-RestartIfChanged=false
380 '' else ""}
381 ${optionalString (def ? stopIfChanged && !def.stopIfChanged) "X-StopIfChanged=false"}
382 ${attrsToSection def.serviceConfig}
383 '';
384 };
385
386 socketToUnit = name: def:
387 { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
388 text = commonUnitText def +
389 ''
390 [Socket]
391 ${attrsToSection def.socketConfig}
392 ${concatStringsSep "\n" (map (s: "ListenStream=${s}") def.listenStreams)}
393 ${concatStringsSep "\n" (map (s: "ListenDatagram=${s}") def.listenDatagrams)}
394 '';
395 };
396
397 timerToUnit = name: def:
398 { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
399 text = commonUnitText def +
400 ''
401 [Timer]
402 ${attrsToSection def.timerConfig}
403 '';
404 };
405
406 pathToUnit = name: def:
407 { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
408 text = commonUnitText def +
409 ''
410 [Path]
411 ${attrsToSection def.pathConfig}
412 '';
413 };
414
415 mountToUnit = name: def:
416 { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
417 text = commonUnitText def +
418 ''
419 [Mount]
420 ${attrsToSection def.mountConfig}
421 '';
422 };
423
424 automountToUnit = name: def:
425 { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
426 text = commonUnitText def +
427 ''
428 [Automount]
429 ${attrsToSection def.automountConfig}
430 '';
431 };
432
433 sliceToUnit = name: def:
434 { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
435 text = commonUnitText def +
436 ''
437 [Slice]
438 ${attrsToSection def.sliceConfig}
439 '';
440 };
441}