lib/services/service: add flags and flagFormat options (#546008)

This commit is contained in:
Robert Hensing
2026-08-07 15:58:33 +00:00
committed by GitHub
3 changed files with 264 additions and 21 deletions

View File

@@ -12,7 +12,31 @@
}:
let
inherit (lib) mkEnableOption mkOption types;
# Paths are interpolated rather than `toString`ed on purpose: interpolation
# copies the path into the store, so the resulting argument still resolves on
# the machine that runs the service. `toString` would yield the path of the
# source tree the configuration was evaluated from, which is not there at
# runtime.
pathOrStr = types.coercedTo types.path (x: "${x}") types.str;
# `argv` and `flags` share a single `lib.mkOrder` space, so flags need a
# priority. This one sits between `lib.modules.defaultOrderPriority` (1000,
# what an unadorned `argv` definition gets) and `lib.mkAfter` (1500): plain
# flags follow plain `argv` entries, while `lib.mkAfter` on `argv` still lands
# after the flags. See the `flags` option description.
unadornedFlagPriority = 1250;
# `attrListWith` re-emits every flag wrapped in `lib.mkOrder`, using
# `lib.modules.defaultOrderPriority` for flags that carried no ordering
# property of their own. Rewrite exactly that priority; anything else is an
# explicit `lib.mkOrder` from the user and is passed through verbatim.
atFlagPriority =
def:
if def.value._type or null == "order" && def.value.priority == lib.modules.defaultOrderPriority then
def // { value = lib.mkOrder unadornedFlagPriority def.value.content; }
else
def;
in
{
# https://nixos.org/manual/nixos/unstable/#modular-services
@@ -49,6 +73,100 @@ in
This is a raw command-line that should not contain any shell escaping.
If expansion of environmental variables is required then use
a shell script or `importas` from `pkgs.execline`.
When `flags` are set, the arguments rendered from them are merged into
`argv`. See `flags` for how the two are ordered against each other.
'';
};
flagFormat = mkOption {
type = types.functionTo (types.attrsOf types.anything);
default = name: {
option = name;
sep = null;
explicitBool = false;
};
description = ''
Function mapping flag names to option format specs
for `lib.cli.toCommandLine`.
Receives the flag name and returns `{ option, sep, explicitBool, formatArg? }`.
'';
example = lib.literalExpression ''
name: {
option = name;
sep = "=";
explicitBool = false;
}
'';
};
flags = mkOption {
type = types.attrListWith {
elemType = types.nullOr (
types.oneOf [
types.bool
types.int
# `pathOrStr`, not `types.path`: `lib.cli.toCommandLine` renders
# values with `lib.generators.mkValueStringDefault`, which has no
# case for paths and would abort.
pathOrStr
]
);
asAttrs = true;
};
default = { };
description = ''
Flags to pass to the service process.
The key is the flag name (e.g. `"--port"`), the value is the flag value.
Each `name = value` pair is rendered via `lib.cli.toCommandLine`
using `flagFormat`.
- `null`: the flag is omitted (regardless of `flagFormat`)
- bool: rendered per `flagFormat.explicitBool`
- `explicitBool = false` (default): `true` emits the bare flag,
`false` is omitted
- `explicitBool = true`: both `true` and `false` are rendered as
explicit arguments via `flagFormat.formatArg`
- string / path / int: rendered as the option's argument, joined to the
option name per `flagFormat.sep` and stringified by
`flagFormat.formatArg`
To pass the same flag multiple times, use the list form with
repeated keys, e.g.
`[ { "--host" = "a"; } { "--host" = "b"; } ]`.
The rendered arguments are merged into `argv`, so `argv` and `flags`
share a single `lib.mkOrder` space:
- A flag with no ordering property of its own is placed at priority
1250, between `lib.modules.defaultOrderPriority` (1000, which is
what an unadorned `argv` definition gets) and `lib.mkAfter` (1500).
Plain flags therefore follow the command name and any other plain
`argv` arguments.
- `lib.mkAfter` on `argv` still lands after the flags, which is how
trailing positional arguments are expressed.
- `lib.mkOrder` on a flag is honoured verbatim against `argv`, so a
sub-command can be placed between two groups of flags.
Because 1250 is substituted for flags that carry no ordering property,
`lib.mkOrder 1000` on a flag is indistinguishable from leaving that
flag unadorned. To order a flag around plain `argv` entries, pick a
priority next to 1000, such as 999 or 1001.
'';
example = lib.literalExpression ''
{
"--port" = "8080";
"--verbose" = true;
# ordered ahead of the unadorned flags above
"--config" = lib.mkOrder 1100 "/etc/foo.conf";
}
# or, for repeated flags:
[
{ "--host" = "localhost"; }
{ "--host" = "0.0.0.0"; }
]
'';
};
@@ -103,5 +221,9 @@ in
process.reloadCommand = lib.mkIf (config.process.reloadSignal != null) (
lib.mkDefault "${pkgs.coreutils}/bin/kill -${config.process.reloadSignal} $MAINPID"
);
process.argv = lib.modules.mapDefinitionValue (
attr: lib.cli.toCommandLine config.process.flagFormat attr
) (lib.mkMerge (map atFlagPriority options.process.flags.valueMeta.definitions));
};
}

View File

@@ -77,6 +77,60 @@ let
];
};
};
# The default `flagFormat`, and one flag of every supported value kind.
flagsDefault = {
process = {
argv = [ "/bin/flagged" ];
flags = {
"--bool-off" = false;
"--bool-on" = true;
"--config" = ./test.nix;
"--count" = 3;
"--name" = "example";
"--unset" = null;
};
};
};
# A `flagFormat` that joins with `=` and spells out booleans.
flagsCustomFormat = {
process = {
argv = [ "/bin/flagged" ];
flagFormat = name: {
option = "--${name}";
sep = "=";
explicitBool = true;
};
flags = {
port = 8080;
quiet = false;
verbose = true;
};
};
};
# The list form, which allows a flag to be repeated.
flagsRepeated = {
process = {
argv = [ "/bin/flagged" ];
flags = [
{ "--host" = "a"; }
{ "--host" = "b"; }
];
};
};
# `argv` and `flags` share one `lib.mkOrder` space.
flagsOrdering = {
process = {
argv = lib.mkMerge [
(lib.mkBefore [ "/bin/gt" ])
(lib.mkOrder 800 [ "server" ])
(lib.mkAfter [ "TRAILING" ])
];
flags = lib.mkMerge [
{ "--listen" = "a"; }
{ "--disable-landlock" = lib.mkOrder 600 true; }
];
};
};
};
};
@@ -91,10 +145,16 @@ let
];
};
# Every service carries some assertions that hold; only the violated ones are of interest here.
failures = lib.filter (a: !a.assertion);
filterEval =
config:
lib.optionalAttrs (config ? process) {
inherit (config) assertions warnings process;
inherit (config) warnings;
assertions = failures config.assertions;
# Only `argv` is relevant here; `process` also carries the reload options.
process = { inherit (config.process) argv; };
}
// {
services = lib.mapAttrs (k: filterEval) config.services;
@@ -156,6 +216,65 @@ let
assertions = [ ];
warnings = [ ];
};
flagsDefault = {
process = {
argv = [
"/bin/flagged"
"--bool-on"
"--config"
"${./test.nix}"
"--count"
"3"
"--name"
"example"
];
};
services = { };
assertions = [ ];
warnings = [ ];
};
flagsCustomFormat = {
process = {
argv = [
"/bin/flagged"
"--port=8080"
"--quiet=false"
"--verbose=true"
];
};
services = { };
assertions = [ ];
warnings = [ ];
};
flagsRepeated = {
process = {
argv = [
"/bin/flagged"
"--host"
"a"
"--host"
"b"
];
};
services = { };
assertions = [ ];
warnings = [ ];
};
flagsOrdering = {
process = {
argv = [
"/bin/gt"
"--disable-landlock"
"server"
"--listen"
"a"
"TRAILING"
];
};
services = { };
assertions = [ ];
warnings = [ ];
};
};
};
@@ -165,7 +284,7 @@ let
];
assert
portable-lib.getAssertions [ "service1" ] exampleEval.config.services.service1 == [
failures (portable-lib.getAssertions [ "service1" ] exampleEval.config.services.service1) == [
{
message = "in service1: you can't enable this for that reason";
assertion = false;
@@ -177,7 +296,7 @@ let
"in service3.services.exclacow: The `bar' service is deprecated and will go away soon!"
];
assert
portable-lib.getAssertions [ "service3" ] exampleEval.config.services.service3 == [
failures (portable-lib.getAssertions [ "service3" ] exampleEval.config.services.service3) == [
{
message = "in service3.services.exclacow: you can't enable this for such reason";
assertion = false;

View File

@@ -10,10 +10,8 @@
}:
let
inherit (lib)
concatMap
getExe
mkOption
optional
types
;
cfg = config.snid;
@@ -141,22 +139,26 @@ in
process.argv = [
(getExe cfg.package)
"-mode"
cfg.mode
]
++ concatMap (l: [
"-listen"
l
]) cfg.listen
++ concatMap (c: [
"-backend-cidr"
c
]) cfg.backendCidrs
++ optional (cfg.defaultHostname != null) "-default-hostname=${cfg.defaultHostname}"
++ optional (cfg.nat46Prefix != null) "-nat46-prefix=${cfg.nat46Prefix}"
++ optional (cfg.backendPort != null) "-backend-port=${toString cfg.backendPort}"
++ optional (cfg.unixDirectory != null) "-unix-directory=${cfg.unixDirectory}"
++ optional cfg.proxyProto "-proxy-proto";
];
process.flagFormat = flag: {
option = "-${flag}";
explicitBool = false;
sep = null;
};
process.flags = lib.mkMerge [
{
mode = cfg.mode;
default-hostname = cfg.defaultHostname;
nat46-prefix = cfg.nat46Prefix;
backend-port = cfg.backendPort;
unix-directory = cfg.unixDirectory;
proxy-proto = cfg.proxyProto;
}
(map (v: { listen = v; }) cfg.listen)
(map (v: { backend-cidr = v; }) cfg.backendCidrs)
];
}
// lib.optionalAttrs (options ? systemd) {
systemd.service = {