From 1acabeebed483fd8a682eb6b7ef538f5b99d35e2 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sun, 20 Jul 2025 01:35:12 +0200 Subject: [PATCH] Add modular services, system.services --- nixos/modules/module-list.nix | 2 + nixos/modules/system/service/README.md | 28 ++++++ .../system/service/portable/service.nix | 58 ++++++++++++ .../modules/system/service/portable/test.nix | 93 +++++++++++++++++++ .../system/service/systemd/service.nix | 79 ++++++++++++++++ .../modules/system/service/systemd/system.nix | 68 ++++++++++++++ nixos/modules/system/service/systemd/test.nix | 89 ++++++++++++++++++ nixos/modules/system/service/systemd/user.nix | 3 + nixos/tests/all-tests.nix | 13 +++ 9 files changed, 433 insertions(+) create mode 100644 nixos/modules/system/service/README.md create mode 100644 nixos/modules/system/service/portable/service.nix create mode 100644 nixos/modules/system/service/portable/test.nix create mode 100644 nixos/modules/system/service/systemd/service.nix create mode 100644 nixos/modules/system/service/systemd/system.nix create mode 100644 nixos/modules/system/service/systemd/test.nix create mode 100644 nixos/modules/system/service/systemd/user.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index e2e04222383f..4962a9bd9437 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1838,6 +1838,8 @@ ./system/boot/uvesafb.nix ./system/boot/zram-as-tmp.nix ./system/etc/etc-activation.nix + ./system/service/systemd/system.nix + ./system/service/systemd/user.nix ./tasks/auto-upgrade.nix ./tasks/bcache.nix ./tasks/cpu-freq.nix diff --git a/nixos/modules/system/service/README.md b/nixos/modules/system/service/README.md new file mode 100644 index 000000000000..ed0a247267eb --- /dev/null +++ b/nixos/modules/system/service/README.md @@ -0,0 +1,28 @@ + +# Modular Services + +This directory defines a modular service infrastructure for NixOS. +See the [Modular Services chapter] in the manual [[source]](../../doc/manual/development/modular-services.md). + +[Modular Services chapter]: https://nixos.org/manual/nixos/unstable/#modular-services + +# Design decision log + +- `system.services.`. Alternatives considered + - `systemServices`: similar to does not allow importing a composition of services into `system`. Not sure if that's a good idea in the first place, but I've kept the possibility open. + - `services.abstract`: used in https://github.com/NixOS/nixpkgs/pull/267111, but too weird. Service modules should fit naturally into the configuration system. + Also "abstract" is wrong, because it has submodules - in other words, evalModules results, concrete services - not abstract at all. + - `services.modular`: only slightly better than `services.abstract`, but still weird + +- No `daemon.*` options. https://github.com/NixOS/nixpkgs/pull/267111/files#r1723206521 + +- For now, do not add an `enable` option, because it's ambiguous. Does it disable at the Nix level (not generate anything) or at the systemd level (generate a service that is disabled)? + +- Move all process options into a `process` option tree. Putting this at the root is messy, because we also have sub-services at that level. Those are rather distinct. Grouping them "by kind" should raise fewer questions. + +- `modules/system/service/systemd/system.nix` has `system` twice. Not great, but + - they have different meanings + 1. These are system-provided modules, provided by the configuration manager + 2. `systemd/system` configures SystemD _system units_. + - This reserves `modules/service` for actual service modules, at least until those are lifted out of NixOS, potentially + diff --git a/nixos/modules/system/service/portable/service.nix b/nixos/modules/system/service/portable/service.nix new file mode 100644 index 000000000000..58772320abb6 --- /dev/null +++ b/nixos/modules/system/service/portable/service.nix @@ -0,0 +1,58 @@ +{ + lib, + config, + options, + ... +}: +let + inherit (lib) mkOption types; + pathOrStr = types.coercedTo types.path (x: "${x}") types.str; + program = + types.coercedTo ( + types.package + // { + # require mainProgram for this conversion + check = v: v.type or null == "derivation" && v ? meta.mainProgram; + } + ) lib.getExe pathOrStr + // { + description = "main program, path or command"; + descriptionClass = "conjunction"; + }; +in +{ + options = { + services = mkOption { + type = types.attrsOf ( + types.submoduleWith { + modules = [ + ./service.nix + ]; + } + ); + description = '' + A collection of [modular services](https://nixos.org/manual/nixos/unstable/#modular-services) that are configured in one go. + + You could consider the sub-service relationship to be an ownership relation. + It **does not** automatically create any other relationship between services (e.g. systemd slices), unless perhaps such a behavior is explicitly defined and enabled in another option. + ''; + default = { }; + visible = "shallow"; + }; + process = { + executable = mkOption { + type = program; + description = '' + The path to the executable that will be run when the service is started. + ''; + }; + args = lib.mkOption { + type = types.listOf pathOrStr; + description = '' + Arguments to pass to the `executable`. + ''; + default = [ ]; + }; + }; + }; +} diff --git a/nixos/modules/system/service/portable/test.nix b/nixos/modules/system/service/portable/test.nix new file mode 100644 index 000000000000..b4a29122fca7 --- /dev/null +++ b/nixos/modules/system/service/portable/test.nix @@ -0,0 +1,93 @@ +# Run: +# nix-instantiate --eval nixos/modules/system/service/portable/test.nix +let + lib = import ../../../../../lib; + + inherit (lib) mkOption types; + + dummyPkg = + name: + derivation { + system = "dummy"; + name = name; + builder = "/bin/false"; + }; + + exampleConfig = { + _file = "${__curPos.file}:${toString __curPos.line}"; + services = { + service1 = { + process = { + executable = "/usr/bin/echo"; # *giggles* + args = [ "hello" ]; + }; + }; + service2 = { + process = { + # No meta.mainProgram, because it's supposedly an executable script _file_, + # not a directory with a bin directory containing the main program. + executable = dummyPkg "cowsay.sh"; + args = [ "world" ]; + }; + }; + service3 = { + process = { + executable = dummyPkg "cowsay-ng" // { + meta.mainProgram = "cowsay"; + }; + args = [ "!" ]; + }; + }; + }; + }; + + exampleEval = lib.evalModules { + modules = [ + { + options.services = mkOption { + type = types.attrsOf ( + types.submoduleWith { + class = "service"; + modules = [ + ./service.nix + ]; + } + ); + }; + } + exampleConfig + ]; + }; + + test = + assert + exampleEval.config == { + services = { + service1 = { + process = { + executable = "/usr/bin/echo"; + args = [ "hello" ]; + }; + services = { }; + }; + service2 = { + process = { + executable = "${dummyPkg "cowsay.sh"}"; + args = [ "world" ]; + }; + services = { }; + }; + service3 = { + process = { + executable = "${dummyPkg "cowsay-ng"}/bin/cowsay"; + args = [ "!" ]; + }; + services = { }; + }; + }; + }; + + "ok"; + +in +test diff --git a/nixos/modules/system/service/systemd/service.nix b/nixos/modules/system/service/systemd/service.nix new file mode 100644 index 000000000000..5eb3b9e7b038 --- /dev/null +++ b/nixos/modules/system/service/systemd/service.nix @@ -0,0 +1,79 @@ +{ + lib, + config, + systemdPackage, + ... +}: +let + inherit (lib) mkOption types; +in +{ + imports = [ + ../portable/service.nix + (lib.mkAliasOptionModule [ "systemd" "service" ] [ "systemd" "services" "" ]) + (lib.mkAliasOptionModule [ "systemd" "socket" ] [ "systemd" "sockets" "" ]) + ]; + options = { + systemd.services = mkOption { + description = '' + This module configures systemd services, with the notable difference that their unit names will be prefixed with the abstract service name. + + This option's value is not suitable for reading, but you can define a module here that interacts with just the unit configuration in the host system configuration. + + Note that this option contains _deferred_ modules. + This means that the module has not been combined with the system configuration yet, no values can be read from this option. + What you can do instead is define a module that reads from the module arguments (such as `config`) that are available when the module is merged into the system configuration. + ''; + type = types.lazyAttrsOf ( + types.deferredModuleWith { + staticModules = [ + # TODO: Add modules for the purpose of generating documentation? + ]; + } + ); + default = { }; + }; + systemd.sockets = mkOption { + description = '' + Declares systemd socket units. Names will be prefixed by the service name / path. + + See {option}`systemd.services`. + ''; + type = types.lazyAttrsOf types.deferredModule; + default = { }; + }; + + # Also import systemd logic into sub-services + # extends the portable `services` option + services = mkOption { + type = types.attrsOf ( + types.submoduleWith { + class = "service"; + modules = [ + ./service.nix + ]; + specialArgs = { + inherit systemdPackage; + }; + } + ); + }; + }; + config = { + # Note that this is the systemd.services option above, not the system one. + systemd.services."" = { + # TODO description; + wantedBy = lib.mkDefault [ "multi-user.target" ]; + serviceConfig = { + Type = lib.mkDefault "simple"; + Restart = lib.mkDefault "always"; + RestartSec = lib.mkDefault "5"; + ExecStart = [ + (systemdPackage.functions.escapeSystemdExecArgs ( + [ config.process.executable ] ++ config.process.args + )) + ]; + }; + }; + }; +} diff --git a/nixos/modules/system/service/systemd/system.nix b/nixos/modules/system/service/systemd/system.nix new file mode 100644 index 000000000000..9e639921b93a --- /dev/null +++ b/nixos/modules/system/service/systemd/system.nix @@ -0,0 +1,68 @@ +{ + lib, + config, + pkgs, + ... +}: + +let + inherit (lib) concatMapAttrs mkOption types; + + dash = + before: after: + if after == "" then + before + else if before == "" then + after + else + "${before}-${after}"; + + makeUnits = + unitType: prefix: service: + concatMapAttrs (unitName: unitModule: { + "${dash prefix unitName}" = + { ... }: + { + imports = [ unitModule ]; + }; + }) service.systemd.${unitType} + // concatMapAttrs ( + subServiceName: subService: makeUnits unitType (dash prefix subServiceName) subService + ) service.services; +in +{ + # First half of the magic: mix systemd logic into the otherwise abstract services + options = { + system.services = mkOption { + description = '' + A collection of NixOS [modular services](https://nixos.org/manual/nixos/unstable/#modular-services) that are configured as systemd services. + ''; + type = types.attrsOf ( + types.submoduleWith { + class = "service"; + modules = [ + ./service.nix + ]; + specialArgs = { + # perhaps: features."systemd" = { }; + inherit pkgs; + systemdPackage = config.systemd.package; + }; + } + ); + default = { }; + visible = "shallow"; + }; + }; + + # Second half of the magic: siphon units that were defined in isolation to the system + config = { + systemd.services = concatMapAttrs ( + serviceName: topLevelService: makeUnits "services" serviceName topLevelService + ) config.system.services; + + systemd.sockets = concatMapAttrs ( + serviceName: topLevelService: makeUnits "sockets" serviceName topLevelService + ) config.system.services; + }; +} diff --git a/nixos/modules/system/service/systemd/test.nix b/nixos/modules/system/service/systemd/test.nix new file mode 100644 index 000000000000..b393763edbe5 --- /dev/null +++ b/nixos/modules/system/service/systemd/test.nix @@ -0,0 +1,89 @@ +# Run: +# nix-build -A nixosTests.modularService + +{ + evalSystem, + runCommand, + hello, + ... +}: + +let + machine = evalSystem ( + { lib, ... }: + { + + # Test input + + system.services.foo = { + process = { + executable = hello; + args = [ + "--greeting" + "hoi" + ]; + }; + }; + system.services.bar = { + process = { + executable = hello; + args = [ + "--greeting" + "hoi" + ]; + }; + systemd.service = { + serviceConfig.X-Bar = "lol crossbar whatever"; + }; + services.db = { + process = { + executable = hello; + args = [ + "--greeting" + "Hi, I'm a database, would you believe it" + ]; + }; + systemd.service = { + serviceConfig.RestartSec = "42"; + }; + }; + }; + + # irrelevant stuff + system.stateVersion = "25.05"; + fileSystems."/".device = "/test/dummy"; + boot.loader.grub.enable = false; + } + ); + + inherit (machine.config.system.build) toplevel; +in +runCommand "test-modular-service-systemd-units" + { + passthru = { + inherit + machine + toplevel + ; + }; + } + '' + echo ${toplevel}/etc/systemd/system/foo.service: + cat -n ${toplevel}/etc/systemd/system/foo.service + ( + set -x + grep -F 'ExecStart=${hello}/bin/hello --greeting hoi' ${toplevel}/etc/systemd/system/foo.service >/dev/null + + grep -F 'ExecStart=${hello}/bin/hello --greeting hoi' ${toplevel}/etc/systemd/system/bar.service >/dev/null + grep -F 'X-Bar=lol crossbar whatever' ${toplevel}/etc/systemd/system/bar.service >/dev/null + + grep 'ExecStart=${hello}/bin/hello --greeting .*database.*' ${toplevel}/etc/systemd/system/bar-db.service >/dev/null + grep -F 'RestartSec=42' ${toplevel}/etc/systemd/system/bar-db.service >/dev/null + + [[ ! -e ${toplevel}/etc/systemd/system/foo.socket ]] + [[ ! -e ${toplevel}/etc/systemd/system/bar.socket ]] + [[ ! -e ${toplevel}/etc/systemd/system/bar-db.socket ]] + ) + echo 🐬👍 + touch $out + '' diff --git a/nixos/modules/system/service/systemd/user.nix b/nixos/modules/system/service/systemd/user.nix new file mode 100644 index 000000000000..514731233b09 --- /dev/null +++ b/nixos/modules/system/service/systemd/user.nix @@ -0,0 +1,3 @@ +# TBD, analogous to system.nix but for user units +{ +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 07ddbf27b7d8..bbd98618e3e7 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -89,6 +89,16 @@ let featureFlags.minimalModules = { }; }; evalMinimalConfig = module: nixosLib.evalModules { modules = [ module ]; }; + evalSystem = + module: + import ../lib/eval-config.nix { + system = null; + modules = [ + ../modules/misc/nixpkgs/read-only.nix + { nixpkgs.pkgs = pkgs; } + module + ]; + }; inherit (rec { @@ -891,6 +901,9 @@ in mjolnir = runTest ./matrix/mjolnir.nix; mobilizon = runTest ./mobilizon.nix; mod_perl = runTest ./mod_perl.nix; + modularService = pkgs.callPackage ../modules/system/service/systemd/test.nix { + inherit evalSystem; + }; molly-brown = runTest ./molly-brown.nix; mollysocket = runTest ./mollysocket.nix; monado = runTest ./monado.nix;