Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions Lib/test/test_getpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,117 @@ def test_venv_posix(self):
actual = getpath(ns, expected)
self.assertEqual(expected, actual)

def test_venv_posix_from_symlinked_base(self):
# gh-128670: the base interpreter is reached through a public symlink
# (e.g. Homebrew's stable opt path) that points to an internal,
# versioned location. base_executable must keep the public path so
# the internal path is not leaked into a child venv's pyvenv.cfg.
ns = MockPosixNamespace(
argv0="/venv/bin/python",
PREFIX="/real",
)
ns.add_known_xfile("/venv/bin/python")
ns.add_known_xfile("/pub/bin/python")
ns.add_known_xfile("/real/bin/python")
ns.add_known_link("/venv/bin/python", "/pub/bin/python")
ns.add_known_link("/pub/bin/python", "/real/bin/python")
ns.add_known_file("/venv/pyvenv.cfg", [
r"home = /pub/bin"
])
ns.add_known_file("/real/lib/python9.8/os.py")
ns.add_known_dir("/real/lib/python9.8/lib-dynload")
expected = dict(
executable="/venv/bin/python",
prefix="/venv",
exec_prefix="/venv",
base_executable="/pub/bin/python",
base_prefix="/real",
base_exec_prefix="/real",
module_search_paths_set=1,
module_search_paths=[
"/real/lib/python98.zip",
"/real/lib/python9.8",
"/real/lib/python9.8/lib-dynload",
],
)
actual = getpath(ns, expected)
self.assertEqual(expected, actual)

def test_venv_posix_from_symlinked_base_versioned(self):
# gh-128670: like the above, but the venv's primary executable is
# 'python' while 'home' only provides the versioned 'python3.8' name.
# base_executable must match on the resolved name, not 'python'.
ns = MockPosixNamespace(
argv0="/venv/bin/python",
PREFIX="/real",
)
ns.add_known_xfile("/venv/bin/python")
ns.add_known_xfile("/pub/bin/python3.8")
ns.add_known_xfile("/real/bin/python3.8")
ns.add_known_link("/venv/bin/python", "/pub/bin/python3.8")
ns.add_known_link("/pub/bin/python3.8", "/real/bin/python3.8")
ns.add_known_file("/venv/pyvenv.cfg", [
r"home = /pub/bin"
])
ns.add_known_file("/real/lib/python9.8/os.py")
ns.add_known_dir("/real/lib/python9.8/lib-dynload")
expected = dict(
executable="/venv/bin/python",
prefix="/venv",
exec_prefix="/venv",
base_executable="/pub/bin/python3.8",
base_prefix="/real",
base_exec_prefix="/real",
module_search_paths_set=1,
module_search_paths=[
"/real/lib/python98.zip",
"/real/lib/python9.8",
"/real/lib/python9.8/lib-dynload",
],
)
actual = getpath(ns, expected)
self.assertEqual(expected, actual)

def test_venv_posix_symlinked_base_mismatch_resolves(self):
# gh-128670 safety: if 'home' does not provide an executable that
# resolves to the running interpreter, base_executable resolves the
# symlink rather than trusting 'home'.
ns = MockPosixNamespace(
argv0="/venv/bin/python",
PREFIX="/real",
)
ns.add_known_xfile("/venv/bin/python")
ns.add_known_xfile("/real/bin/python")
ns.add_known_xfile("/pub/bin/python")
ns.add_known_xfile("/other/bin/python")
ns.add_known_link("/venv/bin/python", "/real/bin/python")
ns.add_known_link("/pub/bin/python", "/other/bin/python")
ns.add_known_file("/venv/pyvenv.cfg", [
r"home = /pub/bin"
])
ns.add_known_file("/real/lib/python9.8/os.py")
ns.add_known_dir("/real/lib/python9.8/lib-dynload")
actual = getpath(ns, {"base_executable": ""})
self.assertEqual(actual["base_executable"], "/real/bin/python")

def test_venv_posix_symlinked_base_no_home_exe(self):
# gh-128670 fallback: if 'home' has no matching executable,
# base_executable resolves the symlink.
ns = MockPosixNamespace(
argv0="/venv/bin/python",
PREFIX="/real",
)
ns.add_known_xfile("/venv/bin/python")
ns.add_known_xfile("/real/bin/python")
ns.add_known_link("/venv/bin/python", "/real/bin/python")
ns.add_known_file("/venv/pyvenv.cfg", [
r"home = /pub/bin"
])
ns.add_known_file("/real/lib/python9.8/os.py")
ns.add_known_dir("/real/lib/python9.8/lib-dynload")
actual = getpath(ns, {"base_executable": ""})
self.assertEqual(actual["base_executable"], "/real/bin/python")

def test_venv_posix_without_home_key(self):
ns = MockPosixNamespace(
argv0="/venv/bin/python3",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Creating a virtual environment from another virtual environment no longer
resolves a symlinked base interpreter further than the original environment
did, so an internal, implementation-detail install path (such as a Homebrew
``Cellar`` directory) is no longer baked into the second environment's
``pyvenv.cfg``.
32 changes: 27 additions & 5 deletions Modules/getpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,15 +382,37 @@ def search_up(prefix, *landmarks, test=isfile):
# the base installation — isn't set (eg. when embedded), try to find
# it in 'home'.
if not base_executable:
# First try to resolve symlinked executables, since that may be
# more accurate than assuming the executable in 'home'.
# Prefer the executable found in 'home' (the public, possibly
# symlinked, location the venv was created from) when it resolves
# to the same real file as the running executable. This avoids
# baking an internal, implementation-detail prefix (e.g. a
# Homebrew Cellar path) into pyvenv.cfg, which then breaks the
# venv when that internal path changes (gh-128670). Match on the
# *resolved* executable's name, since the venv's primary exe may
# be 'python' while 'home' only provides 'python3.X'. Fall back to
# the fully resolved path when 'home' has no matching executable.
try:
base_executable = realpath(executable)
_executable_realpath = realpath(executable)
except OSError:
_executable_realpath = ''
_home_executable = ''
if _executable_realpath:
_home_executable = joinpath(executable_dir,
basename(_executable_realpath))
try:
_home_realpath = realpath(_home_executable) if _home_executable else ''
except OSError:
_home_realpath = ''
if (_executable_realpath and _home_executable
and isfile(_home_executable)
and _home_realpath == _executable_realpath):
base_executable = _home_executable
else:
base_executable = _executable_realpath
if base_executable == executable:
# No change, so probably not a link. Clear it and fall back
base_executable = ''
except OSError:
pass

if not base_executable:
base_executable = joinpath(executable_dir, basename(executable))
# It's possible "python" is executed from within a posix venv but that
Expand Down
Loading