Files
nixpkgs/lib/services/test.nix
cinereal 639d4f74e5 lib/services/service: pin flag ordering relative to argv
`attrListWith` re-emits every flag as an `lib.mkOrder` definition, so `argv` and
`flags` share one ordering space. Unadorned flags arrived at
`lib.modules.defaultOrderPriority`, the same priority as unadorned `argv`, and
only landed after `argv` because the declaring module's `config` happened to be
collected last.

Give flags that carry no ordering property of their own a priority of 1250,
between `lib.modules.defaultOrderPriority` and `lib.mkAfter`. Plain flags now
provably follow plain `argv`, `lib.mkAfter` on `argv` still places trailing
positional arguments after the flags, and an explicit `lib.mkOrder` on a flag is
honoured verbatim, which is what interleaving a sub-command among flags needs.

Also render path flag values through `pathOrStr`. `lib.cli.toCommandLine`
formats values with `lib.generators.mkValueStringDefault`, which has no case for
paths and aborts; coercing first yields the store path, matching `argv`.

Assisted-by: Claude:claude-opus-5
2026-07-26 17:02:34 +02:00

310 lines
7.8 KiB
Nix

# Run:
# nix-instantiate --eval lib/services/test.nix
let
lib = import ../.;
inherit (lib) mkOption types;
portable-lib = import ./lib.nix { inherit lib; };
configured = portable-lib.configure {
serviceManagerPkgs = throw "do not use pkgs in this test";
extraRootModules = [ ];
extraRootSpecialArgs = { };
};
dummyPkg =
name:
derivation {
system = "dummy";
name = name;
builder = "/bin/false";
};
exampleConfig = {
services = {
service1 = {
process = {
argv = [
"/usr/bin/echo" # *giggles*
"hello"
];
};
assertions = [
{
assertion = false;
message = "you can't enable this for that reason";
}
];
warnings = [
"The `foo' service is deprecated and will go away soon!"
];
};
service2 = {
process = {
# No meta.mainProgram, because it's supposedly an executable script _file_,
# not a directory with a bin directory containing the main program.
argv = [
(dummyPkg "cowsay.sh")
"world"
];
};
};
service3 = {
process = {
argv = [ "/bin/false" ];
};
services.exclacow = {
process = {
argv = [
(lib.getExe (
dummyPkg "cowsay-ng"
// {
meta.mainProgram = "cowsay";
}
))
"!"
];
};
assertions = [
{
assertion = false;
message = "you can't enable this for such reason";
}
];
warnings = [
"The `bar' service is deprecated and will go away soon!"
];
};
};
# 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; }
];
};
};
};
};
exampleEval = lib.evalModules {
modules = [
{
options.services = mkOption {
type = types.attrsOf configured.serviceSubmodule;
};
}
exampleConfig
];
};
# 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) 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;
};
test =
assert
filterEval exampleEval.config == {
services = {
service1 = {
process = {
argv = [
"/usr/bin/echo"
"hello"
];
};
services = { };
assertions = [
{
assertion = false;
message = "you can't enable this for that reason";
}
];
warnings = [
"The `foo' service is deprecated and will go away soon!"
];
};
service2 = {
process = {
argv = [
"${dummyPkg "cowsay.sh"}"
"world"
];
};
services = { };
assertions = [ ];
warnings = [ ];
};
service3 = {
process = {
argv = [ "/bin/false" ];
};
services.exclacow = {
process = {
argv = [
"${dummyPkg "cowsay-ng"}/bin/cowsay"
"!"
];
};
services = { };
assertions = [
{
assertion = false;
message = "you can't enable this for such reason";
}
];
warnings = [ "The `bar' service is deprecated and will go away soon!" ];
};
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 = [ ];
};
};
};
assert
portable-lib.getWarnings [ "service1" ] exampleEval.config.services.service1 == [
"in service1: The `foo' service is deprecated and will go away soon!"
];
assert
failures (portable-lib.getAssertions [ "service1" ] exampleEval.config.services.service1) == [
{
message = "in service1: you can't enable this for that reason";
assertion = false;
}
];
assert
portable-lib.getWarnings [ "service3" ] exampleEval.config.services.service3 == [
"in service3.services.exclacow: The `bar' service is deprecated and will go away soon!"
];
assert
failures (portable-lib.getAssertions [ "service3" ] exampleEval.config.services.service3) == [
{
message = "in service3.services.exclacow: you can't enable this for such reason";
assertion = false;
}
];
"ok";
in
test