From ba6fbd28bc50d2531e3307ae00914d1ec3bc75cb Mon Sep 17 00:00:00 2001 From: zaniebot <242828183+zaniebot@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:51:05 +0000 Subject: [PATCH] gh-119696: Find the standard library from the shared library path --- Lib/test/test_getpath.py | 111 +++++++++++++++++- Lib/test/test_venv.py | 15 ++- ...-10-21-20-24-00.gh-issue-119696.p4ThLs.rst | 3 + Modules/getpath.py | 12 +- 4 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-20-24-00.gh-issue-119696.p4ThLs.rst diff --git a/Lib/test/test_getpath.py b/Lib/test/test_getpath.py index abf8913cc950e8..bcbd064985962f 100644 --- a/Lib/test/test_getpath.py +++ b/Lib/test/test_getpath.py @@ -290,6 +290,115 @@ def test_normal_posix(self): actual = getpath(ns, expected) self.assertEqual(expected, actual) + def test_shared_library_posix(self): + for os_name in ('posix', 'darwin'): + for platlibdir in ('lib', 'lib64'): + for argv0 in ('missing-python', '/application/bin/host'): + with self.subTest(os_name=os_name, platlibdir=platlibdir, + argv0=argv0): + ns = MockPosixNamespace( + os_name=os_name, + PREFIX='/configured', + EXEC_PREFIX='/configured', + PLATLIBDIR=platlibdir, + argv0=argv0, + library=f'/moved/bin/../{platlibdir}/libpython9.8.so', + ) + ns.add_known_xfile('/application/bin/host') + for prefix in ('/configured', '/moved'): + ns.add_known_file( + f'{prefix}/{platlibdir}/python9.8/os.py') + ns.add_known_dir( + f'{prefix}/{platlibdir}/python9.8/lib-dynload') + expected = dict( + prefix='/moved', + exec_prefix='/moved', + base_prefix='/moved', + base_exec_prefix='/moved', + module_search_paths=[ + f'/moved/{platlibdir}/python98.zip', + f'/moved/{platlibdir}/python9.8', + f'/moved/{platlibdir}/python9.8/lib-dynload', + ], + ) + self.assertEqual(expected, getpath(ns, expected)) + + def test_shared_library_home_posix(self): + ns = MockPosixNamespace( + PREFIX='/configured', + argv0='missing-python', + library='/moved/lib/libpython9.8.so', + ENV_PYTHONHOME='/pythonhome', + ) + for prefix in ('/moved', '/pythonhome'): + ns.add_known_file(f'{prefix}/lib/python9.8/os.py') + ns.add_known_dir(f'{prefix}/lib/python9.8/lib-dynload') + expected = dict( + prefix='/pythonhome', + exec_prefix='/pythonhome', + base_prefix='/pythonhome', + base_exec_prefix='/pythonhome', + ) + self.assertEqual(expected, getpath(ns, expected)) + + def test_shared_library_relative_path_posix(self): + def abspath(path): + return posixpath.normpath(posixpath.join('/foreign/work', path)) + + for library in ('libpython9.8.so', 'lib/libpython9.8.so'): + with self.subTest(library=library): + ns = MockPosixNamespace( + argv0='/moved/bin/python', + library=library, + abspath=abspath, + ) + ns.add_known_xfile('/moved/bin/python') + for prefix in ('/moved', '/foreign'): + ns.add_known_file(f'{prefix}/lib/python9.8/os.py') + ns.add_known_dir(f'{prefix}/lib/python9.8/lib-dynload') + expected = dict(prefix='/moved', exec_prefix='/moved') + self.assertEqual(expected, getpath(ns, expected)) + + def test_shared_library_without_cwd_posix(self): + def abspath(path): + raise FileNotFoundError('working directory has been removed') + + ns = MockPosixNamespace( + PREFIX='/configured', + argv0='python', + config={'executable': '/moved/bin/python'}, + library='libpython9.8.so', + abspath=abspath, + ) + ns.add_known_xfile('/moved/bin/python') + ns.add_known_file('/moved/lib/python9.8/os.py') + ns.add_known_dir('/moved/lib/python9.8/lib-dynload') + expected = dict(prefix='/moved', exec_prefix='/moved') + self.assertEqual(expected, getpath(ns, expected)) + + def test_shared_library_venv_posix(self): + ns = MockPosixNamespace( + PREFIX='/configured', + argv0='python', + ENV_PATH='/venv/bin:/moved/bin', + library='/moved/lib/libpython9.8.so', + ) + ns.add_known_xfile('/moved/bin/python') + ns.add_known_xfile('/venv/bin/python') + ns.add_known_link('/venv/bin/python', '/moved/bin/python') + ns.add_known_file('/moved/lib/python9.8/os.py') + ns.add_known_dir('/moved/lib/python9.8/lib-dynload') + ns.add_known_file('/venv/pyvenv.cfg', ['home = /moved/bin']) + expected = dict( + executable='/venv/bin/python', + base_executable='/moved/bin/python', + prefix='/venv', + exec_prefix='/venv', + base_prefix='/moved', + base_exec_prefix='/moved', + ) + self.assertEqual(expected, getpath(ns, expected)) + def test_buildpath_posix(self): """Test an in-build-tree layout on POSIX. @@ -1328,7 +1437,7 @@ def __missing__(self, key): def abspath(self, path): if self.isabs(path): - return path + return posixpath.normpath(path) return self.joinpath("/Absolute", path) def basename(self, path): diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 2f30d3108021dc..ab82db7195b217 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -1138,6 +1138,17 @@ def test_zippath_from_non_installed_posix(self): ld_library_path = sysconfig.get_config_var("LIBDIR") if not ld_library_path or sysconfig.is_python_build(): ld_library_path = os.path.abspath(os.path.dirname(sys.executable)) + if (sysconfig.get_config_var('Py_ENABLE_SHARED') + and sys.platform != 'cygwin'): + # Startup can search relative to libpython before the executable. + # Load a copy from the temporary installation to select its + # standard library. + shared_libdir = os.path.join(non_installed_dir, 'lib') + os.makedirs(shared_libdir, exist_ok=True) + instsoname = sysconfig.get_config_var('INSTSONAME') + shutil.copy2(os.path.join(ld_library_path, instsoname), + os.path.join(shared_libdir, instsoname)) + ld_library_path = os.pathsep.join((shared_libdir, ld_library_path)) if sys.platform == 'darwin': ld_library_path_env = "DYLD_LIBRARY_PATH" else: @@ -1154,7 +1165,9 @@ def test_zippath_from_non_installed_posix(self): # correct zip path in pythonpath. target_python = os.path.join(self.env_dir, self.bindir, python_exe) cmd = [target_python, '-S', '-c', 'import sys; print(sys.path)'] - out, err = check_output(cmd) + with EnvironmentVarGuard() as env: + env[ld_library_path_env] = ld_library_path + out, err = check_output(cmd) self.assertTrue(zip_landmark.encode() in out) @requireVenvCreate diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-20-24-00.gh-issue-119696.p4ThLs.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-20-24-00.gh-issue-119696.p4ThLs.rst new file mode 100644 index 00000000000000..b9ea720d6a8fe7 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-20-24-00.gh-issue-119696.p4ThLs.rst @@ -0,0 +1,3 @@ +A relocated POSIX shared build can now find its unpacked standard library at +startup when a launcher's ``argv[0]`` points outside the installation, +provided the loader reports an absolute path to ``libpython``. diff --git a/Modules/getpath.py b/Modules/getpath.py index 2ffb1d964c0fb6..7dec606aeae13c 100644 --- a/Modules/getpath.py +++ b/Modules/getpath.py @@ -577,7 +577,7 @@ def search_up(prefix, *landmarks, test=isfile): stdlib_dir = None - # First try to detect prefix by looking alongside our runtime library, if known + # First try to detect prefix by searching from our runtime library, if known. if library and not prefix: library_dir = dirname(library) if ZIP_LANDMARK: @@ -588,10 +588,14 @@ def search_up(prefix, *landmarks, test=isfile): else: prefix = search_up(library_dir, ZIP_LANDMARK) if STDLIB_SUBDIR and STDLIB_LANDMARKS and not prefix: - if any(isfile(joinpath(library_dir, f)) for f in STDLIB_LANDMARKS): + # A relative loader path may have been resolved against an earlier + # working directory. + if os_name != 'nt' and isabs(library): + prefix = search_up(dirname(abspath(library)), *STDLIB_LANDMARKS) + elif any(isfile(joinpath(library_dir, f)) for f in STDLIB_LANDMARKS): prefix = library_dir - if not stdlib_dir_was_set_in_config: - stdlib_dir = joinpath(prefix, STDLIB_SUBDIR) + if prefix and not stdlib_dir_was_set_in_config: + stdlib_dir = joinpath(prefix, STDLIB_SUBDIR) # Detect prefix by looking for zip file