From a7395af152995fd9f3d492961ac01db0e92b15b1 Mon Sep 17 00:00:00 2001 From: Itamar Oren Date: Wed, 21 Oct 2026 14:45:04 -0700 Subject: [PATCH 1/5] gh-106045: Keep searching from venv home when the resolved base executable has no stdlib GH-156483 searches for a venv's base installation relative to the resolved base executable. When that executable lives outside the installation tree that 'home' in pyvenv.cfg points into (e.g. a launcher binary symlinked into it), nothing is found there and discovery falls back to the compiled-in PREFIX. Only prefer the resolved executable's directory when a standard library landmark is found from there; otherwise keep searching from 'home'. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01A93JzpSGH27i6PNX8re4YV --- Lib/test/test_getpath.py | 33 +++++++++++++++++++++++++++++++++ Modules/getpath.py | 10 +++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_getpath.py b/Lib/test/test_getpath.py index 2907abe4b4cee6..66d41be173601a 100644 --- a/Lib/test/test_getpath.py +++ b/Lib/test/test_getpath.py @@ -475,6 +475,39 @@ def test_venv_symlink_with_misleading_landmarks_posix(self): actual = getpath(ns, expected) self.assertEqual(expected, actual) + def test_venv_symlink_to_launcher_outside_runtime_posix(self): + "Test a venv whose base executable resolves away from its stdlib." + ns = MockPosixNamespace( + argv0="/venv/bin/python", + PREFIX="/usr", + ) + ns.add_known_xfile("/out/launcher") + ns.add_known_xfile("/runtime/bin/python") + ns.add_known_link("/runtime/bin/python", "/out/launcher") + ns.add_known_xfile("/venv/bin/python") + ns.add_known_link("/venv/bin/python", "/runtime/bin/python") + ns.add_known_file("/runtime/lib/python9.8/os.py") + ns.add_known_dir("/runtime/lib/python9.8/lib-dynload") + ns.add_known_file("/venv/pyvenv.cfg", [ + "home = /runtime/bin", + ]) + expected = dict( + executable="/venv/bin/python", + prefix="/venv", + exec_prefix="/venv", + base_executable="/out/launcher", + base_prefix="/runtime", + base_exec_prefix="/runtime", + module_search_paths_set=1, + module_search_paths=[ + "/runtime/lib/python98.zip", + "/runtime/lib/python9.8", + "/runtime/lib/python9.8/lib-dynload", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + def test_venv_non_installed_zip_path_posix(self): "Test a venv created from non-installed python has correct zip path.""" ns = MockPosixNamespace( diff --git a/Modules/getpath.py b/Modules/getpath.py index f5f928f1795de5..500dc86b733cc7 100644 --- a/Modules/getpath.py +++ b/Modules/getpath.py @@ -412,7 +412,15 @@ def search_up(prefix, *landmarks, test=isfile): if base_executable and isfile(base_executable): # Search relative to the resolved base executable rather # than the potentially external symlink in pyvenv.cfg. - executable_dir = real_executable_dir = dirname(base_executable) + # If nothing there locates a standard library (e.g. a + # launcher binary stored outside the installation that + # 'home' points into), keep searching from 'home'. + base_executable_dir = dirname(base_executable) + if ( + isfile(joinpath(base_executable_dir, BUILDDIR_TXT)) + or search_up(base_executable_dir, ZIP_LANDMARK, *STDLIB_LANDMARKS) + ): + executable_dir = real_executable_dir = base_executable_dir # home key found; stop iterating over lines break From 72b32a4f47c160e2d14409a1019833cdfbce2943 Mon Sep 17 00:00:00 2001 From: Itamar Oren Date: Wed, 21 Oct 2026 17:33:36 -0700 Subject: [PATCH 2/5] Keep the resolved base executable directory for exec_prefix discovery Even when stdlib discovery falls back to 'home', the resolved base executable may live under a separate exec_prefix (e.g. ./configure --exec-prefix), so search there for platform dependent libraries before falling back to the compiled-in EXEC_PREFIX. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01A93JzpSGH27i6PNX8re4YV --- Lib/test/test_getpath.py | 34 ++++++++++++++++++++++++++++++++++ Modules/getpath.py | 8 ++++++++ 2 files changed, 42 insertions(+) diff --git a/Lib/test/test_getpath.py b/Lib/test/test_getpath.py index 66d41be173601a..4781a9db3d5d2b 100644 --- a/Lib/test/test_getpath.py +++ b/Lib/test/test_getpath.py @@ -508,6 +508,40 @@ def test_venv_symlink_to_launcher_outside_runtime_posix(self): actual = getpath(ns, expected) self.assertEqual(expected, actual) + def test_venv_symlink_to_separate_exec_prefix_posix(self): + "Test a venv whose base executable lives under a separate exec_prefix." + ns = MockPosixNamespace( + argv0="/venv/bin/python", + PREFIX="/usr", + EXEC_PREFIX="/usr", + ) + ns.add_known_xfile("/exec/bin/python") + ns.add_known_xfile("/runtime/bin/python") + ns.add_known_link("/runtime/bin/python", "/exec/bin/python") + ns.add_known_xfile("/venv/bin/python") + ns.add_known_link("/venv/bin/python", "/runtime/bin/python") + ns.add_known_file("/runtime/lib/python9.8/os.py") + ns.add_known_dir("/exec/lib/python9.8/lib-dynload") + ns.add_known_file("/venv/pyvenv.cfg", [ + "home = /runtime/bin", + ]) + expected = dict( + executable="/venv/bin/python", + prefix="/venv", + exec_prefix="/venv", + base_executable="/exec/bin/python", + base_prefix="/runtime", + base_exec_prefix="/exec", + module_search_paths_set=1, + module_search_paths=[ + "/runtime/lib/python98.zip", + "/runtime/lib/python9.8", + "/exec/lib/python9.8/lib-dynload", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + def test_venv_non_installed_zip_path_posix(self): "Test a venv created from non-installed python has correct zip path.""" ns = MockPosixNamespace( diff --git a/Modules/getpath.py b/Modules/getpath.py index 500dc86b733cc7..b544f662e8bd03 100644 --- a/Modules/getpath.py +++ b/Modules/getpath.py @@ -232,6 +232,7 @@ def search_up(prefix, *landmarks, test=isfile): stdlib_dir_was_set_in_config = bool(stdlib_dir) real_executable_dir = None +base_executable_dir = None platstdlib_dir = None stdlib_zip = None @@ -415,6 +416,8 @@ def search_up(prefix, *landmarks, test=isfile): # If nothing there locates a standard library (e.g. a # launcher binary stored outside the installation that # 'home' points into), keep searching from 'home'. + # base_executable_dir is still used to find platform + # dependent libraries (exec_prefix) in that case. base_executable_dir = dirname(base_executable) if ( isfile(joinpath(base_executable_dir, BUILDDIR_TXT)) @@ -632,6 +635,11 @@ def search_up(prefix, *landmarks, test=isfile): exec_prefix = prefix if not exec_prefix and executable_dir: exec_prefix = search_up(executable_dir, PLATSTDLIB_LANDMARK, test=isdir) + if not exec_prefix and base_executable_dir and base_executable_dir != executable_dir: + # The venv's resolved base executable may live under a separate + # exec_prefix (e.g. ./configure --exec-prefix) even when the + # stdlib was found from 'home'. + exec_prefix = search_up(base_executable_dir, PLATSTDLIB_LANDMARK, test=isdir) if not exec_prefix and EXEC_PREFIX: exec_prefix = EXEC_PREFIX if not exec_prefix or not isdir(joinpath(exec_prefix, PLATSTDLIB_LANDMARK)): From 3efc8e8d1b3102f7fec6b8fdeea1c9defd845831 Mon Sep 17 00:00:00 2001 From: Itamar Oren Date: Thu, 22 Oct 2026 11:08:07 -0700 Subject: [PATCH 3/5] Prefer the resolved base executable for exec_prefix discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ɓukasz Langa --- Lib/test/test_getpath.py | 35 +++++++++++++++++++++++++++++++++++ Modules/getpath.py | 4 ++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_getpath.py b/Lib/test/test_getpath.py index 4781a9db3d5d2b..27482eb7b81e33 100644 --- a/Lib/test/test_getpath.py +++ b/Lib/test/test_getpath.py @@ -542,6 +542,41 @@ def test_venv_symlink_to_separate_exec_prefix_posix(self): actual = getpath(ns, expected) self.assertEqual(expected, actual) + def test_venv_separate_exec_prefix_with_misleading_home_posix(self): + "Test that a stale lib-dynload near home does not override the base executable." + ns = MockPosixNamespace( + argv0="/venv/bin/python", + PREFIX="/stdlib", + EXEC_PREFIX="/build/exec", + ) + ns.add_known_xfile("/exec/bin/python") + ns.add_known_xfile("/public/bin/python") + ns.add_known_link("/public/bin/python", "/exec/bin/python") + ns.add_known_xfile("/venv/bin/python") + ns.add_known_link("/venv/bin/python", "/public/bin/python") + ns.add_known_file("/stdlib/lib/python9.8/os.py") + ns.add_known_dir("/exec/lib/python9.8/lib-dynload") + # An empty directory near the symlink must not win. + ns.add_known_dir("/public/lib/python9.8/lib-dynload") + ns.add_known_file("/venv/pyvenv.cfg", [ + "home = /public/bin", + ]) + expected = dict( + executable="/venv/bin/python", + prefix="/venv", + exec_prefix="/venv", + base_executable="/exec/bin/python", + base_prefix="/stdlib", + base_exec_prefix="/exec", + module_search_paths_set=1, + module_search_paths=[ + "/stdlib/lib/python98.zip", + "/stdlib/lib/python9.8", + "/exec/lib/python9.8/lib-dynload", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) def test_venv_non_installed_zip_path_posix(self): "Test a venv created from non-installed python has correct zip path.""" ns = MockPosixNamespace( diff --git a/Modules/getpath.py b/Modules/getpath.py index b544f662e8bd03..995eb4d70bdb82 100644 --- a/Modules/getpath.py +++ b/Modules/getpath.py @@ -633,13 +633,13 @@ def search_up(prefix, *landmarks, test=isfile): exec_prefix = prefix if not exec_prefix and prefix and isdir(joinpath(prefix, PLATSTDLIB_LANDMARK)): exec_prefix = prefix - if not exec_prefix and executable_dir: - exec_prefix = search_up(executable_dir, PLATSTDLIB_LANDMARK, test=isdir) if not exec_prefix and base_executable_dir and base_executable_dir != executable_dir: # The venv's resolved base executable may live under a separate # exec_prefix (e.g. ./configure --exec-prefix) even when the # stdlib was found from 'home'. exec_prefix = search_up(base_executable_dir, PLATSTDLIB_LANDMARK, test=isdir) + if not exec_prefix and executable_dir: + exec_prefix = search_up(executable_dir, PLATSTDLIB_LANDMARK, test=isdir if not exec_prefix and EXEC_PREFIX: exec_prefix = EXEC_PREFIX if not exec_prefix or not isdir(joinpath(exec_prefix, PLATSTDLIB_LANDMARK)): From 4a133692f288e1fba65649d638b1d4fdeda8753a Mon Sep 17 00:00:00 2001 From: Itamar Oren Date: Thu, 22 Oct 2026 11:14:19 -0700 Subject: [PATCH 4/5] Fix syntax error and spacing from applied review suggestions Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01A93JzpSGH27i6PNX8re4YV --- Lib/test/test_getpath.py | 1 + Modules/getpath.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_getpath.py b/Lib/test/test_getpath.py index 27482eb7b81e33..abf8913cc950e8 100644 --- a/Lib/test/test_getpath.py +++ b/Lib/test/test_getpath.py @@ -577,6 +577,7 @@ def test_venv_separate_exec_prefix_with_misleading_home_posix(self): ) actual = getpath(ns, expected) self.assertEqual(expected, actual) + def test_venv_non_installed_zip_path_posix(self): "Test a venv created from non-installed python has correct zip path.""" ns = MockPosixNamespace( diff --git a/Modules/getpath.py b/Modules/getpath.py index 995eb4d70bdb82..2ffb1d964c0fb6 100644 --- a/Modules/getpath.py +++ b/Modules/getpath.py @@ -639,7 +639,7 @@ def search_up(prefix, *landmarks, test=isfile): # stdlib was found from 'home'. exec_prefix = search_up(base_executable_dir, PLATSTDLIB_LANDMARK, test=isdir) if not exec_prefix and executable_dir: - exec_prefix = search_up(executable_dir, PLATSTDLIB_LANDMARK, test=isdir + exec_prefix = search_up(executable_dir, PLATSTDLIB_LANDMARK, test=isdir) if not exec_prefix and EXEC_PREFIX: exec_prefix = EXEC_PREFIX if not exec_prefix or not isdir(joinpath(exec_prefix, PLATSTDLIB_LANDMARK)): From 4c0f76edbf997f5c5f472352fc06566d82627ff7 Mon Sep 17 00:00:00 2001 From: Itamar Oren Date: Thu, 22 Oct 2026 12:36:48 -0700 Subject: [PATCH 5/5] Add NEWS entry Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01A93JzpSGH27i6PNX8re4YV --- .../2026-10-22-12-30-11.gh-issue-106045.fkMW34.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-22-12-30-11.gh-issue-106045.fkMW34.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-22-12-30-11.gh-issue-106045.fkMW34.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-22-12-30-11.gh-issue-106045.fkMW34.rst new file mode 100644 index 00000000000000..98abf1debe08d6 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-22-12-30-11.gh-issue-106045.fkMW34.rst @@ -0,0 +1,5 @@ +Fix :mod:`venv` path discovery when the base executable resolves to a +location outside the installation pointed to by the ``home`` key in +``pyvenv.cfg``. The standard library is now found from ``home``, and +extension modules from the resolved executable's installation, instead of +falling back to the compiled-in prefix.