diff --git a/Lib/test/test_getpath.py b/Lib/test/test_getpath.py index 83f09f3495547ac..4e64c1c2463a169 100644 --- a/Lib/test/test_getpath.py +++ b/Lib/test/test_getpath.py @@ -290,6 +290,97 @@ 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_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. @@ -1157,7 +1248,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 f484dfdc29653d7..a434b2206c9313f 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -1141,6 +1141,17 @@ def test_zippath_from_non_installed_posix(self): ld_library_path = os.path.join(sys.base_exec_prefix, "lib") 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: 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 000000000000000..9178766800a13c6 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-20-24-00.gh-issue-119696.p4ThLs.rst @@ -0,0 +1,2 @@ +On POSIX, search parent directories of the shared library path for the unpacked +standard library, as already done for the standard-library ZIP file. diff --git a/Modules/getpath.py b/Modules/getpath.py index c322d5c4b4c251d..01241d587d18280 100644 --- a/Modules/getpath.py +++ b/Modules/getpath.py @@ -562,9 +562,14 @@ 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 os_name != 'nt': + try: + library_dir = abspath(library_dir) + except OSError: + pass if ZIP_LANDMARK: if os_name == 'nt': # QUIRK: Windows does not search up for ZIP file @@ -573,10 +578,13 @@ 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): - prefix = library_dir - if not stdlib_dir_was_set_in_config: - stdlib_dir = joinpath(prefix, STDLIB_SUBDIR) + if os_name == 'nt': + if any(isfile(joinpath(library_dir, f)) for f in STDLIB_LANDMARKS): + prefix = library_dir + else: + prefix = search_up(library_dir, *STDLIB_LANDMARKS) + if prefix and not stdlib_dir_was_set_in_config: + stdlib_dir = joinpath(prefix, STDLIB_SUBDIR) # Detect prefix by looking for zip file