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