Skip to content

Commit 52917c5

Browse files
committed
Use the symlink-resolved location of Python in getpath on 3.14+
Mostly fixes #380, and also fixes #713 because we prefer the actual binary path to argv[0].
1 parent 642c300 commit 52917c5

File tree

2 files changed

+142
-0
lines changed

2 files changed

+142
-0
lines changed

cpython-unix/build-cpython.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,13 @@ if [ -n "${CROSS_COMPILING}" ]; then
629629
# TODO: There are probably more of these, see #599.
630630
fi
631631

632+
# Adjust the Python startup logic (getpath.py) to properly locate the installation, even when
633+
# invoked through a symlink or through an incorrect argv[0]. Because this Python is relocatable, we
634+
# don't get to rely on the fallback to the compiled-in installation prefix.
635+
if [[ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_14}" ]]; then
636+
patch -p1 -i "${ROOT}/patch-python-getpath-3.14.patch"
637+
fi
638+
632639
# We patched configure.ac above. Reflect those changes.
633640
autoconf
634641

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
From 4fb328cb883504dde04dfdd0b4d182a0130a0909 Mon Sep 17 00:00:00 2001
2+
From: Geoffrey Thomas <[email protected]>
3+
Date: Mon, 1 Dec 2025 14:11:43 -0500
4+
Subject: [PATCH 1/1] getpath: Fix library detection and canonicalize paths on
5+
Linux
6+
Forwarded: no
7+
8+
The code in getpath.py to look for the stdlib relative to the Python
9+
library did not work in the common layout where libpython itself is in
10+
the lib/ directory; it added an extra lib/ segment. It is also equally
11+
applicable and useful when statically linking libpython into bin/python;
12+
in both cases, we want to go up a directory and then look into
13+
lib/python3.x/. Add an extra dirname() call in getpath.py, and
14+
unconditionally attempt to fill in the "library" variable in getpath.c,
15+
even on builds that are statically linking libpython.
16+
17+
Also, we want to use the realpath'd version of the library's path to
18+
locate the standard library, particularly in the case where the library
19+
is a symlink to an executable statically linking libpython. On macOS
20+
dyld, this is done automatically. On glibc and musl, we often get
21+
relative paths and they are not canonicalized, so instead, use
22+
/proc/self/maps to find the file where libpython is coming from.
23+
24+
(We could instead use the origin, which is canonicalized, but there is
25+
no safe API on glibc to read it and no API at all on musl. Note that and
26+
glibc also uses procfs to do so; see discussion at
27+
https://sourceware.org/bugzilla/show_bug.cgi?id=25263)
28+
---
29+
Modules/getpath.c | 52 ++++++++++++++++++++++++++++++++++++++++------
30+
Modules/getpath.py | 4 ++--
31+
2 files changed, 48 insertions(+), 8 deletions(-)
32+
33+
diff --git a/Modules/getpath.c b/Modules/getpath.c
34+
index 1e75993480a..72860807133 100644
35+
--- a/Modules/getpath.c
36+
+++ b/Modules/getpath.c
37+
@@ -802,14 +802,19 @@ progname_to_dict(PyObject *dict, const char *key)
38+
}
39+
40+
41+
+static void
42+
+fclose_cleanup(FILE **pf) {
43+
+ if (*pf) {
44+
+ fclose(*pf);
45+
+ *pf = NULL;
46+
+ }
47+
+}
48+
+
49+
+
50+
/* Add the runtime library's path to the dict */
51+
static int
52+
library_to_dict(PyObject *dict, const char *key)
53+
{
54+
-/* macOS framework builds do not link against a libpython dynamic library, but
55+
- instead link against a macOS Framework. */
56+
-#if defined(Py_ENABLE_SHARED) || defined(WITH_NEXT_FRAMEWORK)
57+
-
58+
#ifdef MS_WINDOWS
59+
extern HMODULE PyWin_DLLhModule;
60+
if (PyWin_DLLhModule) {
61+
@@ -817,12 +822,47 @@ library_to_dict(PyObject *dict, const char *key)
62+
}
63+
#endif
64+
65+
+ const void *target = (void *)Py_Initialize;
66+
+
67+
+#ifdef __linux__
68+
+ /* Linux libcs do not reliably report the realpath in dladdr dli_fname and
69+
+ * sometimes return relative paths, especially if the returned object is
70+
+ * the main program itself. However, /proc/self/maps will give absolute
71+
+ * realpaths (from the kernel, for the same reason that /proc/self/exe is
72+
+ * canonical), so try to parse and look it up there. (dyld seems to
73+
+ * reliably report the canonical path, so doing this matches the behavior
74+
+ * on macOS.) */
75+
+
76+
+ __attribute__((cleanup(fclose_cleanup)))
77+
+ FILE *maps = fopen("/proc/self/maps", "r");
78+
+ if (maps != NULL) {
79+
+ /* See implementation in fs/proc/task_mmu.c for spacing. The pathname
80+
+ * is the last field and has any \n characters escaped, so we can read
81+
+ * until \n. Note that the filename may have " (deleted)" appended;
82+
+ * we don't bother to handle that specially as the only user of this
83+
+ * value calls dirname() anyway.
84+
+ * TODO(geofft): Consider using PROCMAP_QUERY if supported.
85+
+ */
86+
+ uintptr_t low, high;
87+
+ char filename[PATH_MAX];
88+
+ while (fscanf(maps,
89+
+ "%lx-%lx %*s %*s %*s %*s %[^\n]",
90+
+ &low, &high, filename) == 3) {
91+
+ if (low <= (uintptr_t)target && (uintptr_t)target < high) {
92+
+ if (filename[0] == '/') {
93+
+ return decode_to_dict(dict, key, filename);
94+
+ }
95+
+ break;
96+
+ }
97+
+ }
98+
+ }
99+
+#endif
100+
+
101+
#if HAVE_DLADDR
102+
Dl_info libpython_info;
103+
- if (dladdr(&Py_Initialize, &libpython_info) && libpython_info.dli_fname) {
104+
+ if (dladdr(target, &libpython_info) && libpython_info.dli_fname) {
105+
return decode_to_dict(dict, key, libpython_info.dli_fname);
106+
}
107+
-#endif
108+
#endif
109+
110+
return PyDict_SetItemString(dict, key, Py_None) == 0;
111+
diff --git a/Modules/getpath.py b/Modules/getpath.py
112+
index b89d7427e3f..8c431e53be2 100644
113+
--- a/Modules/getpath.py
114+
+++ b/Modules/getpath.py
115+
@@ -436,7 +436,7 @@ def search_up(prefix, *landmarks, test=isfile):
116+
117+
if not executable_dir and os_name == 'darwin' and library:
118+
# QUIRK: macOS checks adjacent to its library early
119+
- library_dir = dirname(library)
120+
+ library_dir = dirname(dirname(library))
121+
if any(isfile(joinpath(library_dir, p)) for p in STDLIB_LANDMARKS):
122+
# Exceptions here should abort the whole process (to match
123+
# previous behavior)
124+
@@ -570,7 +570,7 @@ def search_up(prefix, *landmarks, test=isfile):
125+
126+
# First try to detect prefix by looking alongside our runtime library, if known
127+
if library and not prefix:
128+
- library_dir = dirname(library)
129+
+ library_dir = dirname(dirname(library))
130+
if ZIP_LANDMARK:
131+
if os_name == 'nt':
132+
# QUIRK: Windows does not search up for ZIP file
133+
--
134+
2.50.1 (Apple Git-155)
135+

0 commit comments

Comments
 (0)