Loading nixos/modules/system/service/systemd/service.nix +60 −1 Original line number Diff line number Diff line Loading @@ -58,6 +58,61 @@ in (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.services = mkOption { description = '' This module configures systemd services, with the notable difference that their unit names will be prefixed with the abstract service name. Loading Loading @@ -106,6 +161,10 @@ in }; }; config = { systemd.lib = { inherit escapeSystemdExecArgs; }; # Note that this is the systemd.services option above, not the system one. systemd.services."" = { # TODO description; Loading @@ -115,7 +174,7 @@ in Restart = lib.mkDefault "always"; RestartSec = lib.mkDefault "5"; ExecStart = [ (escapeSystemdExecArgs config.process.argv) config.systemd.mainExecStart ]; }; }; Loading nixos/modules/system/service/systemd/test.nix +51 −0 Original line number Diff line number Diff line Loading @@ -52,6 +52,45 @@ let }; }; # Test that systemd.mainExecStart overrides process.argv # and allows systemd's specifier and variable substitution system.services.argv-with-subst = { process = { argv = [ hello' "--greeting" "This should be ignored" ]; }; systemd.mainExecStart = ''/bin/sh -c "echo %n and ''${HOME}"''; }; # Test that process.argv escapes % and $ by default system.services.argv-escaped = { process = { argv = [ "/bin/sh" "-c" "echo %n and \${HOME}" ]; }; }; # Test extending process.argv with systemd specifiers system.services.argv-extended = { config, ... }: { process = { argv = [ hello' "--greeting" "Fun $1 fact, remainder is often expressed as m%n" ]; }; systemd.mainExecStart = config.systemd.lib.escapeSystemdExecArgs config.process.argv + " --systemd-unit %n"; }; # irrelevant stuff system.stateVersion = "25.05"; fileSystems."/".device = "/test/dummy"; Loading Loading @@ -83,6 +122,18 @@ runCommand "test-modular-service-systemd-units" grep 'ExecStart="${hello}/bin/hello" "--greeting" ".*database.*"' ${toplevel}/etc/systemd/system/bar-db.service >/dev/null grep -F 'RestartSec=42' ${toplevel}/etc/systemd/system/bar-db.service >/dev/null # Test that systemd.mainExecStart overrides process.argv # Note: %n and $HOME are NOT escaped, allowing systemd to substitute them grep -F 'ExecStart=/bin/sh -c "echo %n and ''${HOME}"' ${toplevel}/etc/systemd/system/argv-with-subst.service >/dev/null # Test that process.argv escapes % as %% and $ as $$ # This prevents systemd from performing specifier/variable substitution grep -F 'ExecStart="/bin/sh" "-c" "echo %%n and $${HOME}"' ${toplevel}/etc/systemd/system/argv-escaped.service >/dev/null # Test extending process.argv with systemd specifiers # The base command should be escaped ($1 -> $$1, m%n -> m%%n), but the appended --systemd-unit %n should not be grep -F 'ExecStart="${hello}/bin/hello" "--greeting" "Fun $$1 fact, remainder is often expressed as m%%n" --systemd-unit %n' ${toplevel}/etc/systemd/system/argv-extended.service >/dev/null [[ ! -e ${toplevel}/etc/systemd/system/foo.socket ]] [[ ! -e ${toplevel}/etc/systemd/system/bar.socket ]] [[ ! -e ${toplevel}/etc/systemd/system/bar-db.socket ]] Loading pkgs/by-name/gh/ghostunnel/package.nix +1 −5 Original line number Diff line number Diff line Loading @@ -5,8 +5,6 @@ lib, nixosTests, ghostunnel, writeScript, runtimeShell, }: buildGoModule rec { Loading Loading @@ -37,9 +35,7 @@ buildGoModule rec { passthru.services.default = { imports = [ (lib.modules.importApply ./service.nix { inherit writeScript runtimeShell; }) (lib.modules.importApply ./service.nix { }) ]; ghostunnel.package = ghostunnel; # FIXME: finalAttrs.finalPackage }; Loading pkgs/by-name/gh/ghostunnel/service.nix +51 −57 Original line number Diff line number Diff line # Non-module dependencies (`importApply`) { writeScript, runtimeShell }: { }: # Service module { Loading Loading @@ -185,29 +185,7 @@ in # TODO assertions process = { argv = # Use a shell if credentials need to be pulled from the environment. optional (builtins.any (v: v != null) [ cfg.keystore cfg.cert cfg.key cfg.cacert ]) ( writeScript "load-credentials" '' #!${runtimeShell} exec $@ ${ concatStringsSep " " ( optional (cfg.keystore != null) "--keystore=$CREDENTIALS_DIRECTORY/keystore" ++ optional (cfg.cert != null) "--cert=$CREDENTIALS_DIRECTORY/cert" ++ optional (cfg.key != null) "--key=$CREDENTIALS_DIRECTORY/key" ++ optional (cfg.cacert != null) "--cacert=$CREDENTIALS_DIRECTORY/cacert" ) } '' ) ++ [ argv = [ (getExe cfg.package) "server" "--listen" Loading @@ -225,8 +203,23 @@ in ++ cfg.extraArguments; }; } // lib.optionalAttrs (options ? systemd) { # refine the service # Refine the service for systemd // lib.optionalAttrs (options ? systemd) ( let # Build credential flags with systemd variable substitution credentialFlags = concatStringsSep " " ( optional (cfg.keystore != null) "--keystore=\${CREDENTIALS_DIRECTORY}/keystore" ++ optional (cfg.cert != null) "--cert=\${CREDENTIALS_DIRECTORY}/cert" ++ optional (cfg.key != null) "--key=\${CREDENTIALS_DIRECTORY}/key" ++ optional (cfg.cacert != null) "--cacert=\${CREDENTIALS_DIRECTORY}/cacert" ); in { # Use mainExecStart to add credential flags with systemd variable substitution systemd.mainExecStart = config.systemd.lib.escapeSystemdExecArgs config.process.argv + lib.optionalString (credentialFlags != "") " ${credentialFlags}"; systemd.service = { after = [ "network.target" ]; wants = [ "network.target" ]; Loading @@ -242,5 +235,6 @@ in ++ optional (cfg.cacert != null) "cacert:${cfg.cacert}"; }; }; }; } ); } Loading
nixos/modules/system/service/systemd/service.nix +60 −1 Original line number Diff line number Diff line Loading @@ -58,6 +58,61 @@ in (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.services = mkOption { description = '' This module configures systemd services, with the notable difference that their unit names will be prefixed with the abstract service name. Loading Loading @@ -106,6 +161,10 @@ in }; }; config = { systemd.lib = { inherit escapeSystemdExecArgs; }; # Note that this is the systemd.services option above, not the system one. systemd.services."" = { # TODO description; Loading @@ -115,7 +174,7 @@ in Restart = lib.mkDefault "always"; RestartSec = lib.mkDefault "5"; ExecStart = [ (escapeSystemdExecArgs config.process.argv) config.systemd.mainExecStart ]; }; }; Loading
nixos/modules/system/service/systemd/test.nix +51 −0 Original line number Diff line number Diff line Loading @@ -52,6 +52,45 @@ let }; }; # Test that systemd.mainExecStart overrides process.argv # and allows systemd's specifier and variable substitution system.services.argv-with-subst = { process = { argv = [ hello' "--greeting" "This should be ignored" ]; }; systemd.mainExecStart = ''/bin/sh -c "echo %n and ''${HOME}"''; }; # Test that process.argv escapes % and $ by default system.services.argv-escaped = { process = { argv = [ "/bin/sh" "-c" "echo %n and \${HOME}" ]; }; }; # Test extending process.argv with systemd specifiers system.services.argv-extended = { config, ... }: { process = { argv = [ hello' "--greeting" "Fun $1 fact, remainder is often expressed as m%n" ]; }; systemd.mainExecStart = config.systemd.lib.escapeSystemdExecArgs config.process.argv + " --systemd-unit %n"; }; # irrelevant stuff system.stateVersion = "25.05"; fileSystems."/".device = "/test/dummy"; Loading Loading @@ -83,6 +122,18 @@ runCommand "test-modular-service-systemd-units" grep 'ExecStart="${hello}/bin/hello" "--greeting" ".*database.*"' ${toplevel}/etc/systemd/system/bar-db.service >/dev/null grep -F 'RestartSec=42' ${toplevel}/etc/systemd/system/bar-db.service >/dev/null # Test that systemd.mainExecStart overrides process.argv # Note: %n and $HOME are NOT escaped, allowing systemd to substitute them grep -F 'ExecStart=/bin/sh -c "echo %n and ''${HOME}"' ${toplevel}/etc/systemd/system/argv-with-subst.service >/dev/null # Test that process.argv escapes % as %% and $ as $$ # This prevents systemd from performing specifier/variable substitution grep -F 'ExecStart="/bin/sh" "-c" "echo %%n and $${HOME}"' ${toplevel}/etc/systemd/system/argv-escaped.service >/dev/null # Test extending process.argv with systemd specifiers # The base command should be escaped ($1 -> $$1, m%n -> m%%n), but the appended --systemd-unit %n should not be grep -F 'ExecStart="${hello}/bin/hello" "--greeting" "Fun $$1 fact, remainder is often expressed as m%%n" --systemd-unit %n' ${toplevel}/etc/systemd/system/argv-extended.service >/dev/null [[ ! -e ${toplevel}/etc/systemd/system/foo.socket ]] [[ ! -e ${toplevel}/etc/systemd/system/bar.socket ]] [[ ! -e ${toplevel}/etc/systemd/system/bar-db.socket ]] Loading
pkgs/by-name/gh/ghostunnel/package.nix +1 −5 Original line number Diff line number Diff line Loading @@ -5,8 +5,6 @@ lib, nixosTests, ghostunnel, writeScript, runtimeShell, }: buildGoModule rec { Loading Loading @@ -37,9 +35,7 @@ buildGoModule rec { passthru.services.default = { imports = [ (lib.modules.importApply ./service.nix { inherit writeScript runtimeShell; }) (lib.modules.importApply ./service.nix { }) ]; ghostunnel.package = ghostunnel; # FIXME: finalAttrs.finalPackage }; Loading
pkgs/by-name/gh/ghostunnel/service.nix +51 −57 Original line number Diff line number Diff line # Non-module dependencies (`importApply`) { writeScript, runtimeShell }: { }: # Service module { Loading Loading @@ -185,29 +185,7 @@ in # TODO assertions process = { argv = # Use a shell if credentials need to be pulled from the environment. optional (builtins.any (v: v != null) [ cfg.keystore cfg.cert cfg.key cfg.cacert ]) ( writeScript "load-credentials" '' #!${runtimeShell} exec $@ ${ concatStringsSep " " ( optional (cfg.keystore != null) "--keystore=$CREDENTIALS_DIRECTORY/keystore" ++ optional (cfg.cert != null) "--cert=$CREDENTIALS_DIRECTORY/cert" ++ optional (cfg.key != null) "--key=$CREDENTIALS_DIRECTORY/key" ++ optional (cfg.cacert != null) "--cacert=$CREDENTIALS_DIRECTORY/cacert" ) } '' ) ++ [ argv = [ (getExe cfg.package) "server" "--listen" Loading @@ -225,8 +203,23 @@ in ++ cfg.extraArguments; }; } // lib.optionalAttrs (options ? systemd) { # refine the service # Refine the service for systemd // lib.optionalAttrs (options ? systemd) ( let # Build credential flags with systemd variable substitution credentialFlags = concatStringsSep " " ( optional (cfg.keystore != null) "--keystore=\${CREDENTIALS_DIRECTORY}/keystore" ++ optional (cfg.cert != null) "--cert=\${CREDENTIALS_DIRECTORY}/cert" ++ optional (cfg.key != null) "--key=\${CREDENTIALS_DIRECTORY}/key" ++ optional (cfg.cacert != null) "--cacert=\${CREDENTIALS_DIRECTORY}/cacert" ); in { # Use mainExecStart to add credential flags with systemd variable substitution systemd.mainExecStart = config.systemd.lib.escapeSystemdExecArgs config.process.argv + lib.optionalString (credentialFlags != "") " ${credentialFlags}"; systemd.service = { after = [ "network.target" ]; wants = [ "network.target" ]; Loading @@ -242,5 +235,6 @@ in ++ optional (cfg.cacert != null) "cacert:${cfg.cacert}"; }; }; }; } ); }