From 1d2f7db48a0db35bf38274a6833201525e925397 Mon Sep 17 00:00:00 2001 From: Birdee <85372418+BirdeeHub@users.noreply.github.com> Date: Thu, 4 Jun 2026 02:51:37 -0700 Subject: [PATCH 1/3] feat(modules.systemd): systemd service options Fixed the flux capacitor again (auto-msg qwen2.5-coder:7b) On branch systemd Changes to be committed: (use "git restore --staged ..." to unstage) modified: modules/systemd/check.nix modified: modules/systemd/config.nix --- modules/systemd/check.nix | 506 ++++++++++++++++++++ modules/systemd/config.nix | 295 ++++++++++++ modules/systemd/module.nix | 924 +++++++++++++++++++++++++++++++++++++ 3 files changed, 1725 insertions(+) create mode 100644 modules/systemd/check.nix create mode 100644 modules/systemd/config.nix create mode 100644 modules/systemd/module.nix diff --git a/modules/systemd/check.nix b/modules/systemd/check.nix new file mode 100644 index 00000000..cabaacd1 --- /dev/null +++ b/modules/systemd/check.nix @@ -0,0 +1,506 @@ +{ + pkgs, + self, + tlib, + lib, + ... +}: + +let + inherit (tlib) + areEqual + test + fileContains + isFile + ; + + evalWith = + extraConfig: + (self.lib.evalModule [ + { inherit pkgs; } + ( + { + config, + lib, + wlib, + ... + }: + { + imports = [ wlib.modules.systemd ]; + package = pkgs.hello; + } + ) + extraConfig + ]).config; + + evalWrapper = extraConfig: (evalWith extraConfig).wrapper; + + # Service file naming: lib/systemd//. + unitPath = + wrapper: type: ext: name: + "${wrapper}/lib/systemd/${type}/${name}.${ext}"; + linkPath = + wrapper: type: ext: name: + "${wrapper}/share/systemd/${type}/${name}.${ext}"; +in +test "systemd" { + ############################################################################## + # Unit keys exist in drv + ############################################################################## + "drv keys" = + let + cfg = evalWith ( + { + config, + lib, + wlib, + ... + }: + { + config.systemd.user = { + service.svc = { + Service.ExecStart = "/bin/true"; + }; + socket.sck = { + Socket.ListenStream = [ "/run/x.sock" ]; + }; + timer.tmr = { + Timer.OnCalendar = [ "daily" ]; + }; + target.tgt = { }; + path.pth = { + Path.PathExists = [ "/tmp" ]; + }; + device.dev = { }; + mount.mnt = { + Mount.What = "/dev/sda1"; + Mount.Where = "/mnt"; + }; + automount.aut = { + Automount.Where = "/mnt"; + }; + swap.swp = { + Swap.What = "/swapfile"; + }; + slice.slc = { }; + scope.scp = { }; + }; + config.systemd.system.service."sys-svc" = { + Service.ExecStart = "/bin/system"; + }; + } + ); + keys = [ + "systemd_user_service_svc" + "systemd_user_socket_sck" + "systemd_user_timer_tmr" + "systemd_user_target_tgt" + "systemd_user_path_pth" + "systemd_user_device_dev" + "systemd_user_mount_mnt" + "systemd_user_automount_aut" + "systemd_user_swap_swp" + "systemd_user_slice_slc" + "systemd_user_scope_scp" + "systemd_system_service_sys_svc" + ]; + in + map (k: { + cond = if cfg.drv ? ${k} then "true" else "false"; + msg = "drv key '${k}' not found in cfg.drv"; + }) keys; + + ############################################################################## + # Content of actual generated files in lib/systemd/ + ############################################################################## + "content" = + let + wrapper = evalWrapper ( + { + config, + lib, + wlib, + ... + }: + { + config.systemd.user.service."test" = { + Unit.Description = "test service"; + Service.Type = "simple"; + Service.ExecStart = "/bin/true"; + Install.WantedBy = [ "multi-user.target" ]; + }; + config.systemd.user.socket."sock" = { + Socket.ListenStream = [ "/run/test.sock" ]; + Install.WantedBy = [ "sockets.target" ]; + }; + config.systemd.user.timer."tmr" = { + Timer.OnCalendar = [ "daily" ]; + }; + config.systemd.user.path."p" = { + Path.PathExists = [ "/tmp/test" ]; + }; + } + ); + svcFile = unitPath wrapper "user" "service" "test"; + sockFile = unitPath wrapper "user" "socket" "sock"; + tmrFile = unitPath wrapper "user" "timer" "tmr"; + pthFile = unitPath wrapper "user" "path" "p"; + svcLink = linkPath wrapper "user" "service" "test"; + sockLink = linkPath wrapper "user" "socket" "sock"; + tmrLink = linkPath wrapper "user" "timer" "tmr"; + pthLink = linkPath wrapper "user" "path" "p"; + in + { + "sections" = [ + (isFile svcFile) + (fileContains svcFile "\\[Unit\\]") + (fileContains svcFile "\\[Service\\]") + (fileContains svcFile "\\[Install\\]") + ]; + "service key-values" = [ + (fileContains svcFile "Description=test service") + (fileContains svcFile "Type=simple") + (fileContains svcFile "ExecStart=/bin/true") + (fileContains svcFile "WantedBy=multi-user.target") + ]; + "socket key-values" = [ + (isFile sockFile) + (fileContains sockFile "ListenStream=/run/test.sock") + (fileContains sockFile "WantedBy=sockets.target") + ]; + "timer key-values" = [ + (isFile tmrFile) + (fileContains tmrFile "OnCalendar=daily") + ]; + "path key-values" = [ + (isFile pthFile) + (fileContains pthFile "PathExists=/tmp/test") + ]; + "symlinks in share" = [ + (isFile svcLink) + (isFile sockLink) + (isFile tmrLink) + (isFile pthLink) + ]; + }; + + ############################################################################## + # Enable/disable filtering + ############################################################################## + "enable/disable" = { + "individual unit" = + let + cfg = evalWith ( + { + config, + lib, + wlib, + ... + }: + { + config.systemd.user.service."disabled" = { + enable = false; + }; + config.systemd.user.service."enabled" = { + Service.ExecStart = "/bin/true"; + }; + } + ); + in + [ + (areEqual (cfg.drv ? systemd_user_service_disabled) false) + (areEqual (cfg.drv ? systemd_user_service_enabled) true) + ]; + + "extension type" = + let + cfg = evalWith ( + { + config, + lib, + wlib, + ... + }: + { + config.systemd.user = { + enable = false; + service.svc = { + Service.ExecStart = "/bin/true"; + }; + }; + } + ); + in + [ + (areEqual (cfg.drv ? systemd_user_service_svc) false) + ]; + + "system scope" = + let + cfg = evalWith ( + { + config, + lib, + wlib, + ... + }: + { + config.systemd.system = { + enable = false; + service.svc = { + Service.ExecStart = "/bin/true"; + }; + }; + config.systemd.user.service."svc" = { + Service.ExecStart = "/bin/true"; + }; + } + ); + in + [ + (areEqual (cfg.drv ? systemd_system_service_svc) false) + (areEqual (cfg.drv ? systemd_user_service_svc) true) + ]; + }; + + ############################################################################## + # Section validation + ############################################################################## + "section validation" = { + "invalid sections throw" = + let + cfg = evalWith ( + { + config, + lib, + wlib, + ... + }: + { + config.systemd.user.service."bad" = { + InvalidSection = { + foo = "bar"; + }; + }; + } + ); + result = builtins.tryEval cfg.drv.systemd_user_service_bad; + in + [ + (areEqual result.success false) + ]; + + "X- custom sections" = + let + cfg = evalWith ( + { + config, + lib, + wlib, + ... + }: + { + config.systemd.user.service."test" = { + "X-Custom" = { + foo = "bar"; + }; + Service.ExecStart = "/bin/true"; + }; + } + ); + in + [ + (areEqual (cfg.drv ? systemd_user_service_test) true) + ]; + }; + + ############################################################################## + # Value handling - check actual file output + ############################################################################## + ############################################################################## + # Merging with existing files from the wrapped package + ############################################################################## + "merges with existing unit files" = + let + wrapper = self.lib.evalPackage [ + { inherit pkgs; } + ( + { + config, + lib, + wlib, + pkgs, + ... + }: + { + imports = [ wlib.modules.systemd ]; + package = pkgs.runCommand "test-pkg" { } '' + mkdir -p $out/lib/systemd/user + cat > $out/lib/systemd/user/test.service <<'EOF' + [Unit] + Description=original service + + [Service] + ExecStart=/bin/original + EOF + mkdir -p $out/bin + echo "# placeholder" > $out/bin/test-pkg + chmod +x $out/bin/test-pkg + ''; + systemd.user.service."test" = { + Service.ExecStart = "/bin/additional"; + Install.WantedBy = [ "multi-user.target" ]; + }; + systemd.system.service."sys" = { + Service.ExecStart = "/bin/system-svc"; + }; + } + ) + ]; + svcFile = unitPath wrapper "user" "service" "test"; + sysFile = unitPath wrapper "system" "service" "sys"; + svcLink = linkPath wrapper "user" "service" "test"; + in + [ + (isFile svcFile) + (fileContains svcFile "Description=original service") + (fileContains svcFile "ExecStart=/bin/original") + (fileContains svcFile "ExecStart=/bin/additional") + (fileContains svcFile "WantedBy=multi-user.target") + (isFile sysFile) + (fileContains sysFile "ExecStart=/bin/system-svc") + (isFile svcLink) + ]; + + ############################################################################## + # overwrite option replaces existing file instead of merging + ############################################################################## + "overwrite replaces existing" = + let + basePkg = pkgs.runCommand "test-pkg-overwrite" { } '' + mkdir -p $out/lib/systemd/user + cat > $out/lib/systemd/user/test.service <<'EOF' + [Unit] + Description=original service + EOF + mkdir -p $out/bin + echo "# placeholder" > $out/bin/test-pkg-overwrite + chmod +x $out/bin/test-pkg-overwrite + ''; + wrapper = + (self.lib.evalModule [ + { inherit pkgs; } + ( + { + config, + lib, + wlib, + ... + }: + { + imports = [ + wlib.modules.default + wlib.modules.systemd + ]; + package = basePkg; + } + ) + { + config.systemd.user.service."test" = { + overwrite = true; + Service.ExecStart = "/bin/only-this"; + }; + } + ]).config.wrapper; + svcFile = unitPath wrapper "user" "service" "test"; + in + [ + (isFile svcFile) + (fileContains svcFile "ExecStart=/bin/only-this") + { + cond = "! grep -q 'Description=original' ${svcFile}"; + msg = "overwrite=true should not contain original content"; + } + ]; + + "value handling" = { + "null" = + let + wrapper = evalWrapper ( + { + config, + lib, + wlib, + ... + }: + { + config.systemd.user.service."test" = { + Service.Type = null; + }; + } + ); + servicepath = (unitPath wrapper "user" "service" "test"); + in + [ + (isFile servicepath) + (fileContains servicepath "# Type is unset") + ]; + + "duplicate list keys" = + let + wrapper = evalWrapper ( + { + config, + lib, + wlib, + ... + }: + { + config.systemd.user.service."test" = { + Service.Environment = [ + "FOO=bar" + "BAZ=qux" + ]; + }; + } + ); + servicepath = (unitPath wrapper "user" "service" "test"); + in + [ + (fileContains servicepath "Environment=FOO=bar") + (fileContains servicepath "Environment=BAZ=qux") + ]; + }; + + ############################################################################## + # passAsFile + ############################################################################## + "passAsFile" = + let + cfg = evalWith ( + { + config, + lib, + wlib, + ... + }: + { + config.systemd.user.service."a" = { + Service.ExecStart = "/bin/a"; + }; + config.systemd.user.service."b" = { + Service.ExecStart = "/bin/b"; + }; + } + ); + expected = [ + "systemd_user_service_a" + "systemd_user_service_b" + ]; + in + [ + (areEqual (builtins.sort builtins.lessThan cfg.drv.passAsFile) ( + builtins.sort builtins.lessThan expected + )) + ]; +} diff --git a/modules/systemd/config.nix b/modules/systemd/config.nix new file mode 100644 index 00000000..586c5e7e --- /dev/null +++ b/modules/systemd/config.nix @@ -0,0 +1,295 @@ +{ + config, + lib, + wlib, + pkgs, + ... +}@top: +let + sectionsByExtension = { + service = [ + "Install" + "Service" + ]; + socket = [ + "Install" + "Socket" + ]; + scope = [ "Scope" ]; + target = [ "Install" ]; + device = [ "Install" ]; + mount = [ + "Install" + "Mount" + ]; + timer = [ + "Install" + "Timer" + ]; + automount = [ + "Install" + "Automount" + ]; + swap = [ + "Install" + "Swap" + ]; + path = [ + "Install" + "Path" + ]; + slice = [ + "Install" + "Slice" + ]; + }; + toSystemdFile = + ext: value: + let + # will filter out top level values outside of headings. + # these are not valid in systemd files, and we use that for enable and install option. + # so we filter them out + filtered = lib.filterAttrs (_: v: builtins.isAttrs v) value; + invalidSections = lib.pipe filtered [ + builtins.attrNames + (lib.subtractLists (sectionsByExtension.${ext} ++ [ "Unit" ])) + (builtins.filter (n: !lib.hasPrefix "X-" n)) + ]; + checked = + if !sectionsByExtension ? "${ext}" then + value + else if invalidSections == [ ] then + filtered + else + throw '' + nix-wrapper-modules: Systemd `.${ext}` file contains invalid sections: ${builtins.concatStringsSep " " invalidSections} + ''; + in + lib.generators.toINI { + listsAsDuplicateKeys = true; + mkKeyValue = + k: v: if v == null then "# ${k} is unset" else "${k}=${lib.generators.mkValueStringDefault { } v}"; + } checked; + mapped = lib.pipe config.systemd [ + (lib.mapAttrsToList ( + type: + lib.flip lib.pipe [ + (v: if !v.enable or false then { } else v) + (lib.filterAttrs (n: v: builtins.isAttrs v)) + (lib.mapAttrsToList ( + ext: + lib.flip lib.pipe [ + (lib.filterAttrs (n: v: v.enable or false)) + (lib.mapAttrsToList ( + name: opts: { + inherit + type + ext + name + opts + ; + inherit (opts) install overwrite; + } + )) + ] + )) + ] + )) + builtins.concatLists + builtins.concatLists + (map ( + v: + v + // { + path = "${placeholder config.outputName}/lib/systemd/${v.type}/${v.name}.${v.ext}"; + links = [ "${placeholder config.outputName}/share/systemd/${v.type}/${v.name}.${v.ext}" ]; + wantedBy = + let + val = v.opts.Install.WantedBy or null; + in + if builtins.isList val then val else [ ]; + requiredBy = + let + val = v.opts.Install.RequiredBy or null; + in + if builtins.isList val then val else [ ]; + upheldBy = + let + val = v.opts.Install.upheldBy or null; + in + if builtins.isList val then val else [ ]; + drvKey = wlib.sanitizeEnvVarName ("systemd_" + v.type + "_" + v.ext + "_" + v.name); + content = toSystemdFile v.ext v.opts; + } + )) + ]; +in +{ + config.drv = builtins.listToAttrs (map (v: lib.nameValuePair v.drvKey v.content) mapped) // { + passAsFile = map (v: v.drvKey) mapped; + }; + config.buildCommand.systemd = { + after = [ + "makeWrapper" + "constructFiles" + "symlinkScript" + ]; + # appends to existing systemd files if they exist + # otherwise creates them + data = + let + mkFindExisting = + path: links: + let + p = lib.escapeShellArg path; + in + builtins.concatStringsSep "\n" ( + [ + ''existingPath=""'' + "if [ -f ${p} ] || [ -L ${p} ]; then" + "existingPath=${p}" + "fi" + ] + ++ map ( + ln: + let + l = lib.escapeShellArg ln; + in + '' + if [ -z "$existingPath" ] && { [ -f ${l} ] || [ -L ${l} ]; }; then + existingPath=${l} + fi + '' + ) links + ); + commands = map (v: '' + ${lib.optionalString (!v.overwrite) (mkFindExisting v.path v.links)} + mkdir -p ${lib.escapeShellArg (dirOf v.path)} + ${ + if v.overwrite then + '' + rm -f ${v.path} + { [ -e "''$${v.drvKey}Path" ] && cat "''$${v.drvKey}Path" || echo "''$${v.drvKey}"; } > ${lib.escapeShellArg v.path} + '' + else + let + path = lib.escapeShellArg v.path; + in + '' + if [ -n "$existingPath" ]; then + path=${path} + tempfile="$(mktemp)" + mkdir -p "$(dirname "$tempfile")" + cat "$(readlink -f "$existingPath")" > "$tempfile" + rm -f ${path} + cat "$tempfile" > ${path} + rm "$tempfile" + { [ -e "''$${v.drvKey}Path" ] && cat "''$${v.drvKey}Path" || echo "''$${v.drvKey}"; } >> ${path} + else + { [ -e "''$${v.drvKey}Path" ] && cat "''$${v.drvKey}Path" || echo "''$${v.drvKey}"; } > ${path} + fi + '' + } + ${builtins.concatStringsSep "\n" ( + map (l: '' + # If a parent dir is a link to the lib/systemd dir and thus these are the same file, leave it + if [ ! ${lib.escapeShellArg v.path} -ef ${lib.escapeShellArg l} ]; then + mkdir -p ${lib.escapeShellArg (dirOf l)} + rm -f ${lib.escapeShellArg l} + ln -s ${lib.escapeShellArg v.path} ${lib.escapeShellArg l} + fi + '') v.links + )} + '') mapped; + in + builtins.concatStringsSep "\n" commands; + }; + + # NIXOS: $out/lib/systemd/system $out/lib/systemd/user + # HM: $out/share/systemd/user + # HJEM: $out/lib/systemd/user $out/etc/systemd/user (use $out/lib/systemd/user because its the same as nixos) + # for the relevant install modules, they will place the value in the systemd.?.packages list + # then, if enableServices is true, they will mirror the wantedBy and requiredBy fields to the nixos/hm/hjem module equivalent + # this is because nixos/hm/hjem map those things themselves + # rather than relying on systemd enable to make these links at runtime + config.install.modules.nixos = + { config, ... }: + let + cfg = top.config.install.getWrapperConfig config; + user = { + user = + lib.optionalAttrs (cfg.systemd.user.enable && builtins.elem "nixos" cfg.systemd.user.install) + ( + lib.pipe mapped [ + (builtins.filter (v: v.type == "user" && builtins.elem "nixos" v.install)) + (map (v: { + ${v.ext or null + "s"}.${v.name or null} = { inherit (v) wantedBy requiredBy upheldBy; }; + })) + (builtins.foldl' lib.recursiveUpdate { }) + ] + ); + }; + system = + lib.optionalAttrs (cfg.systemd.system.enable && builtins.elem "nixos" cfg.systemd.system.install) + ( + lib.pipe mapped [ + (builtins.filter (v: v.type == "system" && builtins.elem "nixos" v.install)) + (map (v: { + ${v.ext or null + "s"}.${v.name or null} = { inherit (v) wantedBy requiredBy upheldBy; }; + })) + (builtins.foldl' lib.recursiveUpdate { }) + ] + ); + in + { + systemd = lib.mkIf cfg.enable (system // user // { packages = [ cfg.wrapper ]; }); + }; + config.install.modules.homeManager = + { config, ... }: + let + cfg = top.config.install.getWrapperConfig config; + user = + lib.optionalAttrs (cfg.systemd.user.enable && builtins.elem "homeManager" cfg.systemd.user.install) + ( + lib.pipe mapped [ + (builtins.filter (v: v.type == "user" && builtins.elem "homeManager" v.install)) + (map (v: { + ${v.ext or null + "s"}.${v.name or null}.Install = { inherit (v) wantedBy requiredBy upheldBy; }; + })) + (builtins.foldl' lib.recursiveUpdate { }) + ] + ); + in + { + systemd.user = lib.mkIf cfg.enable ( + user + // { + packages = [ cfg.wrapper ]; + } + ); + }; + config.install.modules.hjem = + { config, ... }: + let + cfg = top.config.install.getWrapperConfig config; + user = + lib.optionalAttrs (cfg.systemd.system.enable && builtins.elem "hjem" cfg.systemd.user.install) + ( + lib.pipe mapped [ + (builtins.filter (v: v.type == "user" && builtins.elem "hjem" v.install)) + (map (v: { + ${v.ext or null + "s"}.${v.name or null} = { inherit (v) wantedBy requiredBy upheldBy; }; + })) + (builtins.foldl' lib.recursiveUpdate { }) + ] + ); + in + { + systemd = lib.mkIf cfg.enable ( + user + // { + packages = [ cfg.wrapper ]; + } + ); + }; +} diff --git a/modules/systemd/module.nix b/modules/systemd/module.nix new file mode 100644 index 00000000..49c4fe4e --- /dev/null +++ b/modules/systemd/module.nix @@ -0,0 +1,924 @@ +{ + config, + lib, + wlib, + ... +}: +let + atom = lib.types.nullOr ( + lib.types.oneOf [ + lib.types.bool + lib.types.float + lib.types.int + lib.types.str + ] + ); + sectionType = lib.types.attrsOf (lib.types.either (lib.types.listOf atom) atom); + freeformType = lib.types.attrsOf sectionType; + unitMod = { + options = { + Unit = lib.mkOption { + type = lib.types.submodule { + freeformType = sectionType; + options = { + Description = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "A human-readable title for the unit."; + }; + Documentation = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "URIs documenting the unit (http://, https://, man:, info:)."; + }; + Wants = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Weak requirement dependencies — listed units are started if possible."; + }; + Requires = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Strong requirement dependencies — listed units must start or this unit fails."; + }; + BindsTo = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Like Requires, but also stops this unit if the bound unit stops unexpectedly."; + }; + Before = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Ordering: start before the listed units."; + }; + After = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Ordering: start after the listed units."; + }; + OnFailure = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Units activated when this unit enters failed state."; + }; + Conflicts = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Negative dependency — cannot run alongside listed units."; + }; + DefaultDependencies = lib.mkOption { + type = lib.types.nullOr lib.types.bool; + default = null; + description = "Whether to add implicit default dependencies."; + }; + X-Reload-Triggers = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "A list of things to watch for reload."; + # TODO: research: things? What exactly? Watch how? + }; + }; + }; + default = { }; + description = '' + [man systemd.unit](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html) + ''; + }; + }; + }; + installMod = { + options = { + Install = lib.mkOption { + type = lib.types.submodule { + freeformType = sectionType; + options = { + WantedBy = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "A list of units that want this unit (adds Wants= dependency from them to this unit)."; + }; + RequiredBy = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "A list of units that require this unit (adds Requires= dependency from them to this unit)."; + }; + UpheldBy = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "A list of units that uphold this unit (adds Upholds= dependency from them to this unit)."; + }; + }; + }; + default = { }; + description = '' + [man systemd.unit](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html) + + NOTE: nixos module configuration only cares about `WantedBy`, `UpheldBy`, and `RequiredBy` and ignores most other fields. + However, manually enabling the option via `systemd enable ` will take this section into account as normal + ''; + }; + }; + }; + systemdFileMods = { + service = { + inherit freeformType; + imports = [ + unitMod + installMod + ]; + options = { + Service = lib.mkOption { + description = '' + [man systemd.service](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html) + ''; + type = lib.types.submodule { + freeformType = sectionType; + options = { + Type = lib.mkOption { + type = lib.types.nullOr ( + lib.types.enum [ + "simple" + "exec" + "forking" + "oneshot" + "dbus" + "notify" + "notify-reload" + "idle" + ] + ); + default = null; + description = "Startup notification type."; + }; + ExecStart = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Main command(s) to execute when the service starts."; + }; + ExecReload = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Command to reload the service configuration."; + }; + ExecStop = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Command to stop the service."; + }; + Restart = lib.mkOption { + type = lib.types.nullOr ( + lib.types.enum [ + "no" + "on-success" + "on-failure" + "on-abnormal" + "on-watchdog" + "on-abort" + "always" + ] + ); + default = null; + description = "Restart condition for the service process."; + }; + RestartSec = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Sleep duration before a restart attempt."; + }; + User = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "The user to run the service as."; + }; + Group = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "The group to run the service as."; + }; + WorkingDirectory = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Working directory for the service process."; + }; + Environment = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Environment variables to set for the service."; + }; + StandardOutput = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Where to connect standard output (journal, syslog, null, tty, ...)."; + }; + StandardError = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Where to connect standard error (journal, syslog, null, tty, ...)."; + }; + X-ReloadIfChanged = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to reload the service when its unit file changes."; + }; + }; + }; + }; + }; + }; + socket = { + inherit freeformType; + imports = [ + unitMod + installMod + ]; + options = { + Socket = lib.mkOption { + description = '' + [man systemd.socket](https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html) + ''; + type = lib.types.submodule { + freeformType = sectionType; + options = { + ListenStream = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "TCP or UNIX stream socket address to listen on."; + }; + ListenDatagram = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "UDP or UNIX datagram socket address to listen on."; + }; + ListenFIFO = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Named pipe (FIFO) path to listen on."; + }; + Accept = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to accept one connection per service instance."; + }; + Backlog = lib.mkOption { + type = lib.types.nullOr lib.types.int; + default = null; + description = "The listen() backlog number."; + }; + SocketUser = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Owner of the UNIX socket inode."; + }; + SocketGroup = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Group of the UNIX socket inode."; + }; + SocketMode = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Permissions of the UNIX socket (e.g. 0666)."; + }; + Service = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Unit name activated by this socket."; + }; + RemoveOnStop = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to remove the socket/FIFO when the unit stops."; + }; + }; + }; + }; + }; + }; + device = { + inherit freeformType; + imports = [ + unitMod + installMod + ]; + }; + mount = { + inherit freeformType; + imports = [ + unitMod + installMod + ]; + options = { + Mount = lib.mkOption { + description = '' + [man systemd.mount](https://www.freedesktop.org/software/systemd/man/latest/systemd.mount.html) + ''; + type = lib.types.submodule { + freeformType = sectionType; + options = { + What = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Device node, filesystem label, UUID, or path to mount."; + }; + Where = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Absolute mount point path (must match unit filename)."; + }; + Type = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Filesystem type string (e.g. ext4, xfs, btrfs)."; + }; + Options = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Comma-separated mount options."; + }; + DirectoryMode = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Permissions for auto-created mount point directories (e.g. 0755)."; + }; + TimeoutSec = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Maximum time to wait for the mount command to finish."; + }; + }; + }; + }; + }; + }; + automount = { + inherit freeformType; + imports = [ + unitMod + installMod + ]; + options = { + Automount = lib.mkOption { + description = '' + [man systemd.automount](https://www.freedesktop.org/software/systemd/man/latest/systemd.automount.html) + ''; + type = lib.types.submodule { + freeformType = sectionType; + options = { + Where = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Absolute automount point path (must match unit filename)."; + }; + DirectoryMode = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Permissions for auto-created automount point directories (e.g. 0755)."; + }; + TimeoutIdleSec = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Idle time after which systemd attempts to unmount."; + }; + }; + }; + }; + }; + }; + swap = { + inherit freeformType; + imports = [ + unitMod + installMod + ]; + options = { + Swap = lib.mkOption { + description = '' + [man systemd.swap](https://www.freedesktop.org/software/systemd/man/latest/systemd.swap.html) + ''; + type = lib.types.submodule { + freeformType = sectionType; + options = { + What = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Device node, file, or fstab-style identifier for the swap device."; + }; + Priority = lib.mkOption { + type = lib.types.nullOr lib.types.int; + default = null; + description = "Swap priority."; + }; + Options = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Comma-separated swapon options (e.g. discard)."; + }; + TimeoutSec = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Maximum time to wait for swapon to finish."; + }; + }; + }; + }; + }; + }; + target = { + inherit freeformType; + imports = [ + unitMod + installMod + ]; + }; + path = { + inherit freeformType; + imports = [ + unitMod + installMod + ]; + options = { + Path = lib.mkOption { + description = '' + [man systemd.path](https://www.freedesktop.org/software/systemd/man/latest/systemd.path.html) + ''; + type = lib.types.submodule { + freeformType = sectionType; + options = { + PathExists = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Activate the unit when a file or directory exists."; + }; + PathExistsGlob = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Activate when at least one file matching the glob exists."; + }; + PathChanged = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Activate when a file changes (triggers on close-after-write)."; + }; + PathModified = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Activate on any write to the file."; + }; + DirectoryNotEmpty = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Activate when a directory contains at least one entry."; + }; + Unit = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Unit to activate when a path triggers (defaults to the matching .service unit)."; + }; + MakeDirectory = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Create the watched directories before monitoring."; + }; + DirectoryMode = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Permissions for auto-created directories (e.g. 0755)."; + }; + }; + }; + }; + }; + }; + timer = { + inherit freeformType; + imports = [ + unitMod + installMod + ]; + options = { + Timer = lib.mkOption { + description = '' + [man systemd.timer](https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html) + ''; + type = lib.types.submodule { + freeformType = sectionType; + options = { + OnActiveSec = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Timer relative to when this timer unit was activated."; + }; + OnBootSec = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Timer relative to boot time."; + }; + OnStartupSec = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Timer relative to when the service manager started."; + }; + OnUnitActiveSec = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Timer relative to when the triggered unit was last activated."; + }; + OnUnitInactiveSec = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Timer relative to when the triggered unit was last deactivated."; + }; + OnCalendar = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = [ ]; + description = "Realtime (wallclock) calendar event expression."; + }; + AccuracySec = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Scheduling accuracy window (default 1min)."; + }; + RandomizedDelaySec = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Random delay added to each timer firing."; + }; + Unit = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Unit to activate when the timer elapses (defaults to the matching .service unit)."; + }; + Persistent = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Catch up on missed OnCalendar= firings after boot."; + }; + WakeSystem = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Wake the system from suspend to meet the timer deadline."; + }; + }; + }; + }; + }; + }; + slice = { + inherit freeformType; + imports = [ + unitMod + installMod + ]; + options = { + Slice = lib.mkOption { + description = '' + [man systemd.slice](https://www.freedesktop.org/software/systemd/man/latest/systemd.slice.html) + ''; + type = lib.types.submodule { + freeformType = sectionType; + options = { + ConcurrencyHardMax = lib.mkOption { + type = lib.types.nullOr (lib.types.either lib.types.int (lib.types.enum [ "infinity" ])); + default = null; + description = '' + Hard limit on the number of active units within this slice and all descendant slices. + + When the limit is reached, activation of additional units fails immediately. + Use "infinity" to disable the limit. + ''; + }; + ConcurrencySoftMax = lib.mkOption { + type = lib.types.nullOr (lib.types.either lib.types.int (lib.types.enum [ "infinity" ])); + default = null; + description = '' + Soft limit on the number of active units within this slice and all descendant slices. + + When the limit is reached, additional unit activations are queued until the + number of active units falls below the limit. Use "infinity" to disable the limit. + ''; + }; + }; + }; + }; + }; + }; + scope = { + inherit freeformType; + imports = [ unitMod ]; + options = { + Scope = lib.mkOption { + description = '' + [man systemd.scope](https://www.freedesktop.org/software/systemd/man/latest/systemd.scope.html) + ''; + type = lib.types.submodule { + freeformType = sectionType; + options = { + OOMPolicy = lib.mkOption { + type = lib.types.nullOr ( + lib.types.enum [ + "continue" + "stop" + "kill" + ] + ); + default = null; + description = "OOM killer behavior for the scope."; + }; + RuntimeMaxSec = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Maximum time the scope may be active (e.g. 1h, 30m)."; + }; + }; + }; + }; + }; + }; + }; + extensionsSubmodule = { name, ... }: { + options = + let + extraMod = { _prefix, ... }: { + options = + let + id = + if builtins.length _prefix >= 3 then + let + get = builtins.elemAt (lib.takeEnd 3 _prefix); + in + "${get 0} ${get 2}.${get 1}" + else + "systemd"; + in + { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable ${id} unit."; + }; + install = lib.mkOption { + type = lib.types.listOf ( + lib.types.enum [ + "nixos" + "homeManager" + "hjem" + ] + ); + default = [ + "nixos" + "homeManager" + "hjem" + ]; + description = "Create installation logic for other nix module systems for ${id} unit. If a list, only for the named module system classes."; + }; + overwrite = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Overwrite existing unit file instead of appending generated content to it if present."; + }; + }; + }; + in + { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable generation of systemd ${name} units."; + }; + install = lib.mkOption { + type = lib.types.listOf ( + lib.types.enum [ + "nixos" + "homeManager" + "hjem" + ] + ); + default = [ + "nixos" + "homeManager" + "hjem" + ]; + description = "Create installation logic for other nix module systems for ${name} units. If a list, only for the named module system classes."; + }; + service = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + imports = [ + systemdFileMods.service + extraMod + ]; + } + ); + default = { }; + description = '' + Accepts a `Service` section: + [man systemd.service](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html) + + Also accepts the general `Unit` or `Install` sections: + [man systemd.unit](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html) + ''; + }; + socket = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + imports = [ + systemdFileMods.socket + extraMod + ]; + } + ); + default = { }; + description = '' + Accepts a `Socket` section: + [man systemd.socket](https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html) + + Also accepts the general `Unit` or `Install` sections: + [man systemd.unit](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html) + ''; + }; + device = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + imports = [ + systemdFileMods.device + extraMod + ]; + } + ); + default = { }; + description = '' + [man systemd.device](https://www.freedesktop.org/software/systemd/man/latest/systemd.device.html) + + Accepts only the general `Unit` or `Install` sections: + [man systemd.unit](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html) + ''; + }; + mount = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + imports = [ + systemdFileMods.mount + extraMod + ]; + } + ); + default = { }; + description = '' + Accepts a `Mount` section: + [man systemd.mount](https://www.freedesktop.org/software/systemd/man/latest/systemd.mount.html) + + Also accepts the general `Unit` or `Install` sections: + [man systemd.unit](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html) + ''; + }; + automount = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + imports = [ + systemdFileMods.automount + extraMod + ]; + } + ); + default = { }; + description = '' + Accepts an `Automount` section: + [man systemd.automount](https://www.freedesktop.org/software/systemd/man/latest/systemd.automount.html) + + Also accepts the general `Unit` or `Install` sections: + [man systemd.unit](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html) + ''; + }; + swap = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + imports = [ + systemdFileMods.swap + extraMod + ]; + } + ); + default = { }; + description = '' + Accepts a `Swap` section: + [man systemd.swap](https://www.freedesktop.org/software/systemd/man/latest/systemd.swap.html) + + Also accepts the general `Unit` or `Install` sections: + [man systemd.unit](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html) + ''; + }; + target = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + imports = [ + systemdFileMods.target + extraMod + ]; + } + ); + default = { }; + description = '' + [man systemd.target](https://www.freedesktop.org/software/systemd/man/latest/systemd.target.html) + + Accepts only the general `Unit` or `Install` sections: + [man systemd.unit](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html) + ''; + }; + path = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + imports = [ + systemdFileMods.path + extraMod + ]; + } + ); + default = { }; + description = '' + Accepts a `Path` section: + [man systemd.path](https://www.freedesktop.org/software/systemd/man/latest/systemd.path.html) + + Also accepts the general `Unit` or `Install` sections: + [man systemd.unit](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html) + ''; + }; + timer = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + imports = [ + systemdFileMods.timer + extraMod + ]; + } + ); + default = { }; + description = '' + Accepts a `Timer` section: + [man systemd.timer](https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html) + + Also accepts the general `Unit` or `Install` sections: + [man systemd.unit](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html) + ''; + }; + slice = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + imports = [ + systemdFileMods.slice + extraMod + ]; + } + ); + default = { }; + description = '' + Accepts a `Slice` section: + [man systemd.slice](https://www.freedesktop.org/software/systemd/man/latest/systemd.slice.html) + + Also accepts the general `Unit` or `Install` sections: + [man systemd.unit](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html) + ''; + }; + scope = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + imports = [ + systemdFileMods.scope + extraMod + ]; + } + ); + default = { }; + description = '' + Accepts a `Scope` section: + [man systemd.scope](https://www.freedesktop.org/software/systemd/man/latest/systemd.scope.html) + + Also accepts the general `Unit` section, but NOT the `Install` section: + [man systemd.unit](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html) + ''; + }; + }; + }; +in +{ + imports = [ + wlib.modules.default + ./config.nix + ]; + config.meta.maintainers = [ wlib.maintainers.birdee ]; + # systemd..{user, system}.{target, path, timer, service, socket, scope, device, mount, automount, swap, path, slice}.{ relevant filemod + enable, install fields } + # enable is a bool option, and install is a list of strings for which classnames to reflect the values to that module system or not, or a bool true for all or false for none + options.systemd = lib.mkOption { + type = lib.types.submodule { + options = { + user = lib.mkOption { + type = lib.types.submodule extensionsSubmodule; + default = { }; + }; + system = lib.mkOption { + type = lib.types.submodule extensionsSubmodule; + default = { }; + }; + }; + }; + }; +} From 4120e096ce867b3de818a690b4136aad5e44b24e Mon Sep 17 00:00:00 2001 From: Birdee <85372418+BirdeeHub@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:58:02 -0700 Subject: [PATCH 2/3] refactor(wrapperModules.niri): use new systemd API --- wrapperModules/n/niri/module.nix | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/wrapperModules/n/niri/module.nix b/wrapperModules/n/niri/module.nix index 75d26d46..831a15c8 100644 --- a/wrapperModules/n/niri/module.nix +++ b/wrapperModules/n/niri/module.nix @@ -61,7 +61,10 @@ let ); in { - imports = [ wlib.modules.default ]; + imports = [ + wlib.modules.default + wlib.modules.systemd + ]; options = { v2-settings = lib.mkOption { @@ -311,7 +314,7 @@ in }; config.filesToPatch = [ "share/applications/*.desktop" - "share/systemd/user/niri.service" + "lib/systemd/user/niri.service" ]; # NOTE: gives users a nice error message about invalid configs, with actual knowledge of niri's config format config.drv.installPhase = lib.mkIf (!config.disableConfigValidation) '' @@ -353,20 +356,14 @@ in + "\n" + config.settings.extraConfig; }; - config.buildCommand.niriReloadConfig = + config.systemd.user.service.niri = lib.mkIf (!config.disableConfigHotReload && lib.versionAtLeast config.package.version "26.04") { - after = [ "symlinkScript" ]; - data = '' - chmod +w ${placeholder config.outputName}/share/systemd/user/niri.service - cat >> ${placeholder config.outputName}/share/systemd/user/niri.service< Date: Wed, 10 Jun 2026 14:32:43 -0700 Subject: [PATCH 3/3] manual .wants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented a feature to make the coffee machine sing karaoke during mornin mornings (auto-msg qwen2.5-coder:7b) On branch systemd Your branch is up to date with 'private/systemd'. Changes to be committed: (use "git restore --staged ..." to unstage) new file: modules/systemd/SCRATCH.nix Fixed the flux capacitor again (auto-msg qwen2.5-coder:7b) On branch systemd Your branch is up to date with 'private/systemd'. Changes to be committed: (use "git restore --staged ..." to unstage) modified: modules/systemd/SCRATCH.nix modified: modules/systemd/config.nix modified: modules/systemd/module.nix --- modules/systemd/config.nix | 101 ++++++++++++------------------------- modules/systemd/module.nix | 81 +++++++++++++++-------------- 2 files changed, 72 insertions(+), 110 deletions(-) diff --git a/modules/systemd/config.nix b/modules/systemd/config.nix index 586c5e7e..0a7722d5 100644 --- a/modules/systemd/config.nix +++ b/modules/systemd/config.nix @@ -65,11 +65,13 @@ let nix-wrapper-modules: Systemd `.${ext}` file contains invalid sections: ${builtins.concatStringsSep " " invalidSections} ''; in - lib.generators.toINI { + value.prefixedContent or "" + + lib.generators.toINI { listsAsDuplicateKeys = true; mkKeyValue = k: v: if v == null then "# ${k} is unset" else "${k}=${lib.generators.mkValueStringDefault { } v}"; - } checked; + } checked + + value.suffixedContent or ""; mapped = lib.pipe config.systemd [ (lib.mapAttrsToList ( type: @@ -88,7 +90,7 @@ let name opts ; - inherit (opts) install overwrite; + inherit (opts) doInstall overwrite; } )) ] @@ -107,17 +109,17 @@ let let val = v.opts.Install.WantedBy or null; in - if builtins.isList val then val else [ ]; + if builtins.isList val && v.doInstall then val else [ ]; requiredBy = let val = v.opts.Install.RequiredBy or null; in - if builtins.isList val then val else [ ]; + if builtins.isList val && v.doInstall then val else [ ]; upheldBy = let val = v.opts.Install.upheldBy or null; in - if builtins.isList val then val else [ ]; + if builtins.isList val && v.doInstall then val else [ ]; drvKey = wlib.sanitizeEnvVarName ("systemd_" + v.type + "_" + v.ext + "_" + v.name); content = toSystemdFile v.ext v.opts; } @@ -200,6 +202,21 @@ in fi '') v.links )} + ${builtins.concatStringsSep "\n" ( + let + mkInstall = variant: target: '' + mkdir -p ${lib.escapeShellArg "${placeholder config.outputName}/lib/systemd/${v.type}/${target}.${variant}"} + ln -sf ${lib.escapeShellArg v.path} \ + ${lib.escapeShellArg "${placeholder config.outputName}/lib/systemd/${v.type}/${target}.${variant}/${v.name}.${v.ext}"} + mkdir -p ${lib.escapeShellArg "${placeholder config.outputName}/share/systemd/${v.type}/${target}.${variant}"} + ln -sf ${lib.escapeShellArg v.path} \ + ${lib.escapeShellArg "${placeholder config.outputName}/share/systemd/${v.type}/${target}.${variant}/${v.name}.${v.ext}"} + ''; + in + map (mkInstall "wants") v.wantedBy + ++ map (mkInstall "requires") v.requiredBy + ++ map (mkInstall "upholds") v.upheldBy + )} '') mapped; in builtins.concatStringsSep "\n" commands; @@ -208,88 +225,34 @@ in # NIXOS: $out/lib/systemd/system $out/lib/systemd/user # HM: $out/share/systemd/user # HJEM: $out/lib/systemd/user $out/etc/systemd/user (use $out/lib/systemd/user because its the same as nixos) - # for the relevant install modules, they will place the value in the systemd.?.packages list - # then, if enableServices is true, they will mirror the wantedBy and requiredBy fields to the nixos/hm/hjem module equivalent - # this is because nixos/hm/hjem map those things themselves - # rather than relying on systemd enable to make these links at runtime config.install.modules.nixos = { config, ... }: let cfg = top.config.install.getWrapperConfig config; - user = { - user = - lib.optionalAttrs (cfg.systemd.user.enable && builtins.elem "nixos" cfg.systemd.user.install) - ( - lib.pipe mapped [ - (builtins.filter (v: v.type == "user" && builtins.elem "nixos" v.install)) - (map (v: { - ${v.ext or null + "s"}.${v.name or null} = { inherit (v) wantedBy requiredBy upheldBy; }; - })) - (builtins.foldl' lib.recursiveUpdate { }) - ] - ); - }; - system = - lib.optionalAttrs (cfg.systemd.system.enable && builtins.elem "nixos" cfg.systemd.system.install) - ( - lib.pipe mapped [ - (builtins.filter (v: v.type == "system" && builtins.elem "nixos" v.install)) - (map (v: { - ${v.ext or null + "s"}.${v.name or null} = { inherit (v) wantedBy requiredBy upheldBy; }; - })) - (builtins.foldl' lib.recursiveUpdate { }) - ] - ); in { - systemd = lib.mkIf cfg.enable (system // user // { packages = [ cfg.wrapper ]; }); + systemd.packages = lib.mkIf (cfg.enable && builtins.elem "nixos" cfg.install.systemd) [ + cfg.wrapper + ]; }; config.install.modules.homeManager = { config, ... }: let cfg = top.config.install.getWrapperConfig config; - user = - lib.optionalAttrs (cfg.systemd.user.enable && builtins.elem "homeManager" cfg.systemd.user.install) - ( - lib.pipe mapped [ - (builtins.filter (v: v.type == "user" && builtins.elem "homeManager" v.install)) - (map (v: { - ${v.ext or null + "s"}.${v.name or null}.Install = { inherit (v) wantedBy requiredBy upheldBy; }; - })) - (builtins.foldl' lib.recursiveUpdate { }) - ] - ); in { - systemd.user = lib.mkIf cfg.enable ( - user - // { - packages = [ cfg.wrapper ]; - } - ); + systemd.user.packages = lib.mkIf (cfg.enable && builtins.elem "homeManager" cfg.install.systemd) [ + cfg.wrapper + ]; }; config.install.modules.hjem = { config, ... }: let cfg = top.config.install.getWrapperConfig config; - user = - lib.optionalAttrs (cfg.systemd.system.enable && builtins.elem "hjem" cfg.systemd.user.install) - ( - lib.pipe mapped [ - (builtins.filter (v: v.type == "user" && builtins.elem "hjem" v.install)) - (map (v: { - ${v.ext or null + "s"}.${v.name or null} = { inherit (v) wantedBy requiredBy upheldBy; }; - })) - (builtins.foldl' lib.recursiveUpdate { }) - ] - ); in { - systemd = lib.mkIf cfg.enable ( - user - // { - packages = [ cfg.wrapper ]; - } - ); + systemd.packages = lib.mkIf (cfg.enable && builtins.elem "hjem" cfg.install.systemd) [ + cfg.wrapper + ]; }; } diff --git a/modules/systemd/module.nix b/modules/systemd/module.nix index 49c4fe4e..1b8e7dc9 100644 --- a/modules/systemd/module.nix +++ b/modules/systemd/module.nix @@ -504,32 +504,32 @@ let freeformType = sectionType; options = { OnActiveSec = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; + type = lib.types.listOf lib.types.str; + default = [ ]; description = "Timer relative to when this timer unit was activated."; }; OnBootSec = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; + type = lib.types.listOf lib.types.str; + default = [ ]; description = "Timer relative to boot time."; }; OnStartupSec = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; + type = lib.types.listOf lib.types.str; + default = [ ]; description = "Timer relative to when the service manager started."; }; OnUnitActiveSec = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; + type = lib.types.listOf lib.types.str; + default = [ ]; description = "Timer relative to when the triggered unit was last activated."; }; OnUnitInactiveSec = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; + type = lib.types.listOf lib.types.str; + default = [ ]; description = "Timer relative to when the triggered unit was last deactivated."; }; OnCalendar = lib.mkOption { - type = lib.types.nullOr lib.types.str; + type = lib.types.listOf lib.types.str; default = [ ]; description = "Realtime (wallclock) calendar event expression."; }; @@ -656,26 +656,26 @@ let default = true; description = "Enable ${id} unit."; }; - install = lib.mkOption { - type = lib.types.listOf ( - lib.types.enum [ - "nixos" - "homeManager" - "hjem" - ] - ); - default = [ - "nixos" - "homeManager" - "hjem" - ]; - description = "Create installation logic for other nix module systems for ${id} unit. If a list, only for the named module system classes."; + doInstall = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Create .wants .requires and .upholds links for ${id} unit."; }; overwrite = lib.mkOption { type = lib.types.bool; default = false; description = "Overwrite existing unit file instead of appending generated content to it if present."; }; + prefixedContent = lib.mkOption { + type = lib.types.lines; + default = ""; + description = "Content to prepend to the beginning of the generated ${id} unit file."; + }; + suffixedContent = lib.mkOption { + type = lib.types.lines; + default = ""; + description = "Content to append to the end of the generated ${id} unit file."; + }; }; }; in @@ -685,21 +685,6 @@ let default = true; description = "Enable generation of systemd ${name} units."; }; - install = lib.mkOption { - type = lib.types.listOf ( - lib.types.enum [ - "nixos" - "homeManager" - "hjem" - ] - ); - default = [ - "nixos" - "homeManager" - "hjem" - ]; - description = "Create installation logic for other nix module systems for ${name} units. If a list, only for the named module system classes."; - }; service = lib.mkOption { type = lib.types.attrsOf ( lib.types.submodule { @@ -905,8 +890,22 @@ in ./config.nix ]; config.meta.maintainers = [ wlib.maintainers.birdee ]; + options.install.systemd = lib.mkOption { + type = lib.types.listOf ( + lib.types.enum [ + "nixos" + "homeManager" + "hjem" + ] + ); + default = [ + "nixos" + "homeManager" + "hjem" + ]; + description = "Add the service files in the derivation to the specified module systems via the install module"; + }; # systemd..{user, system}.{target, path, timer, service, socket, scope, device, mount, automount, swap, path, slice}.{ relevant filemod + enable, install fields } - # enable is a bool option, and install is a list of strings for which classnames to reflect the values to that module system or not, or a bool true for all or false for none options.systemd = lib.mkOption { type = lib.types.submodule { options = {