diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f3b67ef --- /dev/null +++ b/CHANGELOG.md @@ -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). diff --git a/checks/flags-empty-list.nix b/checks/flags-empty-list.nix index f0ab65f..0a351b9 100644 --- a/checks/flags-empty-list.nix +++ b/checks/flags-empty-list.nix @@ -13,7 +13,6 @@ let "--empty" = [ ]; "--output" = "file.txt"; }; - flagSeparator = " "; }; in diff --git a/checks/flags-false.nix b/checks/flags-false.nix index 0471147..dcd0180 100644 --- a/checks/flags-false.nix +++ b/checks/flags-false.nix @@ -15,7 +15,6 @@ let "--empty" = [ ]; "--output" = "file.txt"; }; - flagSeparator = " "; }; in diff --git a/checks/flags-list.nix b/checks/flags-list.nix index c70e6b8..6092ecf 100644 --- a/checks/flags-list.nix +++ b/checks/flags-list.nix @@ -15,7 +15,6 @@ let ]; "--verbose" = true; }; - flagSeparator = " "; }; wrappedWithEqualsSep = self.lib.wrapPackage { diff --git a/checks/flags-order.nix b/checks/flags-order.nix new file mode 100644 index 0000000..64d79fe --- /dev/null +++ b/checks/flags-order.nix @@ -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 +'' diff --git a/checks/flags-space-separator.nix b/checks/flags-space-separator.nix index 68e0d92..08c58fb 100644 --- a/checks/flags-space-separator.nix +++ b/checks/flags-space-separator.nix @@ -11,7 +11,6 @@ let "--greeting" = "hi"; "--verbose" = true; }; - flagSeparator = " "; }; in diff --git a/default.nix b/default.nix index f5d828b..d2274b4 100644 --- a/default.nix +++ b/default.nix @@ -1,8 +1,8 @@ { pkgs ? import { }, + lib ? pkgs.lib, }: let - lib = pkgs.lib; wlib = import ./lib { inherit lib; }; in { diff --git a/lib/default.nix b/lib/default.nix index 5729140..2957575 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -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, }: @@ -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 @@ -30,7 +34,7 @@ let lib.concatMap ( v: if builtins.isString v then - if flagSeparator == " " then + if flagSeparator == null then [ name v @@ -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 ); @@ -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) @@ -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 ? { }, @@ -469,7 +473,7 @@ let '' ${envString} ${preHook} - ${lib.optionalString (postHook == "") "exec"} ${exePath}${flagsString} "$@" + ${lib.optionalString (postHook == "") "exec"} ${exePath}${flagsString} ${postHook} '' ), diff --git a/lib/modules/command.nix b/lib/modules/command.nix new file mode 100644 index 0000000..9949432 --- /dev/null +++ b/lib/modules/command.nix @@ -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"; + }; +} diff --git a/lib/modules/flags.nix b/lib/modules/flags.nix new file mode 100644 index 0000000..2d9ad8d --- /dev/null +++ b/lib/modules/flags.nix @@ -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 + ); +} diff --git a/lib/modules/wrapper.nix b/lib/modules/wrapper.nix index a950d46..382fcac 100644 --- a/lib/modules/wrapper.nix +++ b/lib/modules/wrapper.nix @@ -6,68 +6,7 @@ }: { _file = "lib/modules/wrapper.nix"; - imports = [ wlib.modules.package ]; - 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.flags = lib.mkOption { - # we want to support: - # --flag = "somestring" ==> --flag "something" - # --flag = true ==> --flag - # --flag = false ==> no flag (used to remove flag via apply) - # --flag = [ "list" "of" "flags" ] ==> --flag list --flag of --flag flags - # --flag = [ [ "list" "of" "flags" ] "test" ]; ==> --flag list of flags --flag test - type = lib.types.lazyAttrsOf ( - 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) - ] - )) - ] - ); - default = { }; - 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. - ''; - }; - options.flagSeparator = lib.mkOption { - type = lib.types.str; - default = " "; - description = '' - Separator between flag names and values when generating args from flags. - " " for "--flag value" or "=" for "--flag=value" - ''; - }; - config.args = wlib.generateArgsFromFlags config.flags config.flagSeparator; - options.args = lib.mkOption { - type = lib.types.listOf lib.types.str; - 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.env = lib.mkOption { - type = lib.types.attrsOf lib.types.str; - default = { }; - description = '' - Environment variables to set in the wrapper. - ''; - }; + imports = [ wlib.modules.command ]; options.filesToPatch = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ "share/applications/*.desktop" ]; @@ -92,41 +31,14 @@ Shell script that runs after patchPhase to modify the wrapper package files. ''; }; - 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"; - }; - options.wrapper = lib.mkOption { + + # Inject "$@" (passthrough of user arguments) into args at order 1001, + # so it comes just after the default flag order (1000). + # Use mkOrder on args to position it; other flags can use order > 1001 + # to appear after "$@" if needed. + config.args = lib.mkOrder 1001 [ "$@" ]; + + options.outputs.wrapper = lib.mkOption { type = lib.types.package; readOnly = true; description = '' @@ -155,4 +67,12 @@ // config.passthru; }; }; + options.wrapper = lib.mkOption { + type = lib.types.package; + readOnly = true; + description = '' + Backward-compatible alias for outputs.wrapper. + ''; + default = config.outputs.wrapper; + }; } diff --git a/modules/ghostty/check.nix b/modules/ghostty/check.nix index da67b62..a4f6fb4 100644 --- a/modules/ghostty/check.nix +++ b/modules/ghostty/check.nix @@ -31,7 +31,7 @@ let in pkgs.runCommand "ghostty" { } '' "${ghosttyWrapped}/bin/ghostty" +validate-config - "${ghosttyWrapped}/bin/ghostty" +version | grep -q "${ghosttyWrapped.version}" + [[ "$(${ghosttyWrapped}/bin/ghostty +version)" == *"${ghosttyWrapped.version}"* ]] "${ghosttyFileWrapped}/bin/ghostty" +validate-config diff --git a/modules/git/check.nix b/modules/git/check.nix index 2e8501a..b972068 100644 --- a/modules/git/check.nix +++ b/modules/git/check.nix @@ -17,6 +17,6 @@ let in pkgs.runCommand "git-test" { } '' - "${gitWrapped}/bin/git" --version | grep -q "git" + [[ "$(${gitWrapped}/bin/git --version)" == *git* ]] touch $out '' diff --git a/modules/i3/check.nix b/modules/i3/check.nix index 02172d6..ba8b54c 100644 --- a/modules/i3/check.nix +++ b/modules/i3/check.nix @@ -24,7 +24,7 @@ pkgs.runCommand "i3-test" { nativeBuildInputs = [ pkgs.dbus ]; } '' dbus-daemon --session --address="$DBUS_SESSION_BUS_ADDRESS" --nofork --nopidfile --print-address & DBUS_PID=$! - "${i3Wrapped}/bin/i3" --version | grep -q "${i3Wrapped.version}" + [[ "$(${i3Wrapped}/bin/i3 --version)" == *"${i3Wrapped.version}"* ]] kill $DBUS_PID 2>/dev/null || true diff --git a/modules/jujutsu/check.nix b/modules/jujutsu/check.nix index 5bced6d..7f39770 100644 --- a/modules/jujutsu/check.nix +++ b/modules/jujutsu/check.nix @@ -16,7 +16,8 @@ let }).wrapper; in pkgs.runCommand "jujutsu-test" { } '' - "${jujutsuWrapped}/bin/jj" config list --user | grep -q 'user.name = "Test User"' - "${jujutsuWrapped}/bin/jj" config list --user | grep -q 'user.email = "test@example.com"' + config_list="$(${jujutsuWrapped}/bin/jj config list --user)" + [[ "$config_list" == *'user.name = "Test User"'* ]] + [[ "$config_list" == *'user.email = "test@example.com"'* ]] touch $out '' diff --git a/modules/kanshi/check.nix b/modules/kanshi/check.nix index 5d93671..c412aed 100644 --- a/modules/kanshi/check.nix +++ b/modules/kanshi/check.nix @@ -4,22 +4,29 @@ }: let + kanshiConfig = pkgs.writeText "kanshi-test-config" '' + profile { + output eDP-1 enable scale 2 + } + ''; + kanshiWrapped = (self.wrapperModules.kanshi.apply { inherit pkgs; - - configFile.content = '' - profile { - output eDP-1 enable scale 2 - } - ''; - + configFile.path = toString kanshiConfig; }).wrapper; in pkgs.runCommand "kanshi-test" { } '' + help_output="$(${kanshiWrapped}/bin/kanshi --help 2>&1)" + [[ "$help_output" == *config* ]] + + test -f "${kanshiConfig}" + grep -qF 'output eDP-1 enable scale 2' "${kanshiConfig}" - "${kanshiWrapped}/bin/kanshi" --help 2>&1 | grep -q "config" + wrapper_script=$(<"${kanshiWrapped}/bin/kanshi") + [[ "$wrapper_script" == *"--config"* ]] + [[ "$wrapper_script" == *"${kanshiConfig}"* ]] touch $out '' diff --git a/modules/mpv/check.nix b/modules/mpv/check.nix index c05b47f..1348d4b 100644 --- a/modules/mpv/check.nix +++ b/modules/mpv/check.nix @@ -15,6 +15,6 @@ let in pkgs.runCommand "mpv-test" { } '' - "${mpvWrapped}/bin/mpv" --version | grep -q "mpv" + [[ "$(${mpvWrapped}/bin/mpv --version)" == *mpv* ]] touch $out '' diff --git a/modules/niri/check.nix b/modules/niri/check.nix index 781f3ac..afe4656 100644 --- a/modules/niri/check.nix +++ b/modules/niri/check.nix @@ -111,7 +111,7 @@ let in pkgs.runCommand "niri-test" { } '' cat ${niriWrapped}/bin/niri - "${niriWrapped}/bin/niri" --version | grep -q "${niriWrapped.version}" + [[ "$(${niriWrapped}/bin/niri --version)" == *"${niriWrapped.version}"* ]] "${niriWrapped}/bin/niri" validate # since config is now checked at build time, testing a bad config is impossible touch $out diff --git a/modules/udiskie/check.nix b/modules/udiskie/check.nix index 66d813f..dbe39ae 100644 --- a/modules/udiskie/check.nix +++ b/modules/udiskie/check.nix @@ -16,6 +16,6 @@ let }).wrapper; in pkgs.runCommand "udiskie-test" { } '' - "${udiskieWrapped}/bin/udiskie" --version | grep -q "udiskie" + [[ "$(${udiskieWrapped}/bin/udiskie --version)" == *udiskie* ]] touch $out ''