Files
cinereal ae9994806c lib/services: fix reload/readiness bugs and add compliance coverage
Fixes several correctness bugs primarily around modular services' recent
reload/notification options (#535695) and adds compliance coverage to guard them.

Fixes:

- `lib/services/service.nix`:
  - the reload-conflict assertion had inverted polarity, so it fired on the
    default configuration
  - the `mkIf` guard on `process.reloadCommand` had a misplaced paren, applying
    `!= null` to the `mkIf` result rather than to the condition
  - `process.reloadSignal` derives `process.reloadCommand`, so the assertion
    could not check `reloadCommand != null` -- that fired on every signal-only
    service. The command is now derived at `mkDefault` priority and the
    assertion is gated on `options.process.reloadCommand.highestPrio`, firing
    only when the user also set `reloadCommand` explicitly.
  - change `notificationProtocol` to a sub-module type
- `nixos/modules/system/service/systemd/service.nix`:
  - `systemd.mainExecReload`'s default ran `escapeSystemdExecArgs` (a list
    escaper) on the `nullOr str` `process.reloadCommand`; this threw
    `expected a list but found a string` and would have mangled `$MAINPID`. It
    now uses `process.reloadCommand` verbatim.
  - the `Type` default read a non-existent
    `config.serviceManager.notificationProtocol` instead of
    `config.notificationProtocol`.

Tests:

Extend the modular-service compliance suite to guard the above:

- Portable (manager-agnostic) eval assertions: `reloadSignal` derives
  `reloadCommand`, the conflict assertion does not fire on signal-only services
  but does when both are set explicitly, and `notificationProtocol.systemd`/`.s6`
  default to `false`.
- systemd-specific eval assertions: `serviceConfig.Type` (simple/notify) and
  `serviceConfig.ExecReload` are asserted on the resolved host units. This
  directly guards the `mainExecReload` fix, which threw before it.
- Runtime reload compliance test: a nested reloadable sub-service is started and
  reloaded, asserting the service observed the reload (recorded a SIGHUP marker).
  `callReload` receives the service's name path (the list of names from the
  top-level service down to the target sub-service); each integration joins it
  per its own unit-naming convention (NixOS dash-joins to the systemd unit name,
  e.g. `reload-inner.service`). Keeping it a path list rather than a read-only
  submodule option keeps the suite manager-agnostic.
- `doc/build-helpers/testers.chapter.md`: document `callReload`.

Follow-up to #535695.

Signed-off-by: cinereal <cinereal@riseup.net>
Assisted-by: Claude:claude-opus-4-8
2026-07-20 11:01:04 +02:00
..

Modular Services

This directory defines a modular service infrastructure for NixOS. See the Modular Services chapter in the manual [source].

Design decision log

Initial design

  • system.services.<name>. 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

Configuration Data (configData) Design

Without a mechanism for adding files, all configuration had to go through process.*, requiring process restarts even when those would have been avoidable. Many services implement automatic reloading or reloading on e.g. SIGUSR1, but those mechanisms need files to read. configData provides such files.

Naming and Terminology

  • configData instead of environment.etc: The name configData is service manager agnostic. While systemd system services can use /etc, other service managers may expose configuration data differently (e.g., different directory, relative paths).

  • path attribute: Each configData entry automatically gets a path attribute set by the service manager implementation, allowing services to reference the location of their configuration files. These paths themselves are not subject to change from generation to generation; only their contents are.

  • name attribute: In environment.etc this would be target but that's confusing, especially for symlinks, as it's not the symlink's target.

Service Manager Integration

  • Portable base: The configData interface is declared in lib/services/config-data.nix, making it available to all service manager implementations.

  • Systemd integration: The systemd implementation (systemd/system.nix) maps configData entries to environment.etc entries under /etc/system-services/.

  • Path computation: systemd/config-data-path.nix recursively computes unique paths for services and sub-services (e.g., /etc/system-services/webserver/ vs /etc/system-services/webserver-api/). Fun fact: for the module system it is a completely normal module, despite its recursive definition. If we parameterize /etc/system-services, it will have to become an importApply style module nonetheless (function returning module).

  • Simple attribute structure: Unlike environment.etc, configData uses a simpler structure with just enable, name, text, source, and path attributes. Complex ownership options were omitted for simplicity and portability. Per-service user creation is still TBD.

No pkgs module argument

The modular service infrastructure avoids exposing pkgs as a module argument to service modules. Instead, derivations and builder functions are provided through lexical closure, making dependency relationships explicit and avoiding uncertainty about where dependencies come from.

Benefits

  • Explicit dependencies: Services declare what they need rather than implicitly depending on pkgs
  • No interference: Service modules can be reused in different contexts without assuming a specific pkgs instance. An unexpected pkgs version is not a failure mode anymore.
  • Clarity: With fewer ways to do things, there's no ambiguity about where dependencies come from (from the module, not the OS or service manager)

Implementation

  • Portable layer: Service modules in lib/services/ do not receive pkgs as a module argument. Any required derivations must be provided by the caller.

  • Systemd integration: The systemd/system.nix module imports config-data.nix as a function, providing pkgs in lexical closure:

    (import lib/services/config-data.nix { inherit pkgs; })
    
  • Service modules:

    1. Should explicitly declare their package dependencies as options rather than using pkgs defaults:
    {
      # Bad: uses pkgs module argument
      foo.package = mkOption {
        default = pkgs.python3;
        # ...
      };
    }
    
    {
      # Good: caller provides the package
      foo.package = mkOption {
        type = types.package;
        description = "Python package to use";
        defaultText = lib.literalMD "The package that provided this module.";
      };
    }
    
    1. passthru.services can still provide a complete module using the package's lexical scope, making the module truly self-contained:

    Package (package.nix):

    {
      lib,
      writeScript,
      runtimeShell,
      # ... other dependencies
    }:
    stdenv.mkDerivation (finalAttrs: {
      # ... package definition
    
      passthru.services.default = {
        imports = [
          (lib.modules.importApply ./service.nix {
            inherit writeScript runtimeShell;
          })
        ];
        someService.package = finalAttrs.finalPackage;
      };
    })
    

    Service module (service.nix):

    # Non-module dependencies (importApply)
    { writeScript, runtimeShell }:
    
    # Service module
    {
      lib,
      config,
      options,
      ...
    }:
    {
      # Service definition using writeScript, runtimeShell from lexical scope
      process.argv = [
        (writeScript "wrapper" ''
          #!${runtimeShell}
          # ... wrapper logic
        '')
        # ... other args
      ];
    }