Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Changelog

## Unreleased

### Breaking changes

- `wrapPackage`: when passing explicit `args`, `"$@"` is no longer
appended automatically by the wrapper template. If you pass custom
`args` and want passthrough, include `"$@"` in your args list.
The default `args` (generated from `flags`) still includes `"$@"`.

- `flagSeparator` default changed from `" "` to `null`. The old `" "`
default was misleading: it produced separate argv entries, not a
space-joined arg. `null` now means separate argv entries. If you
were explicitly passing `flagSeparator = " "` to get separate args,
remove it (or change to `null`).

### Added

- `lib/modules/command.nix`: base module with shared command spec
(args, env, hooks, exePath) used by both wrapper and systemd outputs.
- `lib/modules/flags.nix`: flags module with per-flag ordering via
`{ value, order }` submodules. Default order is 1000. Reading
`config.flags` returns clean values (order is transparent).
- `wrapper.nix` injects `"$@"` into args at order 1001, controllable
via the ordering system.
- `outputs.wrapper` as the canonical output path (config.wrapper is
a backward-compatible alias).
1 change: 0 additions & 1 deletion checks/flags-empty-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ let
"--empty" = [ ];
"--output" = "file.txt";
};
flagSeparator = " ";
};

in
Expand Down
1 change: 0 additions & 1 deletion checks/flags-false.nix
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ let
"--empty" = [ ];
"--output" = "file.txt";
};
flagSeparator = " ";
};

in
Expand Down
1 change: 0 additions & 1 deletion checks/flags-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ let
];
"--verbose" = true;
};
flagSeparator = " ";
};

wrappedWithEqualsSep = self.lib.wrapPackage {
Expand Down
70 changes: 70 additions & 0 deletions checks/flags-order.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
pkgs,
self,
}:

let
helloModule = self.lib.wrapModule (
{ config, ... }:
{
config.package = config.pkgs.hello;
config.flags = {
# default order 1000: before "$@" (which is 1001)
"--greeting" = "hello";
# explicit early order: should come first
"--early" = {
value = true;
order = 500;
};
# explicit late order: should come after "$@"
"--late" = {
value = true;
order = 1500;
};
};
}
);

wrappedPackage = (helloModule.apply { inherit pkgs; }).wrapper;

in
pkgs.runCommand "flags-order-test" { } ''
echo "Testing flag ordering with priorities..."

wrapperScript="${wrappedPackage}/bin/hello"
if [ ! -f "$wrapperScript" ]; then
echo "FAIL: Wrapper script not found"
exit 1
fi

cat "$wrapperScript"

# Flatten the script to a single line for position comparison
flat=$(cat "$wrapperScript" | tr -d '\n' | tr -s ' ')

# --early (500) should come before --greeting (1000)
# --greeting (1000) should come before "$@" (1001)
# "$@" (1001) should come before --late (1500)
earlyPos=$(echo "$flat" | grep -bo -- '--early' | head -1 | cut -d: -f1)
greetingPos=$(echo "$flat" | grep -bo -- '--greeting' | head -1 | cut -d: -f1)
passthruPos=$(echo "$flat" | grep -bo '"\$@"' | head -1 | cut -d: -f1)
latePos=$(echo "$flat" | grep -bo -- '--late' | head -1 | cut -d: -f1)

echo "Positions: early=$earlyPos greeting=$greetingPos passthru=$passthruPos late=$latePos"

if [ "$earlyPos" -ge "$greetingPos" ]; then
echo "FAIL: --early should come before --greeting"
exit 1
fi
if [ "$greetingPos" -ge "$passthruPos" ]; then
echo "FAIL: --greeting should come before \"\$@\""
exit 1
fi
if [ "$passthruPos" -ge "$latePos" ]; then
echo "FAIL: \"\$@\" should come before --late"
exit 1
fi

echo "SUCCESS: Flag ordering test passed"
touch $out
''
1 change: 0 additions & 1 deletion checks/flags-space-separator.nix
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ let
"--greeting" = "hi";
"--verbose" = true;
};
flagSeparator = " ";
};

in
Expand Down
2 changes: 1 addition & 1 deletion default.nix
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
pkgs ? import <nixpkgs> { },
lib ? pkgs.lib,
}:
let
lib = pkgs.lib;
wlib = import ./lib { inherit lib; };
in
{
Expand Down
26 changes: 15 additions & 11 deletions lib/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@
let
/**
flagToArgs {
flagSeparator: str,
flagSeparator: null | str,
name: str,
flag: bool | str | [ str | [ str ] ]
} -> [ str
} -> [ str ]

flagSeparator = null -> ["--flag" "value"] (separate argv entries, default)
flagSeparator = "=" -> ["--flag=value"] (joined with separator)
flagSeparator = " " -> ["--flag value"] (joined with space, single arg)
*/
flagToArgs =
{
flagSeparator ? " ",
flagSeparator ? null,
name,
flag,
}:
Expand All @@ -18,7 +22,7 @@ let
else if flag == true then
[ name ]
else if builtins.isString flag then
if flagSeparator == " " then
if flagSeparator == null then
[
name
flag
Expand All @@ -30,7 +34,7 @@ let
lib.concatMap (
v:
if builtins.isString v then
if flagSeparator == " " then
if flagSeparator == null then
[
name
v
Expand Down Expand Up @@ -249,7 +253,7 @@ let
inherit modules class specialArgs;
};

modules = lib.genAttrs [ "package" "wrapper" "meta" "systemd" ] (
modules = lib.genAttrs [ "package" "flags" "command" "wrapper" "meta" "systemd" ] (
name: import ./modules/${name}.nix
);

Expand Down Expand Up @@ -379,7 +383,7 @@ let
- `runtimeInputs`: List of packages to add to PATH (optional)
- `env`: Attribute set of environment variables to export (optional)
- `flags`: Attribute set of command-line flags to add (optional)
- `flagSeparator`: Separator between flag names and values when generating args from flags (optional, defaults to " ")
- `flagSeparator`: Separator between flag names and values when generating args from flags (optional, defaults to null for separate argv entries, use "=" for joined)
- `args`: List of command-line arguments like argv in execve (optional, auto-generated from flags if not provided)
- `preHook`: Shell script to run before executing the command (optional)
- `postHook`: Shell script to run after executing the command, removes the `exec` call. use with care (optional)
Expand Down Expand Up @@ -445,9 +449,9 @@ let
runtimeInputs ? [ ],
env ? { },
flags ? { },
flagSeparator ? " ",
# " " for "--flag value" or "=" for "--flag=value"
args ? generateArgsFromFlags flags flagSeparator,
flagSeparator ? null,
# null for "--flag" "value" (separate args) or "=" for "--flag=value"
args ? generateArgsFromFlags flags flagSeparator ++ [ "$@" ],
preHook ? "",
postHook ? "",
passthru ? { },
Expand All @@ -469,7 +473,7 @@ let
''
${envString}
${preHook}
${lib.optionalString (postHook == "") "exec"} ${exePath}${flagsString} "$@"
${lib.optionalString (postHook == "") "exec"} ${exePath}${flagsString}
${postHook}
''
),
Expand Down
72 changes: 72 additions & 0 deletions lib/modules/command.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
lib,
wlib,
config,
...
}:
{
_file = "lib/modules/command.nix";
imports = [
wlib.modules.package
wlib.modules.flags
];
options.args = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Command-line arguments to pass to the wrapper (like argv in execve).
This is a list of strings representing individual arguments.
If not specified, will be automatically generated from flags.
'';
};
options.extraPackages = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
description = ''
Additional packages to add to the wrapper's runtime dependencies.
This is useful if the wrapped program needs additional libraries or tools to function correctly.
These packages will be added to the wrapper's runtime dependencies, ensuring they are available when the wrapped program is executed.
'';
};
options.env = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
description = ''
Environment variables to set in the wrapper.
'';
};
options.preHook = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
Shell script to run before executing the command.
'';
};
options.postHook = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
Shell script to run after executing the command.
Removes the `exec` call in the wrapper script which will leave a bash process
in the background, therefore use with care.
'';
};
options.exePath = lib.mkOption {
type = lib.types.path;
description = ''
Path to the executable within the package to be wrapped.
If not specified, the main executable of the package will be used.
'';
default = lib.getExe config.package;
defaultText = "lib.getExe config.package";
};
options.binName = lib.mkOption {
type = lib.types.str;
description = ''
Name of the binary in the resulting wrapper package.
If not specified, the base name of exePath will be used.
'';
default = builtins.baseNameOf config.exePath;
defaultText = "builtins.baseNameOf config.exePath";
};
}
82 changes: 82 additions & 0 deletions lib/modules/flags.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
lib,
wlib,
config,
options,
...
}:
let
flagValueType = lib.types.oneOf [
(lib.types.uniq lib.types.str)
(lib.types.uniq lib.types.bool)
(lib.types.listOf (
lib.types.oneOf [
lib.types.str
(lib.types.listOf lib.types.str)
]
))
];

flagSubmodule = lib.types.submodule {
options.value = lib.mkOption {
type = flagValueType;
description = "The flag value.";
};
options.order = lib.mkOption {
type = lib.types.int;
default = 1000;
description = ''
Order priority for this flag in the generated args list.
Lower numbers come first. Default is 1000.
'';
};
};
in
{
_file = "lib/modules/flags.nix";

options.flags = lib.mkOption {
type = lib.types.lazyAttrsOf (lib.types.coercedTo flagValueType (v: { value = v; }) flagSubmodule);
default = { };
apply = lib.mapAttrs (_: v: v.value);
description = ''
Flags to pass to the wrapper.
The key is the flag name, the value is the flag value.
If the value is true, the flag will be passed without a value.
If the value is false, the flag will not be passed.
If the value is a list, the flag will be passed multiple times with each value.
Can also be set to { value = ...; order = N; } to control ordering in args.
'';
};

options._orderedFlags = lib.mkOption {
type = lib.types.lazyAttrsOf (lib.types.coercedTo flagValueType (v: { value = v; }) flagSubmodule);
internal = true;
default = { };
};

options.flagSeparator = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Separator between flag names and values when generating args from flags.
null (default) for separate argv entries: "--flag" "value"
"=" for joined: "--flag=value"
'';
};

config._orderedFlags = lib.mkAliasDefinitions options.flags;

config.args = lib.mkMerge (
lib.mapAttrsToList (
name: flagDef:
lib.mkOrder flagDef.order (
wlib.flagToArgs {
inherit name;
flag = flagDef.value;
flagSeparator = config.flagSeparator;
}
)
) config._orderedFlags
);
}
Loading
Loading