at 24.11-pre 20 kB view raw
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}