Face indexing was compiled into the APK all along — dr-ui takes dr-face with `inference` on every target, so SCRFD, alignment, MBF, calibration and clustering were all in there. What was missing was the weights, and on Android there was no way to supply them. Route C (docs/faces.md §2.2) says the user obtains the model and the app loads it. On a desktop that is a real gesture: drop two files in ~/.local/share/darkroom/models/ and indexing starts working. On Android it is not a gesture at all. `internal_data_path` is app-private, `run-as` needs a debuggable build, and the in-app fetch route C specifies was never built — so the settings page reported "no face model is installed" on every launch with nothing behind the message. Not "off until you supply weights"; off. So the shape-fixed pair goes into LFS under the APK's assets, assemble-apk.sh copies it into the package, and `android_main` unpacks it to the shared models directory before anything asks whether a model is present. Three things that are not incidental: The models directory is now shared across accounts rather than per-account. Weights are identified by `faces.model_id`, not by who is signed in, so two accounts had no reason to hold two copies — and the unpack runs before any session exists to key a per-account path off. `face_models` still prefers a per-account directory when one is populated, so anyone mid-migration keeps the ability to pin one library to its own pair. The unpack writes under a temporary name and renames. `face_models` decides availability on `is_file()` alone, so a copy truncated by the process being killed would leave a file that passes that test and fails inside tract — reported to the user as a broken model rather than a missing one. assemble-apk.sh refuses an LFS pointer. At ~130 bytes it looks exactly like a model to `cp`, and unchecked it reaches the device and fails in the graph loader instead of telling someone to run `git lfs pull` — the same guard dr-segment's build script applies to yolo26n-seg.onnx. The licensing half is unchanged and recorded in §2.2a: the InsightFace grant is research-only, this is a private repository and a self-installed build, and these files come back out before anything is published. The weights are still not a cargo build input — dr-face has no `models/` directory and no `embedded-model` feature, and nothing in the build reads them. The APK assembly step copies two files and is the only thing in the tree that knows they exist. Verified on device: both models unpack on first launch (2524817 and 13616095 bytes) and the APK carries them at assets/models/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
100 lines
3.9 KiB
Markdown
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 `apps/darkroom-android/android/assets/models/` into the APK, and the entry point
|
|
unpacks it on first launch. **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.
|