| 1 |
# The OxygenFlow Android image |
| 2 |
|
| 3 |
A root file system for building and testing the OxygenHub Android app (`oxygenhub-android`) in a job with no network (oxygenflow#19): |
| 4 |
|
| 5 |
- OpenJDK 17 (`JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64`). |
| 6 |
- The Android SDK in `/opt/android-sdk` (`ANDROID_HOME`): command-line tools 23.0 (checked against Google's published SHA-1), `platforms;android-37.0`, `build-tools;36.0.0` and `ndk;30.0.16248370`, licences accepted. |
| 7 |
- Gradle 9.6.0 as `gradle` (checked against its published SHA-256). Jobs run `gradle --offline`, not `./gradlew`, which would download its distribution. |
| 8 |
- Rust 1.97.1 with the `aarch64-linux-android` and `x86_64-linux-android` targets and cargo-ndk 4.1.2, for the app's `rust-core`. |
| 9 |
- `rust-core`'s crates, vendored in `/opt/vendor`; `/.cargo/config.toml` points cargo at them, as in the rust image. |
| 10 |
- The app's Gradle dependencies in `/opt/gradle-home/caches`, fetched by building the app once while the image is built. |
| 11 |
|
| 12 |
The Gradle cache is read-only in the guest. A job points `GRADLE_RO_DEP_CACHE` at it and gives Gradle its own writable `GRADLE_USER_HOME`, so Gradle reads dependencies from the image and writes everything else under `/tmp`. |
| 13 |
|
| 14 |
## Building and importing |
| 15 |
|
| 16 |
``` |
| 17 |
images/android/build.sh /path/to/oxygenflow-android.tar |
| 18 |
scp /path/to/oxygenflow-android.tar claude@earth:/home/claude/flow/dl/ |
| 19 |
ssh claude@earth 'cd ~/flow && FLOW_HOME=~/flow bin/flow rootfs import dl/oxygenflow-android.tar' |
| 20 |
``` |
| 21 |
|
| 22 |
`build.sh` expects the app next to this repository (`../oxygenhub-android`, subrepos checked out). It downloads and checks the command-line tools and Gradle into `images/android/dl/`, copies the app without its build output, vendors `rust-core`'s crates, then builds the image with Docker and exports it as a plain tarball. The tarball is about 6 GB, so build it where the disk has room (Earth) and delete it once imported. |
| 23 |
|
| 24 |
Rebuild and re-import when the app changes its SDK, NDK, Gradle or dependency versions, or `rust-core/Cargo.lock` changes, then pin the new hash in the app's pipeline. A dependency missing from the image fails the job with Gradle's "No cached version … available for offline mode", or cargo's "no matching package". |
| 25 |
|
| 26 |
## Using it |
| 27 |
|
| 28 |
```kdl |
| 29 |
sandbox "android" { |
| 30 |
runtime "microvm" |
| 31 |
rootfs "cas://blob/<hash printed by flow rootfs import>" |
| 32 |
memory-limit "8GiB" |
| 33 |
cpu-limit 4 |
| 34 |
network "loopback" |
| 35 |
} |
| 36 |
|
| 37 |
job "test" { |
| 38 |
sandbox "android" |
| 39 |
env { |
| 40 |
ANDROID_HOME "/opt/android-sdk" |
| 41 |
JAVA_HOME "/usr/lib/jvm/java-17-openjdk-amd64" |
| 42 |
GRADLE_RO_DEP_CACHE "/opt/gradle-home/caches" |
| 43 |
GRADLE_USER_HOME "/tmp/gradle" |
| 44 |
} |
| 45 |
exec "gradle" "--offline" "--no-daemon" ":app:testDebugUnitTest" |
| 46 |
} |
| 47 |
``` |
| 48 |
|
| 49 |
Use `network "loopback"`, not `"isolated"`: with no interface up, Gradle stops with "Could not determine a usable wildcard IP for this machine". Loopback keeps the job offline. |