Files
DarkRoom/docker/android/README.md
T
dtourolleandClaude Opus 5 2d95807542 Package the models on every platform, not just the phone
The Android bundling landed the weights under that platform's asset directory,
which was the wrong home the moment a second packager wanted them. `makepkg -si`
produced a desktop install with no model at all — the same "no face model is
installed" the phone used to show, for the same reason: nothing put the files
anywhere the app looks.

So `models/face/` at the root is the one copy, and both packagers read it:
assemble-apk.sh bundles it as APK assets, and the PKGBUILD installs it to
/usr/share/darkroom/models. Both refuse an LFS pointer rather than shipping a
130-byte file that fails inside the graph loader on a user's machine.

`face_models` now searches three places, most specific first: the account's own
directory, the shared user directory, then $XDG_DATA_DIRS. So a packaged pair is
found automatically and a pair the user placed by hand still outranks it — which
is what keeps a deliberate choice of weights from being overridden by an
upgrade.

$XDG_DATA_DIRS rather than a hard-coded /usr/share: that is the variable a
distribution, a prefix install or a Nix-style store already sets to say where
its data went, and its documented default is exactly the two paths that would
otherwise have been hard-coded. Empty on Android, which has no such directories
— there the APK's copy is unpacked into the shared user directory instead,
because an asset inside a package is not a path anything can read from.

Verified: the APK still carries both models at assets/models/, the PKGBUILD
parses and installs from the new path, 467 tests pass.

Includes the pkgver 0.6.0 → 0.7.0 bump that was already sitting uncommitted in
the working tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 18:05:23 +02:00

100 lines
3.9 KiB
Markdown

# 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
```bash
# 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.
## Face models
`package.sh` bundles `models/face/` into the APK, and the entry point unpacks it on first launch.
That directory is shared with the Arch package rather than owned by this platform. **The models are in LFS**, so a clone without `git lfs pull` has 130-byte
pointers there; the build detects that and stops rather than shipping them.
This exists because Android offers no other route to a model: the directory the app reads from is
inside app-private storage, `run-as` needs a debuggable build, and the app has no picker and no
fetch. docs/faces.md §2.2a is the decision and its limits — these files come back out before anything
is published.
## 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:
```bash
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:
```bash
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.