diff --git a/Lib/test/test_getpath.py b/Lib/test/test_getpath.py index 83f09f3495547a..6bfe9a1046ca84 100644 --- a/Lib/test/test_getpath.py +++ b/Lib/test/test_getpath.py @@ -497,6 +497,34 @@ def test_symlink_normal_posix(self): actual = getpath(ns, expected) self.assertEqual(expected, actual) + def test_bad_argv0_posix(self): + """Test that executable resolves correctly if argv0 is not Python and we + know real_executable (getpath.c computes it from readlink(/proc/self/exe)). + """ + ns = MockPosixNamespace( + PREFIX="/usr", + argv0="not-python", + real_executable="/usr/bin/python", + ) + ns.add_known_xfile("/usr/bin/python") + ns.add_known_file("/usr/lib/python9.8/os.py") + ns.add_known_dir("/usr/lib/python9.8/lib-dynload") + expected = dict( + executable="/usr/bin/python", + base_executable="/usr/bin/python", + prefix="/usr", + exec_prefix="/usr", + module_search_paths_set=1, + module_search_paths=[ + "/usr/lib/python98.zip", + "/usr/lib/python9.8", + "/usr/lib/python9.8/lib-dynload", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + + def test_symlink_buildpath_posix(self): """Test an in-build-tree layout on POSIX. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-03-21-45-16.gh-issue-124241._GLhVo.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-03-21-45-16.gh-issue-124241._GLhVo.rst new file mode 100644 index 00000000000000..aa22546c677302 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-03-21-45-16.gh-issue-124241._GLhVo.rst @@ -0,0 +1,2 @@ +On POSIX, we now try to read the symlink target from ``/proc/self/exe`` to +determine :data:`sys.executable`. diff --git a/Modules/getpath.c b/Modules/getpath.c index 1e75993480ae36..0b424cfdfb4b3c 100644 --- a/Modules/getpath.c +++ b/Modules/getpath.c @@ -797,6 +797,11 @@ progname_to_dict(PyObject *dict, const char *key) PyMem_RawFree(path); break; } +#elif defined(HAVE_READLINK) + wchar_t resolved[MAXPATHLEN + 1]; + if (_Py_wreadlink(L"/proc/self/exe", resolved, Py_ARRAY_LENGTH(resolved)) > 0) { + return wchar_to_dict(dict, key, resolved); + } #endif return PyDict_SetItemString(dict, key, Py_None) == 0; } diff --git a/Modules/getpath.py b/Modules/getpath.py index 2f4d635a29585c..442cad4fb41b10 100644 --- a/Modules/getpath.py +++ b/Modules/getpath.py @@ -280,6 +280,17 @@ def search_up(prefix, *landmarks, test=isfile): # whether we are in a build tree. This is true even if the # executable path was provided in the config. real_executable = executable +elif os_name == 'posix' and real_executable: + # real_executable is more accurate than the value we have computed for + # executable, so use it instead if it resolves to a different path + # (eg. GH-124241). + # If real_executable and executable resolve to the same path, prefer + # executable, as that is much more likely to be the path the user is using. + try: + if realpath(executable) != real_executable: + executable = real_executable + except OSError: + pass if not executable and program_name and ENV_PATH: # Resolve names against PATH.