The Android image: JDK, SDK, NDK, Gradle and the app's dependencies, offline (closes #19) c8917cf1f931
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key c567a958fa63
- When
- · 2026-10-26 16:37 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
c8917cf1f9310b5fbb0f6a44f016b6b956ab702b689992917cade6ceac375d65- Depends on
- 50354f576ea6 a63d3dcd2aef c3db750ed96e e0918b887850 eb5c4db673e5
- Depended on by
- 8a4f5174526a 72e9cfca63b5 5db09ccf23ab
- In bookmarks
- main
- In tags
- v0.5.0 v0.5.1 v0.5.2 v0.5.3
- Edits
- 6 atoms, 5 tree operations, 6 files
Browse the tree at this change
added images/android/Dockerfile +58 −0
| 1 | + | # The OxygenFlow Android image (oxygenflow#19): a JDK, the Android SDK and |
|
| 2 | + | # NDK, Gradle, cargo-ndk, the crates and Gradle dependencies of the |
|
| 3 | + | # OxygenHub app, so its build and tests run in a job with no network. |
|
| 4 | + | # Built by ./build.sh, which fetches and checks the downloads first. |
|
| 5 | + | FROM rust:1.97.1-slim-bookworm |
|
| 6 | + | ||
| 7 | + | RUN apt-get update \ |
|
| 8 | + | && apt-get install -y --no-install-recommends ca-certificates openjdk-17-jdk-headless unzip make git pkg-config \ |
|
| 9 | + | && rm -rf /var/lib/apt/lists/* |
|
| 10 | + | ||
| 11 | + | # The guest's PATH is fixed and has no /usr/local/cargo/bin, and a rustup |
|
| 12 | + | # proxy would need RUSTUP_HOME: link the toolchain's own binaries instead. |
|
| 13 | + | RUN rustup target add aarch64-linux-android x86_64-linux-android \ |
|
| 14 | + | && cargo install --locked --version 4.1.2 --root /opt/cargo-ndk cargo-ndk \ |
|
| 15 | + | && rm -rf /usr/local/cargo/registry /usr/local/cargo/git \ |
|
| 16 | + | && tc=$(ls -d /usr/local/rustup/toolchains/*/ | head -1) \ |
|
| 17 | + | && for b in cargo rustc rustdoc; do ln -sf "$tc/bin/$b" /usr/local/bin/$b; done \ |
|
| 18 | + | && ln -sf /opt/cargo-ndk/bin/cargo-ndk /usr/local/bin/cargo-ndk |
|
| 19 | + | ||
| 20 | + | # The SDK: Google's command-line tools (checked by build.sh), then the |
|
| 21 | + | # platform, build tools and NDK the app names, licences accepted here. |
|
| 22 | + | ENV ANDROID_HOME=/opt/android-sdk JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 |
|
| 23 | + | COPY dl/cmdline-tools.zip /tmp/cmdline-tools.zip |
|
| 24 | + | RUN mkdir -p "$ANDROID_HOME/cmdline-tools" \ |
|
| 25 | + | && unzip -q /tmp/cmdline-tools.zip -d /tmp/ct \ |
|
| 26 | + | && mv /tmp/ct/cmdline-tools "$ANDROID_HOME/cmdline-tools/latest" \ |
|
| 27 | + | && rm -rf /tmp/ct /tmp/cmdline-tools.zip \ |
|
| 28 | + | && yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses >/dev/null \ |
|
| 29 | + | && "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --install \ |
|
| 30 | + | "platforms;android-37.0" "build-tools;36.0.0" "ndk;30.0.16248370" >/dev/null |
|
| 31 | + | ||
| 32 | + | # Gradle itself (checked by build.sh): jobs run `gradle`, not the wrapper, |
|
| 33 | + | # which would download its distribution. |
|
| 34 | + | ARG GRADLE_VERSION |
|
| 35 | + | COPY dl/gradle.zip /tmp/gradle.zip |
|
| 36 | + | RUN unzip -q /tmp/gradle.zip -d /opt && rm /tmp/gradle.zip \ |
|
| 37 | + | && ln -sf /opt/gradle-${GRADLE_VERSION}/bin/gradle /usr/local/bin/gradle |
|
| 38 | + | ||
| 39 | + | # The app's crates, vendored: cargo reads /.cargo/config.toml for any |
|
| 40 | + | # directory under /, so rust-core builds offline. |
|
| 41 | + | COPY vendor /opt/vendor |
|
| 42 | + | COPY cargo-config.toml /.cargo/config.toml |
|
| 43 | + | ||
| 44 | + | # Gradle's dependencies: build the app once here, with the network, and |
|
| 45 | + | # keep what Gradle fetched as a read-only cache (GRADLE_RO_DEP_CACHE) that |
|
| 46 | + | # a job's own, writable GRADLE_USER_HOME reads first. A failing test does |
|
| 47 | + | # not matter here; only the downloads do. |
|
| 48 | + | COPY app /tmp/app |
|
| 49 | + | RUN cd /tmp/app \ |
|
| 50 | + | && printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties \ |
|
| 51 | + | && (GRADLE_USER_HOME=/opt/gradle-home gradle --no-daemon -q --continue \ |
|
| 52 | + | :app:assembleDebug :app:testDebugUnitTest :app:lintDebug :app:validateDebugScreenshotTest || true) \ |
|
| 53 | + | && test -d /opt/gradle-home/caches/modules-2 \ |
|
| 54 | + | && cd / && rm -rf /tmp/app /opt/gradle-home/daemon /opt/gradle-home/native /opt/gradle-home/wrapper \ |
|
| 55 | + | /opt/gradle-home/caches/[0-9]* /opt/gradle-home/caches/transforms-* /opt/gradle-home/caches/build-cache-* \ |
|
| 56 | + | /opt/gradle-home/caches/journal-* /opt/gradle-home/caches/jars-* \ |
|
| 57 | + | && rm -f /opt/gradle-home/caches/modules-2/*.lock /opt/gradle-home/caches/modules-2/gc.properties \ |
|
| 58 | + | && chmod -R a+rX /opt/gradle-home "$ANDROID_HOME" /opt/vendor |
added images/android/README.md +47 −0
| 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 "isolated" |
|
| 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 | + | ``` |
added images/android/build.sh +41 −0
| 1 | + | #!/bin/sh |
|
| 2 | + | # Build the OxygenFlow Android image and write it as a root file system |
|
| 3 | + | # tarball for `flow rootfs import` (oxygenflow#19). Run from any directory; |
|
| 4 | + | # the app is expected next to this repository (../oxygenhub-android), with |
|
| 5 | + | # its subrepos checked out. Rebuild when the app's SDK, NDK, Gradle or |
|
| 6 | + | # dependency versions change, or rust-core's Cargo.lock does. |
|
| 7 | + | set -eu |
|
| 8 | + | here=$(cd "$(dirname "$0")" && pwd) |
|
| 9 | + | dev=$(cd "$here/../../.." && pwd) |
|
| 10 | + | app="$dev/oxygenhub-android" |
|
| 11 | + | out=${1:-$here/oxygenflow-android.tar} |
|
| 12 | + | # Command-line tools 23.0, as Google's repository2-3.xml lists them. |
|
| 13 | + | CMDLINE_TOOLS=commandlinetools-linux-16111833_latest.zip |
|
| 14 | + | CMDLINE_TOOLS_SHA1=e025545c62a8e64c7559119566a569fb1dec5f60 |
|
| 15 | + | GRADLE_VERSION=9.6.0 |
|
| 16 | + | ||
| 17 | + | mkdir -p "$here/dl" |
|
| 18 | + | if [ ! -f "$here/dl/cmdline-tools.zip" ]; then |
|
| 19 | + | curl -fsSL "https://dl.google.com/android/repository/$CMDLINE_TOOLS" -o "$here/dl/cmdline-tools.zip" |
|
| 20 | + | fi |
|
| 21 | + | echo "$CMDLINE_TOOLS_SHA1 $here/dl/cmdline-tools.zip" | sha1sum -c - >/dev/null |
|
| 22 | + | if [ ! -f "$here/dl/gradle.zip" ]; then |
|
| 23 | + | curl -fsSL "https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip" -o "$here/dl/gradle.zip" |
|
| 24 | + | fi |
|
| 25 | + | # Gradle publishes each distribution's SHA-256 beside it. |
|
| 26 | + | want=$(curl -fsSL "https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip.sha256") |
|
| 27 | + | echo "$want $here/dl/gradle.zip" | sha256sum -c - >/dev/null |
|
| 28 | + | ||
| 29 | + | # The app, without build output, and rust-core's crates vendored. |
|
| 30 | + | rm -rf "$here/app" "$here/vendor" |
|
| 31 | + | mkdir -p "$here/app" |
|
| 32 | + | tar -C "$app" --exclude='./build' --exclude='./app/build' --exclude='./rust-core/target' \ |
|
| 33 | + | --exclude='./.gradle' --exclude='./.oxy' --exclude='./local.properties' -cf - . | tar -C "$here/app" -xf - |
|
| 34 | + | cargo vendor --locked --versioned-dirs --manifest-path "$app/rust-core/Cargo.toml" "$here/vendor" >/dev/null |
|
| 35 | + | chmod -R a+rX "$here/vendor" "$here/app" |
|
| 36 | + | docker build -q -t oxygenflow-android --build-arg GRADLE_VERSION=$GRADLE_VERSION "$here" |
|
| 37 | + | id=$(docker create oxygenflow-android) |
|
| 38 | + | docker export "$id" -o "$out" |
|
| 39 | + | docker rm "$id" >/dev/null |
|
| 40 | + | rm -rf "${here:?}/app" "${here:?}/vendor" |
|
| 41 | + | echo "$out" |
added images/android/cargo-config.toml +9 −0
| 1 | + | # Crates come from the image, never the network (OxygenFlow jobs are isolated). |
|
| 2 | + | [source.crates-io] |
|
| 3 | + | replace-with = "vendored" |
|
| 4 | + | ||
| 5 | + | [source.vendored] |
|
| 6 | + | directory = "/opt/vendor" |
|
| 7 | + | ||
| 8 | + | [net] |
|
| 9 | + | offline = true |
modified .oxyignore +4 −0
| … | 2 unchanged lines |
||
| 3 | 3 | images/rust/vendor/ |
|
| 4 | 4 | images/rust/*.tar |
|
| 5 | 5 | images/rust/npm-cache/ |
|
| 6 | + | images/android/dl/ |
|
| 7 | + | images/android/app/ |
|
| 8 | + | images/android/vendor/ |
|
| 9 | + | images/android/*.tar |
modified CHANGELOG.md +1 −0
| … | 3 unchanged lines |
||
| 4 | 4 | ||
| 5 | 5 | ## Unreleased |
|
| 6 | 6 | ||
| 7 | + | - **The Android image (#19).** `images/android`: OpenJDK 17, the Android SDK (command-line tools 23.0, platform 37.0, build-tools 36.0.0, NDK 30.0), Gradle 9.6.0, Rust with the Android targets and cargo-ndk, and the OxygenHub app's crates and Gradle dependencies, so the app builds and tests offline. Each download is checked against its published hash. |
|
| 7 | 8 | - **`flow runner bench` says when load is not measured (#83):** a Windows runner printed `load 0.00`; it now says `load not measured here`. Its kernel line reads `Windows 10.0.26300` (checked with 0.4.0 in the Windows VM). |
|
| 8 | 9 | ## 0.4.0 (2026-10-26) |
|
| 9 | 10 | ||
| … | 107 unchanged lines |