diff --git a/COMMANDS.md b/COMMANDS.md index 748b98e0..2067e72a 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -315,7 +315,7 @@ Lists all Python versions with the given command installed. ## `pyenv exec` - Usage: pyenv exec [arg1 arg2...] + Usage: pyenv exec [-N|--environment] [arg1 arg2...] Runs an executable by first preparing PATH so that the selected Python version's `bin` directory is at the front. @@ -326,7 +326,18 @@ For example, if the currently selected Python version is 3.9.7: is equivalent to: - PATH="$PYENV_ROOT/versions/3.9.7/bin:$PATH" pip install -r requirements.txt + PYENV_VERSION="3.9.7" PATH="$PYENV_ROOT/versions/3.9.7/bin:$PATH" pip install -r requirements.txt + +The `--environment` option sets `PYTHONHOME` and `LD_LIBRARY_PATH` (`DYLD_LIBRARY_PATH` in macOS) in the program's environment. +This is needed to run programs that embed Python by loading `libpython` without specifying rpath or an explicit path. +This breaks linking for programs that expect to link to a different `libpython` with the same name as one +in selected Python -- thus it's not done by default. + +In macOS, System Integrity Protection (SIP) removes `DYLD_LIBRARY_PATH` from a process' environment +upon `exec` (not `fork`) if the executable being run is protected. The protection covers preinstalled system software +(including preinstalled shells) and does not cover user-installed apps and 3rd-party software. See +[About System Integrity Protection on your Mac - Apple Support](https://support.apple.com/en-us/102149) +for details. ## `pyenv root` diff --git a/libexec/pyenv-exec b/libexec/pyenv-exec index d0f6d071..59a8d6d7 100755 --- a/libexec/pyenv-exec +++ b/libexec/pyenv-exec @@ -2,7 +2,17 @@ # # Summary: Run an executable with the selected Python version # -# Usage: pyenv exec [arg1 arg2...] +# Usage: pyenv exec [-N|--environment] [arg1 arg2...] +# +# -N/--environment Set PYTHONHOME and LD_LIBRARY_PATH (DYLD_LIBRARY_PATH in macOS) +# envvars for the running command. +# This allows to run programs that embed Python without using rpath or +# exact library path to libpython. +# WARNING: For the running command and its child processes, this will +# break linkage for programs that expect to be linked to a different +# libpython instance with the same name! +# WARNING: In macOS, DYLD_LIBRARY_PATH will be unset by the system for +# processes covered by System Integrity Protection. # # Runs an executable by first preparing PATH so that the selected Python # version's `bin' directory is at the front. @@ -18,9 +28,16 @@ set -e # Provide pyenv completions if [ "$1" = "--complete" ]; then + echo --environment exec pyenv-shims --short fi +unset MODIFY_ENV +if [ "$1" = "-N" ] || [ "$1" = "--environment" ]; then + MODIFY_ENV=true + shift +fi + PYENV_VERSION="$(pyenv-version-name -f)" PYENV_COMMAND="$1" @@ -54,4 +71,27 @@ if [ "${PYENV_BIN_PATH#${PYENV_ROOT}}" != "${PYENV_BIN_PATH}" ]; then # Only add to $PATH for non-system version. export PATH="${PYENV_BIN_PATH}:${PATH}" fi +if [ -n "$MODIFY_ENV" ]; then + + # Check if multiple Python versions are selected + VERSIONS=$(pyenv-prefix) + if [[ "$VERSIONS" == *:* ]]; then + VERSION="${VERSIONS%%:*}" + echo "pyenv: Warning: multiple Python versions are selected." \ + "Setting environment variables for the first one, (${VERSION#$PYENV_ROOT})" >&2 + else + VERSION="$VERSIONS" + fi + + # Assign the correct version of PYTHONHOME + export PYTHONHOME="$VERSION" + case "$(uname -s)" in + Darwin) + export DYLD_LIBRARY_PATH="${PYTHONHOME}/lib${DYLD_LIBRARY_PATH:+:${DYLD_LIBRARY_PATH}}" + ;; + *) + export LD_LIBRARY_PATH="${PYTHONHOME}/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" + ;; + esac +fi exec "$PYENV_COMMAND_PATH" "$@" diff --git a/test/exec.bats b/test/exec.bats index ff49912b..fb1422fa 100644 --- a/test/exec.bats +++ b/test/exec.bats @@ -34,6 +34,7 @@ EOF assert_success assert_output <