What happened?
On macOS, relenv's polyglot shell+python launcher (SHEBANG_TPL_MACOS in relenv/common.py) cds into the script's own (post-symlink-resolution) directory while resolving REALPATH, and never returns to the caller's directory before exec-ing the bundled python. Python therefore inherits a cwd of the install dir (e.g. /opt/salt), not the directory the user invoked the command from.
Current template:
SHEBANG_TPL_MACOS = textwrap.dedent(
"""\
#!/bin/sh
"true" ''''
TARGET_FILE=$0
cd "$(dirname "$TARGET_FILE")" || return
TARGET_FILE=$(basename "$TARGET_FILE")
# Iterate down a (possible) chain of symlinks
while [ -L "$TARGET_FILE" ]
do
TARGET_FILE=$(readlink "$TARGET_FILE")
cd "$(dirname "$TARGET_FILE")" || return
TARGET_FILE=$(basename "$TARGET_FILE")
done
PHYS_DIR=$(pwd -P)
REALPATH=$PHYS_DIR/$TARGET_FILE
# shellcheck disable=SC2093
"exec" "$(dirname "$REALPATH")"{} "$REALPATH" "$@"
' '''
"""
)
Note this is macOS-only: SHEBANG_TPL_LINUX uses readlink -f in a single command substitution and never cds, so the caller's cwd is preserved on Linux. The macOS template emulates readlink -f with a cd/readlink loop (since macOS lacks readlink -f pre-Big Sur era coreutils) but forgets to restore the original directory afterwards.
Real-world impact
Every entry point of the official Salt macOS onedir (salt, salt-ssh, salt-call, salt-run, ...) is generated from this template. Any relative path Salt resolves against os.getcwd() breaks: Saltfile auto-discovery from the current directory, config_dir: ./... inside a Saltfile, --config-dir=./..., root_dir: ./ in master config, ssh_pre_flight: ./... in a salt-ssh roster — all resolve against /opt/salt instead of the user's actual cwd.
Originally reported (with full reproducer against com.saltstack.salt 3007.14 / relenv 0.22.4) at saltstack/salt#69027 and re-filed here at the Salt maintainers' request, since the launcher template is generated by relenv and only consumed by Salt at install time.
Reproducer
$ mkdir -p /tmp/saltrepro && cd /tmp/saltrepro
$ cat > Saltfile <<'YAML'
salt-ssh:
config_dir: ./salt_root
YAML
$ mkdir salt_root
$ cat > salt_root/master <<'YAML'
root_dir: ./
file_roots:
base:
- ./states
YAML
$ salt-ssh -l debug '*' test.ping
[DEBUG ] Missing configuration file: /opt/salt/salt_root/master
./salt_root resolved against /opt/salt, not /tmp/saltrepro.
Expected behavior
Python's cwd at entry should be the directory the user invoked the command from, matching the Linux template's behavior. Concretely: capture pwd before the cd/readlink loop and cd back before the exec.
Suggested patch
SHEBANG_TPL_MACOS = textwrap.dedent(
"""\
#!/bin/sh
"true" ''''
ORIG_CWD=$(pwd)
TARGET_FILE=$0
cd "$(dirname "$TARGET_FILE")" || return
TARGET_FILE=$(basename "$TARGET_FILE")
# Iterate down a (possible) chain of symlinks
while [ -L "$TARGET_FILE" ]
do
TARGET_FILE=$(readlink "$TARGET_FILE")
cd "$(dirname "$TARGET_FILE")" || return
TARGET_FILE=$(basename "$TARGET_FILE")
done
PHYS_DIR=$(pwd -P)
REALPATH=$PHYS_DIR/$TARGET_FILE
cd "$ORIG_CWD" || return
# shellcheck disable=SC2093
"exec" "$(dirname "$REALPATH")"{} "$REALPATH" "$@"
' '''
"""
)
Workaround
Invoke the bundled python on the launcher script directly from the desired cwd; the bash blob lives inside a Python triple-quoted string and is ignored when python parses the file:
cd "$REPO_ROOT" && exec /opt/salt/bin/python3.10 /opt/salt/salt-ssh "$@"
Versions
- relenv 0.22.4 (as shipped in Salt 3007.14 official macOS
.pkg)
- macOS 15 / Darwin 25.4.0, Apple Silicon
What happened?
On macOS, relenv's polyglot shell+python launcher (
SHEBANG_TPL_MACOSinrelenv/common.py)cds into the script's own (post-symlink-resolution) directory while resolvingREALPATH, and never returns to the caller's directory beforeexec-ing the bundled python. Python therefore inherits a cwd of the install dir (e.g./opt/salt), not the directory the user invoked the command from.Current template:
Note this is macOS-only:
SHEBANG_TPL_LINUXusesreadlink -fin a single command substitution and nevercds, so the caller's cwd is preserved on Linux. The macOS template emulatesreadlink -fwith acd/readlinkloop (since macOS lacksreadlink -fpre-Big Sur era coreutils) but forgets to restore the original directory afterwards.Real-world impact
Every entry point of the official Salt macOS onedir (
salt,salt-ssh,salt-call,salt-run, ...) is generated from this template. Any relative path Salt resolves againstos.getcwd()breaks:Saltfileauto-discovery from the current directory,config_dir: ./...inside a Saltfile,--config-dir=./...,root_dir: ./in master config,ssh_pre_flight: ./...in a salt-ssh roster — all resolve against/opt/saltinstead of the user's actual cwd.Originally reported (with full reproducer against
com.saltstack.salt3007.14 / relenv 0.22.4) at saltstack/salt#69027 and re-filed here at the Salt maintainers' request, since the launcher template is generated by relenv and only consumed by Salt at install time.Reproducer
./salt_rootresolved against/opt/salt, not/tmp/saltrepro.Expected behavior
Python's cwd at entry should be the directory the user invoked the command from, matching the Linux template's behavior. Concretely: capture
pwdbefore thecd/readlinkloop andcdback before theexec.Suggested patch
Workaround
Invoke the bundled python on the launcher script directly from the desired cwd; the bash blob lives inside a Python triple-quoted string and is ignored when python parses the file:
Versions
.pkg)