From 1f9d4beaa371456a51244342ad2ea33ff29b28dc Mon Sep 17 00:00:00 2001 From: Vincent Fazio Date: Mon, 7 Nov 2022 14:21:13 -0600 Subject: [PATCH] Try alternate filenames for system_executable The value of `sys._base_executable` may not be a real file due to changes made in CPython 3.11. The value is derived from the current executable name and the "home" key from pyvenv.cfg. On POSIX systems, virtual environments deploy "python" for use within the venv however CPython's `make install` and a number of distributions do not provide a system "python" in part because of PEP 394. Virtualenv exposes this via `PythonInfo.system_executable` and can encounter issues when attempting to execute a non-existent file. Attempt to fallback to "python" and "python." if "python" does not exist. Signed-off-by: Vincent Fazio --- docs/changelog/2442.bugfix.rst | 1 + src/virtualenv/discovery/py_info.py | 19 +++++++++++++++- tests/unit/discovery/py_info/test_py_info.py | 23 ++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/2442.bugfix.rst diff --git a/docs/changelog/2442.bugfix.rst b/docs/changelog/2442.bugfix.rst new file mode 100644 index 000000000..58e1f6434 --- /dev/null +++ b/docs/changelog/2442.bugfix.rst @@ -0,0 +1 @@ +In POSIX virtual environments, try alternate binary names if ``sys._base_executable`` does not exist - by :user:`vfazio`. diff --git a/src/virtualenv/discovery/py_info.py b/src/virtualenv/discovery/py_info.py index 7c680eeaa..2a39e506d 100644 --- a/src/virtualenv/discovery/py_info.py +++ b/src/virtualenv/discovery/py_info.py @@ -138,7 +138,24 @@ def _fast_get_system_executable(self): base_executable = getattr(sys, "_base_executable", None) # some platforms may set this to help us if base_executable is not None: # use the saved system executable if present if sys.executable != base_executable: # we know we're in a virtual environment, cannot be us - return base_executable + if os.path.exists(base_executable): + return base_executable + # Python may return "python" because it was invoked from the POSIX virtual environment + # however some installs/distributions do not provide a version-less "python" binary in + # the system install location (see PEP 394) so try to fallback to a versioned binary. + # + # Gate this to Python 3.11 as `sys._base_executable` path resolution is now relative to + # the 'home' key from pyvenv.cfg which often points to the system install location. + major, minor = self.version_info.major, self.version_info.minor + if self.os == "posix" and (major, minor) >= (3, 11): + # search relative to the directory of sys._base_executable + base_dir = os.path.dirname(base_executable) + for base_executable in [ + os.path.join(base_dir, exe) + for exe in ("python{}".format(major), "python{}.{}".format(major, minor)) + ]: + if os.path.exists(base_executable): + return base_executable return None # in this case we just can't tell easily without poking around FS and calling them, bail # if we're not in a virtual environment, this is already a system python, so return the original executable # note we must choose the original and not the pure executable as shim scripts might throw us off diff --git a/tests/unit/discovery/py_info/test_py_info.py b/tests/unit/discovery/py_info/test_py_info.py index f621224ee..0c4b26a02 100644 --- a/tests/unit/discovery/py_info/test_py_info.py +++ b/tests/unit/discovery/py_info/test_py_info.py @@ -378,6 +378,29 @@ def test_custom_venv_install_scheme_is_prefered(mocker): assert pyinfo.install_path("purelib").replace(os.sep, "/") == f"lib/python{pyver}/site-packages" +@pytest.mark.skipif(not (os.name == "posix" and sys.version_info[:2] >= (3, 11)), reason="POSIX 3.11+ specific") +def test_fallback_existent_system_executable(mocker): + current = PythonInfo() + # Posix may execute a "python" out of a venv but try to set the base_executable + # to "python" out of the system installation path. PEP 394 informs distributions + # that "python" is not required and the standard `make install` does not provide one + + # Falsify some data to look like we're in a venv + current.prefix = current.exec_prefix = "/tmp/tmp.izZNCyINRj/venv" + current.executable = current.original_executable = os.path.join(current.prefix, "bin/python") + + # Since we don't know if the distribution we're on provides python, use a binary that should not exist + mocker.patch.object(sys, "_base_executable", os.path.join(os.path.dirname(current.system_executable), "idontexist")) + mocker.patch.object(sys, "executable", current.executable) + + # ensure it falls back to an alternate binary name that exists + current._fast_get_system_executable() + assert os.path.basename(current.system_executable) in [ + f"python{v}" for v in (current.version_info.major, f"{current.version_info.major}.{current.version_info.minor}") + ] + assert os.path.exists(current.system_executable) + + @pytest.mark.skipif(sys.version_info[:2] != (3, 10), reason="3.10 specific") def test_uses_posix_prefix_on_debian_3_10_without_venv(mocker): # this is taken from ubuntu 22.04 /usr/lib/python3.10/sysconfig.py