Files
nixpkgs/nixos/modules/system/service/systemd/service.nix
cinereal 1309adc7f3 modular-services: only emit ExecReload when there is a reload command
`systemd.mainExecReload` fell back to `""` when `process.reloadCommand` was
unset, while `systemd.services."".serviceConfig.ExecReload` was defined
unconditionally. `serviceConfig` entries use `unitOption`, which concatenates
definitions only when one of them is a list; two plain strings go through
`mergeEqualOption` instead. So any service setting `ExecReload` through the
systemd escape hatch failed to evaluate, as in `nixosTests.php85.fpm-modular`.
The `""` fallback was also rendered as a bare `ExecReload=` line in every unit
without a reload command, since `attrsToSection` does not filter empty strings.

Default `systemd.mainExecReload` to `process.reloadCommand` itself, which makes
its existing `defaultText` accurate, and guard the definition with `lib.mkIf`,
so the framework leaves `ExecReload` undefined unless there really is a reload
command.

Assisted-by: Claude:claude-opus-5
Signed-off-by: cinereal <cinereal@riseup.net>
2026-08-02 00:48:36 +02:00

224 lines
8.0 KiB
Nix

{
lib,
config,
systemdPackage,
...
}:
let
inherit (lib)
concatMapStringsSep
isDerivation
isInt
isFloat
isPath
isString
mkOption
replaceStrings
types
;
inherit (builtins) toJSON;
# Local copy of systemd exec argument escaping function.
# TODO: This could perhaps be deduplicated, but it is unclear where it should go.
# Preferably, we don't create a hard dependency on NixOS here, so that this
# module can be reused in a non-NixOS context, such as mutaable services
# in /run/systemd/system.
# Quotes an argument for use in Exec* service lines.
# systemd accepts "-quoted strings with escape sequences, toJSON produces
# a subset of these.
# Additionally we escape % to disallow expansion of % specifiers. Any lone ;
# in the input will be turned it ";" and thus lose its special meaning.
# Every $ is escaped to $$, this makes it unnecessary to disable environment
# substitution for the directive.
escapeSystemdExecArg =
arg:
let
s =
if isPath arg then
"${arg}"
else if isString arg then
arg
else if isInt arg || isFloat arg || isDerivation arg then
toString arg
else
throw "escapeSystemdExecArg only allows strings, paths, numbers and derivations";
in
replaceStrings [ "%" "$" ] [ "%%" "$$" ] (toJSON s);
# Quotes a list of arguments into a single string for use in a Exec*
# line.
escapeSystemdExecArgs = concatMapStringsSep " " escapeSystemdExecArg;
in
{
_class = "service";
imports = [
(lib.mkAliasOptionModule [ "systemd" "service" ] [ "systemd" "services" "" ])
(lib.mkAliasOptionModule [ "systemd" "socket" ] [ "systemd" "sockets" "" ])
];
options = {
systemd.lib = mkOption {
description = ''
Library functions for working with systemd services.
Available functions:
- `escapeSystemdExecArgs`: Escapes a list of arguments for use in ExecStart.
Prevents systemd's specifier (%) and variable ($) substitution by escaping
them to %% and $$ respectively.
Example: `escapeSystemdExecArgs [ "/bin/echo" "Unit %n" ]`
produces `"/bin/echo" "Unit %%n"`
'';
type = types.lazyAttrsOf types.raw;
readOnly = true;
};
systemd.mainExecStart = mkOption {
description = ''
Main command line for systemd's ExecStart with systemd's specifier and
environment variable substitution enabled.
This option sets the primary ExecStart entry. Additional ExecStart entries
can be added via `systemd.service.serviceConfig.ExecStart` with `lib.mkBefore`
or `lib.mkAfter`.
This option allows you to use systemd specifiers like `%n` (unit name),
`%i` (instance), `%t` (runtime directory), and environment variables using
`''${VAR}` syntax in your command line.
By default, this is set to the escaped version of {option}`process.argv`
to prevent systemd substitution. Set this option explicitly to enable
systemd's substitution features.
To extend {option}`process.argv` with systemd specifiers, you can append
to the escaped arguments:
```nix
systemd.mainExecStart =
config.systemd.lib.escapeSystemdExecArgs config.process.argv + " --systemd-unit %n";
```
This pattern allows you to pass the unit name (or other systemd specifiers)
as additional arguments while keeping the base command from {option}`process.argv`
properly escaped.
See {manpage}`systemd.service(5)` (section "COMMAND LINES") for details on
variable substitution and {manpage}`systemd.unit(5)` (section "SPECIFIERS")
for available specifiers like `%n`, `%i`, `%t`.
'';
type = types.str;
default = config.systemd.lib.escapeSystemdExecArgs config.process.argv;
defaultText = lib.literalExpression "config.systemd.lib.escapeSystemdExecArgs config.process.argv";
};
systemd.mainExecReload = mkOption {
description = ''
Main command line for systemd's ExecReload with systemd's specifier and
environment variable substitution enabled.
This option sets the primary ExecReload entry, and is the way to extend the
command line derived from {option}`process.reloadCommand`.
This option allows you to use systemd specifiers like `%n` (unit name),
`%i` (instance), `%t` (runtime directory), and environment variables using
`''${VAR}` syntax in your command line.
By default, it is set to {option}`process.reloadCommand`. Because
{option}`process.reloadCommand` is already a command line (not an argument
list), it is used verbatim so that references like `$MAINPID` are preserved.
When {option}`process.reloadCommand` is unset, this option is `null` and no
`ExecReload` is emitted; a service may then set
`systemd.service.serviceConfig.ExecReload` itself.
To extend {option}`process.reloadCommand` with systemd specifiers, you can append
to the command line:
```nix
systemd.mainExecReload =
config.process.reloadCommand + " --systemd-unit %n";
```
This pattern allows you to pass the unit name (or other systemd specifiers)
as additional arguments.
See {manpage}`systemd.service(5)` (section "COMMAND LINES") for details on
variable substitution and {manpage}`systemd.unit(5)` (section "SPECIFIERS")
for available specifiers like `%n`, `%i`, `%t`.
'';
type = types.nullOr types.str;
default = config.process.reloadCommand;
defaultText = lib.literalExpression "config.process.reloadCommand";
};
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;
};
}
);
# Rendered by the portable docs instead.
visible = false;
};
};
config = {
systemd.lib = {
inherit escapeSystemdExecArgs;
};
# Note that this is the systemd.services option above, not the system one.
systemd.services."" = {
# TODO description;
wantedBy = lib.mkDefault [ "multi-user.target" ];
serviceConfig = {
ExecReload = lib.mkIf (config.systemd.mainExecReload != null) config.systemd.mainExecReload;
Type = lib.mkDefault (if config.notificationProtocol.systemd then "notify" else "simple");
Restart = lib.mkDefault "always";
RestartSec = lib.mkDefault "5";
ExecStart = [
config.systemd.mainExecStart
];
};
};
};
}