From 2cf3cc0f2fdb55e0479e47226e255b52065026cf Mon Sep 17 00:00:00 2001 From: Adrian Ratiu Date: Sat, 11 Jul 2026 12:13:49 +0300 Subject: [PATCH 1/6] 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 | 20 ++++++++++++++++++++ 1 file changed, 20 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 2cf2eba99dda..f22037e87257 --- a/libc-test/build.rs +++ b/libc-test/build.rs @@ -6376,6 +6376,10 @@ struct Versions { netbsd: Option<(u32, u32)>, macos: Option<(u32, u32)>, emscripten: Option<(u32, u32)>, + /// Android API level. Unlike the (major, minor) platform versions + /// above, Android versions its libc ABI with a single increasing + /// integer (e.g. 28). There is no minor component. + android: Option, } impl Versions { @@ -6394,6 +6398,13 @@ impl Versions { #include "gnu/libc-version.h" #endif + #ifdef __ANDROID__ + /* The clang driver predefines __ANDROID_API__ and __ANDROID_MIN_SDK_VERSION__ + * from the API level in the target triple. When that is missing, this header + * supplies the __ANDROID_API_FUTURE__ fallback definition. */ + #include "android/api-level.h" + #endif + #if defined(__FreeBSD__) \ || defined(__NetBSD__) \ || defined(__OpenBSD__) \ @@ -6458,6 +6469,15 @@ 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(), + // The API level is in __ANDROID_API__ (old clang) or __ANDROID_MIN_SDK_VERSION__ + // (modern clang), so take a number from either. When the API is undetected (e.g. + // the triple carries no API level), its consumers fall back to skipping every + // API-gated test as if the toolchain targeted the oldest level possible. + "__ANDROID_API__" | "__ANDROID_MIN_SDK_VERSION__" => { + if let Ok(level) = value.parse() { + ret.android = Some(level); + } + } "__MAC_OS_X_VERSION_MAX_ALLOWED" => { let caps = mac_re.captures(value).unwrap(); let major: u32 = caps[1].parse().unwrap(); From 2ae062e0995193474cc496e69f827134578c4d84 Mon Sep 17 00:00:00 2001 From: Adrian Ratiu Date: Sat, 11 Jul 2026 11:07:27 +0300 Subject: [PATCH 2/6] 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 | 50 +++++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/libc-test/build.rs b/libc-test/build.rs index f22037e87257..e0cd6e799b5d 100755 --- a/libc-test/build.rs +++ b/libc-test/build.rs @@ -2045,6 +2045,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`). Items introduced later must be skipped when + // building below their level and also when no level could be detected. + let skip_below_api = |level: u32| VERSIONS.android.map_or(true, |api| api < level); let mut cfg = ctest_cfg(); cfg.define("_GNU_SOURCE", None); @@ -2237,7 +2241,7 @@ fn test_android(target: &str) { "posix_spawnattr_t" => true, // Added in API level 24 - "if_nameindex" => true, + "if_nameindex" => skip_below_api(24), _ => false, } @@ -2429,40 +2433,32 @@ 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, + // Added in API level 30. + "memfd_create" | "mlock2" | "renameat2" | "statx" | "statx_timestamp" => { + skip_below_api(30) + } // 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, + // Added in API level 28. + "getrandom" | "syncfs" | "aligned_alloc" => skip_below_api(28), - // Added in API level 26, but some tests use level 24. - "getgrent" => true, + // Added in API level 28. + "pthread_attr_getinheritsched" | "pthread_attr_setinheritsched" => skip_below_api(28), - // 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, + // Added in API level 28. + "fread_unlocked" | "fwrite_unlocked" | "fgets_unlocked" | "fflush_unlocked" => { + skip_below_api(28) + } - // Added in API level 26, but some tests use level 24. - "getpwent" | "setpwent" | "endpwent" => true, + // Added in API level 26. + "getgrent" | "setgrent" | "endgrent" | "getpwent" | "setpwent" | "endpwent" => { + skip_below_api(26) + } - // Added in API level 26, but some tests use level 24. - "getdomainname" | "setdomainname" => true, + // Added in API level 26. + "getdomainname" | "setdomainname" => skip_below_api(26), // FIXME(android): bad function pointers: "isalnum" | "isalpha" | "iscntrl" | "isdigit" | "isgraph" | "islower" | "isprint" From 9d3d6fc1276272572a614ee1b392a6ed689c3aeb Mon Sep 17 00:00:00 2001 From: Adrian Ratiu Date: Sat, 11 Jul 2026 11:07:56 +0300 Subject: [PATCH 3/6] 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 | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/libc-test/build.rs b/libc-test/build.rs index e0cd6e799b5d..e1cdd9dde76f 100755 --- a/libc-test/build.rs +++ b/libc-test/build.rs @@ -2476,6 +2476,17 @@ 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" => skip_below_api(28), + _ => 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 cc8308135961ef9d7394abf666654f5458047cd2 Mon Sep 17 00:00:00 2001 From: Adrian Ratiu Date: Mon, 13 Jul 2026 16:45:47 +0300 Subject: [PATCH 4/6] ci: Point each Android toolchain at the API level bionic actually runs 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: arm emulators and the x86_64 system image android-sysimage.sh installs (from x86_64-24_r07.zip) are all API 24. 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. With the toolchain matching the runtime, detection resolves and the new gated skips activate. Raising an image's runtime later only needs its own wrapper name bumped. Assisted-By: Claude Fable 5 Signed-off-by: Adrian Ratiu --- ci/docker/aarch64-linux-android/Dockerfile | 4 ++-- ci/docker/arm-linux-androideabi/Dockerfile | 4 ++-- ci/docker/x86_64-linux-android/Dockerfile | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ci/docker/aarch64-linux-android/Dockerfile b/ci/docker/aarch64-linux-android/Dockerfile index 91c074150f94..420e594108c5 100644 --- a/ci/docker/aarch64-linux-android/Dockerfile +++ b/ci/docker/aarch64-linux-android/Dockerfile @@ -27,9 +27,9 @@ 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-android24-clang \ CARGO_TARGET_AARCH64_LINUX_ANDROID_RUNNER=/tmp/runtest \ - CC_aarch64_linux_android=aarch64-linux-android28-clang \ + CC_aarch64_linux_android=aarch64-linux-android24-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..90124e093fcf 100644 --- a/ci/docker/arm-linux-androideabi/Dockerfile +++ b/ci/docker/arm-linux-androideabi/Dockerfile @@ -27,9 +27,9 @@ 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-androideabi24-clang \ CARGO_TARGET_ARM_LINUX_ANDROIDEABI_RUNNER=/tmp/runtest \ - CC_arm_linux_androideabi=armv7a-linux-androideabi28-clang \ + CC_arm_linux_androideabi=armv7a-linux-androideabi24-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 bc9580e6e1c0..80827d0bdb00 100644 --- a/ci/docker/x86_64-linux-android/Dockerfile +++ b/ci/docker/x86_64-linux-android/Dockerfile @@ -19,8 +19,8 @@ COPY android-sysimage.sh /android/ RUN /android/android-sysimage.sh x86_64 x86_64-24_r07.zip ENV PATH=$PATH:/rust/bin:/android/linux-x86_64/bin \ - CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=x86_64-linux-android28-clang \ - CC_x86_64_linux_android=x86_64-linux-android28-clang \ - CXX_x86_64_linux_android=x86_64-linux-android28-clang++ \ + CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=x86_64-linux-android24-clang \ + CC_x86_64_linux_android=x86_64-linux-android24-clang \ + CXX_x86_64_linux_android=x86_64-linux-android24-clang++ \ AR_x86_64_linux_android=llvm-ar \ HOME=/tmp From 21149ab9630c1417cba4624010e808796bbbb373 Mon Sep 17 00:00:00 2001 From: Adrian Ratiu Date: Fri, 10 Jul 2026 20:24:39 +0300 Subject: [PATCH 5/6] ci: Run x86_64-linux-android tests on a Cuttlefish virtual device The new ci/cuttlefish-setup.sh is heavily inspired by Mesa CI's cuttlefish-runner.sh, credited in the script. For more details, see the following issue, of which this is part of: https://github.com/rust-lang/libc/issues/5262 In a nutshell, the libc x86_64 job never booted Android: it extracted bionic from a 2016-era (API 24) image and ran the test binaries directly on the Ubuntu host kernel. This makes the CI test boot the official stock Android 17 image via Cuttlefish. The virtual device boots inside the test container, self-contained: Cuttlefish host packages are installed in the x86_64-linux-android image and the container entrypoint creates the tap networking via the deb's sysv script as root, then drops to an unprivileged user. The container needs only the virtualization device nodes plus CAP_NET_ADMIN, the same flags upstream uses for its own containerized Cuttlefish CI, so nothing on the CI host is mutated and a local run reduces to the usual ./ci/run-docker.sh with no host setup beyond KVM. The container still cross-compiles with the NDK and drives the device over adb, reusing the runtest-android.rs wrapper unchanged. Device logs are uploaded as CI artifacts (again similar to Mesa). The other Android targets stay on the legacy SDK emulator for now. Assisted-By: Claude Fable 5 Signed-off-by: Adrian Ratiu --- .github/workflows/ci.yaml | 3 + ci/README.md | 8 ++- ci/android-sysimage.sh | 46 --------------- ci/create-artifacts.py | 19 +++++-- ci/cuttlefish-setup.sh | 45 +++++++++++++++ ci/docker/x86_64-linux-android/Dockerfile | 22 +++++-- .../cuttlefish-entrypoint.sh | 57 +++++++++++++++++++ ci/run-docker.sh | 30 ++++++++-- 8 files changed, 167 insertions(+), 63 deletions(-) delete mode 100755 ci/android-sysimage.sh create mode 100755 ci/cuttlefish-setup.sh create mode 100755 ci/docker/x86_64-linux-android/cuttlefish-entrypoint.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c0543beefb39..2656d082d174 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -236,6 +236,9 @@ jobs: - target: x86_64-apple-darwin os: macos-26-intel - target: x86_64-linux-android + env: { TEST_CUTTLEFISH: 1 } + # Keep in sync with the Android build pinned in ci/cuttlefish-setup.sh + artifact-tag: android17 # FIXME: Exec format error (os error 8) # - target: x86_64-unknown-linux-gnux32 - target: x86_64-unknown-linux-musl diff --git a/ci/README.md b/ci/README.md index 4e5b552d5889..54017ccc0d6e 100644 --- a/ci/README.md +++ b/ci/README.md @@ -31,8 +31,12 @@ builds are run on stable/beta/nightly, but are the only ones that do so. The remaining architectures look like: -* Android runs in a [docker image][android-docker] with an emulator, the NDK, - and the SDK already set up. The entire build happens within the docker image. +* Android x86_64 tests run on a [Cuttlefish](https://source.android.com/docs/devices/cuttlefish) + virtual device booted on the CI host by `cuttlefish-setup.sh`; the + [docker image][android-docker] cross-compiles the tests with the NDK and + drives the device over adb. The remaining Android targets run in docker + images with the legacy SDK emulator, the NDK, and the SDK already set up, + with the entire build happening within the docker image. * The MIPS, ARM, and AArch64 builds all use the QEMU userspace emulator to run the generated binary to actually verify the tests pass. * The MUSL build just has to download a MUSL compiler and target libraries and diff --git a/ci/android-sysimage.sh b/ci/android-sysimage.sh deleted file mode 100755 index 98484b3521f6..000000000000 --- a/ci/android-sysimage.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -eux - -URL=https://dl.google.com/android/repository/sys-img/android - -main() { - local arch="${1}" - local name="${2}" - local dest=/system - local td - td="$(mktemp -d)" - - apt-get install --no-install-recommends e2tools - - pushd "${td}" - wget -q --tries=5 "${URL}/${name}" - unzip -q "${name}" - - local system - system="$(find . -name system.img)" - mkdir -p ${dest}/{bin,lib,lib64} - - # Extract android linker and libraries to /system - # This allows android executables to be run directly (or with qemu) - if [ "${arch}" = "x86_64" ] || [ "${arch}" = "arm64" ]; then - e2cp -p "${system}:/bin/linker64" "${dest}/bin/" - e2cp -p "${system}:/lib64/libdl.so" "${dest}/lib64/" - e2cp -p "${system}:/lib64/libc.so" "${dest}/lib64/" - e2cp -p "${system}:/lib64/libm.so" "${dest}/lib64/" - else - e2cp -p "${system}:/bin/linker" "${dest}/bin/" - e2cp -p "${system}:/lib/libdl.so" "${dest}/lib/" - e2cp -p "${system}:/lib/libc.so" "${dest}/lib/" - e2cp -p "${system}:/lib/libm.so" "${dest}/lib/" - fi - - # clean up - apt-get purge --auto-remove -y e2tools - - popd - - rm -rf "${td}" -} - -main "${@}" diff --git a/ci/create-artifacts.py b/ci/create-artifacts.py index 4145e20491e2..2104b9cac383 100755 --- a/ci/create-artifacts.py +++ b/ci/create-artifacts.py @@ -32,11 +32,20 @@ def main(): archive_name = f"archive-{now}" archive_path = f"{archive_name}.tar.gz" - sp.run( - ["tar", "czvf", archive_path, "-C", build_dir, "-T-"], - input=file_list, - check=True, - ) + # --dereference because kernel.log is a symlink into logs/ + tar_cmd = ["tar", "czvf", archive_path, "--dereference", "-C", build_dir, "-T-"] + + # Grab the runtime logs of the Cuttlefish device launched by ci/cuttlefish-setup.sh. + # The device runs inside the test container, which keeps it under the bind-mounted + # target/ so the logs are visible here. + cf_dir = Path(os.getenv("CUTTLEFISH_DIR", "target/cuttlefish")).resolve() + cf_runtime = cf_dir / "cuttlefish" / "instances" / "cvd-1" + cf_logs = ["logs/logcat", "logs/launcher.log", "kernel.log"] + cf_found = [log for log in cf_logs if cf_runtime.joinpath(log).exists()] + if cf_found: + tar_cmd += ["-C", str(cf_runtime), *cf_found] + + sp.run(tar_cmd, input=file_list, check=True) # If we are in GHA, set these env vars for future use gh_env = os.getenv("GITHUB_ENV") diff --git a/ci/cuttlefish-setup.sh b/ci/cuttlefish-setup.sh new file mode 100755 index 000000000000..14bb782d4088 --- /dev/null +++ b/ci/cuttlefish-setup.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +set -eux + +# Fetch and boot a Cuttlefish Android virtual device, heavily inspired by +# Mesa's CI: +# https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/.gitlab-ci/cuttlefish-runner.sh +# +# Runs unprivileged (cvd refuses to run as root) inside the test container, +# invoked by cuttlefish-entrypoint.sh after it has prepared the devices, +# networking and groups (see ci/docker/x86_64-linux-android/). +build="${CUTTLEFISH_BUILD:-15660610}" +target="${CUTTLEFISH_TARGET:-aosp_cf_x86_64_only_phone-userdebug}" +cf_dir="${CUTTLEFISH_DIR:-$HOME/cuttlefish}" + +mkdir -p "$cf_dir" +cvd fetch --default_build="${build}/${target}" --target_directory="$cf_dir" + +cd "$cf_dir" + +# -daemon exits only once the guest has fully booted. +# -enable_sandbox=false is needed because crosvm's minijail device sandbox +# (auto-enabled when /var/empty exists) requires unprivileged user +# namespaces, which Ubuntu 24.04+ blocks via AppArmor by default. +HOME="$cf_dir" timeout 10m ./bin/launch_cvd \ + -daemon \ + -enable_sandbox=false \ + -verbosity=INFO \ + -file_verbosity=DEBUG \ + -enable_audio=false \ + -enable_bootanimation=false \ + -enable_minimal_mode=true \ + -enable_modem_simulator=false \ + -enable_wifi=false \ + -report_anonymous_usage_stats=no \ + -cpus=2 \ + -memory_mb=4096 + +# Check the guest's adbd is reachable on the port the test runner uses, +# and log the Android version actually being tested. +HOME="$cf_dir" ./bin/adb connect 127.0.0.1:6520 +HOME="$cf_dir" ./bin/adb -s 127.0.0.1:6520 wait-for-device +HOME="$cf_dir" ./bin/adb devices +HOME="$cf_dir" ./bin/adb -s 127.0.0.1:6520 shell \ + getprop ro.build.version.release diff --git a/ci/docker/x86_64-linux-android/Dockerfile b/ci/docker/x86_64-linux-android/Dockerfile index 80827d0bdb00..1f1d93c6fe79 100644 --- a/ci/docker/x86_64-linux-android/Dockerfile +++ b/ci/docker/x86_64-linux-android/Dockerfile @@ -1,26 +1,38 @@ FROM ubuntu:26.04 RUN apt-get update && apt-get install -y --no-install-recommends \ + adb \ ca-certificates \ + curl \ gcc \ libc-dev \ python3 \ unzip \ wget +# Cuttlefish host packages: the virtual device the tests run on boots +# inside this container (see cuttlefish-entrypoint.sh for the docker run +# flags this needs). +RUN curl -fsSL https://us-apt.pkg.dev/doc/repo-signing-key.gpg \ + -o /etc/apt/trusted.gpg.d/artifact-registry.asc \ + && echo "deb https://us-apt.pkg.dev/projects/android-cuttlefish-artifacts android-cuttlefish main" \ + >/etc/apt/sources.list.d/artifact-registry.list \ + && apt-get update && apt-get install -y cuttlefish-base cuttlefish-user + WORKDIR /android/ ENV ANDROID_ARCH=x86_64 COPY android-install-ndk.sh /android/ RUN /android/android-install-ndk.sh -# We do not run x86_64-linux-android tests on an android emulator. -# See ci/android-sysimage.sh for information about how tests are run. -COPY android-sysimage.sh /android/ -RUN /android/android-sysimage.sh x86_64 x86_64-24_r07.zip - 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-android24-clang \ + CARGO_TARGET_X86_64_LINUX_ANDROID_RUNNER=/tmp/runtest \ CC_x86_64_linux_android=x86_64-linux-android24-clang \ CXX_x86_64_linux_android=x86_64-linux-android24-clang++ \ AR_x86_64_linux_android=llvm-ar \ HOME=/tmp + +ADD runtest-android.rs /tmp/runtest.rs +COPY cuttlefish-setup.sh docker/x86_64-linux-android/cuttlefish-entrypoint.sh /android/ +ENTRYPOINT ["/android/cuttlefish-entrypoint.sh"] diff --git a/ci/docker/x86_64-linux-android/cuttlefish-entrypoint.sh b/ci/docker/x86_64-linux-android/cuttlefish-entrypoint.sh new file mode 100755 index 000000000000..42c99ae8fa58 --- /dev/null +++ b/ci/docker/x86_64-linux-android/cuttlefish-entrypoint.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +set -eux + +# Boot a Cuttlefish virtual device inside this container, then run the +# tests against it as an unprivileged user (cvd refuses to run as root). +# +# The container needs only /dev/kvm, /dev/net/tun, /dev/vhost-net, +# /dev/vhost-vsock and CAP_NET_ADMIN, passed by ci/run-docker.sh. It's +# the same set upstream uses for its own containerized Cuttlefish CI: +# https://github.com/google/android-cuttlefish/blob/main/.github/workflows/presubmit.yaml + +# The device nodes come in with their host-side ownership: open them up +# for the unprivileged user (container-local, doesn't affect the host). +chmod 666 /dev/kvm /dev/net/tun /dev/vhost-net /dev/vhost-vsock + +# Create the bridges, taps and dnsmasq instances backing the device's +# virtio-net, inside this container's own network namespace. The deb ships +# a sysv script for this, usable without systemd, which is also how +# upstream's container image starts it. +service cuttlefish-host-resources start + +# cvd requires the current process to be in these groups. The groups are +# normally created by the deb postinst, -f covers any it didn't create. +for g in kvm cvdnetwork render; do + groupadd -f "$g" +done +# Match the invoking user's uid so writes to the bind-mounted target/ +# directory stay owned by the host user. +useradd -m -u "${HOST_UID:?}" -G kvm,cvdnetwork,render cf + +run_as_cf() { + runuser -u cf -- env PATH="$PATH" HOME=/tmp "$@" +} + +# Keep the device under target/ (bind-mounted from the host) so its logs +# survive the container for ci/create-artifacts.py to pick up. +export CUTTLEFISH_DIR=/checkout/target/cuttlefish + +run_as_cf /android/cuttlefish-setup.sh + +# The test runner (runtest-android.rs) drives the device with the +# container's adb via $ANDROID_SERIAL. Connect its server to the device +# (this may replace the server started by the fetched adb during boot). +run_as_cf adb connect "$ANDROID_SERIAL" +run_as_cf adb wait-for-device + +run_as_cf rustc /tmp/runtest.rs -o /tmp/runtest + +rc=0 +run_as_cf "$@" || rc=$? + +# Stop the device so its logs are flushed for ci/create-artifacts.py. +run_as_cf env HOME="$CUTTLEFISH_DIR" \ + "$CUTTLEFISH_DIR/bin/stop_cvd" -wait_for_launcher=40 || true + +exit "$rc" diff --git a/ci/run-docker.sh b/ci/run-docker.sh index 2266b2f2d269..699cd4099b3d 100755 --- a/ci/run-docker.sh +++ b/ci/run-docker.sh @@ -57,16 +57,37 @@ run() { docker build "${build_args[@]}" mkdir -p target - if [ -w /dev/kvm ]; then - kvm="--volume /dev/kvm:/dev/kvm" + + extra_args=() + if [ -n "${TEST_CUTTLEFISH:-}" ]; then + # Cuttlefish-based Android targets boot their virtual device inside + # the container (see cuttlefish-entrypoint.sh in the target's docker + # dir): pass the virtualization device nodes and let the entrypoint + # create its tap networking. It starts as root and drops to an + # unprivileged user matching HOST_UID for the device and the tests. + # Same flags as upstream's containerized Cuttlefish CI. + extra_args+=( + --device /dev/kvm + --device /dev/net/tun + --device /dev/vhost-net + --device /dev/vhost-vsock + --cap-add NET_ADMIN + --security-opt seccomp=unconfined + --env CUTTLEFISH_BUILD + --env CUTTLEFISH_TARGET + --env HOST_UID="$(id -u)" + ) else - kvm="" + extra_args+=(--user "$(id -u)":"$(id -g)") + if [ -w /dev/kvm ]; then + extra_args+=(--volume /dev/kvm:/dev/kvm) + fi fi docker run \ --rm \ - --user "$(id -u)":"$(id -g)" \ --env LIBC_BUILD_VERBOSE \ + "${extra_args[@]}" \ --env LIBC_CI \ --env LIBC_CI_ZBUILD_STD \ --env RUSTFLAGS \ @@ -80,7 +101,6 @@ run() { --volume "$(rustc --print sysroot)":/rust:ro,Z \ --volume "$PWD":/checkout:ro,Z \ --volume "$PWD"/target:/checkout/target \ - $kvm \ --init \ --workdir /checkout \ "libc-$target" \ From 6d7a779bedeeb3707b014c5894177ff4021059eb Mon Sep 17 00:00:00 2001 From: Adrian Ratiu Date: Tue, 7 Jul 2026 21:40:46 +0300 Subject: [PATCH 6/6] ci: Run aarch64-linux-android tests with qemu-user against current bionic Replace the 2016-era SDK emulator (slow full-system emulation of an Android 7 image, no KVM) with direct execution of the test binaries under qemu-user, linked against the bionic linker and libraries of a current Android build extracted into /system. Cuttlefish, which x86_64-linux-android now uses, is not an option because GitHub's arm64 runners don't support KVM yet and Cuttlefish can't run without KVM. qemu-user doesn't need KVM and avoids the emulator boot flakiness, so use it as a stopgap until GitHub esposes KVM on its arm64 runners, at which point we can move aarch64 testing to full Cuttlefish too. The clang wrapper stays at the retired emulator runtime API level (24), so the version-gated test skips resolve exactly as before, so this migration only changes the test infrastructure, not the coverage. Assisted-By: Claude Fable 5 Signed-off-by: Adrian Ratiu --- .github/workflows/ci.yaml | 2 + ci/README.md | 7 +- ci/android-bionic-sysroot.sh | 82 ++++++++++++++++++++++ ci/docker/aarch64-linux-android/Dockerfile | 41 ++++------- 4 files changed, 101 insertions(+), 31 deletions(-) create mode 100755 ci/android-bionic-sysroot.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2656d082d174..ae02ede1e531 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -196,6 +196,8 @@ jobs: matrix: include: - target: aarch64-linux-android + # Keep in sync with the Android build pinned in ci/android-bionic-sysroot.sh + artifact-tag: android17 - target: aarch64-unknown-linux-musl - target: aarch64-unknown-linux-musl env: { TEST_MUSL_V1_2_3: 1 } diff --git a/ci/README.md b/ci/README.md index 54017ccc0d6e..db30a58db9a0 100644 --- a/ci/README.md +++ b/ci/README.md @@ -34,8 +34,11 @@ The remaining architectures look like: * Android x86_64 tests run on a [Cuttlefish](https://source.android.com/docs/devices/cuttlefish) virtual device booted on the CI host by `cuttlefish-setup.sh`; the [docker image][android-docker] cross-compiles the tests with the NDK and - drives the device over adb. The remaining Android targets run in docker - images with the legacy SDK emulator, the NDK, and the SDK already set up, + drives the device over adb. Android aarch64 tests run under the QEMU + userspace emulator against the bionic linker and libraries extracted from + a current Android build by `android-bionic-sysroot.sh`, with no Android + system booted. The remaining Android target (32-bit arm) runs in a docker + image with the legacy SDK emulator, the NDK, and the SDK already set up, with the entire build happening within the docker image. * The MIPS, ARM, and AArch64 builds all use the QEMU userspace emulator to run the generated binary to actually verify the tests pass. diff --git a/ci/android-bionic-sysroot.sh b/ci/android-bionic-sysroot.sh new file mode 100755 index 000000000000..388d52e8e42b --- /dev/null +++ b/ci/android-bionic-sysroot.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +set -eux + +# Extract the bionic linker and libraries from a current Android build into +# /system, so the test binaries can run under qemu-user with no Android +# system booted (see the aarch64-linux-android Dockerfile). This is a +# stopgap until GitHub's arm64 runners expose KVM, at which point this +# target can move to Cuttlefish like x86_64-linux-android. +# +# The target_files zip is used because it contains the system partition as +# plain files, unlike the flashable image zips. +# +# The pinned build (currently Android 17) matches ci/cuttlefish-setup.sh; +# bump both together with the android* artifact-tags in ci.yaml. + +build="${ANDROID_SYSROOT_BUILD:-15660610}" +target="${ANDROID_SYSROOT_TARGET:-aosp_cf_arm64_only_phone-userdebug}" + +zip_name="${target%-userdebug}-target_files-${build}.zip" +url="https://ci.android.com/builds/submitted/${build}/${target}/latest/${zip_name}" + +td="$(mktemp -d)" + +# The ci.android.com URL serves an HTML page embedding a short-lived signed +# Cloud Storage URL (JSON-escaped '&'); scrape it to get the artifact. +signed_url="$(wget -q -O - "${url}" | + grep -o 'https://storage.googleapis.com/android-build/[^"]*' | + head -1 | + sed 's/\\u0026/\&/g')" +wget -q --tries=5 -O "${td}/target_files.zip" "${signed_url}" + +# Take the full lib64 rather than cherry-picking libc/libm/libdl: the +# linker pulls in supporting libraries that vary between releases. +# Extraction is best-effort; the sanity checks below decide. +unzip -q "${td}/target_files.zip" \ + 'SYSTEM/apex/com.android.runtime.apex' \ + 'SYSTEM/bin/linker64' \ + 'SYSTEM/bin/bootstrap/*' \ + 'SYSTEM/build.prop' \ + 'SYSTEM/etc/ld.config*' \ + 'SYSTEM/lib64/*' \ + -d "${td}" || true + +mkdir -p /system +mv "${td}/SYSTEM/bin" /system/bin +mv "${td}/SYSTEM/lib64" /system/lib64 +mv "${td}/SYSTEM/build.prop" /system/build.prop +if [ -d "${td}/SYSTEM/etc" ]; then + mv "${td}/SYSTEM/etc" /system/etc +fi + +# Since Android 10 bionic lives in the Runtime APEX and the /system paths +# are symlinks into /apex/com.android.runtime; unpack the apex payload +# there so they resolve. +unzip -q "${td}/SYSTEM/apex/com.android.runtime.apex" apex_payload.img \ + -d "${td}" +mkdir -p /apex/com.android.runtime +case "$(file -b "${td}/apex_payload.img")" in + *EROFS*) + fsck.erofs "--extract=/apex/com.android.runtime" \ + "${td}/apex_payload.img" + ;; + *ext[24]*) + debugfs -R "rdump / /apex/com.android.runtime" \ + "${td}/apex_payload.img" + ;; + *) + echo "unrecognized apex payload filesystem" >&2 + exit 1 + ;; +esac + +rm -rf "${td}" + +# Sanity-check the files the tests need (following the /apex symlinks, so +# this also verifies the apex extraction) and log the Android version. +test -s /system/bin/linker64 +test -s /system/lib64/libc.so +test -s /system/lib64/libm.so +test -s /system/lib64/libdl.so +grep ro.build.version.release /system/build.prop diff --git a/ci/docker/aarch64-linux-android/Dockerfile b/ci/docker/aarch64-linux-android/Dockerfile index 420e594108c5..22158381924d 100644 --- a/ci/docker/aarch64-linux-android/Dockerfile +++ b/ci/docker/aarch64-linux-android/Dockerfile @@ -1,46 +1,29 @@ FROM ubuntu:26.04 -RUN dpkg --add-architecture i386 RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ - expect \ + e2fsprogs \ + erofs-utils \ file \ gcc \ - libc6-dev \ - libpulse0 \ - libstdc++6:i386 \ - openjdk-8-jre \ + libc-dev \ python3 \ + qemu-user \ unzip \ wget WORKDIR /android/ -COPY android* /android/ - -ENV ANDROID_ARCH=aarch64 -ENV PATH=$PATH:/android/linux-x86_64/bin:/android/sdk/cmdline-tools/tools:/android/sdk/platform-tools - +COPY android-install-ndk.sh android-bionic-sysroot.sh /android/ RUN /android/android-install-ndk.sh -RUN /android/android-install-sdk.sh $ANDROID_ARCH -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/* +RUN /android/android-bionic-sysroot.sh -ENV PATH=$PATH:/rust/bin \ +# Test binaries run under qemu-user against the bionic extracted to +# /system by android-bionic-sysroot.sh. This is a stopgap until GitHub's +# arm64 runners expose KVM, at which point we can move to Cuttlefish like +# the x86_64-linux-android test. +ENV PATH=$PATH:/rust/bin:/android/linux-x86_64/bin \ CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android24-clang \ - CARGO_TARGET_AARCH64_LINUX_ANDROID_RUNNER=/tmp/runtest \ + CARGO_TARGET_AARCH64_LINUX_ANDROID_RUNNER=qemu-aarch64 \ CC_aarch64_linux_android=aarch64-linux-android24-clang \ AR_aarch64_linux_android=llvm-ar \ HOME=/tmp - -ADD runtest-android.rs /tmp/runtest.rs -ENTRYPOINT [ \ - "bash", \ - "-c", \ - # set SHELL so android can detect a 64bits system, see - # http://stackoverflow.com/a/41789144 - "SHELL=/bin/dash /android/sdk/emulator/emulator @aarch64 -no-window & \ - rustc /tmp/runtest.rs -o /tmp/runtest && \ - exec \"$@\"", \ - "--" \ -]