OXYGENHUB

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