261 lines
8.3 KiB
Markdown
261 lines
8.3 KiB
Markdown
# Android build — developer setup
|
|
|
|
This doc captures the toolchain install + build invocation for the
|
|
Android target. Steps are runnable on a fresh Debian 13 (trixie) box;
|
|
later sections document physical-device validation, supported platform
|
|
surfaces, and remaining Android follow-ups.
|
|
|
|
> **Status (2026-06-09):** Android build plumbing, app-directory storage,
|
|
> JNI keystore wiring, and safe-area layout fixes have landed. The remaining
|
|
> release gate is a physical-device smoke test; AVD tap injection does not
|
|
> exercise the real touch path reliably enough for launch verification.
|
|
|
|
---
|
|
|
|
## 1. Toolchain install (Debian 13 / trixie)
|
|
|
|
Run as one block. Will pull ~15-20 GB of disk between APT, the SDK,
|
|
the NDK, the system image, and Rust target sysroots. Requires sudo.
|
|
|
|
```bash
|
|
# 1. JDK 21 (Android tooling needs JDK 17+; Debian 13 default is 21).
|
|
sudo apt update && sudo apt install -y openjdk-21-jdk-headless unzip wget
|
|
|
|
# 2. SDK directory + Google's cmdline-tools bootstrap.
|
|
export ANDROID_HOME="$HOME/Android/Sdk"
|
|
mkdir -p "$ANDROID_HOME/cmdline-tools"
|
|
wget -O /tmp/cmdline-tools.zip \
|
|
https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
|
|
unzip -q /tmp/cmdline-tools.zip -d "$ANDROID_HOME/cmdline-tools"
|
|
mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
|
|
rm /tmp/cmdline-tools.zip
|
|
|
|
# 3. Persist env vars.
|
|
{
|
|
echo ''
|
|
echo '# Android dev'
|
|
echo 'export ANDROID_HOME="$HOME/Android/Sdk"'
|
|
echo 'export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/26.3.11579264"'
|
|
echo 'export JAVA_HOME="$(dirname $(dirname $(readlink -f $(which java))))"'
|
|
echo 'export PATH="$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator"'
|
|
} >> ~/.bashrc
|
|
source ~/.bashrc
|
|
|
|
# 4. Accept SDK licences (interactive prompts answered by `yes |`).
|
|
yes | sdkmanager --licenses
|
|
|
|
# 5. Platform packages — ~5 GB.
|
|
sdkmanager \
|
|
"platform-tools" \
|
|
"platforms;android-34" \
|
|
"build-tools;34.0.0" \
|
|
"ndk;26.3.11579264" \
|
|
"emulator" \
|
|
"system-images;android-34;google_apis;x86_64"
|
|
|
|
# 6. AVD for testing (one-time).
|
|
echo no | avdmanager create avd \
|
|
-n bevy_test \
|
|
-k "system-images;android-34;google_apis;x86_64" \
|
|
-d pixel_7
|
|
|
|
# 7. Rust cross-compile targets.
|
|
rustup target add \
|
|
aarch64-linux-android \
|
|
armv7-linux-androideabi \
|
|
x86_64-linux-android \
|
|
i686-linux-android
|
|
|
|
# 8. cargo-apk.
|
|
cargo install cargo-apk
|
|
```
|
|
|
|
Sanity:
|
|
|
|
```bash
|
|
java --version | head -1 # openjdk 21.0.x
|
|
adb --version | head -1 # 35.x or higher
|
|
sdkmanager --list_installed | head # build-tools, emulator, ndk, platforms, system-images
|
|
avdmanager list avd | head # bevy_test
|
|
rustup target list --installed | grep android # 4 targets
|
|
cargo apk --help | head -5
|
|
```
|
|
|
|
If `sdkmanager --version` errors with `JAVA_HOME is not set`, the env
|
|
section in step 3 didn't apply to your shell — `source ~/.bashrc`
|
|
again or open a new terminal.
|
|
|
|
### Optional: emulator runtime libs
|
|
|
|
The Android emulator is dynamically linked against X11/GL/audio. If
|
|
`emulator -list-avds` works but `emulator -avd bevy_test` complains
|
|
about `libX11.so.6`, install:
|
|
|
|
```bash
|
|
sudo apt install -y \
|
|
libx11-6 libxcursor1 libxrandr2 libxi6 libxinerama1 libxxf86vm1 \
|
|
libgl1 libnss3 libpulse0 libxcomposite1
|
|
```
|
|
|
|
Headless emulator launch:
|
|
|
|
```bash
|
|
emulator -avd bevy_test -no-window -gpu swiftshader_indirect &
|
|
adb wait-for-device && adb devices
|
|
# Stop later:
|
|
# adb -s emulator-5554 emu kill
|
|
```
|
|
|
|
Headless + software rendering is fine for "does it boot" smoke tests
|
|
but useless for perf measurement — use a physical Pixel-class device
|
|
over USB for real numbers.
|
|
|
|
---
|
|
|
|
## 2. Build the APK
|
|
|
|
```bash
|
|
cargo apk build -p solitaire_app --target x86_64-linux-android
|
|
```
|
|
|
|
Output:
|
|
|
|
```
|
|
target/debug/apk/ferrous-solitaire.apk
|
|
```
|
|
|
|
Targets shipped via `[package.metadata.android].build_targets` in
|
|
`solitaire_app/Cargo.toml`:
|
|
|
|
| Target | Use |
|
|
|--------|-----|
|
|
| `aarch64-linux-android` | Real phones (modern 64-bit ARM) |
|
|
| `armv7-linux-androideabi` | Older 32-bit ARM phones |
|
|
| `x86_64-linux-android` | The `bevy_test` AVD on this dev box |
|
|
|
|
Build any of them with `--target <triple>`.
|
|
|
|
### Known cosmetic warning
|
|
|
|
After the APK is signed cargo-apk panics with:
|
|
|
|
```
|
|
thread 'main' panicked: Bin is not compatible with Cdylib
|
|
```
|
|
|
|
This happens AFTER the APK is on disk and signed. cargo-apk tries to
|
|
also wrap the desktop `[[bin]]` target alongside the `[lib]`. The APK
|
|
is valid — the panic is cosmetic. **Always use `--lib`**, which is the
|
|
canonical build command (see `CLAUDE.md §15.1`):
|
|
|
|
```bash
|
|
cargo apk build -p solitaire_app --lib
|
|
```
|
|
|
|
Root cause: upstream cargo-apk bug — it does not skip `[[bin]]` targets
|
|
when building for Android. No in-repo fix is possible; `--lib` is the
|
|
accepted workaround.
|
|
|
|
---
|
|
|
|
## 3. Install + run
|
|
|
|
Physical device:
|
|
|
|
```bash
|
|
adb devices # confirm connection
|
|
adb install -r target/debug/apk/ferrous-solitaire.apk
|
|
adb shell am start -n com.ferrousapp.solitaire/android.app.NativeActivity
|
|
adb logcat | grep -iE "RustStdoutStderr|solitaire|panic"
|
|
```
|
|
|
|
Emulator:
|
|
|
|
```bash
|
|
emulator -avd bevy_test -no-window -gpu swiftshader_indirect &
|
|
adb wait-for-device
|
|
adb install target/debug/apk/ferrous-solitaire.apk
|
|
# ... same start + logcat steps as above.
|
|
```
|
|
|
|
If `adb install` errors with `INSTALL_FAILED_NO_MATCHING_ABIS`, the
|
|
emulator is x86_64 but the APK was built for arm — rebuild with the
|
|
`x86_64-linux-android` target, or add an x86_64 system image to the
|
|
AVD.
|
|
|
|
---
|
|
|
|
## 4. Physical-device smoke test
|
|
|
|
Run this on a real phone, preferably a modern 64-bit ARM device with gesture
|
|
navigation enabled.
|
|
|
|
Build and install:
|
|
|
|
```bash
|
|
cargo apk build -p solitaire_app --target aarch64-linux-android --lib
|
|
adb install -r target/debug/apk/ferrous-solitaire.apk
|
|
adb logcat -c
|
|
adb shell am start -n com.ferrousapp.solitaire/android.app.NativeActivity
|
|
adb logcat | grep -iE "RustStdoutStderr|solitaire|panic|WindowInsets"
|
|
```
|
|
|
|
Pass criteria:
|
|
|
|
- App launches without panic or ANR.
|
|
- Safe-area insets arrive after the first few frames and shift HUD/modal
|
|
content away from the status and gesture bars.
|
|
- Every modal's Done button remains above the gesture bar:
|
|
Settings, Help, Pause, Win Summary, and Leaderboard-related dialogs.
|
|
- Drag-and-drop works on tableau, waste, foundation, and stock/recycle paths.
|
|
- Tap-to-select and one-tap modes both respond correctly on card stacks.
|
|
- Leaderboard panel opens, "Set Name" saves, and the "Public name" label updates
|
|
while the panel remains open.
|
|
- Rotate the device once, then repeat one modal and one drag operation.
|
|
- Close and relaunch the app; settings/progress still load.
|
|
|
|
Record the device model, Android version, APK commit, and pass/fail notes in the
|
|
release notes or session handoff. If a failure occurs, keep the filtered logcat
|
|
and note the exact screen/control path that reproduced it.
|
|
|
|
---
|
|
|
|
## 5. Platform support matrix
|
|
|
|
Desktop-only crates and call sites are gated so the workspace cross-compiles.
|
|
Each gate is documented at its call site.
|
|
|
|
| Surface | Desktop | Android |
|
|
|---------|---------|---------|
|
|
| Bevy windowing | x11 + wayland | `android-native-activity` (NativeActivity glue) |
|
|
| Clipboard ("Copy share link") | `arboard` writes URL | Toast surfaces the URL inline |
|
|
| OS keychain (JWT tokens) | `keyring` v4 → Secret Service / Keychain / Credential Store | Android Keystore via JNI |
|
|
| Data directory | Platform data dir | Android app files dir |
|
|
| App entry point | `bin` target → `solitaire_app::run()` | `cdylib` target loaded by NativeActivity |
|
|
|
|
Remaining Android follow-ups:
|
|
|
|
- Touch UX pass — hit-target sizes, modal scaling on small screens,
|
|
app lifecycle (suspend / resume), font scaling.
|
|
- JNI ClipboardManager for share links.
|
|
- Google Play Games sign-in (the `solitaire_gpgs` crate referenced
|
|
in older docs doesn't yet exist).
|
|
|
|
---
|
|
|
|
## 6. Iteration loop
|
|
|
|
```bash
|
|
# Edit code…
|
|
cargo build -p solitaire_app # desktop sanity
|
|
cargo clippy --workspace --all-targets -- -D warnings # gate
|
|
cargo test --workspace # gate
|
|
cargo apk build -p solitaire_app --target x86_64-linux-android --lib
|
|
adb install -r target/debug/apk/ferrous-solitaire.apk # `-r` reinstalls
|
|
adb logcat -c && adb shell am start -n com.ferrousapp.solitaire/android.app.NativeActivity
|
|
adb logcat | grep -iE "RustStdoutStderr|solitaire"
|
|
```
|
|
|
|
`adb logcat` is the canonical way to see Bevy / Rust panic output —
|
|
they end up in the `RustStdoutStderr` tag.
|