From 91a3eecf1be92e7fd3b32718221727b140e35443 Mon Sep 17 00:00:00 2001 From: Adrian Ratiu Date: Sat, 11 Jul 2026 12:13:49 +0300 Subject: [PATCH 1/4] libc-test: Detect the targeted Android API level from the toolchain Add "android" to the Versions platform-version detection. Knowing it allows version-gating the Android test skips instead of hardcoding them. The level can appear in two macros: 1. Old clang defines __ANDROID_API__ directly as an integer. 2. Modern clang defines it as an alias of __ANDROID_MIN_SDK_VERSION__. A toolchain whose target triple carries no API level defines neither number, so nothing is parsed and the level stays undetected. An undetected level (android == None) causes its consumers to treat it conservatively: every API-gated skip stays active, as if the toolchain targeted the oldest level possible. Nothing consumes the value yet, so it has no effect on what is tested. Next commits will add users for it. Suggested-by: Trevor Gross Assisted-By: Claude Fable 5 Signed-off-by: Adrian Ratiu --- libc-test/build.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) mode change 100644 => 100755 libc-test/build.rs diff --git a/libc-test/build.rs b/libc-test/build.rs old mode 100644 new mode 100755 index 61b71b847582..3934895258be --- a/libc-test/build.rs +++ b/libc-test/build.rs @@ -6404,6 +6404,8 @@ struct Versions { macos: Option<(u32, u32)>, emscripten: Option<(u32, u32)>, wasi_sdk: Option<(u32, WasiVersion)>, + /// Android API level (no minor version). + android: Option, } #[derive(Clone, Copy, Debug, Default)] @@ -6432,6 +6434,13 @@ impl Versions { #include "gnu/libc-version.h" #endif + #ifdef __ANDROID__ + /* The clang driver predefines __ANDROID_MIN_SDK_VERSION__ from the API level + * in the target triple; including api-level.h ensures it is defined even on + * toolchains that leave it to the header. */ + #include "android/api-level.h" + #endif + #if defined(__FreeBSD__) \ || defined(__NetBSD__) \ || defined(__OpenBSD__) \ @@ -6501,6 +6510,11 @@ impl Versions { } "__GLIBC__" => ret.glibc.get_or_insert_default().0 = value.parse().unwrap(), "__GLIBC_MINOR__" => ret.glibc.get_or_insert_default().1 = value.parse().unwrap(), + // Clang 12+ predefines the target API level here as a plain integer. + // Every NDK wrapper we build with carries an API level, so parse it like + // the other version macros above and let a missing value fail loudly at + // the unwrap in `test_android` rather than silently skipping tests. + "__ANDROID_MIN_SDK_VERSION__" => ret.android = Some(value.parse().unwrap()), "__MAC_OS_X_VERSION_MAX_ALLOWED" => { let caps = mac_re.captures(value).unwrap(); let major: u32 = caps[1].parse().unwrap(); From 0c6053df4e6ed8a1d2171f2fbed80573bbfc5439 Mon Sep 17 00:00:00 2001 From: Adrian Ratiu Date: Sat, 11 Jul 2026 11:07:27 +0300 Subject: [PATCH 2/4] libc-test: Version-gate the Android API level skips Convert the existing hardcoded "added in API level N" test skips into gates on the detected API level added in the previous commit. Items stay skipped when the toolchain builds below their introduction level (or when no level was detected) and get tested once the toolchain provably targets a high enough level. There are no test coverage changes. Assisted-By: Claude Fable 5 Signed-off-by: Adrian Ratiu --- libc-test/build.rs | 51 +++++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/libc-test/build.rs b/libc-test/build.rs index 3934895258be..5bb823be861f 100755 --- a/libc-test/build.rs +++ b/libc-test/build.rs @@ -2060,6 +2060,10 @@ fn test_android(target: &str) { }; let x86 = target.contains("i686") || target.contains("x86_64"); let aarch64 = target.contains("aarch64"); + // The API level the NDK toolchain targets, which caps the available libc + // API surface (see `Versions`). Detection is required, so unwrap and let a + // toolchain we can't read fail loudly rather than silently skip every test. + let android = VERSIONS.android.unwrap(); let mut cfg = ctest_cfg(); cfg.define("_GNU_SOURCE", None); @@ -2251,8 +2255,7 @@ fn test_android(target: &str) { "posix_spawn_file_actions_t" => true, "posix_spawnattr_t" => true, - // Added in API level 24 - "if_nameindex" => true, + "if_nameindex" if android < 24 => true, _ => false, } @@ -2450,8 +2453,11 @@ fn test_android(target: &str) { "reallocarray" => true, "__system_property_wait" => true, - // Added in API level 30, but tests use level 28. - "memfd_create" | "mlock2" | "renameat2" | "statx" | "statx_timestamp" => true, + "memfd_create" | "mlock2" | "renameat2" | "statx" | "statx_timestamp" + if android < 30 => + { + true + } // Added in API level 33, but tests use level 28. "preadv2" | "pwritev2" => true, @@ -2459,34 +2465,23 @@ fn test_android(target: &str) { // Added in glibc 2.25. "getentropy" => true, - // Added in API level 28, but some tests use level 24. - "getrandom" => true, - - // Added in API level 28, but some tests use level 24. - "syncfs" => true, - - // Added in API level 28, but some tests use level 24. - "pthread_attr_getinheritsched" | "pthread_attr_setinheritsched" => true, - // Added in API level 28, but some tests use level 24. - "fread_unlocked" | "fwrite_unlocked" | "fgets_unlocked" | "fflush_unlocked" => true, - - // Added in API level 28, but some tests use level 24. - "aligned_alloc" => true, + "getrandom" | "syncfs" | "aligned_alloc" if android < 28 => true, - // Added in API level 26, but some tests use level 24. - "getgrent" => true, + "pthread_attr_getinheritsched" | "pthread_attr_setinheritsched" if android < 28 => true, - // Added in API level 26, but some tests use level 24. - "setgrent" => true, - - // Added in API level 26, but some tests use level 24. - "endgrent" => true, + "fread_unlocked" | "fwrite_unlocked" | "fgets_unlocked" | "fflush_unlocked" + if android < 28 => + { + true + } - // Added in API level 26, but some tests use level 24. - "getpwent" | "setpwent" | "endpwent" => true, + "getgrent" | "setgrent" | "endgrent" | "getpwent" | "setpwent" | "endpwent" + if android < 26 => + { + true + } - // Added in API level 26, but some tests use level 24. - "getdomainname" | "setdomainname" => true, + "getdomainname" | "setdomainname" if android < 26 => true, // FIXME(android): bad function pointers: "isalnum" | "isalpha" | "iscntrl" | "isdigit" | "isgraph" | "islower" | "isprint" From d5296f8725d882c61a3dc06edcdb3118d75d28ab Mon Sep 17 00:00:00 2001 From: Adrian Ratiu Date: Sat, 11 Jul 2026 11:07:56 +0300 Subject: [PATCH 3/4] libc-test: Skip termios function pointer checks below Android API 28 Bionic implements the termios API as static inline functions in until API 28 turns them into real libc.so symbols, so below that level the C-side function address is the header's inline, never matching the symbol Rust links against. Skip the function pointer identity check (the only check ctest generates for a foreign function) for the termios family below API 28. This surfaced when pinning the C test compilation to a real API level (see next commit). At level 24, e.g. on arm, all thirteen termios pointer comparisons fail. It is ordered before the pin commit to keep commits bisectable. Assisted-By: Claude Fable 5 Signed-off-by: Adrian Ratiu --- libc-test/build.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/libc-test/build.rs b/libc-test/build.rs index 5bb823be861f..b13a8369849c 100755 --- a/libc-test/build.rs +++ b/libc-test/build.rs @@ -2499,6 +2499,21 @@ fn test_android(target: &str) { } }); + cfg.skip_fn_ptrcheck(move |func| { + match func { + // termios functions are inlines below API 28, so + // their C-side address never matches the symbol Rust links. + "tcdrain" | "tcflow" | "tcflush" | "tcgetattr" | "tcgetsid" | "tcsendbreak" + | "tcsetattr" | "cfgetispeed" | "cfgetospeed" | "cfmakeraw" | "cfsetispeed" + | "cfsetospeed" | "cfsetspeed" + if android < 28 => + { + true + } + _ => false, + } + }); + cfg.skip_struct_field_type(move |struct_, field| { match (struct_.ident(), field.ident()) { // This is a weird union, don't check the type. From 069f0333c65df5e88b1f87f591738b3b059b6fec Mon Sep 17 00:00:00 2001 From: Adrian Ratiu Date: Mon, 13 Jul 2026 16:45:47 +0300 Subject: [PATCH 4/4] ci: Point each Android toolchain at the API level we target Each CC wrapper's name (e.g. x86_64-linux-android28-clang) tells clang which API to target: clang infers "-target" from the invoked binary name whenever none is given explicitly. Ours said 28 everywhere, which only reflects the NDK's link-time stub level, not the level of the bionic that actually runs the tests. Every job now targets API 24 (the level the arm and aarch64 emulators run and the level the retired x86_64 sysimage ran), so the dynamic skips resolve exactly as the old hardcoded ones did and coverage is unchanged. Each Dockerfile declares that level once, in an ANDROID_API build arg. For the arm and aarch64 emulator jobs the arg also selects the system image android-install-sdk.sh installs, so the wrapper cannot drift from the image the tests run on. This mismatch was invisible before because the tests were hardcoded to skip higher API levels regardless of the toolchain: it surfaced after extracting the real API level from the toolchain and wiring in the dynamic skip mechanism (previous commits) instead of hardcoding. Assisted-By: Claude Fable 5 Signed-off-by: Adrian Ratiu --- ci/android-install-sdk.sh | 20 +++++++++++++------- ci/docker/aarch64-linux-android/Dockerfile | 10 +++++++--- ci/docker/arm-linux-androideabi/Dockerfile | 10 +++++++--- ci/docker/x86_64-linux-android/Dockerfile | 10 +++++++--- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/ci/android-install-sdk.sh b/ci/android-install-sdk.sh index 43e1153dcd33..fa1e021ede5b 100755 --- a/ci/android-install-sdk.sh +++ b/ci/android-install-sdk.sh @@ -14,25 +14,31 @@ mkdir -p sdk/cmdline-tools wget -q --tries=20 "https://dl.google.com/android/repository/commandlinetools-linux-${sdk}_latest.zip" unzip -q -d sdk/cmdline-tools "commandlinetools-linux-${sdk}_latest.zip" -case "$1" in +# The API level is passed in (arg 2) so it stays a single source of truth +# shared with the CC wrapper name in the Dockerfile; only the image variant +# is arch-specific here. +arch="$1" +api="$2" +if [ -z "${api}" ]; then + echo "usage: $0 " + exit 1 +fi + +case "${arch}" in arm | armv7) - api=24 image="system-images;android-${api};default;armeabi-v7a" ;; aarch64) - api=24 image="system-images;android-${api};google_apis;arm64-v8a" ;; i686) - api=28 image="system-images;android-${api};default;x86" ;; x86_64) - api=28 image="system-images;android-${api};default;x86_64" ;; *) - echo "invalid arch: $1" + echo "invalid arch: ${arch}" exit 1 ;; esac @@ -66,7 +72,7 @@ cp /android/android-emulator-package.xml /android/sdk/emulator/package.xml echo "no" | ./sdk/cmdline-tools/tools/bin/avdmanager create avd \ - --name "${1}" \ + --name "${arch}" \ --package "${image}" | grep -v = || true rm -rf "commandlinetools-linux-${sdk}_latest.zip" emulator-linux_x64-9058569.zip diff --git a/ci/docker/aarch64-linux-android/Dockerfile b/ci/docker/aarch64-linux-android/Dockerfile index 91c074150f94..5d0ee0f9c1e4 100644 --- a/ci/docker/aarch64-linux-android/Dockerfile +++ b/ci/docker/aarch64-linux-android/Dockerfile @@ -18,18 +18,22 @@ WORKDIR /android/ COPY android* /android/ ENV ANDROID_ARCH=aarch64 +# Single source of truth for the API level: it selects the emulator system +# image (passed to android-install-sdk.sh) and names the CC wrapper below, so +# the toolchain can never drift from the image the tests run on. +ARG ANDROID_API=24 ENV PATH=$PATH:/android/linux-x86_64/bin:/android/sdk/cmdline-tools/tools:/android/sdk/platform-tools RUN /android/android-install-ndk.sh -RUN /android/android-install-sdk.sh $ANDROID_ARCH +RUN /android/android-install-sdk.sh $ANDROID_ARCH $ANDROID_API RUN mv /root/.android /tmp RUN chmod 777 -R /tmp/.android RUN chmod 755 /android/sdk/cmdline-tools/tools/* /android/sdk/emulator/qemu/linux-x86_64/* ENV PATH=$PATH:/rust/bin \ - CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android28-clang \ + CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android${ANDROID_API}-clang \ CARGO_TARGET_AARCH64_LINUX_ANDROID_RUNNER=/tmp/runtest \ - CC_aarch64_linux_android=aarch64-linux-android28-clang \ + CC_aarch64_linux_android=aarch64-linux-android${ANDROID_API}-clang \ AR_aarch64_linux_android=llvm-ar \ HOME=/tmp diff --git a/ci/docker/arm-linux-androideabi/Dockerfile b/ci/docker/arm-linux-androideabi/Dockerfile index 657ff81097c7..3317d53cbfca 100644 --- a/ci/docker/arm-linux-androideabi/Dockerfile +++ b/ci/docker/arm-linux-androideabi/Dockerfile @@ -18,18 +18,22 @@ WORKDIR /android/ COPY android* /android/ ENV ANDROID_ARCH=arm +# Single source of truth for the API level: it selects the emulator system +# image (passed to android-install-sdk.sh) and names the CC wrapper below, so +# the toolchain can never drift from the image the tests run on. +ARG ANDROID_API=24 ENV PATH=$PATH:/android/linux-x86_64/bin:/android/sdk/cmdline-tools/tools:/android/sdk/platform-tools RUN /android/android-install-ndk.sh -RUN /android/android-install-sdk.sh $ANDROID_ARCH +RUN /android/android-install-sdk.sh $ANDROID_ARCH $ANDROID_API RUN mv /root/.android /tmp RUN chmod 777 -R /tmp/.android RUN chmod 755 /android/sdk/cmdline-tools/tools/* /android/sdk/emulator/qemu/linux-x86_64/* ENV PATH=$PATH:/rust/bin \ - CARGO_TARGET_ARM_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi28-clang \ + CARGO_TARGET_ARM_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi${ANDROID_API}-clang \ CARGO_TARGET_ARM_LINUX_ANDROIDEABI_RUNNER=/tmp/runtest \ - CC_arm_linux_androideabi=armv7a-linux-androideabi28-clang \ + CC_arm_linux_androideabi=armv7a-linux-androideabi${ANDROID_API}-clang \ AR_arm_linux_androideabi=llvm-ar \ HOME=/tmp diff --git a/ci/docker/x86_64-linux-android/Dockerfile b/ci/docker/x86_64-linux-android/Dockerfile index 62eee968057b..4b72b8c382fe 100644 --- a/ci/docker/x86_64-linux-android/Dockerfile +++ b/ci/docker/x86_64-linux-android/Dockerfile @@ -21,15 +21,19 @@ RUN curl -fsSL https://us-apt.pkg.dev/doc/repo-signing-key.gpg \ WORKDIR /android/ ENV ANDROID_ARCH=x86_64 +# The API level we compile against: this only names the CC wrapper and the toolchain we use. +# The Android Cuttlefish device is pinned separately and is newer, i.e. capable of testing +# newer APIs. The toolchain wrapper is held at the same baseline as arm/aarch64 for now. +ARG ANDROID_API=24 COPY android-install-ndk.sh /android/ RUN /android/android-install-ndk.sh ENV PATH=$PATH:/rust/bin:/android/linux-x86_64/bin \ ANDROID_SERIAL=127.0.0.1:6520 \ - CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=x86_64-linux-android28-clang \ + CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=x86_64-linux-android${ANDROID_API}-clang \ CARGO_TARGET_X86_64_LINUX_ANDROID_RUNNER=/tmp/runtest \ - CC_x86_64_linux_android=x86_64-linux-android28-clang \ - CXX_x86_64_linux_android=x86_64-linux-android28-clang++ \ + CC_x86_64_linux_android=x86_64-linux-android${ANDROID_API}-clang \ + CXX_x86_64_linux_android=x86_64-linux-android${ANDROID_API}-clang++ \ AR_x86_64_linux_android=llvm-ar \ HOME=/tmp