From f6acce4ab6c4641a165428a97d2b655fae85fd86 Mon Sep 17 00:00:00 2001 From: Geoffrey Thomas Date: Tue, 16 Dec 2025 13:49:03 -0500 Subject: [PATCH 1/2] Implement the getpath library name patch on 3.10 --- cpython-unix/build-cpython.sh | 2 + .../patch-python-getpath-library-3.10.patch | 124 ++++++++++++++++++ src/verify_distribution.py | 4 - 3 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 cpython-unix/patch-python-getpath-library-3.10.patch diff --git a/cpython-unix/build-cpython.sh b/cpython-unix/build-cpython.sh index e5f5c05c..d4ac5a6b 100755 --- a/cpython-unix/build-cpython.sh +++ b/cpython-unix/build-cpython.sh @@ -629,6 +629,8 @@ if [[ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_11}" ]]; then patch -p1 -i "${ROOT}/patch-python-getpath-backport-${PYTHON_MAJMIN_VERSION}.patch" fi patch -p1 -i "${ROOT}/patch-python-getpath-library.patch" +else + patch -p1 -i "${ROOT}/patch-python-getpath-library-3.10.patch" fi # Another, similar change to getpath: When reading inside a venv use the base_executable path to diff --git a/cpython-unix/patch-python-getpath-library-3.10.patch b/cpython-unix/patch-python-getpath-library-3.10.patch new file mode 100644 index 00000000..895f24d4 --- /dev/null +++ b/cpython-unix/patch-python-getpath-library-3.10.patch @@ -0,0 +1,124 @@ +From cfb5d870686a52f9c6d0ee72b1a1c035eb0a19dd Mon Sep 17 00:00:00 2001 +From: Geoffrey Thomas +Date: Tue, 16 Dec 2025 13:43:43 -0500 +Subject: [PATCH 1/1] [3.10] getpath: Fix library detection and canonicalize + paths on Linux +Forwarded: not-needed + +This is an implementation of the corresponding patch for 3.11+ +(patch-python-getpath-library.patch) to the pure-C getpath +implementation in 3.10. See that patch's commit message for the details +and motivation. + +3.10 getpath appears to loop over parent directories to the calculated +argv[0] path, so the issue mentioned in the original commit message +about the extra lib/ segment should not arise. +--- + Modules/getpath.c | 77 +++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 77 insertions(+) + +diff --git a/Modules/getpath.c b/Modules/getpath.c +index ef6dd59a084..41255647bcd 100644 +--- a/Modules/getpath.c ++++ b/Modules/getpath.c +@@ -8,6 +8,7 @@ + + #include + #include ++#include + + #ifdef __APPLE__ + # include +@@ -952,6 +953,15 @@ calculate_program_macos(wchar_t **abs_path_p) + #endif /* __APPLE__ */ + + ++static void ++fclose_cleanup(FILE **pf) { ++ if (*pf) { ++ fclose(*pf); ++ *pf = NULL; ++ } ++} ++ ++ + static PyStatus + calculate_program_impl(PyCalculatePath *calculate, _PyPathConfig *pathconfig) + { +@@ -959,6 +969,73 @@ calculate_program_impl(PyCalculatePath *calculate, _PyPathConfig *pathconfig) + + PyStatus status; + ++ const void *target = __builtin_extract_return_addr( ++ __builtin_return_address(0) ++ ); ++ size_t len; ++ ++#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 rest[PATH_MAX + 1]; ++ while (fscanf(maps, "%lx-%lx %*s %*s %*s %*s", &low, &high) == 2) { ++ if (fgets(rest, PATH_MAX + 1, maps) == NULL) { ++ break; ++ } ++ size_t restlen = strlen(rest); ++ if (restlen >= PATH_MAX) { ++ // If the line is too long our parsing will be out of sync. ++ break; ++ } ++ if (restlen > 0 && rest[restlen - 1] == '\n') { ++ rest[restlen - 1] = '\0'; ++ } else { ++ // EOF or error before we got to end of line. ++ break; ++ } ++ ++ if (low <= (uintptr_t)target && (uintptr_t)target < high) { ++ // Skip past padding spaces in the filename. ++ const char *filename = rest + strspn(rest, " "); ++ if (filename[0] == '/') { ++ pathconfig->program_full_path = Py_DecodeLocale(filename, &len); ++ if (pathconfig->program_full_path == NULL) { ++ return DECODE_LOCALE_ERR("mapped code path", len); ++ } ++ return _PyStatus_OK(); ++ } ++ break; ++ } ++ } ++ } ++#endif ++ ++ Dl_info libpython_info; ++ if (dladdr(target, &libpython_info) && libpython_info.dli_fname) { ++ pathconfig->program_full_path = Py_DecodeLocale(libpython_info.dli_fname, &len); ++ if (pathconfig->program_full_path == NULL) { ++ return DECODE_LOCALE_ERR("shared object path", len); ++ } ++ return _PyStatus_OK(); ++ } ++ + /* If there is no slash in the argv0 path, then we have to + * assume python is on the user's $PATH, since there's no + * other way to find a directory to start the search from. If +-- +2.50.1 (Apple Git-155) + diff --git a/src/verify_distribution.py b/src/verify_distribution.py index 1e2ce0be..76f11710 100644 --- a/src/verify_distribution.py +++ b/src/verify_distribution.py @@ -272,10 +272,6 @@ 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): From 6465725db3320efaf8a2db51429e628902f27707 Mon Sep 17 00:00:00 2001 From: Geoffrey Thomas Date: Wed, 17 Dec 2025 15:42:57 -0500 Subject: [PATCH 2/2] Change the patch to set argv0_path instead of program_full_path which turns into sys.executable --- .../patch-python-getpath-library-3.10.patch | 153 +++++++++++++----- 1 file changed, 114 insertions(+), 39 deletions(-) diff --git a/cpython-unix/patch-python-getpath-library-3.10.patch b/cpython-unix/patch-python-getpath-library-3.10.patch index 895f24d4..0b1c97dc 100644 --- a/cpython-unix/patch-python-getpath-library-3.10.patch +++ b/cpython-unix/patch-python-getpath-library-3.10.patch @@ -1,4 +1,4 @@ -From cfb5d870686a52f9c6d0ee72b1a1c035eb0a19dd Mon Sep 17 00:00:00 2001 +From 770c9997a58a50144ae433119f32e3ffa9d2c26c Mon Sep 17 00:00:00 2001 From: Geoffrey Thomas Date: Tue, 16 Dec 2025 13:43:43 -0500 Subject: [PATCH 1/1] [3.10] getpath: Fix library detection and canonicalize @@ -14,11 +14,11 @@ and motivation. argv[0] path, so the issue mentioned in the original commit message about the extra lib/ segment should not arise. --- - Modules/getpath.c | 77 +++++++++++++++++++++++++++++++++++++++++++++++ - 1 file changed, 77 insertions(+) + Modules/getpath.c | 134 ++++++++++++++++++++++------------------------ + 1 file changed, 65 insertions(+), 69 deletions(-) diff --git a/Modules/getpath.c b/Modules/getpath.c -index ef6dd59a084..41255647bcd 100644 +index ef6dd59a084..6fd02a25277 100644 --- a/Modules/getpath.c +++ b/Modules/getpath.c @@ -8,6 +8,7 @@ @@ -29,31 +29,68 @@ index ef6dd59a084..41255647bcd 100644 #ifdef __APPLE__ # include -@@ -952,6 +953,15 @@ calculate_program_macos(wchar_t **abs_path_p) - #endif /* __APPLE__ */ +@@ -1090,87 +1091,84 @@ resolve_symlinks(wchar_t **path_p) + #endif /* HAVE_READLINK */ +-#ifdef WITH_NEXT_FRAMEWORK +-static PyStatus +-calculate_argv0_path_framework(PyCalculatePath *calculate, _PyPathConfig *pathconfig) +-{ +- NSModule pythonModule; +- +- /* On Mac OS X we have a special case if we're running from a framework. +- This is because the python home should be set relative to the library, +- which is in the framework, not relative to the executable, which may +- be outside of the framework. Except when we're in the build +- directory... */ +- pythonModule = NSModuleForSymbol(NSLookupAndBindSymbol("_Py_Initialize")); +- +- /* Use dylib functions to find out where the framework was loaded from */ +- const char* modPath = NSLibraryNameForModule(pythonModule); +- if (modPath == NULL) { +- return _PyStatus_OK(); +- } +- +- /* We're in a framework. +- See if we might be in the build directory. The framework in the +- build directory is incomplete, it only has the .dylib and a few +- needed symlinks, it doesn't have the Lib directories and such. +- If we're running with the framework from the build directory we must +- be running the interpreter in the build directory, so we use the +- build-directory-specific logic to find Lib and such. */ +- size_t len; +- wchar_t* wbuf = Py_DecodeLocale(modPath, &len); +- if (wbuf == NULL) { +- return DECODE_LOCALE_ERR("framework location", len); +static void +fclose_cleanup(FILE **pf) { + if (*pf) { + fclose(*pf); + *pf = NULL; -+ } + } +} -+ -+ - static PyStatus - calculate_program_impl(PyCalculatePath *calculate, _PyPathConfig *pathconfig) - { -@@ -959,6 +969,73 @@ calculate_program_impl(PyCalculatePath *calculate, _PyPathConfig *pathconfig) - PyStatus status; +- /* Path: reduce(modPath) / lib_python / LANDMARK */ +- PyStatus status; +- wchar_t *parent = _PyMem_RawWcsdup(wbuf); +- if (parent == NULL) { +- status = _PyStatus_NO_MEMORY(); +- goto done; +- } ++static PyStatus ++calculate_argv0_path_library(PyCalculatePath *calculate, _PyPathConfig *pathconfig) ++{ + const void *target = __builtin_extract_return_addr( + __builtin_return_address(0) + ); ++ wchar_t *wbuf = NULL; + size_t len; -+ + +- reduce(parent); +- wchar_t *lib_python = joinpath2(parent, calculate->lib_python); +- PyMem_RawFree(parent); +#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 @@ -79,18 +116,14 @@ index ef6dd59a084..41255647bcd 100644 + if (fgets(rest, PATH_MAX + 1, maps) == NULL) { + break; + } -+ size_t restlen = strlen(rest); -+ if (restlen >= PATH_MAX) { ++ if (strlen(rest) >= PATH_MAX) { + // If the line is too long our parsing will be out of sync. + break; + } -+ if (restlen > 0 && rest[restlen - 1] == '\n') { -+ rest[restlen - 1] = '\0'; -+ } else { -+ // EOF or error before we got to end of line. -+ break; -+ } -+ + +- if (lib_python == NULL) { +- status = _PyStatus_NO_MEMORY(); +- goto done; + if (low <= (uintptr_t)target && (uintptr_t)target < high) { + // Skip past padding spaces in the filename. + const char *filename = rest + strspn(rest, " "); @@ -99,26 +132,68 @@ index ef6dd59a084..41255647bcd 100644 + if (pathconfig->program_full_path == NULL) { + return DECODE_LOCALE_ERR("mapped code path", len); + } -+ return _PyStatus_OK(); + } + break; + } + } -+ } + } +#endif -+ -+ Dl_info libpython_info; -+ if (dladdr(target, &libpython_info) && libpython_info.dli_fname) { -+ pathconfig->program_full_path = Py_DecodeLocale(libpython_info.dli_fname, &len); -+ if (pathconfig->program_full_path == NULL) { -+ return DECODE_LOCALE_ERR("shared object path", len); -+ } -+ return _PyStatus_OK(); -+ } -+ - /* If there is no slash in the argv0 path, then we have to - * assume python is on the user's $PATH, since there's no - * other way to find a directory to start the search from. If + +- int module; +- status = ismodule(lib_python, &module); +- PyMem_RawFree(lib_python); +- +- if (_PyStatus_EXCEPTION(status)) { +- goto done; +- } +- if (!module) { +- /* We are in the build directory so use the name of the +- executable - we know that the absolute path is passed */ +- PyMem_RawFree(calculate->argv0_path); +- calculate->argv0_path = _PyMem_RawWcsdup(pathconfig->program_full_path); +- if (calculate->argv0_path == NULL) { +- status = _PyStatus_NO_MEMORY(); +- goto done; ++ if (wbuf == NULL) { ++ Dl_info libpython_info; ++ if (dladdr(target, &libpython_info) && libpython_info.dli_fname) { ++ wbuf = Py_DecodeLocale(libpython_info.dli_fname, &len); ++ if (wbuf == NULL) { ++ return DECODE_LOCALE_ERR("shared object path", len); ++ } + } +- +- status = _PyStatus_OK(); +- goto done; + } + + /* Use the location of the library as argv0_path */ + PyMem_RawFree(calculate->argv0_path); + calculate->argv0_path = wbuf; + return _PyStatus_OK(); +- +-done: +- PyMem_RawFree(wbuf); +- return status; + } +-#endif + + + static PyStatus +@@ -1184,12 +1182,10 @@ calculate_argv0_path(PyCalculatePath *calculate, + return _PyStatus_NO_MEMORY(); + } + +-#ifdef WITH_NEXT_FRAMEWORK +- status = calculate_argv0_path_framework(calculate, pathconfig); ++ status = calculate_argv0_path_library(calculate, pathconfig); + if (_PyStatus_EXCEPTION(status)) { + return status; + } +-#endif + + status = resolve_symlinks(&calculate->argv0_path); + if (_PyStatus_EXCEPTION(status)) { -- 2.50.1 (Apple Git-155)