diff --git a/cpython-unix/build-cpython.sh b/cpython-unix/build-cpython.sh index 33391336..a11e4f0c 100755 --- a/cpython-unix/build-cpython.sh +++ b/cpython-unix/build-cpython.sh @@ -629,6 +629,24 @@ if [ -n "${CROSS_COMPILING}" ]; then # TODO: There are probably more of these, see #599. fi +# Adjust the Python startup logic (getpath.py) to properly locate the installation, even when +# invoked through a symlink or through an incorrect argv[0]. Because this Python is relocatable, we +# don't get to rely on the fallback to the compiled-in installation prefix. +if [[ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_14}" ]]; then + patch -p1 -i "${ROOT}/patch-python-getpath-3.14.patch" +fi + +# Another, similar change to getpath: When reading inside a venv use the base_executable path to +# determine executable_dir when valid. This allows venv to be created from symlinks and covers some +# cases the above patch doesn't. See: +# https://github.com/python/cpython/issues/106045#issuecomment-2594628161 +# 3.10 does not use getpath.py only getpath.c, no patch is applied +if [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_14}" ]; then + patch -p1 -i "${ROOT}/patch-getpath-use-base_executable-for-executable_dir-314.patch" +elif [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_11}" ]; then + patch -p1 -i "${ROOT}/patch-getpath-use-base_executable-for-executable_dir.patch" +fi + # We patched configure.ac above. Reflect those changes. autoconf diff --git a/cpython-unix/patch-getpath-use-base_executable-for-executable_dir-314.patch b/cpython-unix/patch-getpath-use-base_executable-for-executable_dir-314.patch new file mode 100644 index 00000000..581a91a4 --- /dev/null +++ b/cpython-unix/patch-getpath-use-base_executable-for-executable_dir-314.patch @@ -0,0 +1,14 @@ +diff --git a/Modules/getpath.py b/Modules/getpath.py +index ceb605a75c8..164d708ffca 100644 +--- a/Modules/getpath.py ++++ b/Modules/getpath.py +@@ -411,6 +411,9 @@ def search_up(prefix, *landmarks, test=isfile): + if isfile(candidate): + base_executable = candidate + break ++ if base_executable and isfile(base_executable): ++ # Update the executable directory to be based on the resolved base executable ++ executable_dir = real_executable_dir = dirname(base_executable) + # home key found; stop iterating over lines + break + diff --git a/cpython-unix/patch-getpath-use-base_executable-for-executable_dir.patch b/cpython-unix/patch-getpath-use-base_executable-for-executable_dir.patch new file mode 100644 index 00000000..e6c740af --- /dev/null +++ b/cpython-unix/patch-getpath-use-base_executable-for-executable_dir.patch @@ -0,0 +1,15 @@ +diff --git a/Modules/getpath.py b/Modules/getpath.py +index 1f1bfcb4f64..ff5b18cc385 100644 +--- a/Modules/getpath.py ++++ b/Modules/getpath.py +@@ -398,6 +398,9 @@ def search_up(prefix, *landmarks, test=isfile): + if isfile(candidate): + base_executable = candidate + break ++ if base_executable and isfile(base_executable): ++ # Update the executable directory to be based on the resolved base executable ++ executable_dir = real_executable_dir = dirname(base_executable) + break + else: + venv_prefix = None + diff --git a/cpython-unix/patch-python-getpath-3.14.patch b/cpython-unix/patch-python-getpath-3.14.patch new file mode 100644 index 00000000..af237c4a --- /dev/null +++ b/cpython-unix/patch-python-getpath-3.14.patch @@ -0,0 +1,135 @@ +From 4fb328cb883504dde04dfdd0b4d182a0130a0909 Mon Sep 17 00:00:00 2001 +From: Geoffrey Thomas +Date: Mon, 1 Dec 2025 14:11:43 -0500 +Subject: [PATCH 1/1] getpath: Fix library detection and canonicalize paths on + Linux +Forwarded: no + +The code in getpath.py to look for the stdlib relative to the Python +library did not work in the common layout where libpython itself is in +the lib/ directory; it added an extra lib/ segment. It is also equally +applicable and useful when statically linking libpython into bin/python; +in both cases, we want to go up a directory and then look into +lib/python3.x/. Add an extra dirname() call in getpath.py, and +unconditionally attempt to fill in the "library" variable in getpath.c, +even on builds that are statically linking libpython. + +Also, we want to use the realpath'd version of the library's path to +locate the standard library, particularly in the case where the library +is a symlink to an executable statically linking libpython. On macOS +dyld, this is done automatically. On glibc and musl, we often get +relative paths and they are not canonicalized, so instead, use +/proc/self/maps to find the file where libpython is coming from. + +(We could instead use the origin, which is canonicalized, but there is +no safe API on glibc to read it and no API at all on musl. Note that and +glibc also uses procfs to do so; see discussion at +https://sourceware.org/bugzilla/show_bug.cgi?id=25263) +--- + Modules/getpath.c | 52 ++++++++++++++++++++++++++++++++++++++++------ + Modules/getpath.py | 4 ++-- + 2 files changed, 48 insertions(+), 8 deletions(-) + +diff --git a/Modules/getpath.c b/Modules/getpath.c +index 1e75993480a..72860807133 100644 +--- a/Modules/getpath.c ++++ b/Modules/getpath.c +@@ -802,14 +802,19 @@ progname_to_dict(PyObject *dict, const char *key) + } + + ++static void ++fclose_cleanup(FILE **pf) { ++ if (*pf) { ++ fclose(*pf); ++ *pf = NULL; ++ } ++} ++ ++ + /* Add the runtime library's path to the dict */ + static int + library_to_dict(PyObject *dict, const char *key) + { +-/* macOS framework builds do not link against a libpython dynamic library, but +- instead link against a macOS Framework. */ +-#if defined(Py_ENABLE_SHARED) || defined(WITH_NEXT_FRAMEWORK) +- + #ifdef MS_WINDOWS + extern HMODULE PyWin_DLLhModule; + if (PyWin_DLLhModule) { +@@ -817,12 +822,47 @@ library_to_dict(PyObject *dict, const char *key) + } + #endif + ++ const void *target = (void *)Py_Initialize; ++ ++#ifdef __linux__ ++ /* Linux libcs do not reliably report the realpath in dladdr dli_fname and ++ * sometimes return relative paths, especially if the returned object is ++ * the main program itself. However, /proc/self/maps will give absolute ++ * realpaths (from the kernel, for the same reason that /proc/self/exe is ++ * canonical), so try to parse and look it up there. (dyld seems to ++ * reliably report the canonical path, so doing this matches the behavior ++ * on macOS.) */ ++ ++ __attribute__((cleanup(fclose_cleanup))) ++ FILE *maps = fopen("/proc/self/maps", "r"); ++ if (maps != NULL) { ++ /* See implementation in fs/proc/task_mmu.c for spacing. The pathname ++ * is the last field and has any \n characters escaped, so we can read ++ * until \n. Note that the filename may have " (deleted)" appended; ++ * we don't bother to handle that specially as the only user of this ++ * value calls dirname() anyway. ++ * TODO(geofft): Consider using PROCMAP_QUERY if supported. ++ */ ++ uintptr_t low, high; ++ char filename[PATH_MAX]; ++ while (fscanf(maps, ++ "%lx-%lx %*s %*s %*s %*s %[^\n]", ++ &low, &high, filename) == 3) { ++ if (low <= (uintptr_t)target && (uintptr_t)target < high) { ++ if (filename[0] == '/') { ++ return decode_to_dict(dict, key, filename); ++ } ++ break; ++ } ++ } ++ } ++#endif ++ + #if HAVE_DLADDR + Dl_info libpython_info; +- if (dladdr(&Py_Initialize, &libpython_info) && libpython_info.dli_fname) { ++ if (dladdr(target, &libpython_info) && libpython_info.dli_fname) { + return decode_to_dict(dict, key, libpython_info.dli_fname); + } +-#endif + #endif + + return PyDict_SetItemString(dict, key, Py_None) == 0; +diff --git a/Modules/getpath.py b/Modules/getpath.py +index b89d7427e3f..8c431e53be2 100644 +--- a/Modules/getpath.py ++++ b/Modules/getpath.py +@@ -436,7 +436,7 @@ def search_up(prefix, *landmarks, test=isfile): + + if not executable_dir and os_name == 'darwin' and library: + # QUIRK: macOS checks adjacent to its library early +- library_dir = dirname(library) ++ library_dir = dirname(dirname(library)) + if any(isfile(joinpath(library_dir, p)) for p in STDLIB_LANDMARKS): + # Exceptions here should abort the whole process (to match + # previous behavior) +@@ -570,7 +570,7 @@ def search_up(prefix, *landmarks, test=isfile): + + # First try to detect prefix by looking alongside our runtime library, if known + if library and not prefix: +- library_dir = dirname(library) ++ library_dir = dirname(dirname(library)) + if ZIP_LANDMARK: + if os_name == 'nt': + # QUIRK: Windows does not search up for ZIP file +-- +2.50.1 (Apple Git-155) + diff --git a/src/verify_distribution.py b/src/verify_distribution.py index 8a25b1bd..baeec2b5 100644 --- a/src/verify_distribution.py +++ b/src/verify_distribution.py @@ -5,8 +5,11 @@ import importlib.machinery import os import struct +import subprocess import sys +import tempfile import unittest +from pathlib import Path TERMINFO_DIRS = [ "/etc/terminfo", @@ -269,6 +272,38 @@ def assertLibc(value): assertLibc(importlib.machinery.EXTENSION_SUFFIXES[0]) + @unittest.skipIf( + sys.version_info[:2] < (3, 11), + "not yet implemented", + ) + @unittest.skipIf(os.name == "nt", "no symlinks or argv[0] on Windows") + def test_getpath(self): + def assertPythonWorks(path: Path, argv0: str = None): + output = subprocess.check_output( + [argv0 or path, "-c", "print(42)"], executable=path, text=True + ) + self.assertEqual(output.strip(), "42") + + with tempfile.TemporaryDirectory(prefix="verify-distribution-") as t: + tmpdir = Path(t) + symlink = tmpdir / "python" + symlink.symlink_to(sys.executable) + with self.subTest(msg="symlink without venv"): + assertPythonWorks(symlink) + + # TODO: --copies does not work right + for flag in ("--symlinks",): + with self.subTest(flag=flag): + venv = tmpdir / f"venv_{flag}" + subprocess.check_call( + [symlink, "-m", "venv", flag, "--without-pip", venv] + ) + assertPythonWorks(venv / "bin" / "python") + + # TODO: does not yet work on ARM64 + # with self.subTest(msg="weird argv[0]"): + # assertPythonWorks(sys.executable, argv0="/dev/null") + if __name__ == "__main__": unittest.main()