fix(android): auto-discover SDK/NDK in build script, strip native libs
build_android_apk.sh no longer requires all four env vars to be set manually. It probes common SDK paths and uses the newest installed build-tools/NDK/platform when vars are absent. Also adds llvm-strip pass to strip debug symbols from .so files before packaging (controlled by STRIP_NATIVE_LIBS, default 1), moves the debug keystore to a stable target/android/debug.keystore path, and prints resolved paths at start. Also adds scripts/ANDROID_TESTING.md and scripts/android_smoke.sh for on-device smoke testing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
# Android testing
|
||||
|
||||
This directory contains lightweight Android test helpers for Ferrous Solitaire.
|
||||
They are intended to run against either a physical Android device or an emulator
|
||||
connected through `adb`. When no device is connected the smoke script can
|
||||
automatically launch an AVD for you.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Android SDK and NDK installed.
|
||||
- `adb` available on `PATH`.
|
||||
- One device/emulator visible in `adb devices`, **or** at least one AVD created
|
||||
(the script will launch one automatically if `LAUNCH_AVD=1`, which is the default).
|
||||
- If multiple devices are connected, set `ADB_SERIAL` to the target device serial.
|
||||
- Environment variables required by `scripts/build_android_apk.sh` when building:
|
||||
|
||||
```sh
|
||||
export ANDROID_HOME=/path/to/android-sdk
|
||||
export ANDROID_NDK_HOME=/path/to/android-ndk
|
||||
export BUILD_TOOLS_VERSION=34.0.0
|
||||
export PLATFORM=android-34
|
||||
```
|
||||
|
||||
## Smoke test
|
||||
|
||||
From the workspace root (`Rusty_Solitaire/`):
|
||||
|
||||
```sh
|
||||
scripts/android_smoke.sh
|
||||
```
|
||||
|
||||
The smoke test first checks whether `adb` can see a ready device. If no device
|
||||
is connected and `LAUNCH_AVD=1` (default), it:
|
||||
|
||||
1. locates the `emulator` binary under `ANDROID_HOME` or `PATH`,
|
||||
2. picks the first available AVD (or uses `AVD_NAME`),
|
||||
3. launches the emulator in the foreground (or headless with `AVD_HEADLESS=1`),
|
||||
4. waits for `sys.boot_completed=1` before proceeding,
|
||||
5. dismisses the lock screen so the screenshot shows the app.
|
||||
|
||||
Once a device is ready (auto-launched or pre-existing) the script:
|
||||
|
||||
1. builds the APK using `scripts/build_android_apk.sh`,
|
||||
2. installs it with `adb install -r -d` so debug smoke builds can replace newer local builds,
|
||||
3. force-stops the package by default for a clean launch,
|
||||
4. clears `logcat`,
|
||||
5. launches `com.ferrousapp.solitaire/android.app.NativeActivity`,
|
||||
6. waits for the app to settle,
|
||||
7. verifies the process is still running,
|
||||
8. captures a screenshot and `logcat`, and
|
||||
9. fails on fatal log patterns such as native crashes, JNI fatal errors, real ANRs,
|
||||
and Rust panics.
|
||||
|
||||
On exit the script kills any emulator it launched (`SHUTDOWN_AVD_ON_EXIT=1` by
|
||||
default). Set `SHUTDOWN_AVD_ON_EXIT=0` to keep the emulator open for inspection.
|
||||
|
||||
Artifacts are written to `target/android-smoke/<timestamp>/` by default. A successful run includes:
|
||||
|
||||
- `device.txt` — selected device and display metadata,
|
||||
- `df-data-before.txt` / `df-data-after.txt` — emulator/device storage snapshots,
|
||||
- `emulator.log` — stdout/stderr from the emulator process (AVD runs only),
|
||||
- `emulator.pid` — PID of the emulator process (AVD runs only),
|
||||
- `launch.png` — screenshot after the wait period,
|
||||
- `logcat.txt` — full captured log,
|
||||
- `log-summary.txt` — grep summary for warnings, errors, JNI, safe-area, and crash terms, and
|
||||
- `pid.txt` — running app process id.
|
||||
|
||||
## Creating an AVD
|
||||
|
||||
If no AVDs exist, create one before running the smoke test:
|
||||
|
||||
```sh
|
||||
# Install a system image
|
||||
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" \
|
||||
'system-images;android-34;google_apis;x86_64'
|
||||
|
||||
# Create the AVD
|
||||
"$ANDROID_HOME/cmdline-tools/latest/bin/avdmanager" create avd \
|
||||
-n Pixel_7_API_34 \
|
||||
-k 'system-images;android-34;google_apis;x86_64' \
|
||||
--device 'pixel_7'
|
||||
```
|
||||
|
||||
Then run the smoke test — it will pick `Pixel_7_API_34` automatically:
|
||||
|
||||
```sh
|
||||
scripts/android_smoke.sh
|
||||
```
|
||||
|
||||
## Faster iteration
|
||||
|
||||
If you already built the APK and only want to reinstall/relaunch:
|
||||
|
||||
```sh
|
||||
BUILD_APK=0 scripts/android_smoke.sh
|
||||
```
|
||||
|
||||
If the APK is already installed and you only want to relaunch/capture logs:
|
||||
|
||||
```sh
|
||||
BUILD_APK=0 INSTALL_APK=0 scripts/android_smoke.sh
|
||||
```
|
||||
|
||||
By default the script force-stops the package before launch so logcat and screenshots represent a clean app start. To test warm-launch behavior instead:
|
||||
|
||||
```sh
|
||||
BUILD_APK=0 INSTALL_APK=0 FORCE_STOP=0 scripts/android_smoke.sh
|
||||
```
|
||||
|
||||
This is also useful when an already-installed build is good enough for launch/log checks. On install failure, the script writes `adb-install.txt`, storage snapshots, and installed-package diagnostics to the output directory.
|
||||
|
||||
If install fails with `INSTALL_FAILED_UPDATE_INCOMPATIBLE`, the smoke script uninstalls the package and retries once by default (`RESET_ON_SIGNATURE_MISMATCH=1`). This resets app data on the device/emulator. Disable it with:
|
||||
|
||||
```sh
|
||||
RESET_ON_SIGNATURE_MISMATCH=0 scripts/android_smoke.sh
|
||||
```
|
||||
|
||||
To write artifacts to a stable path:
|
||||
|
||||
```sh
|
||||
OUT_DIR=target/android-smoke/latest scripts/android_smoke.sh
|
||||
```
|
||||
|
||||
When reusing an output directory, previous files are removed by default so stale artifacts do not contaminate the latest result. To keep existing files:
|
||||
|
||||
```sh
|
||||
CLEAN_OUT_DIR=0 OUT_DIR=target/android-smoke/latest scripts/android_smoke.sh
|
||||
```
|
||||
|
||||
To target a specific device when more than one is attached:
|
||||
|
||||
```sh
|
||||
ADB_SERIAL=emulator-5554 scripts/android_smoke.sh
|
||||
```
|
||||
|
||||
To wait longer for safe-area inset polling or slow devices:
|
||||
|
||||
```sh
|
||||
WAIT_SECS=8 scripts/android_smoke.sh
|
||||
```
|
||||
|
||||
## AVD options
|
||||
|
||||
To pick a specific AVD by name instead of auto-selecting the first one:
|
||||
|
||||
```sh
|
||||
AVD_NAME=Pixel_7_API_34 scripts/android_smoke.sh
|
||||
```
|
||||
|
||||
To run headless (no emulator window) — useful in CI or on a display-less machine:
|
||||
|
||||
```sh
|
||||
AVD_HEADLESS=1 scripts/android_smoke.sh
|
||||
```
|
||||
|
||||
To give a slow machine more time to boot the emulator (default is 120 s):
|
||||
|
||||
```sh
|
||||
AVD_BOOT_TIMEOUT=180 scripts/android_smoke.sh
|
||||
```
|
||||
|
||||
To keep the emulator running after the test (useful for manual inspection):
|
||||
|
||||
```sh
|
||||
SHUTDOWN_AVD_ON_EXIT=0 scripts/android_smoke.sh
|
||||
```
|
||||
|
||||
To pass extra flags to the emulator (e.g. disable snapshot for a completely
|
||||
cold boot, or change GPU mode):
|
||||
|
||||
```sh
|
||||
AVD_EXTRA_ARGS="-gpu swiftshader_indirect" scripts/android_smoke.sh
|
||||
```
|
||||
|
||||
To disable AVD auto-launch entirely and fail immediately if no device is
|
||||
connected:
|
||||
|
||||
```sh
|
||||
LAUNCH_AVD=0 scripts/android_smoke.sh
|
||||
```
|
||||
|
||||
For build-only validation without requiring a connected device, use the lower-level APK builder directly:
|
||||
|
||||
```sh
|
||||
scripts/build_android_apk.sh
|
||||
```
|
||||
|
||||
For smoke testing, `scripts/android_smoke.sh` defaults to the connected device's primary ABI when `BUILD_APK=1`, which keeps emulator APKs much smaller than the full multi-ABI default. You can still override it explicitly:
|
||||
|
||||
```sh
|
||||
ABIS=x86_64 scripts/android_smoke.sh
|
||||
```
|
||||
|
||||
For build-only validation, `scripts/build_android_apk.sh` still defaults to all configured ABIs unless you set `ABIS` yourself:
|
||||
|
||||
```sh
|
||||
ABIS=x86_64 scripts/build_android_apk.sh
|
||||
```
|
||||
|
||||
The APK builder signs debug builds with a persistent keystore at `target/android/debug.keystore` by default. This avoids signature churn across smoke-test runs.
|
||||
|
||||
The APK builder also strips native debug symbols by default before packaging (`STRIP_NATIVE_LIBS=1`). This keeps debug APKs installable on emulators with limited `/data` storage. To preserve native debug symbols for low-level debugging:
|
||||
|
||||
```sh
|
||||
STRIP_NATIVE_LIBS=0 ABIS=x86_64 scripts/build_android_apk.sh
|
||||
```
|
||||
|
||||
## Device checklist
|
||||
|
||||
The script is only a smoke test. Before shipping Android builds, also verify:
|
||||
|
||||
- safe-area insets arrive and shift the HUD after a few seconds,
|
||||
- HUD does not overlap the top status bar,
|
||||
- modal Done buttons are above the gesture/navigation bar,
|
||||
- stock tap works,
|
||||
- drag-and-drop works on tableau, waste, and foundation piles,
|
||||
- Settings/Help/Profile modals open and close,
|
||||
- login tokens persist after app restart, and
|
||||
- `target/android-smoke/.../logcat.txt` contains no fatal JNI/native crash output.
|
||||
|
||||
## Notes
|
||||
|
||||
- `adb shell input tap X Y` uses physical pixels, not Bevy logical pixels.
|
||||
- The project’s common test device mapping is physical `1080×2400`, Bevy logical
|
||||
`900×2000`, scale factor `1.20`; multiply logical coordinates by `1.20` for
|
||||
scripted `adb shell input` commands on that device.
|
||||
- Keep generated screenshots/logs under `target/android-smoke/` so they stay out
|
||||
of source control.
|
||||
Executable
+362
@@ -0,0 +1,362 @@
|
||||
#!/usr/bin/env bash
|
||||
# Android smoke test for Ferrous Solitaire.
|
||||
#
|
||||
# Builds (optional), installs, launches, captures logcat + screenshot, and
|
||||
# fails on fatal Android log patterns. Designed as a lightweight device/emulator
|
||||
# sanity check rather than a full UI automation suite.
|
||||
#
|
||||
# Required:
|
||||
# adb on PATH
|
||||
# Android SDK/NDK env required by scripts/build_android_apk.sh when BUILD_APK=1
|
||||
#
|
||||
# Optional environment:
|
||||
# BUILD_APK=1|0 Build APK before install (default: 1)
|
||||
# INSTALL_APK=1|0 Install APK before launch (default: 1)
|
||||
# RESET_ON_SIGNATURE_MISMATCH=1|0
|
||||
# Uninstall/retry if debug signatures differ (default: 1)
|
||||
# LAUNCH_APP=1|0 Launch app before checks (default: 1)
|
||||
# FORCE_STOP=1|0 Force-stop package before launch for clean logs (default: 1)
|
||||
# CAPTURE_SCREENSHOT=1|0 Capture screenshot (default: 1)
|
||||
# ADB_SERIAL=... Device serial to use when multiple devices are connected
|
||||
# APK_PATH=... APK to install (default: target/debug/apk/ferrous-solitaire.apk)
|
||||
# PACKAGE=... Android package (default: com.ferrousapp.solitaire)
|
||||
# ACTIVITY=... Activity class (default: android.app.NativeActivity)
|
||||
# OUT_DIR=... Artifact directory (default: target/android-smoke/<timestamp>)
|
||||
# CLEAN_OUT_DIR=1|0 Remove prior artifacts from OUT_DIR first (default: 1)
|
||||
# WAIT_SECS=... Seconds to wait after launch (default: 5)
|
||||
# ABIS=... Passed to build script. If unset and BUILD_APK=1,
|
||||
# defaults to the connected device's primary ABI.
|
||||
#
|
||||
# AVD auto-launch (used when no device/emulator is already connected):
|
||||
# LAUNCH_AVD=1|0 Auto-launch an AVD when no device is ready (default: 1)
|
||||
# AVD_NAME=... AVD name to launch (default: first from `emulator -list-avds`)
|
||||
# AVD_BOOT_TIMEOUT=... Seconds to wait for the emulator to finish booting (default: 120)
|
||||
# AVD_HEADLESS=1|0 Run with -no-window -no-audio for CI/no-display environments (default: 0)
|
||||
# AVD_EXTRA_ARGS=... Extra arguments appended verbatim to the emulator command line
|
||||
# SHUTDOWN_AVD_ON_EXIT=1|0
|
||||
# Kill the AVD this script launched on exit (default: 1).
|
||||
# Set to 0 to leave the emulator running after the test.
|
||||
#
|
||||
# Examples:
|
||||
# scripts/android_smoke.sh
|
||||
# BUILD_APK=0 scripts/android_smoke.sh
|
||||
# LAUNCH_AVD=0 scripts/android_smoke.sh # error out if no device, never auto-launch
|
||||
# AVD_NAME=Pixel_7_API_34 scripts/android_smoke.sh
|
||||
# AVD_HEADLESS=1 scripts/android_smoke.sh # CI / no-display
|
||||
# SHUTDOWN_AVD_ON_EXIT=0 scripts/android_smoke.sh # keep emulator open after test
|
||||
# OUT_DIR=target/android-smoke/latest WAIT_SECS=8 scripts/android_smoke.sh
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
BUILD_APK="${BUILD_APK:-1}"
|
||||
INSTALL_APK="${INSTALL_APK:-1}"
|
||||
RESET_ON_SIGNATURE_MISMATCH="${RESET_ON_SIGNATURE_MISMATCH:-1}"
|
||||
LAUNCH_APP="${LAUNCH_APP:-1}"
|
||||
FORCE_STOP="${FORCE_STOP:-1}"
|
||||
CAPTURE_SCREENSHOT="${CAPTURE_SCREENSHOT:-1}"
|
||||
APK_PATH="${APK_PATH:-target/debug/apk/ferrous-solitaire.apk}"
|
||||
PACKAGE="${PACKAGE:-com.ferrousapp.solitaire}"
|
||||
ACTIVITY="${ACTIVITY:-android.app.NativeActivity}"
|
||||
WAIT_SECS="${WAIT_SECS:-5}"
|
||||
OUT_DIR="${OUT_DIR:-target/android-smoke/$(date +%Y%m%d-%H%M%S)}"
|
||||
CLEAN_OUT_DIR="${CLEAN_OUT_DIR:-1}"
|
||||
REMOTE_SCREENSHOT="/sdcard/ferrous-solitaire-smoke.png"
|
||||
|
||||
LAUNCH_AVD="${LAUNCH_AVD:-1}"
|
||||
AVD_NAME="${AVD_NAME:-}"
|
||||
AVD_BOOT_TIMEOUT="${AVD_BOOT_TIMEOUT:-120}"
|
||||
AVD_HEADLESS="${AVD_HEADLESS:-0}"
|
||||
AVD_EXTRA_ARGS="${AVD_EXTRA_ARGS:-}"
|
||||
SHUTDOWN_AVD_ON_EXIT="${SHUTDOWN_AVD_ON_EXIT:-1}"
|
||||
|
||||
ADB=(adb)
|
||||
if [ -n "${ADB_SERIAL:-}" ]; then
|
||||
ADB+=( -s "$ADB_SERIAL" )
|
||||
fi
|
||||
|
||||
# PID of any emulator we start so the EXIT trap can clean it up.
|
||||
_LAUNCHED_EMULATOR_PID=""
|
||||
|
||||
_cleanup_emulator() {
|
||||
if [ -n "$_LAUNCHED_EMULATOR_PID" ] && [ "$SHUTDOWN_AVD_ON_EXIT" = "1" ]; then
|
||||
echo ">>> shutdown emulator (PID $_LAUNCHED_EMULATOR_PID)"
|
||||
kill "$_LAUNCHED_EMULATOR_PID" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap _cleanup_emulator EXIT
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
echo "missing required command: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
if [ "$CLEAN_OUT_DIR" = "1" ]; then
|
||||
find "$OUT_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
||||
fi
|
||||
require_cmd adb
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Device / emulator availability
|
||||
# ---------------------------------------------------------------------------
|
||||
DEVICE_STATE="$("${ADB[@]}" get-state 2>/dev/null || true)"
|
||||
if [ "$DEVICE_STATE" != "device" ]; then
|
||||
if [ "$LAUNCH_AVD" != "1" ]; then
|
||||
adb devices > "$OUT_DIR/adb-devices.txt" 2>&1 || true
|
||||
if [ -n "${ADB_SERIAL:-}" ]; then
|
||||
echo "Android device '$ADB_SERIAL' is not connected/ready (state: ${DEVICE_STATE:-unknown})." >&2
|
||||
else
|
||||
echo "No Android device/emulator is connected and ready." >&2
|
||||
fi
|
||||
echo "Run 'adb devices' or start an emulator, then retry." >&2
|
||||
echo "Device list saved to $OUT_DIR/adb-devices.txt" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- locate emulator binary -----------------------------------------------
|
||||
# Priority: ANDROID_HOME env → PATH → common SDK install locations.
|
||||
_find_sdk_root() {
|
||||
for candidate in \
|
||||
"$HOME/Android/Sdk" \
|
||||
"$HOME/Library/Android/sdk" \
|
||||
"/opt/android-sdk" \
|
||||
"/usr/lib/android-sdk"; do
|
||||
[ -d "$candidate" ] && echo "$candidate" && return
|
||||
done
|
||||
}
|
||||
|
||||
EMULATOR_BIN=""
|
||||
if [ -n "${ANDROID_HOME:-}" ] && [ -x "$ANDROID_HOME/emulator/emulator" ]; then
|
||||
EMULATOR_BIN="$ANDROID_HOME/emulator/emulator"
|
||||
elif command -v emulator >/dev/null 2>&1; then
|
||||
EMULATOR_BIN="$(command -v emulator)"
|
||||
else
|
||||
_SDK_ROOT="$(_find_sdk_root)"
|
||||
if [ -n "$_SDK_ROOT" ] && [ -x "$_SDK_ROOT/emulator/emulator" ]; then
|
||||
EMULATOR_BIN="$_SDK_ROOT/emulator/emulator"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$EMULATOR_BIN" ]; then
|
||||
echo "No Android device found and 'emulator' binary is not available." >&2
|
||||
echo " • Install the Android SDK emulator component, or" >&2
|
||||
echo " • Set ANDROID_HOME to your SDK root, or" >&2
|
||||
echo " • Start a device/emulator manually then retry with LAUNCH_AVD=0." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo ">>> emulator binary: $EMULATOR_BIN"
|
||||
|
||||
# --- select AVD -----------------------------------------------------------
|
||||
if [ -z "$AVD_NAME" ]; then
|
||||
AVD_NAME="$("$EMULATOR_BIN" -list-avds 2>/dev/null | head -n 1 | tr -d '\r')"
|
||||
if [ -z "$AVD_NAME" ]; then
|
||||
echo "No AVDs found. Create one first, for example:" >&2
|
||||
echo " sdkmanager 'system-images;android-34;google_apis;x86_64'" >&2
|
||||
echo " avdmanager create avd -n Pixel_7_API_34 \\" >&2
|
||||
echo " -k 'system-images;android-34;google_apis;x86_64' --device 'pixel_7'" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo ">>> auto-selected AVD: $AVD_NAME"
|
||||
fi
|
||||
|
||||
# --- launch emulator -------------------------------------------------------
|
||||
EMULATOR_ARGS=( -avd "$AVD_NAME" -no-snapshot-load )
|
||||
[ "$AVD_HEADLESS" = "1" ] && EMULATOR_ARGS+=( -no-window -no-audio )
|
||||
# Split AVD_EXTRA_ARGS on whitespace only (disable glob expansion).
|
||||
set -f
|
||||
# shellcheck disable=SC2206
|
||||
[ -n "$AVD_EXTRA_ARGS" ] && EMULATOR_ARGS+=( $AVD_EXTRA_ARGS )
|
||||
set +f
|
||||
|
||||
echo ">>> launch emulator: $AVD_NAME"
|
||||
"$EMULATOR_BIN" "${EMULATOR_ARGS[@]}" > "$OUT_DIR/emulator.log" 2>&1 &
|
||||
_LAUNCHED_EMULATOR_PID=$!
|
||||
echo "$_LAUNCHED_EMULATOR_PID" > "$OUT_DIR/emulator.pid"
|
||||
echo " emulator PID: $_LAUNCHED_EMULATOR_PID"
|
||||
echo " emulator log: $OUT_DIR/emulator.log"
|
||||
|
||||
# --- wait for adb transport -----------------------------------------------
|
||||
# Poll adb get-state (≠ wait-for-device which blocks indefinitely) so we can
|
||||
# honour AVD_BOOT_TIMEOUT for the whole boot sequence.
|
||||
echo ">>> waiting for device to appear in adb (timeout: ${AVD_BOOT_TIMEOUT}s)"
|
||||
_ELAPSED=0
|
||||
while true; do
|
||||
_STATE="$("${ADB[@]}" get-state 2>/dev/null || true)"
|
||||
if [ "$_STATE" = "device" ] || [ "$_STATE" = "offline" ]; then
|
||||
break
|
||||
fi
|
||||
if [ "$_ELAPSED" -ge "$AVD_BOOT_TIMEOUT" ]; then
|
||||
echo "Device did not appear in adb within ${AVD_BOOT_TIMEOUT}s" >&2
|
||||
echo "emulator log:" >&2
|
||||
tail -20 "$OUT_DIR/emulator.log" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
sleep 3
|
||||
_ELAPSED=$(( _ELAPSED + 3 ))
|
||||
echo " ... ${_ELAPSED}s / ${AVD_BOOT_TIMEOUT}s"
|
||||
done
|
||||
|
||||
# Capture emulator serial (emulator-5554 etc.) so all subsequent adb calls
|
||||
# target the right device when ADB_SERIAL was not set by the caller.
|
||||
if [ -z "${ADB_SERIAL:-}" ]; then
|
||||
_EMU_SERIAL="$(adb devices 2>/dev/null | awk '/^emulator-/{print $1; exit}' | tr -d '\r')"
|
||||
if [ -n "$_EMU_SERIAL" ]; then
|
||||
ADB_SERIAL="$_EMU_SERIAL"
|
||||
ADB=(adb -s "$ADB_SERIAL")
|
||||
echo ">>> detected emulator serial: $ADB_SERIAL"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- wait for full Android boot -------------------------------------------
|
||||
# adb get-state returning "device" means the transport is up, but the
|
||||
# Android framework may still be initialising. Poll sys.boot_completed.
|
||||
echo ">>> waiting for boot_completed (timeout: ${AVD_BOOT_TIMEOUT}s)"
|
||||
_BOOT_ELAPSED=0
|
||||
_BOOT_INTERVAL=5
|
||||
while true; do
|
||||
_BOOT="$("${ADB[@]}" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')"
|
||||
if [ "$_BOOT" = "1" ]; then
|
||||
echo ">>> emulator boot complete"
|
||||
break
|
||||
fi
|
||||
if [ "$_BOOT_ELAPSED" -ge "$AVD_BOOT_TIMEOUT" ]; then
|
||||
echo "Emulator did not finish booting within ${AVD_BOOT_TIMEOUT}s" >&2
|
||||
echo "emulator log:" >&2
|
||||
tail -20 "$OUT_DIR/emulator.log" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
sleep "$_BOOT_INTERVAL"
|
||||
_BOOT_ELAPSED=$(( _BOOT_ELAPSED + _BOOT_INTERVAL ))
|
||||
echo " ... ${_BOOT_ELAPSED}s / ${AVD_BOOT_TIMEOUT}s (boot_completed='${_BOOT}')"
|
||||
done
|
||||
|
||||
# Dismiss the lock screen so later screencap shows the app, not the keyguard.
|
||||
"${ADB[@]}" shell input keyevent 82 2>/dev/null || true
|
||||
|
||||
# Final sanity check — device must be fully ready before we proceed.
|
||||
DEVICE_STATE="$("${ADB[@]}" get-state 2>/dev/null || true)"
|
||||
if [ "$DEVICE_STATE" != "device" ]; then
|
||||
echo "Emulator is running but adb state is '${DEVICE_STATE:-unknown}'." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Device metadata
|
||||
# ---------------------------------------------------------------------------
|
||||
{
|
||||
echo "adb_serial=${ADB_SERIAL:-default}"
|
||||
echo "package=$PACKAGE"
|
||||
echo "activity=$ACTIVITY"
|
||||
echo "device_state=$DEVICE_STATE"
|
||||
"${ADB[@]}" shell getprop ro.product.model 2>/dev/null | tr -d '\r' | sed 's/^/product_model=/'
|
||||
"${ADB[@]}" shell getprop ro.build.version.release 2>/dev/null | tr -d '\r' | sed 's/^/android_release=/'
|
||||
"${ADB[@]}" shell getprop ro.build.version.sdk 2>/dev/null | tr -d '\r' | sed 's/^/android_sdk=/'
|
||||
"${ADB[@]}" shell wm size 2>/dev/null | tr -d '\r' | sed 's/^/wm_size=/'
|
||||
"${ADB[@]}" shell wm density 2>/dev/null | tr -d '\r' | sed 's/^/wm_density=/'
|
||||
} > "$OUT_DIR/device.txt"
|
||||
"${ADB[@]}" shell df -h /data > "$OUT_DIR/df-data-before.txt" 2>&1 || true
|
||||
|
||||
if [ "$BUILD_APK" = "1" ]; then
|
||||
if [ -z "${ABIS:-}" ]; then
|
||||
DEVICE_ABI="$("${ADB[@]}" shell getprop ro.product.cpu.abi 2>/dev/null | tr -d '\r')"
|
||||
case "$DEVICE_ABI" in
|
||||
x86_64|arm64-v8a|armeabi-v7a)
|
||||
export ABIS="$DEVICE_ABI"
|
||||
;;
|
||||
armeabi*)
|
||||
export ABIS="armeabi-v7a"
|
||||
;;
|
||||
*)
|
||||
echo "Could not map device ABI '$DEVICE_ABI'; using build script default ABIS." >&2
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
echo ">>> build Android APK${ABIS:+ (ABIS=$ABIS)}"
|
||||
scripts/build_android_apk.sh
|
||||
fi
|
||||
|
||||
if [ "$INSTALL_APK" = "1" ]; then
|
||||
[ -f "$APK_PATH" ] || {
|
||||
echo "APK not found: $APK_PATH" >&2
|
||||
echo "Set APK_PATH or run with BUILD_APK=1." >&2
|
||||
exit 1
|
||||
}
|
||||
ls -lh "$APK_PATH" > "$OUT_DIR/apk.txt"
|
||||
echo ">>> install $APK_PATH"
|
||||
if ! "${ADB[@]}" install -r -d "$APK_PATH" > "$OUT_DIR/adb-install.txt" 2>&1; then
|
||||
if [ "$RESET_ON_SIGNATURE_MISMATCH" = "1" ] && grep -q "INSTALL_FAILED_UPDATE_INCOMPATIBLE" "$OUT_DIR/adb-install.txt"; then
|
||||
echo ">>> signature mismatch; uninstalling $PACKAGE and retrying install"
|
||||
"${ADB[@]}" uninstall "$PACKAGE" > "$OUT_DIR/adb-uninstall-before-retry.txt" 2>&1 || true
|
||||
if "${ADB[@]}" install -r -d "$APK_PATH" > "$OUT_DIR/adb-install-retry.txt" 2>&1; then
|
||||
cat "$OUT_DIR/adb-install-retry.txt" >> "$OUT_DIR/adb-install.txt"
|
||||
else
|
||||
cat "$OUT_DIR/adb-install.txt" >&2
|
||||
cat "$OUT_DIR/adb-install-retry.txt" >&2
|
||||
"${ADB[@]}" shell df -h /data > "$OUT_DIR/df-data-after-install-failure.txt" 2>&1 || true
|
||||
"${ADB[@]}" shell pm list packages | grep -F "$PACKAGE" > "$OUT_DIR/installed-package.txt" 2>&1 || true
|
||||
echo "APK install retry failed. Diagnostics saved in $OUT_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
cat "$OUT_DIR/adb-install.txt" >&2
|
||||
"${ADB[@]}" shell df -h /data > "$OUT_DIR/df-data-after-install-failure.txt" 2>&1 || true
|
||||
"${ADB[@]}" shell pm list packages | grep -F "$PACKAGE" > "$OUT_DIR/installed-package.txt" 2>&1 || true
|
||||
echo "APK install failed. Diagnostics saved in $OUT_DIR" >&2
|
||||
echo "If the package is already installed and you only need launch/log checks, retry with INSTALL_APK=0." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$FORCE_STOP" = "1" ]; then
|
||||
echo ">>> force-stop $PACKAGE"
|
||||
"${ADB[@]}" shell am force-stop "$PACKAGE" || true
|
||||
fi
|
||||
|
||||
echo ">>> clear logcat"
|
||||
"${ADB[@]}" logcat -c
|
||||
|
||||
if [ "$LAUNCH_APP" = "1" ]; then
|
||||
echo ">>> launch $PACKAGE/$ACTIVITY"
|
||||
"${ADB[@]}" shell am start -n "$PACKAGE/$ACTIVITY" > "$OUT_DIR/am-start.txt"
|
||||
fi
|
||||
|
||||
echo ">>> wait ${WAIT_SECS}s"
|
||||
sleep "$WAIT_SECS"
|
||||
|
||||
PID="$("${ADB[@]}" shell pidof "$PACKAGE" | tr -d '\r' || true)"
|
||||
if [ -z "$PID" ]; then
|
||||
"${ADB[@]}" logcat -d > "$OUT_DIR/logcat.txt" || true
|
||||
echo "app process is not running after launch: $PACKAGE" >&2
|
||||
echo "logcat saved to $OUT_DIR/logcat.txt" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "$PID" > "$OUT_DIR/pid.txt"
|
||||
|
||||
if [ "$CAPTURE_SCREENSHOT" = "1" ]; then
|
||||
echo ">>> capture screenshot"
|
||||
"${ADB[@]}" shell screencap -p "$REMOTE_SCREENSHOT"
|
||||
"${ADB[@]}" pull "$REMOTE_SCREENSHOT" "$OUT_DIR/launch.png" >/dev/null
|
||||
"${ADB[@]}" shell rm -f "$REMOTE_SCREENSHOT" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
echo ">>> capture logcat"
|
||||
"${ADB[@]}" logcat -d > "$OUT_DIR/logcat.txt"
|
||||
grep -iE "panic|fatal|jni|native crash|\bANR\b|exception|error|warn|keystore|safe_area" "$OUT_DIR/logcat.txt" > "$OUT_DIR/log-summary.txt" || true
|
||||
"${ADB[@]}" shell df -h /data > "$OUT_DIR/df-data-after.txt" 2>&1 || true
|
||||
|
||||
# Fatal patterns only. Avoid matching generic "error" because Android logs are
|
||||
# noisy and many non-fatal framework lines contain that word.
|
||||
if grep -iE "fatal exception|jni detected error|native crash|signal [0-9]+|ANR in|Application Not Responding|Input dispatching timed out|thread exiting with uncaught exception|panicked at" "$OUT_DIR/logcat.txt"; then
|
||||
echo "Android smoke test found fatal log output" >&2
|
||||
echo "Artifacts saved in $OUT_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ">>> Android smoke test passed"
|
||||
echo "Artifacts saved in $OUT_DIR"
|
||||
@@ -6,11 +6,15 @@
|
||||
# ndk-build crate that we couldn't isolate; running each Android toolchain
|
||||
# step explicitly gives us a debuggable pipeline.
|
||||
#
|
||||
# Required environment:
|
||||
# ANDROID_HOME Path to Android SDK root
|
||||
# ANDROID_NDK_HOME Path to the specific NDK version
|
||||
# BUILD_TOOLS_VERSION e.g. "34.0.0"
|
||||
# PLATFORM e.g. "android-34"
|
||||
# Environment:
|
||||
# ANDROID_HOME Path to Android SDK root. If unset, common SDK
|
||||
# locations such as ~/Android/Sdk are tried.
|
||||
# ANDROID_NDK_HOME Path to the specific NDK version. If unset, the
|
||||
# newest $ANDROID_HOME/ndk/* directory is used.
|
||||
# BUILD_TOOLS_VERSION e.g. "34.0.0". If unset, newest installed build-tools
|
||||
# version is used.
|
||||
# PLATFORM e.g. "android-34". If unset, newest installed
|
||||
# $ANDROID_HOME/platforms/android-* platform is used.
|
||||
#
|
||||
# Optional environment:
|
||||
# PROFILE "debug" (default) | "release"
|
||||
@@ -19,7 +23,8 @@
|
||||
# fit the runner's disk budget — a full three-ABI
|
||||
# debug build can exceed 25 GB of target/ output.
|
||||
# APK_OUT Output APK path (default: target/$PROFILE/apk/ferrous-solitaire.apk)
|
||||
# KEYSTORE Path to keystore for signing (default: generates a debug keystore)
|
||||
# STRIP_NATIVE_LIBS 1 to strip .so files before packaging (default: 1)
|
||||
# KEYSTORE Path to keystore for signing (default: target/android/debug.keystore)
|
||||
# KEYSTORE_PASS Keystore password (default: "android" for the generated debug keystore)
|
||||
# KEY_ALIAS Key alias (default: "androiddebugkey")
|
||||
# KEY_PASS Key password (default: same as KEYSTORE_PASS)
|
||||
@@ -28,18 +33,63 @@
|
||||
# $APK_OUT Signed, zipaligned APK
|
||||
set -euo pipefail
|
||||
|
||||
: "${ANDROID_HOME:?ANDROID_HOME must be set}"
|
||||
: "${ANDROID_NDK_HOME:?ANDROID_NDK_HOME must be set}"
|
||||
: "${BUILD_TOOLS_VERSION:?BUILD_TOOLS_VERSION must be set}"
|
||||
: "${PLATFORM:?PLATFORM must be set (e.g. android-34)}"
|
||||
infer_latest_dir_name() {
|
||||
local pattern="$1"
|
||||
local latest=""
|
||||
shopt -s nullglob
|
||||
local dirs=( $pattern )
|
||||
shopt -u nullglob
|
||||
if [ ${#dirs[@]} -gt 0 ]; then
|
||||
latest="$(printf '%s\n' "${dirs[@]}" | sort -V | tail -n 1)"
|
||||
basename "$latest"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -z "${ANDROID_HOME:-}" ]; then
|
||||
for candidate in "$HOME/Android/Sdk" "$HOME/Library/Android/sdk" "/opt/android-sdk" "/usr/lib/android-sdk"; do
|
||||
if [ -d "$candidate" ]; then
|
||||
ANDROID_HOME="$candidate"
|
||||
export ANDROID_HOME
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
: "${ANDROID_HOME:?ANDROID_HOME must be set or discoverable under a common SDK path}"
|
||||
|
||||
if [ -z "${ANDROID_NDK_HOME:-}" ]; then
|
||||
NDK_VERSION="$(infer_latest_dir_name "$ANDROID_HOME/ndk/*")"
|
||||
if [ -n "$NDK_VERSION" ]; then
|
||||
ANDROID_NDK_HOME="$ANDROID_HOME/ndk/$NDK_VERSION"
|
||||
export ANDROID_NDK_HOME
|
||||
fi
|
||||
fi
|
||||
: "${ANDROID_NDK_HOME:?ANDROID_NDK_HOME must be set or discoverable under ANDROID_HOME/ndk}"
|
||||
|
||||
if [ -z "${BUILD_TOOLS_VERSION:-}" ]; then
|
||||
BUILD_TOOLS_VERSION="$(infer_latest_dir_name "$ANDROID_HOME/build-tools/*")"
|
||||
export BUILD_TOOLS_VERSION
|
||||
fi
|
||||
: "${BUILD_TOOLS_VERSION:?BUILD_TOOLS_VERSION must be set or discoverable under ANDROID_HOME/build-tools}"
|
||||
|
||||
if [ -z "${PLATFORM:-}" ]; then
|
||||
PLATFORM="$(infer_latest_dir_name "$ANDROID_HOME/platforms/android-*")"
|
||||
export PLATFORM
|
||||
fi
|
||||
: "${PLATFORM:?PLATFORM must be set or discoverable under ANDROID_HOME/platforms}"
|
||||
|
||||
PROFILE="${PROFILE:-debug}"
|
||||
ABIS="${ABIS:-arm64-v8a armeabi-v7a x86_64}"
|
||||
APK_OUT="${APK_OUT:-target/${PROFILE}/apk/ferrous-solitaire.apk}"
|
||||
STRIP_NATIVE_LIBS="${STRIP_NATIVE_LIBS:-1}"
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
echo ">>> Android SDK: $ANDROID_HOME"
|
||||
echo ">>> Android NDK: $ANDROID_NDK_HOME"
|
||||
echo ">>> Build tools: $BUILD_TOOLS_VERSION"
|
||||
echo ">>> Platform: $PLATFORM"
|
||||
|
||||
BT="$ANDROID_HOME/build-tools/$BUILD_TOOLS_VERSION"
|
||||
PLATFORM_JAR="$ANDROID_HOME/platforms/$PLATFORM/android.jar"
|
||||
MANIFEST="solitaire_app/android/AndroidManifest.xml"
|
||||
@@ -69,6 +119,24 @@ fi
|
||||
echo ">>> cargo ndk ${CARGO_NDK_ARGS[*]}"
|
||||
cargo ndk "${CARGO_NDK_ARGS[@]}"
|
||||
|
||||
if [ "$STRIP_NATIVE_LIBS" = "1" ]; then
|
||||
LLVM_STRIP=""
|
||||
shopt -s nullglob
|
||||
STRIP_CANDIDATES=( "$ANDROID_NDK_HOME"/toolchains/llvm/prebuilt/*/bin/llvm-strip )
|
||||
shopt -u nullglob
|
||||
if [ ${#STRIP_CANDIDATES[@]} -gt 0 ]; then
|
||||
LLVM_STRIP="${STRIP_CANDIDATES[0]}"
|
||||
fi
|
||||
if [ -z "$LLVM_STRIP" ]; then
|
||||
echo "llvm-strip not found under ANDROID_NDK_HOME; native libraries will remain unstripped" >&2
|
||||
else
|
||||
echo ">>> strip native libraries with $LLVM_STRIP"
|
||||
find "$STAGING/lib" -name '*.so' -print0 | while IFS= read -r -d '' so; do
|
||||
"$LLVM_STRIP" --strip-debug "$so"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- 2. compile + link resources and manifest ------------------------------
|
||||
if [ -d "$RES_DIR" ]; then
|
||||
echo ">>> aapt2 compile resources"
|
||||
@@ -120,11 +188,15 @@ rm -f "$STAGING/app-unsigned.apk"
|
||||
|
||||
# --- 5. sign ---------------------------------------------------------------
|
||||
if [ -z "${KEYSTORE:-}" ]; then
|
||||
# Generate a deterministic debug keystore on the fly.
|
||||
KEYSTORE="$STAGING/debug.keystore"
|
||||
KEYSTORE_PASS="${KEYSTORE_PASS:-android}"
|
||||
KEY_ALIAS="${KEY_ALIAS:-androiddebugkey}"
|
||||
KEY_PASS="${KEY_PASS:-$KEYSTORE_PASS}"
|
||||
KEYSTORE="target/android/debug.keystore"
|
||||
fi
|
||||
|
||||
KEYSTORE_PASS="${KEYSTORE_PASS:-android}"
|
||||
KEY_ALIAS="${KEY_ALIAS:-androiddebugkey}"
|
||||
KEY_PASS="${KEY_PASS:-$KEYSTORE_PASS}"
|
||||
|
||||
if [ ! -f "$KEYSTORE" ]; then
|
||||
mkdir -p "$(dirname "$KEYSTORE")"
|
||||
echo ">>> generating debug keystore at $KEYSTORE"
|
||||
keytool -genkeypair -v \
|
||||
-keystore "$KEYSTORE" \
|
||||
|
||||
Reference in New Issue
Block a user