The APK has been debug-signed with a key generated on the spot, which is right for putting a build on a test device and useless for anything else: a different signature every run, so nothing can ever update in place. Four secrets now select a real signature -- ANDROID_KEYSTORE_BASE64 and its password, alias and key password. The names are JellyTau's, because that repo already signs its Android build this way against this same runner and one convention across both is one thing to remember. Absence of the secrets is not an error. A fork or a branch build has no access to them and should still produce an installable APK, so the debug path stays exactly as it was. The reverse is an error: if a keystore is supplied and cannot be read, the build fails rather than quietly falling back to a debug key, because a release that is silently debug-signed is worse than no release. Passwords reach apksigner and keytool as `env:`, never `pass:`. `pass:` puts the password in the process table for anything on the box to read. The keystore is written to a 0700 mktemp directory and never into the workspace, which is both what actions/cache saves and what the upload step globs. Also: upload-artifact drops from v4 to v3. v4 was a guess about what this Gitea supports. v3 is what JellyTau uploads its APK with on this runner today, which makes it the version known to work rather than the one that ought to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Android build environment
Reproducible container for cross-compiling DarkRoom to Android. CI and local builds use the same image, so a break appears in one place rather than two.
Use
# Cross-compile for a device ABI
./docker/android/build.sh cargo ndk -t arm64-v8a build --release
# All four ABIs
./docker/android/build.sh cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 -t x86 build --release
# Type-check without linking
./docker/android/build.sh cargo check --target aarch64-linux-android
# Interactive shell
./docker/android/build.sh
# After editing the Dockerfile
./docker/android/build.sh --rebuild
Prefers podman, falls back to docker. First run builds the image, which takes several minutes;
after that it is cached.
In CI
.gitea/workflows/android-image.yml builds this image and pushes it to
gitea.tourolle.paris/dtourolle/darkroom-android, which the Android job then runs inside. It is
called on every push and finishes in seconds unless something here changed — the image is tagged
with the git tree hash of docker/android/, so a rebuild happens when and only when a file in this
directory does.
Nothing needs pushing by hand. Editing the Dockerfile is the trigger; latest follows
automatically. To force a rebuild without a content change, run the workflow from the Gitea UI with
force set to true.
The job runs on the host runner rather than in a container, because it needs the Docker daemon and the runner host's cached registry credentials.
Pinned versions
| Component | Version | Why this one |
|---|---|---|
| JDK | 17 | AGP's stable target. The host's JDK 25 breaks Gradle. |
| Compile SDK | API 36 | Required for Play distribution |
| Min API | 28 (Android 9) | Floor where Vulkan support is dependable (NFR-COMPAT-1) |
| Build tools | 36.0.0 | Matches compile SDK |
| NDK | 27.2.12479018 (r27c) | Current LTS |
| Rust | 1.92.0 | Slint 1.17 requires it; cargo-ndk 4.1.x needs ≥ 1.86 |
| cargo-ndk | 4.1.2 |
Bumping any of these is a reviewable change, not silent drift.
Compile SDK versus min API
These are different and both matter. ANDROID_API (36) is what the SDK compiles against — the
newest APIs available. MIN_API (28) is what native code links against, and determines the
oldest device that can run the result.
cargo-ndk defaults to API 21 if not told otherwise, which is far below what this app needs.
CARGO_NDK_PLATFORM and the per-target linker paths both pin it to MIN_API. Verify with:
file target/aarch64-linux-android/release/*.so
# → "... for Android 28, built by NDK r27c"
Caching
The cargo registry and target directory persist in ~/.cache/darkroom-android, so rebuilds do not
re-download the crate index. Clear with:
rm -rf ~/.cache/darkroom-android
The Android target dir is kept separate from the host's target/ — sharing one directory between
host and container builds causes constant rebuilds as they invalidate each other's fingerprints.
What is not here
No emulator, no adb device access. This image cross-compiles; it does not run or deploy. Device
testing (spikes S2 and S10, which need two GPU vendors) happens on real hardware — the emulator's
GPU behaviour is not representative of Adreno or Mali, which is precisely what those spikes exist
to test.