Skip to content

Commit 09fad5d

Browse files
[3.14] gh-106045: Keep searching from venv home when the resolved base executable has no stdlib (GH-160123) (#160355)
gh-106045: Keep searching from venv home when the resolved base executable has no stdlib (GH-160123) GH-156483 made `getpath` locate a venv's base installation relative to the *resolved* base executable instead of the `home` key in `pyvenv.cfg`. When the resolved executable lives outside the installation tree (e.g. a launcher binary symlinked into it), nothing is found there and discovery falls back to the compiled-in `PREFIX`. The venv then either fails to start or silently runs with another installation's standard library. This PR: - Keeps GH-156483's behavior when the resolved executable's directory locates a standard library (`pybuilddir.txt`, the stdlib zip, or `os.py`). Otherwise it keeps searching from `home`, as before GH-156483. - In that fallback case, still searches the resolved executable's directory for `lib-dynload` when determining `exec_prefix`: after `prefix`, before `home` and the compiled-in `EXEC_PREFIX`. Layouts with a separate exec prefix (e.g. `./configure --exec-prefix`) then keep loading extension modules from the right place, and a stale `lib-dynload` near the `home` symlink can't take precedence over the one beside the resolved executable. The tests added by GH-156483 pass unchanged. Three new `test_getpath` cases cover the launcher-outside-the-installation and separate-exec-prefix layouts, and a misleading `lib-dynload` near `home`; each fails without this change. <details> <summary>Reproducer</summary> On `main` (0a86a34), with a plain `./configure --prefix=$HOME/py-under-test && make && make install`: ```sh #!/bin/sh # Usage: venv-home-repro.sh <installed-prefix> <X.Y> set -eu PREFIX=$1; VER=$2 W=$(mktemp -d) mkdir -p "$W/runtime/bin" "$W/runtime/lib" "$W/launcher" cp -R "$PREFIX/lib/python$VER" "$W/runtime/lib/" cp "$PREFIX/bin/python$VER" "$W/launcher/python3" ln -s "$W/launcher/python3" "$W/runtime/bin/python3" PYTHONHOME="$W/runtime" "$W/runtime/bin/python3" -m venv --without-pip "$W/venv" grep '^home' "$W/venv/pyvenv.cfg" echo "expected base_prefix: $W/runtime" "$W/venv/bin/python" -c 'import os, sys; print("actual base_prefix:", sys.base_prefix); print("os.py loaded from:", os.__file__)' ``` Before: ``` home = /tmp/tmp.NFug9wEr4h/runtime/bin expected base_prefix: /tmp/tmp.NFug9wEr4h/runtime actual base_prefix: /home/user/py-under-test os.py loaded from: /home/user/py-under-test/lib/python3.16/os.py ``` (With `$HOME/py-under-test/lib/python3.16` removed, the venv instead fails with `ModuleNotFoundError: No module named 'encodings'`.) After: ``` home = /tmp/tmp.8Zne2otFrw/runtime/bin expected base_prefix: /tmp/tmp.8Zne2otFrw/runtime actual base_prefix: /tmp/tmp.8Zne2otFrw/runtime os.py loaded from: /tmp/tmp.8Zne2otFrw/runtime/lib/python3.16/os.py ``` </details> (cherry picked from commit 5b55b50) Co-authored-by: Itamar Oren <itamarost@gmail.com>
1 parent e083505 commit 09fad5d

3 files changed

Lines changed: 125 additions & 1 deletion

File tree

‎Lib/test/test_getpath.py‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,109 @@ def test_venv_symlink_with_misleading_landmarks_posix(self):
475475
actual = getpath(ns, expected)
476476
self.assertEqual(expected, actual)
477477

478+
def test_venv_symlink_to_launcher_outside_runtime_posix(self):
479+
"Test a venv whose base executable resolves away from its stdlib."
480+
ns = MockPosixNamespace(
481+
argv0="/venv/bin/python",
482+
PREFIX="/usr",
483+
)
484+
ns.add_known_xfile("/out/launcher")
485+
ns.add_known_xfile("/runtime/bin/python")
486+
ns.add_known_link("/runtime/bin/python", "/out/launcher")
487+
ns.add_known_xfile("/venv/bin/python")
488+
ns.add_known_link("/venv/bin/python", "/runtime/bin/python")
489+
ns.add_known_file("/runtime/lib/python9.8/os.py")
490+
ns.add_known_dir("/runtime/lib/python9.8/lib-dynload")
491+
ns.add_known_file("/venv/pyvenv.cfg", [
492+
"home = /runtime/bin",
493+
])
494+
expected = dict(
495+
executable="/venv/bin/python",
496+
prefix="/venv",
497+
exec_prefix="/venv",
498+
base_executable="/out/launcher",
499+
base_prefix="/runtime",
500+
base_exec_prefix="/runtime",
501+
module_search_paths_set=1,
502+
module_search_paths=[
503+
"/runtime/lib/python98.zip",
504+
"/runtime/lib/python9.8",
505+
"/runtime/lib/python9.8/lib-dynload",
506+
],
507+
)
508+
actual = getpath(ns, expected)
509+
self.assertEqual(expected, actual)
510+
511+
def test_venv_symlink_to_separate_exec_prefix_posix(self):
512+
"Test a venv whose base executable lives under a separate exec_prefix."
513+
ns = MockPosixNamespace(
514+
argv0="/venv/bin/python",
515+
PREFIX="/usr",
516+
EXEC_PREFIX="/usr",
517+
)
518+
ns.add_known_xfile("/exec/bin/python")
519+
ns.add_known_xfile("/runtime/bin/python")
520+
ns.add_known_link("/runtime/bin/python", "/exec/bin/python")
521+
ns.add_known_xfile("/venv/bin/python")
522+
ns.add_known_link("/venv/bin/python", "/runtime/bin/python")
523+
ns.add_known_file("/runtime/lib/python9.8/os.py")
524+
ns.add_known_dir("/exec/lib/python9.8/lib-dynload")
525+
ns.add_known_file("/venv/pyvenv.cfg", [
526+
"home = /runtime/bin",
527+
])
528+
expected = dict(
529+
executable="/venv/bin/python",
530+
prefix="/venv",
531+
exec_prefix="/venv",
532+
base_executable="/exec/bin/python",
533+
base_prefix="/runtime",
534+
base_exec_prefix="/exec",
535+
module_search_paths_set=1,
536+
module_search_paths=[
537+
"/runtime/lib/python98.zip",
538+
"/runtime/lib/python9.8",
539+
"/exec/lib/python9.8/lib-dynload",
540+
],
541+
)
542+
actual = getpath(ns, expected)
543+
self.assertEqual(expected, actual)
544+
545+
def test_venv_separate_exec_prefix_with_misleading_home_posix(self):
546+
"Test that a stale lib-dynload near home does not override the base executable."
547+
ns = MockPosixNamespace(
548+
argv0="/venv/bin/python",
549+
PREFIX="/stdlib",
550+
EXEC_PREFIX="/build/exec",
551+
)
552+
ns.add_known_xfile("/exec/bin/python")
553+
ns.add_known_xfile("/public/bin/python")
554+
ns.add_known_link("/public/bin/python", "/exec/bin/python")
555+
ns.add_known_xfile("/venv/bin/python")
556+
ns.add_known_link("/venv/bin/python", "/public/bin/python")
557+
ns.add_known_file("/stdlib/lib/python9.8/os.py")
558+
ns.add_known_dir("/exec/lib/python9.8/lib-dynload")
559+
# An empty directory near the symlink must not win.
560+
ns.add_known_dir("/public/lib/python9.8/lib-dynload")
561+
ns.add_known_file("/venv/pyvenv.cfg", [
562+
"home = /public/bin",
563+
])
564+
expected = dict(
565+
executable="/venv/bin/python",
566+
prefix="/venv",
567+
exec_prefix="/venv",
568+
base_executable="/exec/bin/python",
569+
base_prefix="/stdlib",
570+
base_exec_prefix="/exec",
571+
module_search_paths_set=1,
572+
module_search_paths=[
573+
"/stdlib/lib/python98.zip",
574+
"/stdlib/lib/python9.8",
575+
"/exec/lib/python9.8/lib-dynload",
576+
],
577+
)
578+
actual = getpath(ns, expected)
579+
self.assertEqual(expected, actual)
580+
478581
def test_venv_non_installed_zip_path_posix(self):
479582
"Test a venv created from non-installed python has correct zip path."""
480583
ns = MockPosixNamespace(
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix :mod:`venv` path discovery when the base executable resolves to a
2+
location outside the installation pointed to by the ``home`` key in
3+
``pyvenv.cfg``. The standard library is now found from ``home``, and
4+
extension modules from the resolved executable's installation, instead of
5+
falling back to the compiled-in prefix.

‎Modules/getpath.py‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ def search_up(prefix, *landmarks, test=isfile):
232232
stdlib_dir_was_set_in_config = bool(stdlib_dir)
233233

234234
real_executable_dir = None
235+
base_executable_dir = None
235236
platstdlib_dir = None
236237

237238
# ******************************************************************************
@@ -411,7 +412,17 @@ def search_up(prefix, *landmarks, test=isfile):
411412
if base_executable and isfile(base_executable):
412413
# Search relative to the resolved base executable rather
413414
# than the potentially external symlink in pyvenv.cfg.
414-
executable_dir = real_executable_dir = dirname(base_executable)
415+
# If nothing there locates a standard library (e.g. a
416+
# launcher binary stored outside the installation that
417+
# 'home' points into), keep searching from 'home'.
418+
# base_executable_dir is still used to find platform
419+
# dependent libraries (exec_prefix) in that case.
420+
base_executable_dir = dirname(base_executable)
421+
if (
422+
isfile(joinpath(base_executable_dir, BUILDDIR_TXT))
423+
or search_up(base_executable_dir, ZIP_LANDMARK, *STDLIB_LANDMARKS)
424+
):
425+
executable_dir = real_executable_dir = base_executable_dir
415426
# home key found; stop iterating over lines
416427
break
417428

@@ -621,6 +632,11 @@ def search_up(prefix, *landmarks, test=isfile):
621632
exec_prefix = prefix
622633
if not exec_prefix and prefix and isdir(joinpath(prefix, PLATSTDLIB_LANDMARK)):
623634
exec_prefix = prefix
635+
if not exec_prefix and base_executable_dir and base_executable_dir != executable_dir:
636+
# The venv's resolved base executable may live under a separate
637+
# exec_prefix (e.g. ./configure --exec-prefix) even when the
638+
# stdlib was found from 'home'.
639+
exec_prefix = search_up(base_executable_dir, PLATSTDLIB_LANDMARK, test=isdir)
624640
if not exec_prefix and executable_dir:
625641
exec_prefix = search_up(executable_dir, PLATSTDLIB_LANDMARK, test=isdir)
626642
if not exec_prefix and EXEC_PREFIX:

0 commit comments

Comments
 (0)