From 7a562c474c6885b1d6f3ac74962bb920483bfbe1 Mon Sep 17 00:00:00 2001 From: Xiao Yuan <47032563+yuanx749@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:15:28 +0300 Subject: [PATCH 1/6] gh-127636: Fix tarfile extracting trailing slash member names (GH-152984) Fixes tarfile.TarFile.extract to accept archive member names with a trailing forward slash, including those returned by tarfile.TarFile.getnames very old style tar files may have these. new archivers likely do not do this. --- Lib/tarfile.py | 4 +++- Lib/test/test_tarfile.py | 13 +++++++++++++ .../2026-07-04-00-19-23.gh-issue-127636.o_uD9U.rst | 3 +++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-04-00-19-23.gh-issue-127636.o_uD9U.rst diff --git a/Lib/tarfile.py b/Lib/tarfile.py index f46e938fd314ddb..c4cbbdb980857d4 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -2196,7 +2196,9 @@ def getmember(self, name): than once in the archive, its last occurrence is assumed to be the most up-to-date version. """ - tarinfo = self._getmember(name.rstrip('/')) + tarinfo = self._getmember(name) + if tarinfo is None and name.endswith('/'): + tarinfo = self._getmember(name.rstrip('/')) if tarinfo is None: raise KeyError("filename %r not found" % name) return tarinfo diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index 10106c3ada9ba52..62edb7115ed682b 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -255,6 +255,19 @@ def test_add_dir_getmember(self): self.add_dir_and_getmember('bar') self.add_dir_and_getmember('a'*101) + def test_extract_name_with_trailing_slash(self): + # gh-127636: './mydir/' is deliberately a regular-file member + # (REGTYPE, not DIRTYPE) whose stored name ends in a slash. It + # extracts as a file. Do not "fix" this by setting DIRTYPE; the + # trailing-slash name on a non-directory is what is being tested. + with tarfile.open(tmpname, 'w') as tar: + tar.addfile(tarfile.TarInfo('./mydir/')) + with os_helper.temp_dir() as tmpdir, tarfile.open(tmpname) as tar: + names = tar.getnames() + self.assertEqual(names, ['./mydir/']) + tar.extract(names[0], tmpdir, filter='fully_trusted') + self.assertTrue(os.path.isfile(os.path.join(tmpdir, 'mydir'))) + @unittest.skipUnless(hasattr(os, "getuid") and hasattr(os, "getgid"), "Missing getuid or getgid implementation") def add_dir_and_getmember(self, name): diff --git a/Misc/NEWS.d/next/Library/2026-07-04-00-19-23.gh-issue-127636.o_uD9U.rst b/Misc/NEWS.d/next/Library/2026-07-04-00-19-23.gh-issue-127636.o_uD9U.rst new file mode 100644 index 000000000000000..51957a73b5603b2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-04-00-19-23.gh-issue-127636.o_uD9U.rst @@ -0,0 +1,3 @@ +Fix :meth:`tarfile.TarFile.extract` to accept archive member names with a +trailing forward slash, including those returned by +:meth:`tarfile.TarFile.getnames`. Contributed by Xiao Yuan. From 26387858990674f8cd20566f89c700fa1de5d829 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" <68491+gpshead@users.noreply.github.com> Date: Mon, 7 Sep 2026 23:34:58 -0700 Subject: [PATCH 2/6] gh-123018: Keep the libedit history file header when truncating (GH-157165) libedit's history_truncate_file() keeps the last N lines of the file, which drops the "_HiStOrY_V2_" header line that its own write_history() emits and that its read_history() requires. So on a libedit build, readline.write_history_file() or readline.append_history_file() after readline.set_history_length() produced a file that readline.read_history_file() rejected with EINVAL. Under the libedit emulation, truncate the file ourselves and keep the header, resolving the default "~/.history" the same way libedit does. Apple's libedit fork already preserves the header, so the workaround is not compiled on macOS. --- Lib/test/test_readline.py | 38 ++++ ...-09-08-04-10-00.gh-issue-123018.hV2kQe.rst | 6 + Modules/readline.c | 185 +++++++++++++++++- 3 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-08-04-10-00.gh-issue-123018.hV2kQe.rst diff --git a/Lib/test/test_readline.py b/Lib/test/test_readline.py index 11ec57a259a9abf..6af26accc13d71a 100644 --- a/Lib/test/test_readline.py +++ b/Lib/test/test_readline.py @@ -169,6 +169,44 @@ def test_write_read_limited_history(self): # Readline seems to report an additional history element. self.assertIn(readline.get_current_history_length(), (2, 3)) + def test_write_read_zero_length_history(self): + previous_length = readline.get_history_length() + self.addCleanup(readline.set_history_length, previous_length) + + readline.clear_history() + readline.add_history("first line") + readline.set_history_length(0) + readline.write_history_file(TESTFN) + self.addCleanup(os.remove, TESTFN) + + readline.clear_history() + # libedit cannot read an empty history file, only one that still + # has its header line. How many items remain is not checked: + # libedit's own history_truncate_file() ignores a length of 0. + readline.read_history_file(TESTFN) + + @unittest.skipUnless(hasattr(readline, "append_history_file"), + "append_history not available") + def test_append_limited_history(self): + previous_length = readline.get_history_length() + self.addCleanup(readline.set_history_length, previous_length) + + readline.clear_history() + readline.add_history("first line") + readline.add_history("second line") + readline.write_history_file(TESTFN) + self.addCleanup(os.remove, TESTFN) + + readline.add_history("third line") + readline.set_history_length(2) + readline.append_history_file(1, TESTFN) + + readline.clear_history() + readline.read_history_file(TESTFN) + self.assertEqual(readline.get_history_item(1), "second line") + self.assertEqual(readline.get_history_item(2), "third line") + self.assertEqual(readline.get_history_item(3), None) + class TestReadline(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2026-09-08-04-10-00.gh-issue-123018.hV2kQe.rst b/Misc/NEWS.d/next/Library/2026-09-08-04-10-00.gh-issue-123018.hV2kQe.rst new file mode 100644 index 000000000000000..d75aa6950eb0711 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-08-04-10-00.gh-issue-123018.hV2kQe.rst @@ -0,0 +1,6 @@ +Fix :func:`readline.write_history_file` and +:func:`readline.append_history_file` producing a history file that +:func:`readline.read_history_file` could not load when a limit was set with +:func:`readline.set_history_length` and Python was built against +``libedit``. This works around `NetBSD PR 60322 +`_. diff --git a/Modules/readline.c b/Modules/readline.c index fc79a5866dfd38c..e0ff7a9ffebf794 100644 --- a/Modules/readline.c +++ b/Modules/readline.c @@ -10,16 +10,27 @@ /* Standard definitions */ #include "Python.h" +#include "pycore_fileutils.h" // _Py_open_noraise() #include "pycore_pyatomic_ft_wrappers.h" #include "pycore_pylifecycle.h" // _Py_SetLocaleFromEnv() #include // errno +#include // getpwuid_r() #include // SIGWINCH #include // free() #include // strdup() +#ifdef HAVE_FCNTL_H +# include // O_RDWR +#endif #ifdef HAVE_SYS_SELECT_H # include // select() #endif +#ifdef HAVE_SYS_STAT_H +# include // fstat() +#endif +#ifdef HAVE_UNISTD_H +# include // ftruncate() +#endif #if defined(HAVE_SETLOCALE) /* GNU readline() mistakenly sets the LC_CTYPE locale. @@ -319,6 +330,176 @@ readline_read_history_file_impl(PyObject *module, PyObject *filename_obj) static int _history_length = -1; /* do not truncate history by default */ +#ifndef __APPLE__ +/* macOS libedit carries a patch fixing the bug this works around. */ +/* Return libedit's default history file, "~/.history". This must resolve + * the same file as libedit's private _default_history_file() + * (https://cvsweb.netbsd.org/bsdweb.cgi/src/lib/libedit/readline.c) so that we + * truncate the same file write_history() and append_history() just wrote: the + * home directory comes from the password database, $HOME is not consulted. + * The result must be freed with PyMem_RawFree(). */ +static char * +_py_libedit_default_history_file(void) +{ + struct passwd *pw = NULL; + char *path = NULL; +#ifdef HAVE_GETPWUID_R + struct passwd pwd; + char *buf = NULL; + Py_ssize_t bufsize = sysconf(_SC_GETPW_R_SIZE_MAX); + if (bufsize == -1) { + bufsize = 1024; + } + for (;;) { + char *newbuf = PyMem_RawRealloc(buf, bufsize); + if (newbuf == NULL) { + break; + } + buf = newbuf; + int status = getpwuid_r(getuid(), &pwd, buf, bufsize, &pw); + if (status == 0) { + break; + } + pw = NULL; + if (status != ERANGE || bufsize > (PY_SSIZE_T_MAX >> 1)) { + break; + } + bufsize <<= 1; + } +#else + pw = getpwuid(getuid()); +#endif + if (pw != NULL && pw->pw_dir != NULL) { + size_t len = strlen(pw->pw_dir) + sizeof("/.history"); + path = PyMem_RawMalloc(len); + if (path != NULL) { + PyOS_snprintf(path, len, "%s/.history", pw->pw_dir); + } + } +#ifdef HAVE_GETPWUID_R + PyMem_RawFree(buf); +#endif + return path; +} + +/* libedit's history_truncate_file() keeps the last nlines lines of the + * file, which drops the "_HiStOrY_V2_" header line that its own + * write_history() emits and that its read_history() requires: once + * truncated, the file can no longer be loaded (gh-123018). Truncate the + * file ourselves and keep the header. + * + * Upstream libedit (NetBSD, and the portable releases from thrysoee.dk) + * still has this bug, reported as https://gnats.netbsd.org/60322. Apple's + * libedit fork patches its history_truncate_file() to copy the first line, + * so this workaround is not compiled on macOS. We only keep the header + * when it is actually there, so a file without one (e.g. written by GNU + * readline) is not given a bogus header. + * + * History files are small, so the whole file is read into memory and + * rewritten in place, as libedit does. GNU readline instead writes a + * temporary file and renames it over the original. */ +static int +_py_libedit_history_truncate_file(const char *filename, int nlines) +{ + static const char cookie[] = "_HiStOrY_V2_\n"; + const size_t cookie_len = sizeof(cookie) - 1; + char *default_file = NULL; + char *buf = NULL; + FILE *fp = NULL; + int ret = -1; + + if (filename == NULL) { + default_file = _py_libedit_default_history_file(); + if (default_file == NULL) { + return -1; + } + filename = default_file; + } + int fd = _Py_open_noraise(filename, O_RDWR); + if (fd < 0) { + goto done; + } + fp = fdopen(fd, "r+"); + if (fp == NULL) { + close(fd); + goto done; + } + struct stat st; + if (fstat(fd, &st) != 0) { + goto done; + } + size_t size = (size_t)st.st_size; + buf = PyMem_RawMalloc(size); + if (buf == NULL || fread(buf, 1, size, fp) != size) { + goto done; + } + + size_t header = 0; + if (size >= cookie_len && memcmp(buf, cookie, cookie_len) == 0) { + header = cookie_len; + } + const char *end = buf + size; + + /* Count the lines following the header. */ + size_t total = 0; + for (const char *p = buf + header; + (p = memchr(p, '\n', end - p)) != NULL; + p++) { + total++; + } + if (end > buf + header && end[-1] != '\n') { + total++; /* an unterminated last line */ + } + if (total <= (size_t)nlines) { + ret = 0; /* nothing to drop */ + goto done; + } + + /* Skip the leading lines, keeping the header and the last nlines. */ + const char *tail = buf + header; + for (size_t skip = total - (size_t)nlines; skip > 0; skip--) { + const char *nl = memchr(tail, '\n', end - tail); + if (nl == NULL) { + tail = end; /* the unterminated last line is dropped too */ + break; + } + tail = nl + 1; + } + size_t taillen = end - tail; + if (fseek(fp, 0, SEEK_SET) != 0 + || fwrite(buf, 1, header, fp) != header + || fwrite(tail, 1, taillen, fp) != taillen + || fflush(fp) != 0 + || ftruncate(fd, (off_t)(header + taillen)) != 0) { + goto done; + } + ret = 0; + +done: + if (fp != NULL) { + fclose(fp); + } + PyMem_RawFree(buf); + PyMem_RawFree(default_file); + return ret; +} +#endif /* !__APPLE__ */ + +/* Truncate the history file after write_history() or append_history(). + * Like the history_truncate_file() calls this replaces, failures are + * ignored by the callers: the history was already saved successfully and + * a failed truncation only leaves the file longer than requested. */ +static int +_py_history_truncate_file(const char *filename, int nlines) +{ +#ifndef __APPLE__ + if (using_libedit_emulation) { + return _py_libedit_history_truncate_file(filename, nlines); + } +#endif + return history_truncate_file(filename, nlines); +} + /* Exported function to save a readline history file */ /*[clinic input] @@ -360,7 +541,7 @@ readline_write_history_file_impl(PyObject *module, PyObject *filename_obj) errno = err = write_history(filename); int history_length = FT_ATOMIC_LOAD_INT_RELAXED(_history_length); if (!err && history_length >= 0) - history_truncate_file(filename, history_length); + _py_history_truncate_file(filename, history_length); Py_XDECREF(filename_bytes); errno = err; if (errno) @@ -419,7 +600,7 @@ readline_append_history_file_impl(PyObject *module, int nelements, nelements - libedit_append_replace_history_offset, filename); int history_length = FT_ATOMIC_LOAD_INT_RELAXED(_history_length); if (!err && history_length >= 0) - history_truncate_file(filename, history_length); + _py_history_truncate_file(filename, history_length); Py_XDECREF(filename_bytes); errno = err; if (errno) From adea841addefe2161afa817c90470f66904149ee Mon Sep 17 00:00:00 2001 From: Md Arif <111168803+sabamdarif@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:39:19 +0530 Subject: [PATCH 3/6] gh-152936: Make privileged functions available on Android (#152977) Expose the chroot, initgroups, setegid, seteuid, setgid, sethostname, setregid, setresgid, setresuid, setreuid, and setuid functions on Android. Previously, these methods were excluded by an autoconf guard; they're now included with a permission check to prevent issues invoking them as a non-root user. Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> Co-authored-by: Malcolm Smith --- Doc/library/os.rst | 50 +++++++++--- Doc/library/socket.rst | 5 +- Doc/whatsnew/3.16.rst | 17 ++++ Lib/test/test_os/test_posix.py | 8 ++ ...-07-08-06-54-57.gh-issue-152936.TaFl0J.rst | 6 ++ Modules/posixmodule.c | 80 +++++++++++++++++++ Modules/socketmodule.c | 9 +++ configure | 7 +- configure.ac | 7 +- 9 files changed, 166 insertions(+), 23 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-08-06-54-57.gh-issue-152936.TaFl0J.rst diff --git a/Doc/library/os.rst b/Doc/library/os.rst index 596597b1ab223ee..6e0e67d2a613c85 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -574,10 +574,13 @@ process and user. the groups of which the specified username is a member, plus the specified group id. - .. availability:: Unix, not WASI, not Android. + .. availability:: Unix, not WASI. .. versionadded:: 3.2 + .. versionchanged:: 3.16 + Support for Android now exists. + .. function:: putenv(key, value, /) @@ -610,21 +613,30 @@ process and user. Set the current process's effective group id. - .. availability:: Unix, not WASI, not Android. + .. availability:: Unix, not WASI. + + .. versionchanged:: 3.16 + Support for Android now exists. .. function:: seteuid(euid, /) Set the current process's effective user id. - .. availability:: Unix, not WASI, not Android. + .. availability:: Unix, not WASI. + + .. versionchanged:: 3.16 + Support for Android now exists. .. function:: setgid(gid, /) Set the current process' group id. - .. availability:: Unix, not WASI, not Android. + .. availability:: Unix, not WASI. + + .. versionchanged:: 3.16 + Support for Android now exists. .. function:: setgroups(groups, /) @@ -718,32 +730,44 @@ process and user. Set the current process's real and effective group ids. - .. availability:: Unix, not WASI, not Android. + .. availability:: Unix, not WASI. + + .. versionchanged:: 3.16 + Support for Android now exists. .. function:: setresgid(rgid, egid, sgid, /) Set the current process's real, effective, and saved group ids. - .. availability:: Unix, not WASI, not Android, not macOS, not iOS. + .. availability:: Unix, not WASI, not macOS, not iOS. .. versionadded:: 3.2 + .. versionchanged:: 3.16 + Support for Android now exists. + .. function:: setresuid(ruid, euid, suid, /) Set the current process's real, effective, and saved user ids. - .. availability:: Unix, not WASI, not Android, not macOS, not iOS. + .. availability:: Unix, not WASI, not macOS, not iOS. .. versionadded:: 3.2 + .. versionchanged:: 3.16 + Support for Android now exists. + .. function:: setreuid(ruid, euid, /) Set the current process's real and effective user ids. - .. availability:: Unix, not WASI, not Android. + .. availability:: Unix, not WASI. + + .. versionchanged:: 3.16 + Support for Android now exists. .. function:: getsid(pid, /) @@ -766,7 +790,10 @@ process and user. Set the current process's user id. - .. availability:: Unix, not WASI, not Android. + .. availability:: Unix, not WASI. + + .. versionchanged:: 3.16 + Support for Android now exists. .. placed in this section since it relates to errno.... a little weak @@ -2324,11 +2351,14 @@ features: Change the root directory of the current process to *path*. - .. availability:: Unix, not WASI, not Android. + .. availability:: Unix, not WASI. .. versionchanged:: 3.6 Accepts a :term:`path-like object`. + .. versionchanged:: 3.16 + Support for Android now exists. + .. function:: fchdir(fd) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 836aa91bb0885b1..fb9249df4be33ef 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -1372,10 +1372,13 @@ The :mod:`!socket` module also offers various network-related services: .. audit-event:: socket.sethostname name socket.sethostname - .. availability:: Unix, not Android. + .. availability:: Unix. .. versionadded:: 3.3 + .. versionchanged:: 3.16 + Support for Android now exists. + .. function:: if_nameindex() diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 858dc3b8a878e79..243bd078d37c998 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -474,6 +474,14 @@ os process via a pidfd. Available on Linux 5.6+. (Contributed by Maurycy Pawłowski-Wieroński in :gh:`149464`.) +* The following functions are now available on Android: + :func:`os.chroot`, :func:`os.initgroups`, :func:`os.setegid`, + :func:`os.seteuid`, :func:`os.setgid`, :func:`os.setregid`, + :func:`os.setresgid`, :func:`os.setresuid`, :func:`os.setreuid`, + and :func:`os.setuid`. Calling them without sufficient privileges + now raises :exc:`PermissionError` instead of the functions being unavailable. + (Contributed by Md Arif in :gh:`152936`.) + pydoc ----- @@ -513,6 +521,15 @@ shlex (Contributed by Jay Berry in :gh:`148846`.) +socket +------ + +* The :func:`socket.sethostname` function is now available on Android. + Calling it without sufficient privileges now raises :exc:`PermissionError` + instead of the function being unavailable. + (Contributed by Md Arif in :gh:`152936`.) + + sqlite3 ------- diff --git a/Lib/test/test_os/test_posix.py b/Lib/test/test_os/test_posix.py index 814f945aac7453c..f13ad46aac45aa1 100644 --- a/Lib/test/test_os/test_posix.py +++ b/Lib/test/test_os/test_posix.py @@ -107,6 +107,10 @@ def test_getresgid(self): @unittest.skipUnless(hasattr(posix, 'setresuid'), 'test needs posix.setresuid()') def test_setresuid(self): + # Android blocks this function for non-root users regardless of the arguments. + if support.is_android and os.getuid() != 0: + self.assertRaises(PermissionError, posix.setresuid, -1, -1, -1) + return current_user_ids = posix.getresuid() self.assertIsNone(posix.setresuid(*current_user_ids)) # -1 means don't change that value. @@ -124,6 +128,10 @@ def test_setresuid_exception(self): @unittest.skipUnless(hasattr(posix, 'setresgid'), 'test needs posix.setresgid()') def test_setresgid(self): + # Android blocks this function for non-root users regardless of the arguments. + if support.is_android and os.getuid() != 0: + self.assertRaises(PermissionError, posix.setresgid, -1, -1, -1) + return current_group_ids = posix.getresgid() self.assertIsNone(posix.setresgid(*current_group_ids)) # -1 means don't change that value. diff --git a/Misc/NEWS.d/next/Library/2026-07-08-06-54-57.gh-issue-152936.TaFl0J.rst b/Misc/NEWS.d/next/Library/2026-07-08-06-54-57.gh-issue-152936.TaFl0J.rst new file mode 100644 index 000000000000000..d8021b248977f1f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-08-06-54-57.gh-issue-152936.TaFl0J.rst @@ -0,0 +1,6 @@ +:func:`os.chroot`, :func:`os.initgroups`, :func:`os.setegid`, +:func:`os.seteuid`, :func:`os.setgid`, :func:`os.setregid`, +:func:`os.setresgid`, :func:`os.setresuid`, :func:`os.setreuid`, +:func:`os.setuid`, and :func:`socket.sethostname` are now available on +Android. Calling them without sufficient privileges now raises +:exc:`PermissionError` instead of the functions being unavailable. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 847237142854da1..7636d51485c730d 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -4399,6 +4399,14 @@ static PyObject * os_chroot_impl(PyObject *module, path_t *path) /*[clinic end generated code: output=de80befc763a4475 input=14822965652c3dc3]*/ { +#ifdef __ANDROID__ + // On Android, calling this function as a non-root user leads to a process crash + // rather than returning a permission error. + if (getuid() != 0) { + errno = EPERM; + return path_error(path); + } +#endif int res; Py_BEGIN_ALLOW_THREADS res = chroot(path->narrow); @@ -9875,6 +9883,14 @@ os_initgroups_impl(PyObject *module, PyObject *oname, gid_t gid) /*[clinic end generated code: output=59341244521a9e3f input=7e4514dff4526a95]*/ #endif { +#ifdef __ANDROID__ + // On Android, calling this function as a non-root user leads to a process crash + // rather than returning a permission error. + if (getuid() != 0) { + errno = EPERM; + return posix_error(); + } +#endif const char *username = PyBytes_AS_STRING(oname); if (initgroups(username, gid) == -1) @@ -10301,6 +10317,14 @@ static PyObject * os_setuid_impl(PyObject *module, uid_t uid) /*[clinic end generated code: output=a0a41fd0d1ec555f input=c921a3285aa22256]*/ { +#ifdef __ANDROID__ + // On Android, calling this function as a non-root user leads to a process crash + // rather than returning a permission error. + if (getuid() != 0) { + errno = EPERM; + return posix_error(); + } +#endif if (setuid(uid) < 0) return posix_error(); Py_RETURN_NONE; @@ -10322,6 +10346,14 @@ static PyObject * os_seteuid_impl(PyObject *module, uid_t euid) /*[clinic end generated code: output=102e3ad98361519a input=ba93d927e4781aa9]*/ { +#ifdef __ANDROID__ + // On Android, calling this function as a non-root user leads to a process crash + // rather than returning a permission error. + if (getuid() != 0) { + errno = EPERM; + return posix_error(); + } +#endif if (seteuid(euid) < 0) return posix_error(); Py_RETURN_NONE; @@ -10343,6 +10375,14 @@ static PyObject * os_setegid_impl(PyObject *module, gid_t egid) /*[clinic end generated code: output=4e4b825a6a10258d input=4080526d0ccd6ce3]*/ { +#ifdef __ANDROID__ + // On Android, calling this function as a non-root user leads to a process crash + // rather than returning a permission error. + if (getuid() != 0) { + errno = EPERM; + return posix_error(); + } +#endif if (setegid(egid) < 0) return posix_error(); Py_RETURN_NONE; @@ -10365,6 +10405,14 @@ static PyObject * os_setreuid_impl(PyObject *module, uid_t ruid, uid_t euid) /*[clinic end generated code: output=62d991210006530a input=0ca8978de663880c]*/ { +#ifdef __ANDROID__ + // On Android, calling this function as a non-root user leads to a process crash + // rather than returning a permission error. + if (getuid() != 0) { + errno = EPERM; + return posix_error(); + } +#endif if (setreuid(ruid, euid) < 0) { return posix_error(); } else { @@ -10389,6 +10437,14 @@ static PyObject * os_setregid_impl(PyObject *module, gid_t rgid, gid_t egid) /*[clinic end generated code: output=aa803835cf5342f3 input=c59499f72846db78]*/ { +#ifdef __ANDROID__ + // On Android, calling this function as a non-root user leads to a process crash + // rather than returning a permission error. + if (getuid() != 0) { + errno = EPERM; + return posix_error(); + } +#endif if (setregid(rgid, egid) < 0) return posix_error(); Py_RETURN_NONE; @@ -10409,6 +10465,14 @@ static PyObject * os_setgid_impl(PyObject *module, gid_t gid) /*[clinic end generated code: output=bdccd7403f6ad8c3 input=27d30c4059045dc6]*/ { +#ifdef __ANDROID__ + // On Android, calling this function as a non-root user leads to a process crash + // rather than returning a permission error. + if (getuid() != 0) { + errno = EPERM; + return posix_error(); + } +#endif if (setgid(gid) < 0) return posix_error(); Py_RETURN_NONE; @@ -15506,6 +15570,14 @@ static PyObject * os_setresuid_impl(PyObject *module, uid_t ruid, uid_t euid, uid_t suid) /*[clinic end generated code: output=834a641e15373e97 input=9e33cb79a82792f3]*/ { +#ifdef __ANDROID__ + // On Android, calling this function as a non-root user leads to a process crash + // rather than returning a permission error. + if (getuid() != 0) { + errno = EPERM; + return posix_error(); + } +#endif if (setresuid(ruid, euid, suid) < 0) return posix_error(); Py_RETURN_NONE; @@ -15529,6 +15601,14 @@ static PyObject * os_setresgid_impl(PyObject *module, gid_t rgid, gid_t egid, gid_t sgid) /*[clinic end generated code: output=6aa402f3d2e514a9 input=33e9e0785ef426b1]*/ { +#ifdef __ANDROID__ + // On Android, calling this function as a non-root user leads to a process crash + // rather than returning a permission error. + if (getuid() != 0) { + errno = EPERM; + return posix_error(); + } +#endif if (setresgid(rgid, egid, sgid) < 0) return posix_error(); Py_RETURN_NONE; diff --git a/Modules/socketmodule.c b/Modules/socketmodule.c index 82899572f80255a..d189c6d478836c4 100644 --- a/Modules/socketmodule.c +++ b/Modules/socketmodule.c @@ -5988,6 +5988,15 @@ _socket_sethostname(PyObject *module, PyObject *hnobj) Py_buffer buf; int res, flag = 0; +#ifdef __ANDROID__ + // On Android, calling this function as a non-root user leads to a process crash + // rather than returning a permission error. + if (getuid() != 0) { + errno = EPERM; + return set_error(); + } +#endif + #if defined(_AIX) || (defined(__sun) && defined(__SVR4) && Py_SUNOS_VERSION <= 510) /* issue #18259, sethostname is not declared in any useful header file on AIX * the same is true for Solaris 10 */ diff --git a/configure b/configure index 7e4844263a3491c..423e3724cfc9131 100755 --- a/configure +++ b/configure @@ -20366,14 +20366,9 @@ printf "%s\n" "$MACHDEP_OBJS" >&6; } fi if test "$ac_sys_system" = "Linux-android"; then - # When these functions are used in an unprivileged process, they crash rather - # than returning an error. - blocked_funcs="chroot initgroups setegid seteuid setgid sethostname - setregid setresgid setresuid setreuid setuid" - # These functions are unimplemented and always return an error # (https://android.googlesource.com/platform/system/sepolicy/+/refs/heads/android13-release/public/domain.te#1044) - blocked_funcs="$blocked_funcs sem_open sem_unlink" + blocked_funcs="sem_open sem_unlink" # Before API level 23, when fchmodat is called with the unimplemented flag # AT_SYMLINK_NOFOLLOW, instead of returning ENOTSUP as it should, it actually diff --git a/configure.ac b/configure.ac index 8345c97349b7382..d754522bb029be3 100644 --- a/configure.ac +++ b/configure.ac @@ -5494,14 +5494,9 @@ else fi if test "$ac_sys_system" = "Linux-android"; then - # When these functions are used in an unprivileged process, they crash rather - # than returning an error. - blocked_funcs="chroot initgroups setegid seteuid setgid sethostname - setregid setresgid setresuid setreuid setuid" - # These functions are unimplemented and always return an error # (https://android.googlesource.com/platform/system/sepolicy/+/refs/heads/android13-release/public/domain.te#1044) - blocked_funcs="$blocked_funcs sem_open sem_unlink" + blocked_funcs="sem_open sem_unlink" # Before API level 23, when fchmodat is called with the unimplemented flag # AT_SYMLINK_NOFOLLOW, instead of returning ENOTSUP as it should, it actually From 0b3b154060936e1b120f5b9793ed3f8647df8480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20P=C3=A9ron?= Date: Tue, 8 Sep 2026 09:26:48 +0200 Subject: [PATCH 4/6] gh-156780: Emscripten: add missing EM_JS_DEPS (#156798) Explicitly declares some Javascript dependencies to support static builds. Co-authored-by: Claude Opus 5 --- Python/emscripten_syscalls.c | 8 ++++++++ Python/emscripten_trampoline.c | 3 +++ 2 files changed, 11 insertions(+) diff --git a/Python/emscripten_syscalls.c b/Python/emscripten_syscalls.c index 48ca208dedb14bb..cdd5fc6e91e89bc 100644 --- a/Python/emscripten_syscalls.c +++ b/Python/emscripten_syscalls.c @@ -132,6 +132,10 @@ EM_JS_MACROS(void, _emscripten_promising_main_js, (void), { }; }) +EM_JS_DEPS(_emscripten_promising_main, + "$FS,$PATH,$FS_getMode,$resolveGlobalSymbol," + "emscripten_exit_with_live_runtime"); + __attribute__((constructor)) void _emscripten_promising_main(void) { _emscripten_promising_main_js(); } @@ -199,6 +203,8 @@ EM_JS_MACROS(__externref_t, __maybe_fd_read_async, ( }; ); +EM_JS_DEPS(__maybe_fd_read_async, "$SYSCALLS"); + // Bind original fd_read syscall to __wasi_fd_read_orig(). __wasi_errno_t __wasi_fd_read_orig(__wasi_fd_t fd, const __wasi_iovec_t *iovs, size_t iovs_len, __wasi_size_t *nread) @@ -280,6 +286,8 @@ EM_JS_MACROS(__externref_t, __maybe_poll_async, (intptr_t fds, int nfds, int tim })(); }); +EM_JS_DEPS(__maybe_poll_async, "$FS"); + // Bind original poll syscall to syscall_poll_orig(). int syscall_poll_orig(intptr_t fds, int nfds, int timeout) __attribute__((__import_module__("env"), diff --git a/Python/emscripten_trampoline.c b/Python/emscripten_trampoline.c index 1833311ca74d9dd..75cfde6b76f2174 100644 --- a/Python/emscripten_trampoline.c +++ b/Python/emscripten_trampoline.c @@ -94,6 +94,9 @@ addOnPreRun(function setEmscriptenTrampoline() { }); ); +EM_JS_DEPS(_PyEM_TrampolineCall, + "$wasmTable,$wasmMemory,$addFunction,$addOnPreRun"); + PyObject* _PyEM_TrampolineCall(PyCFunctionWithKeywords func, PyObject* self, From 465b3b80f98e0b1b3c116cab6f00cb9f4311c3cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20P=C3=A9ron?= Date: Tue, 8 Sep 2026 09:54:41 +0200 Subject: [PATCH 5/6] gh-156109: Allow static, non-framework iOS builds (#156110) Allows a static build of iOS for users that want to build and embed a libPython into an iOS binary. This configuration can't use shared modules or binary wheels, but it can be useful under some circumstances, so there's no reason to explicitly exclude it. Co-authored-by: Claude Opus 5 Co-authored-by: Russell Keith-Magee --- Doc/using/configure.rst | 10 ++ Doc/using/ios.rst | 7 ++ ...-08-20-13-32-54.gh-issue-156109.ZTJHi9.rst | 5 + Platforms/Apple/iOS/README.md | 79 ++++++++++++- configure | 110 ++++++++++-------- configure.ac | 106 +++++++++-------- 6 files changed, 216 insertions(+), 101 deletions(-) create mode 100644 Misc/NEWS.d/next/Build/2026-08-20-13-32-54.gh-issue-156109.ZTJHi9.rst diff --git a/Doc/using/configure.rst b/Doc/using/configure.rst index 8b4940ceb9521a7..88b5f35a7967967 100644 --- a/Doc/using/configure.rst +++ b/Doc/using/configure.rst @@ -1342,6 +1342,16 @@ See :source:`Platforms/Apple/iOS/README.md`. Specify the name for the framework (default: ``Python``). +An iOS build configured without ``--enable-framework`` produces a static +``libpython``, for embedding directly in an app binary. Such a build cannot +load extension modules at runtime, and so does not support binary wheels; it +requires ``MODULE_BUILDTYPE=static`` and :option:`--disable-test-modules`, and +rejects :option:`--enable-shared`. See +:source:`Platforms/Apple/iOS/README.md` for the full list of limitations. + +.. versionadded:: 3.16 + iOS builds may be configured without a framework. + Cross Compiling Options ----------------------- diff --git a/Doc/using/ios.rst b/Doc/using/ios.rst index 31d9e2f2c816e73..80b9b3c3a07f063 100644 --- a/Doc/using/ios.rst +++ b/Doc/using/ios.rst @@ -142,6 +142,13 @@ should ensure these stub binaries are on your path. Installing Python on iOS ======================== +The official iOS release artefact is a framework build, distributed as an +``XCFramework``; this is the configuration described in the rest of this +document, and the only one that supports binary extension modules. Static +builds, where ``libpython`` and every extension module are linked directly into +the app binary, are also possible, with limitations; see +:source:`Platforms/Apple/iOS/README.md` for details. + Tools for building iOS apps --------------------------- diff --git a/Misc/NEWS.d/next/Build/2026-08-20-13-32-54.gh-issue-156109.ZTJHi9.rst b/Misc/NEWS.d/next/Build/2026-08-20-13-32-54.gh-issue-156109.ZTJHi9.rst new file mode 100644 index 000000000000000..4b8c1c197ddf5db --- /dev/null +++ b/Misc/NEWS.d/next/Build/2026-08-20-13-32-54.gh-issue-156109.ZTJHi9.rst @@ -0,0 +1,5 @@ +iOS builds may now be configured without a framework, producing a static +``libpython`` for embedding in an app binary. Such a build cannot load extension +modules at runtime, and requires ``MODULE_BUILDTYPE=static`` and +``--disable-test-modules`` to be requested explicitly. A shared iOS build must +still be a framework build. diff --git a/Platforms/Apple/iOS/README.md b/Platforms/Apple/iOS/README.md index faeeead1df03a2e..4bf083fb25ea065 100644 --- a/Platforms/Apple/iOS/README.md +++ b/Platforms/Apple/iOS/README.md @@ -90,8 +90,13 @@ Python build for a single framework, the following options are available. installed. If `DIR` is not specified, the framework will be installed into a subdirectory of the `iOS/Frameworks` folder. - This argument *must* be provided when configuring iOS builds. iOS does not - support non-framework builds. + This argument is required for any iOS build that will be distributed, and + for any build that needs to load binary extension modules. + + Omitting it builds a static `libpython` for embedding directly in an app + binary, instead of a `Python.framework`. That configuration comes with + significant restrictions; see [Building a static + Python](#building-a-static-python) below. * `--with-framework-name=NAME` @@ -113,9 +118,11 @@ framework to contain non-library content, so the iOS build will produce a The `lib` folder will be needed at runtime to support the Python library. If you want to use Python in a real iOS project, you need to produce multiple -`Python.framework` builds, one for each ABI and architecture. iOS builds of -Python *must* be constructed as framework builds. To support this, you must -provide the `--enable-framework` flag when configuring the build. The build +`Python.framework` builds, one for each ABI and architecture. Unless you are +statically linking Python into your app (see [Building a static +Python](#building-a-static-python) below), iOS builds of Python *must* be +constructed as framework builds. To support this, you must provide the +`--enable-framework` flag when configuring the build. The build also requires the use of cross-compilation. The minimal commands for building Python for the ARM64 iOS simulator will look something like: ``` @@ -216,6 +223,68 @@ target, provide the version number as part of the `--host` argument - for example, `--host=arm64-apple-ios15.4-simulator` would compile an ARM64 simulator build with a deployment target of 15.4. +### Building a static Python + +The official iOS release artefact is a framework build. However, if you are +embedding Python in an app that links `libpython` at compile time, you can +instead build a static `libpython3.x.a`, and link that archive directly into +your app binary. + +The App Store requirement that binary modules be packaged as signed frameworks +does not apply to a static build, because a static build loads nothing at +runtime; but for the same reason, this configuration cannot use *any* binary +module that isn't compiled into the app binary. The restrictions that follow +from that must be opted into explicitly at configure time: + +* `MODULE_BUILDTYPE=static` is required. There is no framework for a shared + extension module to link against, so every extension module, including the + ones in the standard library, must be linked into `libpython`. + +* `--disable-test-modules` is required. Some test modules must be compiled as + shared libraries (see `Modules/Setup.stdlib.in`), so they cannot be built in + this configuration at all. + +A non-framework build is selected by omitting `--enable-framework`; +`--disable-framework` is accepted as an explicit spelling of the same thing. + +The minimal commands for a static build targeting ARM64 iOS devices are then: +``` +export PATH="$(pwd)/Platforms/Apple/iOS/Resources/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" +./configure \ + --prefix=/path/to/install/dir \ + --disable-test-modules \ + MODULE_BUILDTYPE=static \ + --host=arm64-apple-ios \ + --build=arm64-apple-darwin \ + --with-build-python=/path/to/python.exe +make +make install +``` +This produces a `libpython3.x.a` containing the interpreter and the standard +library's extension modules; `make install` installs that archive, along with +the standard library's Python source, into the location given by `--prefix`. +Unlike a framework build, `--prefix` is not set for you, so specify it +explicitly - otherwise `libpython` will be installed into `/usr/local`. + +#### Limitations of a static build + +* **Binary wheels cannot be used.** There is no `libpython` dylib for a + third-party extension module to link against, and a static Python has nothing + to `dlopen` in any case. Pure Python wheels work as normal; any package with a + C extension must be compiled into the app binary alongside `libpython`. + +* **The standard library's extension modules are not loadable modules.** They + live in the archive, not in `.framework` bundles in the app's `Frameworks` + folder, so the packaging described in + [Using Python on iOS](https://docs.python.org/3/using/ios.html) does not apply + to them. + +* **The test suite cannot be run as-is**, as the test modules are not built. + +* This configuration is not covered by the `Platforms/Apple` build script, nor + by CPython's CI. It is not the configuration used to produce official + releases. + ## Testing Python on iOS ### Testing a multi-architecture framework diff --git a/configure b/configure index 423e3724cfc9131..ae3f0450dc7af1b 100755 --- a/configure +++ b/configure @@ -4441,29 +4441,25 @@ then : case $enableval in no) - case $ac_sys_system in - iOS) as_fn_error $? "iOS builds must use --enable-framework" "$LINENO" 5 ;; - *) - PYTHONFRAMEWORK= - PYTHONFRAMEWORKDIR=no-framework - PYTHONFRAMEWORKPREFIX= - PYTHONFRAMEWORKINSTALLDIR= - PYTHONFRAMEWORKINSTALLNAMEPREFIX= - RESSRCDIR= - FRAMEWORKINSTALLFIRST= - FRAMEWORKINSTALLLAST= - FRAMEWORKALTINSTALLFIRST= - FRAMEWORKALTINSTALLLAST= - FRAMEWORKPYTHONW= - INSTALLTARGETS="commoninstall bininstall maninstall" - - if test "x${prefix}" = "xNONE"; then - FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" - else - FRAMEWORKUNIXTOOLSPREFIX="${prefix}" - fi - enable_framework= - esac + PYTHONFRAMEWORK= + PYTHONFRAMEWORKDIR=no-framework + PYTHONFRAMEWORKPREFIX= + PYTHONFRAMEWORKINSTALLDIR= + PYTHONFRAMEWORKINSTALLNAMEPREFIX= + RESSRCDIR= + FRAMEWORKINSTALLFIRST= + FRAMEWORKINSTALLLAST= + FRAMEWORKALTINSTALLFIRST= + FRAMEWORKALTINSTALLLAST= + FRAMEWORKPYTHONW= + INSTALLTARGETS="commoninstall bininstall maninstall" + + if test "x${prefix}" = "xNONE"; then + FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" + else + FRAMEWORKUNIXTOOLSPREFIX="${prefix}" + fi + enable_framework= ;; *) PYTHONFRAMEWORKPREFIX="${enableval}" @@ -4558,28 +4554,24 @@ then : else case e in #( e) - case $ac_sys_system in - iOS) as_fn_error $? "iOS builds must use --enable-framework" "$LINENO" 5 ;; - *) - PYTHONFRAMEWORK= - PYTHONFRAMEWORKDIR=no-framework - PYTHONFRAMEWORKPREFIX= - PYTHONFRAMEWORKINSTALLDIR= - PYTHONFRAMEWORKINSTALLNAMEPREFIX= - RESSRCDIR= - FRAMEWORKINSTALLFIRST= - FRAMEWORKINSTALLLAST= - FRAMEWORKALTINSTALLFIRST= - FRAMEWORKALTINSTALLLAST= - FRAMEWORKPYTHONW= - INSTALLTARGETS="commoninstall bininstall maninstall" - if test "x${prefix}" = "xNONE" ; then - FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" - else - FRAMEWORKUNIXTOOLSPREFIX="${prefix}" - fi - enable_framework= - esac + PYTHONFRAMEWORK= + PYTHONFRAMEWORKDIR=no-framework + PYTHONFRAMEWORKPREFIX= + PYTHONFRAMEWORKINSTALLDIR= + PYTHONFRAMEWORKINSTALLNAMEPREFIX= + RESSRCDIR= + FRAMEWORKINSTALLFIRST= + FRAMEWORKINSTALLLAST= + FRAMEWORKALTINSTALLFIRST= + FRAMEWORKALTINSTALLLAST= + FRAMEWORKPYTHONW= + INSTALLTARGETS="commoninstall bininstall maninstall" + if test "x${prefix}" = "xNONE" ; then + FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" + else + FRAMEWORKUNIXTOOLSPREFIX="${prefix}" + fi + enable_framework= ;; esac fi @@ -8027,6 +8019,9 @@ printf "%s\n" "#define Py_ENABLE_SHARED 1" >>confdefs.h RUNSHARED=DYLD_LIBRARY_PATH=`pwd`${DYLD_LIBRARY_PATH:+:${DYLD_LIBRARY_PATH}} ;; iOS) + if test -z "$PYTHONFRAMEWORK"; then + as_fn_error $? "iOS shared builds must use --enable-framework; an iOS app can only load a signed framework, never a bare dylib" "$LINENO" 5 + fi LDLIBRARY='libpython$(LDVERSION).dylib' ;; AIX*) @@ -14540,7 +14535,11 @@ printf "%s\n" "#define THREAD_STACK_SIZE 0x$stack_size" >>confdefs.h fi LINKFORSHARED="$LINKFORSHARED" elif test $ac_sys_system = "iOS"; then - LINKFORSHARED="-Wl,-stack_size,$stack_size $LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK)' + LINKFORSHARED="-Wl,-stack_size,$stack_size $LINKFORSHARED" + + if test "$enable_framework"; then + LINKFORSHARED="$LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK)' + fi fi ;; OpenUNIX*|UnixWare*) LINKFORSHARED="-Wl,-Bexport";; @@ -28288,8 +28287,8 @@ if test "$PY_ENABLE_SHARED" = "1" && ( test -n "$ANDROID_API_LEVEL" || test "$MA LIBPYTHON="-lpython${VERSION}${ABIFLAGS}" fi -# On iOS the shared libraries must be linked with the Python framework -if test "$ac_sys_system" = "iOS"; then +# On iOS the shared libraries must be linked with the framework, when built +if test "$ac_sys_system" = "iOS" && test "$enable_framework"; then MODULE_DEPS_SHARED="$MODULE_DEPS_SHARED \$(PYTHONFRAMEWORKDIR)/\$(PYTHONFRAMEWORK)" fi @@ -35111,6 +35110,13 @@ fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $TEST_MODULES" >&5 printf "%s\n" "$TEST_MODULES" >&6; } +if test "$ac_sys_system" = "iOS" && test -z "$PYTHONFRAMEWORK" +then : + if test "$TEST_MODULES" != "no" +then : + as_fn_error $? "iOS non-framework builds must use --disable-test-modules" "$LINENO" 5 +fi +fi # Check for --with-build-details-suffix @@ -35401,6 +35407,14 @@ case $host_cpu in #( esac +if test "$ac_sys_system" = "iOS" && test -z "$PYTHONFRAMEWORK" +then : + if test "$MODULE_BUILDTYPE" != "static" +then : + as_fn_error $? "iOS non-framework builds must use MODULE_BUILDTYPE=static" "$LINENO" 5 +fi +fi + MODULE_BLOCK= diff --git a/configure.ac b/configure.ac index d754522bb029be3..1c42900cb975c35 100644 --- a/configure.ac +++ b/configure.ac @@ -578,29 +578,25 @@ AC_ARG_ENABLE([framework], case $enableval in no) - case $ac_sys_system in - iOS) AC_MSG_ERROR([iOS builds must use --enable-framework]) ;; - *) - PYTHONFRAMEWORK= - PYTHONFRAMEWORKDIR=no-framework - PYTHONFRAMEWORKPREFIX= - PYTHONFRAMEWORKINSTALLDIR= - PYTHONFRAMEWORKINSTALLNAMEPREFIX= - RESSRCDIR= - FRAMEWORKINSTALLFIRST= - FRAMEWORKINSTALLLAST= - FRAMEWORKALTINSTALLFIRST= - FRAMEWORKALTINSTALLLAST= - FRAMEWORKPYTHONW= - INSTALLTARGETS="commoninstall bininstall maninstall" - - if test "x${prefix}" = "xNONE"; then - FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" - else - FRAMEWORKUNIXTOOLSPREFIX="${prefix}" - fi - enable_framework= - esac + PYTHONFRAMEWORK= + PYTHONFRAMEWORKDIR=no-framework + PYTHONFRAMEWORKPREFIX= + PYTHONFRAMEWORKINSTALLDIR= + PYTHONFRAMEWORKINSTALLNAMEPREFIX= + RESSRCDIR= + FRAMEWORKINSTALLFIRST= + FRAMEWORKINSTALLLAST= + FRAMEWORKALTINSTALLFIRST= + FRAMEWORKALTINSTALLLAST= + FRAMEWORKPYTHONW= + INSTALLTARGETS="commoninstall bininstall maninstall" + + if test "x${prefix}" = "xNONE"; then + FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" + else + FRAMEWORKUNIXTOOLSPREFIX="${prefix}" + fi + enable_framework= ;; *) PYTHONFRAMEWORKPREFIX="${enableval}" @@ -688,28 +684,24 @@ AC_ARG_ENABLE([framework], esac esac ],[ - case $ac_sys_system in - iOS) AC_MSG_ERROR([iOS builds must use --enable-framework]) ;; - *) - PYTHONFRAMEWORK= - PYTHONFRAMEWORKDIR=no-framework - PYTHONFRAMEWORKPREFIX= - PYTHONFRAMEWORKINSTALLDIR= - PYTHONFRAMEWORKINSTALLNAMEPREFIX= - RESSRCDIR= - FRAMEWORKINSTALLFIRST= - FRAMEWORKINSTALLLAST= - FRAMEWORKALTINSTALLFIRST= - FRAMEWORKALTINSTALLLAST= - FRAMEWORKPYTHONW= - INSTALLTARGETS="commoninstall bininstall maninstall" - if test "x${prefix}" = "xNONE" ; then - FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" - else - FRAMEWORKUNIXTOOLSPREFIX="${prefix}" - fi - enable_framework= - esac + PYTHONFRAMEWORK= + PYTHONFRAMEWORKDIR=no-framework + PYTHONFRAMEWORKPREFIX= + PYTHONFRAMEWORKINSTALLDIR= + PYTHONFRAMEWORKINSTALLNAMEPREFIX= + RESSRCDIR= + FRAMEWORKINSTALLFIRST= + FRAMEWORKINSTALLLAST= + FRAMEWORKALTINSTALLFIRST= + FRAMEWORKALTINSTALLLAST= + FRAMEWORKPYTHONW= + INSTALLTARGETS="commoninstall bininstall maninstall" + if test "x${prefix}" = "xNONE" ; then + FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" + else + FRAMEWORKUNIXTOOLSPREFIX="${prefix}" + fi + enable_framework= ]) AC_SUBST([PYTHONFRAMEWORK]) AC_SUBST([PYTHONFRAMEWORKIDENTIFIER]) @@ -1676,6 +1668,9 @@ if test $enable_shared = "yes"; then RUNSHARED=DYLD_LIBRARY_PATH=`pwd`${DYLD_LIBRARY_PATH:+:${DYLD_LIBRARY_PATH}} ;; iOS) + if test -z "$PYTHONFRAMEWORK"; then + AC_MSG_ERROR([iOS shared builds must use --enable-framework; an iOS app can only load a signed framework, never a bare dylib]) + fi LDLIBRARY='libpython$(LDVERSION).dylib' ;; AIX*) @@ -3836,7 +3831,11 @@ then fi LINKFORSHARED="$LINKFORSHARED" elif test $ac_sys_system = "iOS"; then - LINKFORSHARED="-Wl,-stack_size,$stack_size $LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK)' + LINKFORSHARED="-Wl,-stack_size,$stack_size $LINKFORSHARED" + + if test "$enable_framework"; then + LINKFORSHARED="$LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK)' + fi fi ;; OpenUNIX*|UnixWare*) LINKFORSHARED="-Wl,-Bexport";; @@ -6778,8 +6777,8 @@ if test "$PY_ENABLE_SHARED" = "1" && ( test -n "$ANDROID_API_LEVEL" || test "$MA LIBPYTHON="-lpython${VERSION}${ABIFLAGS}" fi -# On iOS the shared libraries must be linked with the Python framework -if test "$ac_sys_system" = "iOS"; then +# On iOS the shared libraries must be linked with the framework, when built +if test "$ac_sys_system" = "iOS" && test "$enable_framework"; then MODULE_DEPS_SHARED="$MODULE_DEPS_SHARED \$(PYTHONFRAMEWORKDIR)/\$(PYTHONFRAMEWORK)" fi @@ -8169,6 +8168,11 @@ AC_ARG_ENABLE([test-modules], AS_VAR_IF([enable_test_modules], [yes], [TEST_MODULES=yes], [TEST_MODULES=no]) ], [TEST_MODULES=yes]) AC_MSG_RESULT([$TEST_MODULES]) +dnl Some test modules can only be built as shared libraries (see +dnl Modules/Setup.stdlib.in), which a non-framework iOS build cannot do. +AS_IF([test "$ac_sys_system" = "iOS" && test -z "$PYTHONFRAMEWORK"], + [AS_IF([test "$TEST_MODULES" != "no"], + [AC_MSG_ERROR([iOS non-framework builds must use --disable-test-modules])])]) AC_SUBST([TEST_MODULES]) # Check for --with-build-details-suffix @@ -8408,6 +8412,12 @@ AS_CASE([$host_cpu], ) AC_SUBST([MODULE_BUILDTYPE]) +dnl A non-framework iOS build has no framework for a shared extension module +dnl to link against, so every extension module must be built into libpython. +AS_IF([test "$ac_sys_system" = "iOS" && test -z "$PYTHONFRAMEWORK"], + [AS_IF([test "$MODULE_BUILDTYPE" != "static"], + [AC_MSG_ERROR([iOS non-framework builds must use MODULE_BUILDTYPE=static])])]) + dnl _MODULE_BLOCK_ADD([VAR], [VALUE]) dnl internal: adds $1=quote($2) to MODULE_BLOCK AC_DEFUN([_MODULE_BLOCK_ADD], [AS_VAR_APPEND([MODULE_BLOCK], ["$1=_AS_QUOTE([$2])$as_nl"])]) From 024b6bceb1b1b3c52ccaa431f154d28d94b0bef5 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Tue, 8 Sep 2026 06:22:45 -0400 Subject: [PATCH 6/6] gh-156774: Speed up pdb startup with asyncio guard (#156775) --- Lib/pdb.py | 7 ++++++- .../Library/2026-08-31-11-40-00.gh-issue-156774.Pd8Lz1.rst | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-31-11-40-00.gh-issue-156774.Pd8Lz1.rst diff --git a/Lib/pdb.py b/Lib/pdb.py index 458eb8352652366..c3440a5d11a588a 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -84,7 +84,6 @@ import signal import socket import typing -import asyncio import inspect import weakref import builtins @@ -102,6 +101,8 @@ from types import CodeType from warnings import deprecated +lazy import asyncio + try: import _pyrepl.utils except ModuleNotFoundError: @@ -902,6 +903,10 @@ def _hold_exceptions(self, exceptions): self._chained_exception_index = 0 def _get_asyncio_task(self): + # If asyncio has never been imported there cannot be a running task, + # so skip the import rather than pay for it on every interaction. + if 'asyncio' not in sys.modules: + return None try: task = asyncio.current_task() except RuntimeError: diff --git a/Misc/NEWS.d/next/Library/2026-08-31-11-40-00.gh-issue-156774.Pd8Lz1.rst b/Misc/NEWS.d/next/Library/2026-08-31-11-40-00.gh-issue-156774.Pd8Lz1.rst new file mode 100644 index 000000000000000..f5f64c494296ccf --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-31-11-40-00.gh-issue-156774.Pd8Lz1.rst @@ -0,0 +1,2 @@ +Speed up :mod:`pdb` startup by only importing :mod:`asyncio` when the program +being debugged has already imported it.