From 51ae3c0ee585e2bc56d0a58c619eefa1b4b4f929 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Thu, 30 Jul 2026 03:04:46 +0100 Subject: [PATCH 1/2] Make `os.get_terminal_size` check `isatty` before calling `ioctl` (#154885) Calling ioctl on stdout raises warnings on Android. Ensure we have a TTY before doing terminal size calls. --- Lib/test/test_os/test_os.py | 7 +------ .../Library/2026-07-29-16-53-50.gh-issue-154885.ptofmI.rst | 2 ++ Modules/posixmodule.c | 7 +++++++ 3 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-29-16-53-50.gh-issue-154885.ptofmI.rst diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 3e5ad52c4ab130d..328a0dbeb99f8fa 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -3970,12 +3970,7 @@ def test_does_not_crash(self): try: size = os.get_terminal_size() except OSError as e: - known_errnos = [errno.EINVAL, errno.ENOTTY] - if sys.platform == "android": - # The Android testbed redirects the native stdout to a pipe, - # which returns a different error code. - known_errnos.append(errno.EACCES) - if sys.platform == "win32" or e.errno in known_errnos: + if sys.platform == "win32" or e.errno in (errno.EINVAL, errno.ENOTTY): # Under win32 a generic OSError can be thrown if the # handle cannot be retrieved self.skipTest("failed to query terminal size") diff --git a/Misc/NEWS.d/next/Library/2026-07-29-16-53-50.gh-issue-154885.ptofmI.rst b/Misc/NEWS.d/next/Library/2026-07-29-16-53-50.gh-issue-154885.ptofmI.rst new file mode 100644 index 000000000000000..7c3839ad1a59e4f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-29-16-53-50.gh-issue-154885.ptofmI.rst @@ -0,0 +1,2 @@ +:func:`os.get_terminal_size` now checks ``isatty`` before calling ``ioctl``, +which reduces log noise on Android. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index f754d0e18b5fb09..c34e3fc5eb600df 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -15971,6 +15971,13 @@ os_get_terminal_size_impl(PyObject *module, int fd) #ifdef TERMSIZE_USE_IOCTL { + // On Android, stdout is probably not connected, and calling TIOCGWINSZ + // on an invalid file descriptor causes a log message "avc: denied { + // ioctl }". Some common tools such as pytest call get_terminal_size + // very often, so check it's a TTY first to avoid cluttering the log. + if (!isatty(fd)) + return PyErr_SetFromErrno(PyExc_OSError); + struct winsize w; if (ioctl(fd, TIOCGWINSZ, &w)) return PyErr_SetFromErrno(PyExc_OSError); From f4b1d3e891d0d2055e53df3ccb84030bcaa148b5 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Thu, 30 Jul 2026 03:31:40 +0100 Subject: [PATCH 2/2] Minor fixes for Android (#154895) A collection of small cleanups for Android support: * Clarifies the documentation around version number handling for iOS and Android in os.uname and platform.release * Ensures that automated NDK installs surface messages written to stderr * Makes the Android NDK check more robust for incomplete downloads * Corrects some linting errors in Android build scripts --- Doc/library/os.rst | 4 ++-- Doc/library/platform.rst | 5 ++--- Platforms/Android/__main__.py | 5 +++-- Platforms/Android/android-env.sh | 4 ++-- .../app/src/main/java/org/python/testbed/MainActivity.kt | 9 ++++----- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Doc/library/os.rst b/Doc/library/os.rst index be7ea26356bebe3..7bdf415db655f32 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -802,9 +802,9 @@ process and user. Returns information identifying the current operating system. The return value is a :class:`uname_result`. - On macOS, iOS and Android, this returns the *kernel* name and version (i.e., + On macOS, iOS and Android, this returns the *kernel* name and release (i.e., ``'Darwin'`` on macOS and iOS; ``'Linux'`` on Android). :func:`platform.uname` - can be used to get the user-facing operating system name and version on iOS and + can be used to get the user-facing operating system name and release on iOS and Android. .. seealso:: diff --git a/Doc/library/platform.rst b/Doc/library/platform.rst index 1d30966794fd1bf..f728a7b32e789f4 100644 --- a/Doc/library/platform.rst +++ b/Doc/library/platform.rst @@ -141,6 +141,8 @@ Cross platform Returns the system's release, e.g. ``'2.2.0'`` or ``'NT'``. An empty string is returned if the value cannot be determined. + On iOS and Android, this is the user-facing OS release. To obtain the + Darwin or Linux kernel release, use :func:`os.uname`. .. function:: system() @@ -163,9 +165,6 @@ Cross platform Returns the system's release version, e.g. ``'#3 on degas'``. An empty string is returned if the value cannot be determined. - On iOS and Android, this is the user-facing OS version. To obtain the - Darwin or Linux kernel version, use :func:`os.uname`. - .. function:: uname() Fairly portable uname interface. Returns a :func:`~collections.namedtuple` diff --git a/Platforms/Android/__main__.py b/Platforms/Android/__main__.py index 78f94b317ab0478..705ca6f26221fda 100755 --- a/Platforms/Android/__main__.py +++ b/Platforms/Android/__main__.py @@ -158,7 +158,7 @@ def android_env(host): f"PREFIX={prefix}; " f". {ENV_SCRIPT}; " f"export", - check=True, shell=True, capture_output=True, encoding='utf-8', + check=True, shell=True, stdout=subprocess.PIPE, encoding='utf-8', ).stdout env = {} @@ -625,7 +625,8 @@ async def read_int(size): except ValueError: priority = LogPriority.UNKNOWN - payload_fields = (await read_bytes(payload_len - 1)).split(b"\0") + payload = await read_bytes(payload_len - 1) + payload_fields = payload.split(b"\0") if len(payload_fields) < 2: raise ValueError( f"payload {payload!r} does not contain at least 2 " diff --git a/Platforms/Android/android-env.sh b/Platforms/Android/android-env.sh index 5859c0eac4a88fb..59ce2eeb7d62244 100644 --- a/Platforms/Android/android-env.sh +++ b/Platforms/Android/android-env.sh @@ -7,7 +7,7 @@ : "${PREFIX:-}" # Path in which to find required libraries -# Print all messages on stderr so they're visible when running within build-wheel. +# Print all messages on stderr so they're visible when stdout is captured. log() { echo "$1" >&2 } @@ -27,7 +27,7 @@ fail() { ndk_version=27.3.13750724 ndk=$ANDROID_HOME/ndk/$ndk_version -if ! [ -e "$ndk" ]; then +if ! [ -e "$ndk/package.xml" ]; then log "Installing NDK - this may take several minutes" yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "ndk;$ndk_version" fi diff --git a/Platforms/Android/testbed/app/src/main/java/org/python/testbed/MainActivity.kt b/Platforms/Android/testbed/app/src/main/java/org/python/testbed/MainActivity.kt index dc49cdb9a9f7395..c8fe3acd849ac7a 100644 --- a/Platforms/Android/testbed/app/src/main/java/org/python/testbed/MainActivity.kt +++ b/Platforms/Android/testbed/app/src/main/java/org/python/testbed/MainActivity.kt @@ -28,12 +28,11 @@ class PythonTestRunner(val context: Context) { * @param args Python command-line, encoded as JSON. * @return The Python exit status: zero on success, nonzero on failure. */ fun run(args: String) : Int { - // We leave argument 0 as an empty string, which is a placeholder for the - // executable name in embedded mode. + // Argument 0 is a placeholder for the executable name in embedded mode. val argsJsonArray = JSONArray(args) - val argsStringArray = Array(argsJsonArray.length() + 1) { it -> ""} - for (i in 0..(argsJsonArray.length() + 1) { i -> + if (i == 0) "" + else argsJsonArray.getString(i - 1) } // Python needs this variable to help it find the temporary directory,