Migration
This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs.
Contact me
users/ryan/modules/targets/guix/shepherd.nix
main
· 19.2 KB · 408 lines
raw
| 1 | # systemd user units (as home-manager models them) -> GNU Shepherd services. |
| 2 | # |
| 3 | # Fail-closed by design: every [Section] Key must be either translated with |
| 4 | # the same semantics, or on the explicit `ignoredKeys` list below (keys that |
| 5 | # don't change what runs or with what privileges). Anything else is an |
| 6 | # eval-time error, unless the user acknowledges it per unit via |
| 7 | # `targets.guix.shepherd.unenforced`, in which case it's dropped *with a |
| 8 | # build warning*. Sandboxing (Protect*, Private*, SystemCallFilter, ...) is |
| 9 | # deliberately never translated: a service that declares it is relying on it. |
| 10 | { lib, setpriv }: |
| 11 | let |
| 12 | inherit (lib) |
| 13 | concatStringsSep concatMapStringsSep concatMapStrings concatMap mapAttrs mapAttrsToList |
| 14 | filterAttrs attrNames hasSuffix removeSuffix optional optionals |
| 15 | optionalString elem toList last unique; |
| 16 | |
| 17 | # ---------------------------------------------------------------- helpers |
| 18 | |
| 19 | # JSON string escapes are a subset of Guile's (\" \\ \n \uXXXX). |
| 20 | q = builtins.toJSON; |
| 21 | sym = s: "(string->symbol ${q s})"; |
| 22 | |
| 23 | # HM's unit types leave null / [] placeholders for typed-but-unset keys |
| 24 | # (Description, Documentation, X-*-Triggers, Environment, ExecStart); HM's |
| 25 | # own INI renderer drops them, so do the same. |
| 26 | clean = unit: |
| 27 | filterAttrs (_: s: s != { }) |
| 28 | (mapAttrs (_: filterAttrs (_: v: v != null && v != [ ])) unit); |
| 29 | |
| 30 | str = v: if builtins.isBool v then (if v then "true" else "false") else toString v; |
| 31 | # Scalar keys: systemd's last assignment wins. |
| 32 | scalar = v: str (last (toList v)); |
| 33 | isTrue = v: elem (scalar v) [ "true" "yes" "on" "1" ]; |
| 34 | |
| 35 | # ------------------------------------------------------- key allowlists |
| 36 | |
| 37 | # Keys with no effect on what runs, how, or with what privileges. |
| 38 | ignoredKeys = { |
| 39 | Unit = [ |
| 40 | "Description" # consumed as #:documentation |
| 41 | "Documentation" |
| 42 | "After" # pure ordering; dependencies below imply ordering in Shepherd |
| 43 | "Before" |
| 44 | "RefuseManualStart" |
| 45 | "RefuseManualStop" |
| 46 | "X-SwitchMethod" |
| 47 | ]; |
| 48 | Service = [ "ExecReload" ]; # only reachable via `systemctl reload` |
| 49 | Socket = [ ]; |
| 50 | Install = [ ]; |
| 51 | }; |
| 52 | |
| 53 | limitMap = { |
| 54 | LimitCPU = "cpu"; LimitFSIZE = "fsize"; LimitDATA = "data"; |
| 55 | LimitSTACK = "stack"; LimitCORE = "core"; LimitRSS = "rss"; |
| 56 | LimitNOFILE = "nofile"; LimitAS = "as"; LimitNPROC = "nproc"; |
| 57 | LimitMEMLOCK = "memlock"; |
| 58 | }; |
| 59 | |
| 60 | translatedKeys = { |
| 61 | # Triggers are embedded in the generated file, so a trigger change is a |
| 62 | # content change and activation reloads the service (reload -> restart, |
| 63 | # the conservative direction). |
| 64 | Unit = [ "Wants" "Requires" "BindsTo" "PartOf" "X-Restart-Triggers" "X-Reload-Triggers" ]; |
| 65 | Service = [ |
| 66 | "Type" "ExecStart" "Environment" "WorkingDirectory" "Restart" |
| 67 | "RestartSec" "UMask" "NoNewPrivileges" |
| 68 | ] ++ attrNames limitMap; |
| 69 | Socket = [ |
| 70 | "ListenStream" "FileDescriptorName" "Service" "Accept" |
| 71 | "SocketMode" "DirectoryMode" |
| 72 | ]; |
| 73 | Install = [ "WantedBy" ]; |
| 74 | }; |
| 75 | |
| 76 | sectionsFor = kind: [ "Unit" "Install" (if kind == "service" then "Service" else "Socket") ]; |
| 77 | |
| 78 | # ----------------------------------------------------- systemd specifiers |
| 79 | |
| 80 | specExprs = { |
| 81 | t = ''(getenv "XDG_RUNTIME_DIR")''; |
| 82 | h = ''(getenv "HOME")''; |
| 83 | U = "(number->string (getuid))"; |
| 84 | u = "(passwd:name (getpwuid (getuid)))"; |
| 85 | "%" = q "%"; |
| 86 | }; |
| 87 | specParts = s: builtins.split "%(.)" s; |
| 88 | badSpecs = s: |
| 89 | concatMap |
| 90 | (p: optional (builtins.isList p && !(specExprs ? ${builtins.head p})) "%${builtins.head p}") |
| 91 | (specParts s); |
| 92 | # -> Scheme expression, evaluated at service start (not at load). |
| 93 | expand = s: |
| 94 | let |
| 95 | exprs = concatMap |
| 96 | (p: if builtins.isList p then [ specExprs.${builtins.head p} ] else optional (p != "") (q p)) |
| 97 | (specParts s); |
| 98 | in |
| 99 | if exprs == [ ] then q "" |
| 100 | else if builtins.length exprs == 1 then builtins.head exprs |
| 101 | else "(string-append ${concatStringsSep " " exprs})"; |
| 102 | |
| 103 | # ------------------------------------------------- command-line splitting |
| 104 | |
| 105 | # systemd quoting, minus the parts we refuse: backslash escapes (C-style in |
| 106 | # systemd, so not a literal-char escape) and $VAR expansion are rejected by |
| 107 | # cmdErrors rather than approximated. |
| 108 | tokenize = s: |
| 109 | let |
| 110 | ws = c: c == " " || c == "\t" || c == "\n"; |
| 111 | step = st: c: |
| 112 | if st.q != null then |
| 113 | (if c == st.q then st // { q = null; } else st // { cur = st.cur + c; }) |
| 114 | else if c == "\"" || c == "'" then |
| 115 | st // { q = c; cur = if st.cur == null then "" else st.cur; } |
| 116 | else if ws c then |
| 117 | (if st.cur == null then st else st // { out = st.out ++ [ st.cur ]; cur = null; }) |
| 118 | else |
| 119 | st // { cur = (if st.cur == null then "" else st.cur) + c; }; |
| 120 | end = builtins.foldl' step { out = [ ]; cur = null; q = null; } |
| 121 | (lib.stringToCharacters s); |
| 122 | in |
| 123 | { |
| 124 | tokens = end.out ++ optional (end.cur != null) end.cur; |
| 125 | unterminated = end.q != null; |
| 126 | }; |
| 127 | |
| 128 | cmdErrors = what: s: |
| 129 | let t = tokenize s; first = if t.tokens == [ ] then "" else builtins.head t.tokens; |
| 130 | in |
| 131 | optional (t.tokens == [ ]) "${what} is empty" |
| 132 | ++ optional t.unterminated "${what} has an unterminated quote" |
| 133 | ++ optional (lib.hasInfix "\\" s) "${what} uses backslash escapes (not translated)" |
| 134 | ++ optional (lib.hasInfix "$" s) "${what} uses $VAR expansion (not translated)" |
| 135 | ++ optional (builtins.match "[-@:+!|].*" first != null) |
| 136 | "${what} uses an executable prefix (${builtins.substring 0 1 first}) with no Shepherd equivalent" |
| 137 | ++ map (sp: "${what} uses unsupported specifier ${sp}") (badSpecs s); |
| 138 | |
| 139 | # ----------------------------------------------------------- validation |
| 140 | |
| 141 | octal = s: builtins.match "0?[0-7]{3,4}" s != null; |
| 142 | limitVal = s: builtins.match "(infinity|[0-9]+)(:(infinity|[0-9]+))?" s != null; |
| 143 | seconds = s: builtins.match "([0-9]+)s?" s; |
| 144 | |
| 145 | keyErrors = { kind, name, unit, unenforced }: |
| 146 | let |
| 147 | full = "${name}.${kind}"; |
| 148 | allowed = sectionsFor kind; |
| 149 | in |
| 150 | concatMap |
| 151 | (sec: |
| 152 | if !(elem sec allowed) then |
| 153 | optional (!(elem "${sec}.*" unenforced)) "${full}: section [${sec}] has no Shepherd equivalent" |
| 154 | else |
| 155 | concatMap |
| 156 | (key: optional |
| 157 | (!(elem key (translatedKeys.${sec} ++ ignoredKeys.${sec})) |
| 158 | && !(elem "${sec}.${key}" unenforced)) |
| 159 | "${full}: ${sec}.${key} has no Shepherd equivalent") |
| 160 | (attrNames unit.${sec})) |
| 161 | (attrNames unit); |
| 162 | |
| 163 | # Names Shepherd knows about, used to resolve dependency edges. |
| 164 | graphicalSym = "hm-graphical-session"; |
| 165 | |
| 166 | # Which service a socket unit activates (systemd default: same name). |
| 167 | socketTarget = sname: sock: |
| 168 | removeSuffix ".service" (scalar (sock.Socket.Service or "${sname}.service")); |
| 169 | |
| 170 | resolveDep = { self, services, sockets }: dep: |
| 171 | if dep == "graphical-session.target" then { ok = graphicalSym; } |
| 172 | else if hasSuffix ".service" dep && services ? ${removeSuffix ".service" dep} then |
| 173 | { ok = removeSuffix ".service" dep; } |
| 174 | else if hasSuffix ".socket" dep && sockets ? ${removeSuffix ".socket" dep} then |
| 175 | let target = socketTarget (removeSuffix ".socket" dep) sockets.${removeSuffix ".socket" dep}; |
| 176 | in if target == self then { skip = true; } else { ok = target; } |
| 177 | else { err = "dependency ${dep} is not a translated unit"; }; |
| 178 | |
| 179 | serviceErrors = { name, unit, services, sockets, unenforced }: |
| 180 | let |
| 181 | full = "${name}.service"; |
| 182 | svc = unit.Service or { }; |
| 183 | exec = toList (svc.ExecStart or [ ]); |
| 184 | deps = concatMap (k: toList (unit.Unit.${k} or [ ])) [ "Wants" "Requires" "BindsTo" "PartOf" ]; |
| 185 | envTokens = concatMap (e: (tokenize (str e)).tokens) (toList (svc.Environment or [ ])); |
| 186 | limits = filterAttrs (k: _: limitMap ? ${k}) svc; |
| 187 | in |
| 188 | keyErrors { kind = "service"; inherit name unit unenforced; } |
| 189 | ++ optional (!(elem (scalar (svc.Type or "simple")) [ "simple" "exec" ])) |
| 190 | "${full}: Type=${scalar svc.Type} is not translated (only simple/exec)" |
| 191 | ++ optional (builtins.length exec != 1) |
| 192 | "${full}: needs exactly one ExecStart (has ${toString (builtins.length exec)})" |
| 193 | ++ concatMap (cmdErrors "${full}: ExecStart") (map str exec) |
| 194 | ++ concatMap (e: optional (!(lib.hasInfix "=" e)) "${full}: Environment entry '${e}' is not K=V") envTokens |
| 195 | ++ concatMap (e: map (sp: "${full}: Environment uses unsupported specifier ${sp}") (badSpecs e)) envTokens |
| 196 | ++ optional (lib.any (e: lib.hasInfix "$" e || lib.hasInfix "\\" e) (map str (toList (svc.Environment or [ ])))) |
| 197 | "${full}: Environment uses escapes or $VAR (not translated)" |
| 198 | ++ optionals (svc ? WorkingDirectory) ( |
| 199 | let d = scalar svc.WorkingDirectory; in |
| 200 | optional (lib.hasPrefix "-" d) "${full}: WorkingDirectory=-... (ignore-missing) is not translated" |
| 201 | ++ map (sp: "${full}: WorkingDirectory uses unsupported specifier ${sp}") (badSpecs d)) |
| 202 | ++ optional (svc ? Restart && !(elem (scalar svc.Restart) [ "no" "always" "on-failure" "on-abnormal" ])) |
| 203 | "${full}: Restart=${scalar svc.Restart} is not translated" |
| 204 | ++ optional (svc ? RestartSec && seconds (scalar svc.RestartSec) == null) |
| 205 | "${full}: RestartSec must be whole seconds" |
| 206 | ++ optional (svc ? UMask && !(octal (scalar svc.UMask))) "${full}: UMask must be octal" |
| 207 | ++ mapAttrsToList (k: v: "${full}: ${k}=${scalar v} is not a plain integer/infinity limit") |
| 208 | (filterAttrs (_: v: !(limitVal (scalar v))) limits) |
| 209 | ++ concatMap (d: let r = resolveDep { self = name; inherit services sockets; } d; |
| 210 | in optional (r ? err) "${full}: ${r.err}") deps |
| 211 | ++ concatMap (t: optional (!(elem t [ "default.target" "graphical-session.target" ])) |
| 212 | "${full}: WantedBy=${t} is not translated") |
| 213 | (toList (unit.Install.WantedBy or [ ])); |
| 214 | |
| 215 | socketErrors = { name, unit, services, unenforced }: |
| 216 | let |
| 217 | full = "${name}.socket"; |
| 218 | sock = unit.Socket or { }; |
| 219 | target = socketTarget name unit; |
| 220 | paths = map str (toList (sock.ListenStream or [ ])); |
| 221 | dirMode = if sock ? DirectoryMode then scalar sock.DirectoryMode else null; |
| 222 | in |
| 223 | keyErrors { kind = "socket"; inherit name unit unenforced; } |
| 224 | ++ optional (!(services ? ${target})) "${full}: activates ${target}.service, which is not translated" |
| 225 | ++ optional (paths == [ ]) "${full}: no ListenStream" |
| 226 | ++ concatMap (p: optional (builtins.match "(/|%t|%h).*" p == null) |
| 227 | "${full}: ListenStream=${p} is not a unix socket path (TCP/UDP not translated)") |
| 228 | paths |
| 229 | ++ concatMap (p: map (sp: "${full}: ListenStream uses unsupported specifier ${sp}") (badSpecs p)) paths |
| 230 | ++ optional (sock ? Accept && isTrue sock.Accept) "${full}: Accept=yes (inetd-style) is not translated" |
| 231 | ++ optional (dirMode != null && !(octal dirMode)) "${full}: DirectoryMode must be octal" |
| 232 | # Shepherd can set the parent directory's mode but not the socket's. A |
| 233 | # 0700 parent makes the socket's own mode moot; anything looser doesn't. |
| 234 | ++ optional (sock ? SocketMode && !(elem dirMode [ "0700" "700" ])) |
| 235 | "${full}: SocketMode is only honoured with DirectoryMode=0700 (Shepherd can't chmod the socket)" |
| 236 | ++ concatMap (t: optional (t != "sockets.target") "${full}: WantedBy=${t} is not translated") |
| 237 | (toList (unit.Install.WantedBy or [ ])); |
| 238 | |
| 239 | # ------------------------------------------------------------- emission |
| 240 | |
| 241 | prelude = '' |
| 242 | (use-modules (shepherd service)) |
| 243 | |
| 244 | ;; Unit Environment= first, then Shepherd's *current* environment (which |
| 245 | ;; the compositor bridge updates via `herd eval root (setenv ...)`). |
| 246 | (define (hm-env overrides) |
| 247 | (let ((keys (map (lambda (kv) (substring kv 0 (string-index kv #\=))) |
| 248 | overrides))) |
| 249 | (append overrides |
| 250 | (filter (lambda (kv) |
| 251 | (let ((i (string-index kv #\=))) |
| 252 | (not (and i (member (substring kv 0 i) keys))))) |
| 253 | (environ))))) |
| 254 | ''; |
| 255 | |
| 256 | limitExpr = v: |
| 257 | let |
| 258 | parts = lib.splitString ":" (scalar v); |
| 259 | one = x: if x == "infinity" then "#f" else x; |
| 260 | in |
| 261 | if builtins.length parts == 1 then "${one (builtins.head parts)} ${one (builtins.head parts)}" |
| 262 | else "${one (builtins.elemAt parts 0)} ${one (builtins.elemAt parts 1)}"; |
| 263 | |
| 264 | octalExpr = s: "#o${lib.removePrefix "0" s}"; |
| 265 | |
| 266 | emitService = { name, unit, services, sockets }: |
| 267 | let |
| 268 | svc = unit.Service or { }; |
| 269 | mySockets = filterAttrs (sn: s: socketTarget sn s == name) sockets; |
| 270 | socketActivated = mySockets != { }; |
| 271 | |
| 272 | argv0 = (tokenize (str (builtins.head (toList svc.ExecStart)))).tokens; |
| 273 | argv = optionals (svc ? NoNewPrivileges && isTrue svc.NoNewPrivileges) |
| 274 | [ "${setpriv}/bin/setpriv" "--no-new-privs" ] |
| 275 | ++ argv0; |
| 276 | cmd = "(list ${concatMapStringsSep " " expand argv})"; |
| 277 | |
| 278 | env = concatMap (e: (tokenize (str e)).tokens) (toList (svc.Environment or [ ])); |
| 279 | envExpr = "(hm-env (list ${concatMapStringsSep " " (kv: |
| 280 | let i = lib.stringLength (builtins.head (lib.splitString "=" kv)); in |
| 281 | "(string-append ${q (builtins.substring 0 (i + 1) kv)} ${expand (builtins.substring (i + 1) (-1) kv)})") |
| 282 | env}))"; |
| 283 | |
| 284 | deps = unique (concatMap |
| 285 | (d: let r = resolveDep { self = name; inherit services sockets; } d; |
| 286 | in optional (r ? ok) r.ok) |
| 287 | (concatMap (k: toList (unit.Unit.${k} or [ ])) [ "Wants" "Requires" "BindsTo" "PartOf" ])); |
| 288 | wantedBy = toList (unit.Install.WantedBy or [ ]); |
| 289 | graphical = elem "graphical-session.target" wantedBy || elem graphicalSym deps; |
| 290 | requirement = unique (deps ++ optional graphical graphicalSym); |
| 291 | autostart = elem "default.target" wantedBy |
| 292 | || lib.any (s: elem "sockets.target" (toList (s.Install.WantedBy or [ ]))) (lib.attrValues mySockets); |
| 293 | |
| 294 | limits = filterAttrs (k: _: limitMap ? ${k}) svc; |
| 295 | opts = concatStringsSep "\n " |
| 296 | ([ "#:environment-variables ${envExpr}" ] |
| 297 | ++ optional (svc ? WorkingDirectory) |
| 298 | "#:directory ${let d = scalar svc.WorkingDirectory; in if d == "~" then specExprs.h else expand d}" |
| 299 | ++ optional (svc ? UMask) "#:file-creation-mask ${octalExpr (scalar svc.UMask)}" |
| 300 | ++ optional (limits != { }) "#:resource-limits (list ${concatStringsSep " " |
| 301 | (mapAttrsToList (k: v: "(list '${limitMap.${k}} ${limitExpr v})") limits)})"); |
| 302 | |
| 303 | endpoints = concatStringsSep "\n " (concatMap |
| 304 | (sn: |
| 305 | let |
| 306 | s = mySockets.${sn}.Socket; |
| 307 | fdName = scalar (s.FileDescriptorName or sn); |
| 308 | dirMode = if s ? DirectoryMode then octalExpr (scalar s.DirectoryMode) else "#o755"; |
| 309 | in |
| 310 | map (p: "(endpoint (make-socket-address AF_UNIX ${expand (str p)}) #:name ${q fdName} #:socket-directory-permissions ${dirMode})") |
| 311 | (toList s.ListenStream)) |
| 312 | (attrNames mySockets)); |
| 313 | |
| 314 | # Socket units keep listening after the daemon exits regardless of |
| 315 | # Restart= (that's socket-activation semantics), hence respawn? here. |
| 316 | respawn = socketActivated || elem (scalar (svc.Restart or "no")) [ "always" "on-failure" "on-abnormal" ]; |
| 317 | restartSec = if svc ? RestartSec then builtins.head (seconds (scalar svc.RestartSec)) else null; |
| 318 | |
| 319 | constructor = |
| 320 | if socketActivated then '' |
| 321 | (make-systemd-constructor ${cmd} |
| 322 | (list ${endpoints}) |
| 323 | #:lazy-start? #t |
| 324 | ${opts})'' |
| 325 | else '' |
| 326 | (make-forkexec-constructor ${cmd} |
| 327 | ${opts})''; |
| 328 | in |
| 329 | { |
| 330 | inherit graphical autostart; |
| 331 | text = '' |
| 332 | ;; Generated by home-manager (targets.guix) from ${name}.service${ |
| 333 | optionalString socketActivated " + ${concatMapStringsSep ", " (s: "${s}.socket") (attrNames mySockets)}" |
| 334 | }. |
| 335 | ;; Do not edit; regenerated on every activation.${ |
| 336 | concatMapStrings (t: "\n;; trigger: ${str t}") |
| 337 | (concatMap (k: toList (unit.Unit.${k} or [ ])) [ "X-Restart-Triggers" "X-Reload-Triggers" ])} |
| 338 | ${prelude} |
| 339 | (register-services |
| 340 | (list |
| 341 | (service (list ${sym name}) |
| 342 | #:documentation ${q (scalar (unit.Unit.Description or "${name} (from home-manager)"))} |
| 343 | #:requirement (list ${concatMapStringsSep " " sym requirement}) |
| 344 | #:respawn? ${if respawn then "#t" else "#f"}${ |
| 345 | optionalString (restartSec != null) "\n #:respawn-delay ${restartSec}"} |
| 346 | ;; Constructed at start time so specifiers and the inherited |
| 347 | ;; environment reflect the session at that moment. |
| 348 | #:start (lambda args |
| 349 | (apply ${constructor} |
| 350 | args)) |
| 351 | #:stop ${if socketActivated then "(make-systemd-destructor)" else "(make-kill-destructor)"}))) |
| 352 | ${optionalString (autostart && !graphical) "\n(start-in-the-background (list ${sym name}))"} |
| 353 | ''; |
| 354 | }; |
| 355 | |
| 356 | graphicalSessionFile = '' |
| 357 | ;; Generated by home-manager (targets.guix). Marker for graphical-session.target: |
| 358 | ;; started by the compositor bridge after it pushes its environment into |
| 359 | ;; Shepherd; stopping it stops every graphical service. |
| 360 | (use-modules (shepherd service)) |
| 361 | (register-services |
| 362 | (list (service (list ${sym graphicalSym}) |
| 363 | #:documentation "Graphical session (home-manager graphical-session.target)" |
| 364 | #:start (const #t) |
| 365 | #:stop (const #f)))) |
| 366 | ''; |
| 367 | in |
| 368 | { |
| 369 | inherit graphicalSym; |
| 370 | |
| 371 | # -> { errors, warnings, files = { "<name>.scm" = text; }, graphical = [ names ] } |
| 372 | translate = { systemdUser, unenforced, ignoreUnits }: |
| 373 | let |
| 374 | keep = kind: units: filterAttrs (n: _: !(elem "${n}.${kind}" ignoreUnits)) (mapAttrs (_: clean) units); |
| 375 | services = keep "service" (systemdUser.services or { }); |
| 376 | sockets = keep "socket" (systemdUser.sockets or { }); |
| 377 | unen = full: unenforced.${full} or [ ]; |
| 378 | |
| 379 | # Units that make things happen on their own. Targets and slices are |
| 380 | # inert unless something references them, and any such reference |
| 381 | # (Wants=/WantedBy=/PartOf=/Slice=) is already an error above. |
| 382 | otherKinds = [ "timers" "paths" "mounts" "automounts" ]; |
| 383 | otherErrors = concatMap |
| 384 | (k: map (n: "${n}.${removeSuffix "s" k}: ${removeSuffix "s" k} units are not translated (add to targets.guix.shepherd.ignoreUnits to skip)") |
| 385 | (attrNames (keep (removeSuffix "s" k) (systemdUser.${k} or { })))) |
| 386 | otherKinds; |
| 387 | |
| 388 | errors = |
| 389 | concatMap (n: serviceErrors { name = n; unit = services.${n}; inherit services sockets; unenforced = unen "${n}.service"; }) (attrNames services) |
| 390 | ++ concatMap (n: socketErrors { name = n; unit = sockets.${n}; inherit services; unenforced = unen "${n}.socket"; }) (attrNames sockets) |
| 391 | ++ otherErrors |
| 392 | ++ concatMap (full: optional (!(services ? ${removeSuffix ".service" full}) && !(sockets ? ${removeSuffix ".socket" full})) |
| 393 | "targets.guix.shepherd.unenforced: ${full} is not a defined service/socket") |
| 394 | (attrNames unenforced); |
| 395 | |
| 396 | warnings = concatMap |
| 397 | (full: map (k: "targets.guix: ${full}: ${k} dropped (not enforced under Shepherd)") unenforced.${full}) |
| 398 | (attrNames unenforced); |
| 399 | |
| 400 | emitted = mapAttrs (n: u: emitService { name = n; unit = u; inherit services sockets; }) services; |
| 401 | in |
| 402 | { |
| 403 | inherit errors warnings; |
| 404 | graphical = attrNames (filterAttrs (_: e: e.graphical) emitted); |
| 405 | files = { "${graphicalSym}.scm" = graphicalSessionFile; } |
| 406 | // lib.mapAttrs' (n: e: lib.nameValuePair "${n}.scm" e.text) emitted; |
| 407 | }; |
| 408 | } |