at 23.11-pre 15 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 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 = replaceStrings [ "\\" "@" ] [ "-" "_" ] (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 = "${pkgs.writeText "X-Restart-Triggers" (toString config.restartTriggers)}"; } 293 // optionalAttrs (config ? reloadTriggers && config.reloadTriggers != []) 294 { X-Reload-Triggers = "${pkgs.writeText "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}