1 { config, lib, pkgs, utils }:
52 inherit (lib.strings) toJSON;
55 lndir = "${pkgs.buildPackages.xorg.lndir}/bin/lndir";
56 systemd = cfg.package;
59 shellEscape = s: (replaceStrings [ "\\" ] [ "\\\\" ] s);
61 mkPathSafeName = replaceStrings ["@" ":" "\\" "[" "]"] ["-" "-" "-" "" ""];
63 # a type for options that take a unit name
64 unitNameType = types.strMatching "[a-zA-Z0-9@%:_.\\-]+[.](service|socket|device|mount|automount|swap|target|path|timer|scope|slice)";
66 makeUnit = name: unit:
68 pkgs.runCommand "unit-${mkPathSafeName name}"
69 { preferLocalBuild = true;
70 allowSubstitutes = false;
71 # unit.text can be null. But variables that are null listed in
72 # passAsFile are ignored by nix, resulting in no file being created,
73 # making the mv operation fail.
74 text = optionalString (unit.text != null) unit.text;
75 passAsFile = [ "text" ];
78 name=${shellEscape name}
79 mkdir -p "$out/$(dirname -- "$name")"
80 mv "$textPath" "$out/$name"
83 pkgs.runCommand "unit-${mkPathSafeName name}-disabled"
84 { preferLocalBuild = true;
85 allowSubstitutes = false;
88 name=${shellEscape name}
89 mkdir -p "$out/$(dirname "$name")"
90 ln -s /dev/null "$out/$name"
93 boolValues = [true false "yes" "no"];
95 digits = map toString (range 0 9);
99 l = reverseList (stringToCharacters s);
103 || (elem suffix (["K" "M" "G" "T"] ++ digits)
104 && all (num: elem num digits) nums);
106 assertByteFormat = name: group: attr:
107 optional (attr ? ${name} && ! isByteFormat attr.${name})
108 "Systemd ${group} field `${name}' must be in byte format [0-9]+[KMGT].";
110 toIntBaseDetected = value: assert (match "[0-9]+|0x[0-9a-fA-F]+" value) != null; (builtins.fromTOML "v=${value}").v;
112 hexChars = stringToCharacters "0123456789abcdefABCDEF";
114 isMacAddress = s: stringLength s == 17
115 && flip all (splitString ":" s) (bytes:
116 all (byte: elem byte hexChars) (stringToCharacters bytes)
119 assertMacAddress = name: group: attr:
120 optional (attr ? ${name} && ! isMacAddress attr.${name})
121 "Systemd ${group} field `${name}' must be a valid MAC address.";
123 assertNetdevMacAddress = name: group: attr:
124 optional (attr ? ${name} && (! isMacAddress attr.${name} && attr.${name} != "none"))
125 "Systemd ${group} field `${name}` must be a valid MAC address or the special value `none`.";
127 isNumberOrRangeOf = check: v:
131 parts = splitString "-" v;
132 lower = toIntBase10 (head parts);
133 upper = if tail parts != [] then toIntBase10 (head (tail parts)) else lower;
135 length parts <= 2 && lower <= upper && check lower && check upper;
136 isPort = i: i >= 0 && i <= 65535;
137 isPortOrPortRange = isNumberOrRangeOf isPort;
139 assertPort = name: group: attr:
140 optional (attr ? ${name} && ! isPort attr.${name})
141 "Error on the systemd ${group} field `${name}': ${attr.name} is not a valid port number.";
143 assertPortOrPortRange = name: group: attr:
144 optional (attr ? ${name} && ! isPortOrPortRange attr.${name})
145 "Error on the systemd ${group} field `${name}': ${attr.name} is not a valid port number or range of port numbers.";
147 assertValueOneOf = name: values: group: attr:
148 optional (attr ? ${name} && !elem attr.${name} values)
149 "Systemd ${group} field `${name}' cannot have value `${toString attr.${name}}'.";
151 assertValuesSomeOfOr = name: values: default: group: attr:
152 optional (attr ? ${name} && !(all (x: elem x values) (splitString " " attr.${name}) || attr.${name} == default))
153 "Systemd ${group} field `${name}' cannot have value `${toString attr.${name}}'.";
155 assertHasField = name: group: attr:
156 optional (!(attr ? ${name}))
157 "Systemd ${group} field `${name}' must exist.";
159 assertRange = name: min: max: group: attr:
160 optional (attr ? ${name} && !(min <= attr.${name} && max >= attr.${name}))
161 "Systemd ${group} field `${name}' is outside the range [${toString min},${toString max}]";
163 assertRangeOrOneOf = name: min: max: values: group: attr:
164 optional (attr ? ${name} && !(((isInt attr.${name} || isFloat attr.${name}) && min <= attr.${name} && max >= attr.${name}) || elem attr.${name} values))
165 "Systemd ${group} field `${name}' is not a value in range [${toString min},${toString max}], or one of ${toString values}";
167 assertRangeWithOptionalMask = name: min: max: group: attr:
168 if (attr ? ${name}) then
169 if isInt attr.${name} then
170 assertRange name min max group attr
171 else if isString attr.${name} then
173 fields = match "([0-9]+|0x[0-9a-fA-F]+)(/([0-9]+|0x[0-9a-fA-F]+))?" attr.${name};
174 in if fields == null then ["Systemd ${group} field `${name}' must either be an integer or two integers separated by a slash (/)."]
176 value = toIntBaseDetected (elemAt fields 0);
177 mask = mapNullable toIntBaseDetected (elemAt fields 2);
179 optional (!(min <= value && max >= value)) "Systemd ${group} field `${name}' has main value outside the range [${toString min},${toString max}]."
180 ++ optional (mask != null && !(min <= mask && max >= mask)) "Systemd ${group} field `${name}' has mask outside the range [${toString min},${toString max}]."
181 else ["Systemd ${group} field `${name}' must either be an integer or a string."]
184 assertMinimum = name: min: group: attr:
185 optional (attr ? ${name} && attr.${name} < min)
186 "Systemd ${group} field `${name}' must be greater than or equal to ${toString min}";
188 assertOnlyFields = fields: group: attr:
189 let badFields = filter (name: ! elem name fields) (attrNames attr); in
190 optional (badFields != [ ])
191 "Systemd ${group} has extra fields [${concatStringsSep " " badFields}].";
193 assertInt = name: group: attr:
194 optional (attr ? ${name} && !isInt attr.${name})
195 "Systemd ${group} field `${name}' is not an integer";
197 assertRemoved = name: see: group: attr:
198 optional (attr ? ${name})
199 "Systemd ${group} field `${name}' has been removed. See ${see}";
201 assertKeyIsSystemdCredential = name: group: attr:
202 optional (attr ? ${name} && !(hasPrefix "@" attr.${name}))
203 "Systemd ${group} field `${name}' is not a systemd credential";
205 checkUnitConfig = group: checks: attrs: let
206 # We're applied at the top-level type (attrsOf unitOption), so the actual
207 # unit options might contain attributes from mkOverride and mkIf that we need to
208 # convert into single values before checking them.
209 defs = mapAttrs (const (v:
210 if v._type or "" == "override" then v.content
211 else if v._type or "" == "if" then v.content
214 errors = concatMap (c: c group defs) checks;
215 in if errors == [] then true
216 else trace (concatStringsSep "\n" errors) false;
218 checkUnitConfigWithLegacyKey = legacyKey: group: checks: attrs:
220 dump = lib.generators.toPretty { }
221 (lib.generators.withRecursion { depthLimit = 2; throwOnDepthLimit = false; } attrs);
225 else if ! attrs?${legacyKey}
227 else if removeAttrs attrs [ legacyKey ] == {}
228 then attrs.${legacyKey}
234 must not mix unit options with the legacy key '${legacyKey}'.
236 This can be fixed by moving all settings from within ${legacyKey}
240 checkUnitConfig group checks attrs';
243 if x == true then "true"
244 else if x == false then "false"
248 concatStrings (concatLists (mapAttrsToList (name: value:
250 ${name}=${toOption x}
252 (if isList value then value else [value]))
255 generateUnits = { allowCollisions ? true, type, units, upstreamUnits, upstreamWants, packages ? cfg.packages, package ? cfg.package }:
263 in pkgs.runCommand "${type}-units"
264 { preferLocalBuild = true;
265 allowSubstitutes = false;
269 # Copy the upstream systemd units we're interested in.
270 for i in ${toString upstreamUnits}; do
271 fn=${package}/example/systemd/${typeDir}/$i
272 if ! [ -e $fn ]; then echo "missing $fn"; false; fi
274 target="$(readlink "$fn")"
275 if [ ''${target:0:3} = ../ ]; then
276 ln -s "$(readlink -f "$fn")" $out/
285 # Copy .wants links, but only those that point to units that
286 # we're interested in.
287 for i in ${toString upstreamWants}; do
288 fn=${package}/example/systemd/${typeDir}/$i
289 if ! [ -e $fn ]; then echo "missing $fn"; false; fi
290 x=$out/$(basename $fn)
295 if ! [ -e $y ]; then rm $y; fi
299 # Symlink all units provided listed in systemd.packages.
300 packages="${toString packages}"
302 # Filter duplicate directories
303 declare -A unique_packages
304 for k in $packages ; do unique_packages[$k]=1 ; done
306 for i in ''${!unique_packages[@]}; do
307 for fn in $i/etc/systemd/${typeDir}/* $i/lib/systemd/${typeDir}/*; do
308 if ! [[ "$fn" =~ .wants$ ]]; then
309 if [[ -d "$fn" ]]; then
310 targetDir="$out/$(basename "$fn")"
311 mkdir -p "$targetDir"
312 ${lndir} "$fn" "$targetDir"
320 # Symlink units defined by systemd.units where override strategy
321 # shall be automatically detected. If these are also provided by
322 # systemd or systemd.packages, then add them as
323 # <unit-name>.d/overrides.conf, which makes them extend the
325 for i in ${toString (mapAttrsToList
327 (filterAttrs (n: v: (attrByPath [ "overrideStrategy" ] "asDropinIfExists" v) == "asDropinIfExists") units))}; do
329 if [ -e $out/$fn ]; then
330 if [ "$(readlink -f $i/$fn)" = /dev/null ]; then
331 ln -sfn /dev/null $out/$fn
333 ${if allowCollisions then ''
335 ln -s $i/$fn $out/$fn.d/overrides.conf
337 echo "Found multiple derivations configuring $fn!"
346 # Symlink units defined by systemd.units which shall be
347 # treated as drop-in file.
348 for i in ${toString (mapAttrsToList
350 (filterAttrs (n: v: v ? overrideStrategy && v.overrideStrategy == "asDropin") units))}; do
353 ln -s $i/$fn $out/$fn.d/overrides.conf
356 # Create service aliases from aliases option.
357 ${concatStrings (mapAttrsToList (name: unit:
358 concatMapStrings (name2: ''
359 ln -sfn '${name}' $out/'${name2}'
360 '') (unit.aliases or [])) units)}
362 # Create .wants, .upholds and .requires symlinks from the wantedBy, upheldBy and
363 # requiredBy options.
364 ${concatStrings (mapAttrsToList (name: unit:
365 concatMapStrings (name2: ''
366 mkdir -p $out/'${name2}.wants'
367 ln -sfn '../${name}' $out/'${name2}.wants'/
368 '') (unit.wantedBy or [])) units)}
370 ${concatStrings (mapAttrsToList (name: unit:
371 concatMapStrings (name2: ''
372 mkdir -p $out/'${name2}.upholds'
373 ln -sfn '../${name}' $out/'${name2}.upholds'/
374 '') (unit.upheldBy or [])) units)}
376 ${concatStrings (mapAttrsToList (name: unit:
377 concatMapStrings (name2: ''
378 mkdir -p $out/'${name2}.requires'
379 ln -sfn '../${name}' $out/'${name2}.requires'/
380 '') (unit.requiredBy or [])) units)}
382 ${optionalString (type == "system") ''
383 # Stupid misc. symlinks.
384 ln -s ${cfg.defaultUnit} $out/default.target
385 ln -s ${cfg.ctrlAltDelUnit} $out/ctrl-alt-del.target
386 ln -s rescue.target $out/kbrequest.target
388 mkdir -p $out/getty.target.wants/
389 ln -s ../autovt@tty1.service $out/getty.target.wants/
391 ln -s ../remote-fs.target $out/multi-user.target.wants/
395 makeJobScript = { name, text, enableStrictShellChecks }:
397 scriptName = replaceStrings [ "\\" "@" ] [ "-" "_" ] (shellEscape name);
399 if ! enableStrictShellChecks then
400 pkgs.writeShellScriptBin scriptName ''
406 pkgs.writeShellApplication {
410 ).overrideAttrs (_: {
411 # The derivation name is different from the script file name
412 # to keep the script file name short to avoid cluttering logs.
413 name = "unit-script-${scriptName}";
417 unitConfig = { config, name, options, ... }: {
420 optionalAttrs (config.requires != [])
421 { Requires = toString config.requires; }
422 // optionalAttrs (config.wants != [])
423 { Wants = toString config.wants; }
424 // optionalAttrs (config.upholds != [])
425 { Upholds = toString config.upholds; }
426 // optionalAttrs (config.after != [])
427 { After = toString config.after; }
428 // optionalAttrs (config.before != [])
429 { Before = toString config.before; }
430 // optionalAttrs (config.bindsTo != [])
431 { BindsTo = toString config.bindsTo; }
432 // optionalAttrs (config.partOf != [])
433 { PartOf = toString config.partOf; }
434 // optionalAttrs (config.conflicts != [])
435 { Conflicts = toString config.conflicts; }
436 // optionalAttrs (config.requisite != [])
437 { Requisite = toString config.requisite; }
438 // optionalAttrs (config ? restartTriggers && config.restartTriggers != [])
439 { X-Restart-Triggers = "${pkgs.writeText "X-Restart-Triggers-${name}" (pipe config.restartTriggers [
441 (map (x: if isPath x then "${x}" else x))
444 // optionalAttrs (config ? reloadTriggers && config.reloadTriggers != [])
445 { X-Reload-Triggers = "${pkgs.writeText "X-Reload-Triggers-${name}" (pipe config.reloadTriggers [
447 (map (x: if isPath x then "${x}" else x))
450 // optionalAttrs (config.description != "") {
451 Description = config.description; }
452 // optionalAttrs (config.documentation != []) {
453 Documentation = toString config.documentation; }
454 // optionalAttrs (config.onFailure != []) {
455 OnFailure = toString config.onFailure; }
456 // optionalAttrs (config.onSuccess != []) {
457 OnSuccess = toString config.onSuccess; }
458 // optionalAttrs (options.startLimitIntervalSec.isDefined) {
459 StartLimitIntervalSec = toString config.startLimitIntervalSec;
460 } // optionalAttrs (options.startLimitBurst.isDefined) {
461 StartLimitBurst = toString config.startLimitBurst;
468 nixosConfig = config;
470 { name, lib, config, ... }: {
472 name = "${name}.service";
473 environment.PATH = mkIf (config.path != []) "${makeBinPath config.path}:${makeSearchPathOutput "bin" "sbin" config.path}";
475 enableStrictShellChecks = lib.mkOptionDefault nixosConfig.systemd.enableStrictShellChecks;
479 pathConfig = { name, config, ... }: {
481 name = "${name}.path";
485 socketConfig = { name, config, ... }: {
487 name = "${name}.socket";
491 sliceConfig = { name, config, ... }: {
493 name = "${name}.slice";
497 targetConfig = { name, config, ... }: {
499 name = "${name}.target";
503 timerConfig = { name, config, ... }: {
505 name = "${name}.timer";
509 stage2ServiceConfig = {
510 imports = [ serviceConfig ];
511 # Default path for systemd services. Should be quite minimal.
512 config.path = mkAfter [
521 stage1ServiceConfig = serviceConfig;
523 mountConfig = { config, ... }: {
525 name = "${utils.escapeSystemdPath config.where}.mount";
527 { What = config.what;
528 Where = config.where;
529 } // optionalAttrs (config.type != "") {
531 } // optionalAttrs (config.options != "") {
532 Options = config.options;
537 automountConfig = { config, ... }: {
539 name = "${utils.escapeSystemdPath config.where}.automount";
541 { Where = config.where;
546 commonUnitText = def: lines: ''
548 ${attrsToSection def.unitConfig}
549 '' + lines + optionalString (def.wantedBy != [ ]) ''
552 WantedBy=${concatStringsSep " " def.wantedBy}
556 { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
560 ${attrsToSection def.unitConfig}
565 { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
566 text = commonUnitText def (''
568 '' + (let env = cfg.globalEnvironment // def.environment;
569 in concatMapStrings (n:
570 let s = optionalString (env.${n} != null)
571 "Environment=${toJSON "${n}=${env.${n}}"}\n";
572 # systemd max line length is now 1MiB
573 # https://github.com/systemd/systemd/commit/e6dde451a51dc5aaa7f4d98d39b8fe735f73d2af
574 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))
575 + (if def ? reloadIfChanged && def.reloadIfChanged then ''
576 X-ReloadIfChanged=true
577 '' else if (def ? restartIfChanged && !def.restartIfChanged) then ''
578 X-RestartIfChanged=false
580 + optionalString (def ? stopIfChanged && !def.stopIfChanged) ''
581 X-StopIfChanged=false
582 '' + attrsToSection def.serviceConfig);
586 { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
587 text = commonUnitText def ''
589 ${attrsToSection def.socketConfig}
590 ${concatStringsSep "\n" (map (s: "ListenStream=${s}") def.listenStreams)}
591 ${concatStringsSep "\n" (map (s: "ListenDatagram=${s}") def.listenDatagrams)}
596 { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
597 text = commonUnitText def ''
599 ${attrsToSection def.timerConfig}
604 { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
605 text = commonUnitText def ''
607 ${attrsToSection def.pathConfig}
612 { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
613 text = commonUnitText def ''
615 ${attrsToSection def.mountConfig}
619 automountToUnit = def:
620 { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
621 text = commonUnitText def ''
623 ${attrsToSection def.automountConfig}
628 { inherit (def) name aliases wantedBy requiredBy upheldBy enable overrideStrategy;
629 text = commonUnitText def ''
631 ${attrsToSection def.sliceConfig}
635 # Create a directory that contains systemd definition files from an attrset
636 # that contains the file names as keys and the content as values. The values
637 # in that attrset are determined by the supplied format.
638 definitions = directoryName: format: definitionAttrs:
640 listOfDefinitions = mapAttrsToList
641 (name: format.generate "${name}.conf")
644 pkgs.runCommand directoryName { } ''
646 ${(concatStringsSep "\n"
647 (map (pkg: "cp ${pkg} $out/${pkg.name}") listOfDefinitions)
651 # The maximum number of characters allowed in a GPT partition label. This
652 # limit is specified by UEFI and enforced by systemd-repart.
653 # Corresponds to GPT_LABEL_MAX from systemd's gpt.h.
654 GPTMaxLabelLength = 36;