From 62bc58355634c1ed634e8960710b13bf0bafd6a6 Mon Sep 17 00:00:00 2001 From: Tobias Nilsson Date: Tue, 24 Mar 2026 23:24:16 +0100 Subject: [PATCH 1/5] Make ephemeral exec and scie compatible --- pex/pex.py | 7 ++ pex/venv/venv_pex.py | 9 +++ .../scie/test_scie_ephemeral_run.py | 66 +++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 tests/integration/scie/test_scie_ephemeral_run.py diff --git a/pex/pex.py b/pex/pex.py index 7752c8817..e2cd51f2f 100644 --- a/pex/pex.py +++ b/pex/pex.py @@ -882,6 +882,13 @@ def run(self, args=(), with_chroot=False, blocking=True, setsid=False, env=None, env = os.environ.copy() self._clean_environment(env=env, strip_pex_env=self._pex_info.strip_pex_env) + # These are set by the scie to point back at itself and its installed PEX. In + # ephemeral run mode (indicated by _PEX_CLI_RUN), they must not propagate to the + # child PEX process or they corrupt the child's boot. + if "_PEX_CLI_RUN" in env: + env.pop("__PEX_ENTRY_POINT__", None) + env.pop("__PEX_EXE__", None) + kwargs = dict(subprocess_daemon_kwargs() if setsid else {}, **kwargs) TRACER.log("PEX.run invoking {}".format(" ".join(self.cmdline(args)))) diff --git a/pex/venv/venv_pex.py b/pex/venv/venv_pex.py index eed4cf116..489011f34 100644 --- a/pex/venv/venv_pex.py +++ b/pex/venv/venv_pex.py @@ -85,6 +85,12 @@ def _resolve_resource_path( ) +def _strip_scie_environment(): + # type: () -> None + os.environ.pop("__PEX_ENTRY_POINT__", None) + os.environ.pop("__PEX_EXE__", None) + + def boot( shebang_python, # type: str venv_bin_dir, # type: str @@ -328,6 +334,7 @@ def _value(self, key): pex_script = pex_overrides.get("PEX_SCRIPT") if pex_overrides else script if pex_script: + _strip_scie_environment() script_path = os.path.join(venv_bin_dir, pex_script) safe_execv([script_path] + sys.argv[1:]) @@ -413,6 +420,8 @@ def _value(self, key): sys.argv[1:1] = [arg.format(pex=replacements) for arg in inject_args] module_name, _, function = entry_point.partition(":") + + _strip_scie_environment() if not function: import runpy diff --git a/tests/integration/scie/test_scie_ephemeral_run.py b/tests/integration/scie/test_scie_ephemeral_run.py new file mode 100644 index 000000000..d5fcb9e6b --- /dev/null +++ b/tests/integration/scie/test_scie_ephemeral_run.py @@ -0,0 +1,66 @@ +# Copyright 2026 Pex project contributors. +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import absolute_import + +import subprocess +import sys + +import pytest + +from pex.typing import TYPE_CHECKING +from testing import make_env, run_pex_command +from testing.pytest_utils.tmp import Tempdir +from testing.scie import skip_if_no_provider + +if TYPE_CHECKING: + from typing import List + + +@pytest.mark.parametrize( + "execution_mode_args", + [ + pytest.param([], id="ZIPAPP"), + pytest.param(["--venv"], id="VENV"), + ], +) +@skip_if_no_provider +def test_scie_ephemeral_run( + tmpdir, # type: Tempdir + pex_wheel, # type: str + execution_mode_args, # type: List[str] +): + # type: (...) -> None + + pex_scie = tmpdir.join("pex") + run_pex_command( + args=[pex_wheel, "-c", "pex", "-o", pex_scie, "--scie", "eager"] + execution_mode_args + ).assert_success() + + ic = "CPython=={major}.{minor}.*".format(major=sys.version_info[0], minor=sys.version_info[1]) + + # Verify the scie can perform an ephemeral run with `-- -c`. + output = subprocess.check_output( + args=[ + pex_scie, + "--interpreter-constraint", + ic, + "--", + "-c", + "import sys; print(sys.executable)", + ], + env=make_env(PATH=None), + ) + assert output.decode("utf-8").strip() + + # Verify the scie can drop into a REPL via ephemeral run. + process = subprocess.Popen( + args=[pex_scie, "--interpreter-constraint", ic, "--"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=make_env(PATH=None), + ) + stdout, stderr = process.communicate(input=b"import sys; print(sys.executable)\nquit()\n") + assert process.returncode == 0, stderr.decode("utf-8") + assert b">>>" in stdout or b">>>" in stderr From bb2cd1d5e0cb01536722a73568ce2f5d1e8afeab Mon Sep 17 00:00:00 2001 From: John Sirois Date: Thu, 26 Mar 2026 06:38:57 -0700 Subject: [PATCH 2/5] Logically simplify / centralize scie-recursion fix. --- pex/bin/pex.py | 5 +++++ pex/pex.py | 7 ------- pex/pex_boot.py | 7 +++---- pex/venv/venv_pex.py | 9 --------- 4 files changed, 8 insertions(+), 20 deletions(-) diff --git a/pex/bin/pex.py b/pex/bin/pex.py index fd05b1d11..a7dd5c254 100644 --- a/pex/bin/pex.py +++ b/pex/bin/pex.py @@ -1289,6 +1289,11 @@ def main(args=None): try: with global_environment(options) as env: + # These are set if we're running from within a Pex PEX scie and already installed. + # We clear them to allow recursive use of Pex / PEX. + env.pop("__PEX_EXE__", None) + env.pop("__PEX_ENTRY_POINT__", None) + try: resolver_configuration = resolver_options.configure( options, diff --git a/pex/pex.py b/pex/pex.py index e2cd51f2f..7752c8817 100644 --- a/pex/pex.py +++ b/pex/pex.py @@ -882,13 +882,6 @@ def run(self, args=(), with_chroot=False, blocking=True, setsid=False, env=None, env = os.environ.copy() self._clean_environment(env=env, strip_pex_env=self._pex_info.strip_pex_env) - # These are set by the scie to point back at itself and its installed PEX. In - # ephemeral run mode (indicated by _PEX_CLI_RUN), they must not propagate to the - # child PEX process or they corrupt the child's boot. - if "_PEX_CLI_RUN" in env: - env.pop("__PEX_ENTRY_POINT__", None) - env.pop("__PEX_EXE__", None) - kwargs = dict(subprocess_daemon_kwargs() if setsid else {}, **kwargs) TRACER.log("PEX.run invoking {}".format(" ".join(self.cmdline(args)))) diff --git a/pex/pex_boot.py b/pex/pex_boot.py index 11ed684dc..c69b1b1d1 100644 --- a/pex/pex_boot.py +++ b/pex/pex_boot.py @@ -229,12 +229,11 @@ def boot( if os.path.isfile(pex_exe): sys.argv[0] = pex_exe - overridden_entry_point = os.environ.get("__PEX_ENTRY_POINT__", None) + overridden_pex = os.environ.get("__PEX_EXE__", None) sys.path[0] = os.path.abspath(sys.path[0]) - sys.path.insert( - 0, os.path.abspath(os.path.join(overridden_entry_point or entry_point, bootstrap_dir)) - ) + sys.path.insert(0, os.path.abspath(os.path.join(overridden_pex or entry_point, bootstrap_dir))) + overridden_entry_point = os.environ.get("__PEX_ENTRY_POINT__", None) if overridden_entry_point and overridden_entry_point != entry_point: # This PEX has already been installed out of band; so we short-circuit to execute the # pre-installed PEX. diff --git a/pex/venv/venv_pex.py b/pex/venv/venv_pex.py index 489011f34..eed4cf116 100644 --- a/pex/venv/venv_pex.py +++ b/pex/venv/venv_pex.py @@ -85,12 +85,6 @@ def _resolve_resource_path( ) -def _strip_scie_environment(): - # type: () -> None - os.environ.pop("__PEX_ENTRY_POINT__", None) - os.environ.pop("__PEX_EXE__", None) - - def boot( shebang_python, # type: str venv_bin_dir, # type: str @@ -334,7 +328,6 @@ def _value(self, key): pex_script = pex_overrides.get("PEX_SCRIPT") if pex_overrides else script if pex_script: - _strip_scie_environment() script_path = os.path.join(venv_bin_dir, pex_script) safe_execv([script_path] + sys.argv[1:]) @@ -420,8 +413,6 @@ def _value(self, key): sys.argv[1:1] = [arg.format(pex=replacements) for arg in inject_args] module_name, _, function = entry_point.partition(":") - - _strip_scie_environment() if not function: import runpy From 9e53921a1ea6a100954ab430ed20ae224300cf5f Mon Sep 17 00:00:00 2001 From: John Sirois Date: Thu, 26 Mar 2026 06:41:18 -0700 Subject: [PATCH 3/5] Prepare a release. --- CHANGES.md | 6 ++++++ pex/version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 56963c2d1..70085b5fd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Release Notes +## 2.91.5 + +This release fixes Pex PEX scie behavior to match Pex. + +* Make ephemeral exec, venv repl and scie compatible (#3129) + ## 2.91.4 This release brings 2 performance fixes from @tobni: diff --git a/pex/version.py b/pex/version.py index 10c259b40..9047d3f61 100644 --- a/pex/version.py +++ b/pex/version.py @@ -1,4 +1,4 @@ # Copyright 2015 Pex project contributors. # Licensed under the Apache License, Version 2.0 (see LICENSE). -__version__ = "2.91.4" +__version__ = "2.91.5" From 2669cb9467d68a9caa13c1b7d5250dc298a3604a Mon Sep 17 00:00:00 2001 From: John Sirois Date: Thu, 26 Mar 2026 07:06:52 -0700 Subject: [PATCH 4/5] Fix unrelated test broken by requests 2.33.0 release. --- tests/integration/cli/commands/test_lock_subset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/cli/commands/test_lock_subset.py b/tests/integration/cli/commands/test_lock_subset.py index 2b40fb73b..87a9a266b 100644 --- a/tests/integration/cli/commands/test_lock_subset.py +++ b/tests/integration/cli/commands/test_lock_subset.py @@ -139,7 +139,7 @@ def test_lock_subset_miss(lock): # type: (str) -> None _, original_locked_reqs = index(lock) - requests_version = original_locked_reqs[ProjectName("requests")].pin.version + requests_version = original_locked_reqs[ProjectName("requests")].pin.version.raw run_pex3( "lock", "subset", "--lock", lock, "requests!={version}".format(version=requests_version) ).assert_failure( From 4d8e2b6fafa0f0292b428be58fe6cb18b7d4e75a Mon Sep 17 00:00:00 2001 From: John Sirois Date: Thu, 26 Mar 2026 12:00:40 -0700 Subject: [PATCH 5/5] Fix test for PyPy. --- tests/integration/scie/test_scie_ephemeral_run.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/integration/scie/test_scie_ephemeral_run.py b/tests/integration/scie/test_scie_ephemeral_run.py index d5fcb9e6b..505f0ad51 100644 --- a/tests/integration/scie/test_scie_ephemeral_run.py +++ b/tests/integration/scie/test_scie_ephemeral_run.py @@ -9,7 +9,7 @@ import pytest from pex.typing import TYPE_CHECKING -from testing import make_env, run_pex_command +from testing import IS_PYPY, make_env, run_pex_command from testing.pytest_utils.tmp import Tempdir from testing.scie import skip_if_no_provider @@ -37,7 +37,9 @@ def test_scie_ephemeral_run( args=[pex_wheel, "-c", "pex", "-o", pex_scie, "--scie", "eager"] + execution_mode_args ).assert_success() - ic = "CPython=={major}.{minor}.*".format(major=sys.version_info[0], minor=sys.version_info[1]) + ic = "{impl}=={major}.{minor}.*".format( + impl="PyPy" if IS_PYPY else "CPython", major=sys.version_info[0], minor=sys.version_info[1] + ) # Verify the scie can perform an ephemeral run with `-- -c`. output = subprocess.check_output(