Skip to content

Commit 749e5b4

Browse files
authored
Android platform support (#372)
* cmake(android): skip linking -lpthread on Android On Android pthread symbols are provided by bionic libc; there is no separate libpthread. Adding -lpthread triggers a link error (-lpthread not found). Guard pthread linkage with (UNIX AND NOT ANDROID) and document the reason. Other UNIX platforms (Linux, *BSD) still link against pthread explicitly; no behavior change elsewhere. * cmake: guard OpenSSL find when imported targets already provided Wrap find_package(OpenSSL) in a conditional to avoid resolving host libraries when prebuilt OpenSSL::SSL/OpenSSL::Crypto targets are injected (Android embedding). pthread linking remains skipped on Android where pthread is part of libc. * Add a CI workflow for building libcoro on Android * Run tests on Android emulator * test(android) update default timeout to 20min * docs: Add Android support guide to README * cmake: Improve version detection using git describe
1 parent ff2b4fc commit 749e5b4

File tree

16 files changed

+1501
-20
lines changed

16 files changed

+1501
-20
lines changed

.githooks/readme-template.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
*
4747
* [Requirements](#requirements)
4848
* [Build Instructions](#build-instructions)
49+
* [Android Support](#android-support)
4950
* [Contributing](#contributing)
5051
* [Support](#support)
5152

@@ -508,6 +509,85 @@ Requests/sec: 325778.99
508509
Transfer/sec: 18.33MB
509510
```
510511

512+
## Android Support
513+
514+
libcoro ships with an Android test harness that builds and runs the library on Android devices and emulators. This is intended to validate coroutine primitives on Android and to sanity-check networking/TLS integration using OpenSSL where available.
515+
516+
### Status at a glance
517+
- Toolchain: Android Gradle Plugin 8.5.2, Gradle Wrapper, NDK r29 (29.0.13846066), CMake 3.22.1
518+
- Minimum SDK: 24, Target SDK: 34
519+
- ABIs: arm64-v8a, armeabi-v7a, x86, x86_64 (APK contains selected ABIs; CI builds per-ABI)
520+
- JNI test app package: `com.example.libcorotest`
521+
- TLS: supported via prebuilt OpenSSL static libraries per-ABI
522+
- Networking: enabled in Android builds (`LIBCORO_FEATURE_NETWORKING`, `LIBCORO_FEATURE_TLS`)
523+
524+
### Project layout
525+
- `test/android/` — Android application project (Gradle)
526+
- `src/main/cpp/CMakeLists.txt` — integrates libcoro and links in the canonical test sources
527+
- `src/main/cpp/main.cpp` — JNI host that launches Catch2-based libcoro tests with live log streaming
528+
- `scripts/build_openssl.sh` — helper to produce per-ABI OpenSSL static libs under `external/openssl/<ABI>/`
529+
530+
### Building the Android test APK locally
531+
Prerequisites: Android SDK + NDK r29, CMake 3.22.1 (installed via SDK), JDK 17.
532+
533+
From repo root:
534+
535+
```bash
536+
cd test/android
537+
# Optionally build OpenSSL for required ABIs (script downloads/compiles):
538+
bash scripts/build_openssl.sh --abis arm64-v8a,armeabi-v7a,x86_64,x86 --api 24
539+
540+
# Single-ABI debug build (faster iterations):
541+
gradle assembleDebug -PciAbi=x86_64 -PcustomBuildDir=build-x86_64
542+
543+
# Multi-ABI build when experimenting locally (produces a fat APK per chosen filters):
544+
gradle assembleDebug
545+
```
546+
547+
Notes:
548+
- You can override the Gradle build directory via `-PcustomBuildDir=...` (used in CI).
549+
- You can restrict to a specific ABI via `-PciAbi=<abi>`.
550+
551+
### Running tests on an emulator
552+
Tests are executed by launching the app, which loads a shared library `libcoroTest.so` that embeds the libcoro test suite (Catch2). Output is streamed to Logcat with tag `coroTest` and also mirrored into the app sandbox file `files/libcoro-tests.log`.
553+
554+
You can pass test options by pushing a simple properties file to the device:
555+
556+
```
557+
filter=~[benchmark] ~[bench] ~[semaphore] ~[io_scheduler]
558+
timeout=600
559+
```
560+
561+
Place it at `/data/local/tmp/coro_test_config.properties` or inside the app sandbox at `files/coro_test_config.properties`. The JNI runner falls back to defaults when the sandbox is not writable.
562+
563+
Default exclusions in emulator runs skip slow/fragile suites (benchmarks, some networking/TLS servers, long-running schedulers). See `test/android/src/main/cpp/main.cpp` for the current filter set.
564+
565+
### CI pipeline
566+
The GitHub Actions workflow `.github/workflows/ci-android.yml` performs:
567+
- Per-ABI matrix builds (arm64-v8a, armeabi-v7a, x86, x86_64)
568+
- OpenSSL prebuild per ABI via `scripts/build_openssl.sh`
569+
- Emulator provisioning on x86_64 (Android 30), headless launch, storage readiness checks
570+
- Pushing test configuration (filter/timeout) and running the app
571+
- Collecting `coroTest` Logcat into `emulator.log` and exporting `libcoro-tests.log`
572+
573+
The test filter excludes particularly slow suites to keep runs under a 10-minute global timeout inside the app. Adjust `TEST_FILTER`/`TEST_TIMEOUT` env vars in the workflow as needed.
574+
575+
### Known limitations on Android
576+
- Network server tests (e.g., TCP/TLS servers) are skipped in CI to avoid emulator networking flakiness
577+
- Some timing-sensitive `condition_variable` cases may be excluded on emulators due to short timeouts
578+
- The Android harness is for validation; apps should link `libcoro` as a regular CMake target in their own projects
579+
580+
### Using libcoro in your Android CMake project
581+
Add libcoro as a subdirectory in your native CMake and link it to your library target. Example snippet for your module’s CMakeLists:
582+
583+
```cmake
584+
add_subdirectory(${CMAKE_SOURCE_DIR}/path/to/libcoro libcoro_build)
585+
target_link_libraries(your-lib PRIVATE libcoro log)
586+
target_compile_definitions(your-lib PRIVATE LIBCORO_FEATURE_NETWORKING LIBCORO_FEATURE_TLS)
587+
```
588+
589+
If you require TLS, provide OpenSSL for the target ABI (static or shared) and set `OPENSSL_ROOT_DIR`/`OPENSSL_USE_STATIC_LIBS` accordingly.
590+
511591
### Requirements
512592
C++20 Compiler with coroutine support
513593
g++ [10.2.1, 10.3.1, 11, 12, 13]

.github/workflows/ci-android.yml

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
name: ci-android
2+
3+
on:
4+
pull_request:
5+
workflow_dispatch:
6+
7+
jobs:
8+
ci-android:
9+
name: ci-android-${{ matrix.abi }}${{ matrix.abi == 'x86_64' && '+test' || '' }}
10+
runs-on: ubuntu-latest
11+
timeout-minutes: 70
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
abi: [arm64-v8a, armeabi-v7a, x86, x86_64]
16+
env:
17+
TEST_FILTER: "~[benchmark] ~[bench] ~[semaphore] ~[io_scheduler] ~[ring_buffer] ~[thread_pool] ~[tcp_server] ~[tls_server] ~[dns] ~*net::* ~*udp* ~*ip_address* ~*wait_for* ~*wait_until*"
18+
TEST_TIMEOUT: "600"
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
with:
23+
submodules: recursive
24+
25+
- name: Set up JDK
26+
uses: actions/setup-java@v4
27+
with:
28+
distribution: temurin
29+
java-version: '17'
30+
31+
- name: Set up Android SDK tools
32+
uses: android-actions/setup-android@v3
33+
34+
- name: Cache Gradle & native build
35+
uses: actions/cache@v4
36+
with:
37+
path: |
38+
~/.gradle/caches
39+
~/.gradle/wrapper
40+
~/.android/build-cache
41+
test/android/.cxx
42+
test/android/.gradle
43+
test/android/build-${{ matrix.abi }}
44+
key: gradle-${{ runner.os }}-${{ matrix.abi }}-${{ hashFiles('test/android/**/*.gradle*','test/android/gradle.properties') }}
45+
restore-keys: |
46+
gradle-${{ runner.os }}-${{ matrix.abi }}-
47+
gradle-${{ runner.os }}-
48+
49+
- name: Accept Android licenses
50+
run: |
51+
yes | sdkmanager --licenses > /dev/null || true
52+
53+
- name: Install CMake (required for externalNativeBuild)
54+
run: |
55+
sdkmanager --install "cmake;3.22.1" > /dev/null
56+
57+
- name: Build OpenSSL (if missing for ABI)
58+
working-directory: test/android
59+
run: |
60+
ABI="${{ matrix.abi }}"
61+
ROOT="$PWD"
62+
OUT_DIR="external/openssl/$ABI/lib"
63+
if [ -f "$OUT_DIR/libssl.a" ] && [ -f "$OUT_DIR/libcrypto.a" ]; then
64+
echo "OpenSSL already present for $ABI"; exit 0; fi
65+
echo "Building OpenSSL for $ABI";
66+
bash scripts/build_openssl.sh --abis "$ABI" --api 24
67+
68+
- name: Build debug APK (single ABI)
69+
working-directory: test/android
70+
env:
71+
ANDROID_MATRIX_ABI: ${{ matrix.abi }}
72+
run: |
73+
echo "Building for ABI: ${{ matrix.abi }}"
74+
export GRADLE_USER_HOME="$PWD/.gradle"
75+
BUILD_DIR="build-${{ matrix.abi }}"
76+
gradle clean assembleDebug --stacktrace --no-daemon -PciAbi='${{ matrix.abi }}' -PcustomBuildDir=$BUILD_DIR
77+
ls -la "$BUILD_DIR/outputs/apk/debug" || true
78+
echo "Verify native lib for ABI present" || true
79+
find "$BUILD_DIR" -type f -path "*${{ matrix.abi }}*" -name "*.so" | head -n 20 || true
80+
81+
- name: Upload APK artifact (per ABI)
82+
if: always()
83+
uses: actions/upload-artifact@v4
84+
with:
85+
name: apk-${{ matrix.abi }}
86+
path: |
87+
test/android/build-${{ matrix.abi }}/outputs/apk/debug/*.apk
88+
test/android/build/outputs/apk/debug/*.apk
89+
90+
- name: Install emulator runtime dependencies
91+
if: matrix.abi == 'x86_64'
92+
run: |
93+
sudo apt-get update
94+
sudo apt-get install -y \
95+
libpulse0 libnss3 libxcomposite1 libxcursor1 libxdamage1 \
96+
libxi6 libxrandr2 libxtst6 libasound2 libx11-6 libx11-xcb1 \
97+
libxcb1 libxss1 libglu1-mesa libdbus-1-3 ca-certificates \
98+
fonts-liberation libwayland-client0 libwayland-cursor0 || true
99+
100+
- name: Create AVD
101+
if: matrix.abi == 'x86_64'
102+
run: |
103+
set -euo pipefail
104+
export ANDROID_AVD_HOME="$HOME/.android/avd"
105+
export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-$ANDROID_HOME}"
106+
export EMU_BIN="$ANDROID_SDK_ROOT/emulator/emulator"
107+
# Avoid -p: create AVD home only if missing
108+
[ -d "$ANDROID_AVD_HOME" ] || mkdir "$ANDROID_AVD_HOME"
109+
sdkmanager --channel=0 --install "emulator" "platform-tools" > /dev/null || true
110+
sdkmanager --channel=0 --install "system-images;android-30;default;x86_64" > /dev/null
111+
avdmanager delete avd -n test || true
112+
echo "no" | avdmanager create avd -n test -k "system-images;android-30;default;x86_64" --force
113+
"$EMU_BIN" -list-avds || true
114+
if ! "$EMU_BIN" -list-avds | grep -q '^test$'; then
115+
echo "AVD 'test' creation failed" >&2; exit 1; fi
116+
117+
- name: Launch emulator
118+
if: matrix.abi == 'x86_64'
119+
run: |
120+
set -euo pipefail
121+
export ADB="${ANDROID_SDK_ROOT:-$ANDROID_HOME}/platform-tools/adb"
122+
export EMU_BIN="${ANDROID_SDK_ROOT:-$ANDROID_HOME}/emulator/emulator"
123+
export ANDROID_AVD_HOME="$HOME/.android/avd"
124+
export QT_QPA_PLATFORM=offscreen
125+
export ANDROID_EMULATOR_USE_SYSTEM_LIBS=1
126+
"$ADB" kill-server || true
127+
"$ADB" start-server
128+
LOG_FILE=emulator_stdout.log
129+
nohup "$EMU_BIN" -avd test -no-window -no-audio -no-boot-anim -accel off -gpu swiftshader_indirect -no-snapshot -wipe-data -netfast > "$LOG_FILE" 2>&1 &
130+
EMU_PID=$!
131+
sleep 2
132+
kill -0 "$EMU_PID" || { echo "Emulator exited early" >&2; tail -n 100 "$LOG_FILE" || true; exit 1; }
133+
echo "Waiting for emulator device..."
134+
for i in $(seq 1 150); do
135+
DEV=$("$ADB" devices | awk '/emulator-/{print $1; exit}')
136+
[ -n "$DEV" ] && break
137+
sleep 2
138+
done
139+
[ -n "$DEV" ] || { echo "No emulator device" >&2; tail -n 120 "$LOG_FILE" || true; exit 1; }
140+
"$ADB" -s "$DEV" wait-for-device
141+
echo "Waiting boot..."
142+
for i in $(seq 1 150); do
143+
BOOTED=$("$ADB" -s "$DEV" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')
144+
BOOTANIM=$("$ADB" -s "$DEV" shell getprop service.bootanim.exit 2>/dev/null | tr -d '\r')
145+
[ "$BOOTED" = "1" ] && [ "$BOOTANIM" = "1" ] && break
146+
sleep 2
147+
done
148+
"$ADB" -s "$DEV" shell settings put global window_animation_scale 0 || true
149+
"$ADB" -s "$DEV" shell settings put global transition_animation_scale 0 || true
150+
"$ADB" -s "$DEV" shell settings put global animator_duration_scale 0 || true
151+
echo "Waiting for package manager..."; for i in $(seq 1 120); do "$ADB" -s "$DEV" shell pm list packages >/dev/null 2>&1 && break; sleep 2; done
152+
153+
- name: Install & run tests (x86_64)
154+
if: matrix.abi == 'x86_64'
155+
run: |
156+
set -euo pipefail
157+
ADB="${ANDROID_SDK_ROOT:-$ANDROID_HOME}/platform-tools/adb"
158+
DEV=$("$ADB" devices | awk '/emulator-/{print $1; exit}')
159+
APK=$(ls test/android/build-${{ matrix.abi }}/outputs/apk/debug/*.apk 2>/dev/null | head -n1 || ls test/android/build/outputs/apk/debug/*.apk 2>/dev/null | head -n1 || true)
160+
[ -n "$DEV" ] || { echo "No device" >&2; exit 1; }
161+
[ -n "$APK" ] || { echo "APK missing" >&2; exit 1; }
162+
echo "Install attempts..."
163+
for i in $(seq 1 5); do
164+
OUT=$("$ADB" -s "$DEV" install -r "$APK" 2>&1) || true
165+
echo "$OUT"; echo "$OUT" | grep -q "Success" && break
166+
sleep 5
167+
done
168+
echo "Waiting for package manager (pm) to be responsive..."
169+
PM_READY=0
170+
for i in $(seq 1 120); do
171+
"$ADB" -s "$DEV" shell pm list packages >/dev/null 2>&1 && { PM_READY=1; break; }
172+
sleep 2
173+
done
174+
[ "$PM_READY" -eq 1 ] || { echo "Package manager not ready" >&2; exit 1; }
175+
echo "Probing storage readiness (/sdcard)..."
176+
set +e
177+
STORAGE_READY=0
178+
for i in $(seq 1 90); do
179+
"$ADB" -s "$DEV" shell 'echo 42 > /sdcard/ci_probe 2>/dev/null' >/dev/null 2>&1
180+
"$ADB" -s "$DEV" shell 'cat /sdcard/ci_probe' 2>/dev/null | grep -q '^42$'
181+
if [ $? -eq 0 ]; then STORAGE_READY=1; break; fi
182+
sleep 2
183+
done
184+
set -e
185+
if [ "$STORAGE_READY" -ne 1 ]; then
186+
echo "Storage not fully ready (continuing)" >&2
187+
"$ADB" -s "$DEV" shell ls -ld /sdcard 2>/dev/null || true
188+
"$ADB" -s "$DEV" shell df -h /sdcard 2>/dev/null || true
189+
fi
190+
PKG=com.example.libcorotest
191+
echo "Initial launch to create internal storage dir..."
192+
"$ADB" -s "$DEV" shell am start -n $PKG/.MainActivity >/dev/null 2>&1 || true
193+
sleep 5
194+
echo "Prepare test config"
195+
{
196+
[ -n "${TEST_FILTER}" ] && printf 'filter=%s\n' "${TEST_FILTER}" || true
197+
printf 'timeout=%s\n' "${TEST_TIMEOUT}";
198+
} > coro_test_config.properties
199+
"$ADB" -s "$DEV" push coro_test_config.properties /data/local/tmp/coro_test_config.properties >/dev/null
200+
echo "Copy config into app sandbox (best-effort)"
201+
# Make config copy non-fatal: fallback to defaults if copy fails
202+
COPY_OK=0
203+
# First attempt: try copying through run-as with absolute paths
204+
PKG_DIR="/data/data/$PKG"
205+
if "$ADB" -s "$DEV" shell "run-as $PKG test -d . && run-as $PKG test -w ." 2>/dev/null; then
206+
echo "App sandbox writable, attempting config copy..."
207+
"$ADB" -s "$DEV" shell run-as $PKG sh -c "test -d files || mkdir files" 2>/dev/null || true
208+
if "$ADB" -s "$DEV" shell run-as $PKG cp /data/local/tmp/coro_test_config.properties files/coro_test_config.properties 2>/dev/null; then
209+
COPY_OK=1
210+
echo "Config copied successfully"
211+
"$ADB" -s "$DEV" shell run-as $PKG sh -c 'ls -l files/coro_test_config.properties; head -n 3 files/coro_test_config.properties' || true
212+
fi
213+
fi
214+
if [ $COPY_OK -ne 1 ]; then
215+
echo "Config copy failed or sandbox not writable - using default test settings" >&2
216+
echo "Tests will run with: filter=\"${TEST_FILTER:-*}\", timeout=${TEST_TIMEOUT}s"
217+
fi
218+
echo "Force-stop and relaunch for tests"
219+
"$ADB" -s "$DEV" shell am force-stop $PKG || true
220+
"$ADB" -s "$DEV" logcat -c || true
221+
"$ADB" -s "$DEV" shell am start -n $PKG/.MainActivity
222+
TIMEOUT=3600
223+
while [ $TIMEOUT -gt 0 ]; do
224+
amstack=$("$ADB" -s "$DEV" shell dumpsys activity activities | grep -E "Activities=.*com.example.libcorotest" || true)
225+
[ -z "$amstack" ] && break
226+
sleep 2; TIMEOUT=$((TIMEOUT-2))
227+
done
228+
"$ADB" -s "$DEV" logcat -v time -d -s coroTest:I > emulator.log || true
229+
tail -n 200 emulator.log || true
230+
231+
- name: Assert success (x86_64)
232+
if: matrix.abi == 'x86_64'
233+
run: |
234+
grep -q "Exit code: 0" emulator.log || { echo "Tests did not report success" >&2; exit 1; }
235+
grep -q "No tests ran" emulator.log && { echo "No tests executed" >&2; exit 1; } || true
236+
237+
- name: Extract test log
238+
if: always() && matrix.abi == 'x86_64'
239+
run: |
240+
set -e
241+
ADB="${ANDROID_SDK_ROOT:-$ANDROID_HOME}/platform-tools/adb"
242+
DEV=$("$ADB" devices | awk '/emulator-/{print $1; exit}')
243+
if [ -n "$DEV" ]; then
244+
"$ADB" -s "$DEV" shell run-as com.example.libcorotest cat files/libcoro-tests.log > libcoro-tests.log 2>/dev/null || echo "libcoro-tests.log not found" >&2
245+
fi
246+
[ -f libcoro-tests.log ] && tail -n 40 libcoro-tests.log || true
247+
248+
- name: Upload logs
249+
if: always() && matrix.abi == 'x86_64'
250+
uses: actions/upload-artifact@v4
251+
with:
252+
name: emulator-logs
253+
path: |
254+
emulator.log
255+
test/android/build-${{ matrix.abi }}/outputs/apk/debug/*.apk
256+
test/android/build/outputs/apk/debug/*.apk
257+
libcoro-tests.log

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,14 @@
4040

4141
/.vscode/
4242
/.idea/
43+
44+
# Android / Gradle build artifacts
45+
test/android/.gradle/
46+
test/android/.cxx/
47+
test/android/build/
48+
test/android/build-*/
49+
test/android/**/build/
50+
*.apk
51+
local.properties
52+
**/.gradle/
53+
**/.cxx/

0 commit comments

Comments
 (0)