Files
IronStorage/android/README.md
T
hugo bf54b9d0a2
Android ARM verification / rust (push) Successful in 50m42s
Android ARM verification / android (push) Canceled after 1m40s
Establish native Linux ARM64 Android CI and verification gates (#94)
2026-10-08 19:36:26 +02:00

171 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Native Android build
Architecture and issue ownership: [ADR](../docs/android/architecture.md) and
[parity matrix](../docs/android/parity.md). The native foundation implements #89–#94, including private storage, Keystore
callbacks and build gates. Both variants invoke Rust through the generated
UniFFI API. Onboarding and the feature screens remain owned by their milestone
issues. The full [CI and emulator workflow](../docs/android/ci.md) documents the
ARM-only runner/toolchain policy and artifact audits.
## Reproducible setup
Use Rust 1.97.1 (pinned in rust-toolchain.toml), cargo-ndk 4.1.2, a JDK 25 installation
(Android Studio's JetBrains Runtime 25 is suitable), Android SDK Platform 37,
build-tools 36.0.0 and NDK 30.0.16248370. The app emits JVM 17-compatible code
and supports minSdk 26. Set JAVA_HOME and ANDROID_HOME/ANDROID_SDK_ROOT or create
an ignored local.properties containing sdk.dir. Cargo must be in the IDE/build
process PATH, or set CARGO to its absolute executable path. No machine paths are
committed.
The committed Gradle 9.8.1 wrapper verifies the distribution SHA-256. Its JAR
SHA-256 is 3b8a25775a69158b5ad2b1d17a80a88dc7b40a352a73eb27d06c612a7ce68e98,
verified against the official Gradle version metadata. AGP 9.4.1 includes Kotlin
support; the Kotlin JVM/Compose plugin pin upgrades its classpath consistently
to 2.4.21. Do not additionally apply kotlin-android with built-in Kotlin enabled.
The version catalog is the single source for Android dependency/SDK/NDK pins.
From this directory:
```sh
./gradlew auditNativePackages :core:testDebugUnitTest :app:testDebugUnitTest :app:lintDebug :app:lintRelease ktlintCheck
./gradlew :app:connectedDebugAndroidTest :rust-bridge:connectedDebugAndroidTest
```
Repeat the first command to verify configuration-cache reuse. Dependency
verification is strict by default; committed SHA-256 metadata covers resolved
build/test artifacts. Locks pin resolvable module configurations. To update an
approved version, edit the catalog, verify the upstream artifact/release/license,
then explicitly regenerate checksums/locks, inspect the diff and run the gates:
```sh
./gradlew :app:assembleDebug :app:assembleRelease :app:testDebugUnitTest :app:lintDebug ktlintCheck --write-verification-metadata sha256 --write-locks
```
## Boundaries and fixtures
The app composition root injects an Android-facing ShellRepository. Compose
features consume that contract through a Hilt ViewModel and collect immutable
StateFlow state with lifecycle awareness. Only core depends on rust-bridge;
platform implements core OS contracts. Gradle rejects forbidden module edges
and competing domain dependencies. The Rust android-build tool checks Kotlin
source/import boundaries and subprocess paths before every Android build.
These static checks accompany code review; they do not prove arbitrary Kotlin
code cannot conceal a domain implementation.
Debug controls invoke the storage-owned MobileShell::fixture through the native
API for Loading/Empty/Ready/Locked/Error. A build-generated fixture inventory is
used only as the test oracle. Release calls the same Rust Empty fixture without
debug controls; repository initialization is owned by #90. Feature code consumes
only the facade, including typed native recovery states.
## Identity, signing and update
All app build types keep de.rfc1437.ironstorage; test IDs are derived by AGP.
Version code 1 and name 1.0.1 are deterministic initial metadata. Debug signing
uses the standard local debug key. Release has no configured signing key and
produces an unsigned APK; #112 configures protected upload signing. Do not add
signing secrets or store passwords to the repository. Native symbol level FULL
and the selected two ARM ABIs are packaged. Native runtime packaging is described
below; no development network endpoint is included.
The manifest exports the launcher and the reviewed AndroidX profile receiver
protected by the privileged DUMP permission. Backup/cloud/device-transfer
exclusions and cleartext denial are explicit. Feature-specific services/permissions
are added with their owning issues. Resources use values directories for future
localization; vector/adaptive/monochrome icons and R8 consumer rules are present.
The launcher uses an unchanged copy of the iPhone vault artwork from
apple/Assets.xcassets/AppIcon.appiconset/AppIcon.png, inset for Android's adaptive
mask. Its themed monochrome layer keeps the round vault, wheel and hinge motif.
Keep the two platforms' launcher branding aligned when updating that artwork.
Install updates over the existing simulator app without uninstall or data reset:
```sh
adb -s DEVICE install -r app/build/outputs/apk/debug/app-debug.apk
```
Keep the same AVD and application ID. Never wipe its data/keychain or seed a test
app-data package as an update. After closing an issue, clean its project build
outputs and Cargo target; preserve the emulator and shared SDK/toolchain caches.
AGP's leaveApksInstalledAfterRun option is enabled so instrumented checks do not
uninstall the app after running. Keep it enabled for this persistent test device.
## Rust packaging and integrity
Install the pinned build tools once:
```sh
rustup toolchain install 1.97.1 --profile minimal
rustup target add --toolchain 1.97.1 aarch64-linux-android armv7-linux-androideabi
cargo install cargo-ndk --version 4.1.2 --locked
```
Gradle checks the actual Cargo/cargo-ndk/NDK revisions before building. Each
variant compiles crates/android with cargo-ndk into its own generated jniLibs,
then verifies ELF architecture, 16 KB LOAD alignment, and byte-identical UniFFI
metadata across both ARM ABIs. A packaging step selects only the Android library
and rejects accidental inclusion of the dependency Apple cdylib. The thin Android crate re-exports the existing
UniFFI component; it duplicates no storage rules or converters. #91 owns the
platform-neutral contract extraction. No handwritten JNI or unsafe Rust is added.
The workspace's UniFFI 0.32.0 CLI generates Android-mode immutable Kotlin
records from the arm64 library, using the global defaults in uniffi.toml.
A version-checked preparation step redacts record toString, typed/internal
exception diagnostics and unexpected callback stack traces, mechanically renames
Rust error message fields to rustMessage in Kotlin,
and translates every integrity guard to a typed failure. Checksums remain enabled
and retain every native function and expected value. UniFFI checksums are u16:
JNA's generated Int return is normalized to its lower 16 bits before comparison;
AArch64 may leave unrelated upper bits in the return register. Contract versions
are compared at their full width. A failed initializer remains failed until an
app restart/update, rather than reporting a different error on each retry.
NativeRuntime performs OS library/ABI preflight, loads the library and initializes
UniFFI before entering Rust. Missing/unsupported libraries, broken APKs, contract
or checksum mismatch, and load failures yield typed, non-secret recovery states.
It rejects main-thread calls. The core facade schedules blocking native calls on
Dispatchers.IO; cancellation propagates and a superseded result cannot overwrite
newer state. Blocking fixture calls finish in Rust when a coroutine is cancelled;
no thread is forcibly stopped. Long storage operations must use their Rust-owned
cancellation methods when their feature is integrated.
#93 installs Rust-to-Kotlin Keystore callbacks through generated UniFFI/JNA
adapters: JNA attaches a calling native
thread to the JVM; retain the callback for the full Rust owner lifetime, release
it after unregistering, never assume the callback runs on the Android main thread,
and marshal UI/Activity work onto Main. Keep storage decisions in Rust and avoid
blocking the main thread or calling back into a held Rust lock.
JNA 5.19.1 is packaged as its Android AAR, filtered to arm64-v8a, armeabi-v7a only. The release set is arm64-v8a and armeabi-v7a under the user amendment to
#88, not by silently omitting an ABI. All source/lock/config files are task inputs;
outputs live only under build/. Debug and release compile independently, and
unchanged builds reuse their outputs. R8 keeps generated/JNA reflection entry
points. AGP strips release libraries and exports FULL native symbols to
app/build/outputs/native-debug-symbols/release/native-debug-symbols.zip; retain
that file and outputs/mapping/release/mapping.txt with the matching release.
AAB base/lib entries let Play produce per-ABI splits.
The auditNativePackages gate inspects every native dependency in both APKs and
the AAB, checks both ARM ABI directories and both Rust/JNA libraries, and rejects
ELF segments below 16 KB alignment or APK native entries that are compressed or
not ZIP-aligned to 16 KB. Follow the official [16 KB runtime validation](https://developer.android.com/guide/practices/page-sizes):
use an actual 16 KB system image, check adb shell getconf PAGE_SIZE is 16384,
disable page-size compatibility mode, and run both instrumented test suites.
An ELF/ZIP audit is not a substitute for that runtime test. Milestone validation
is emulator-only per the user instruction of 2026-10-08; physical devices are
outside the test scope for every issue. The user removed x86_64 Android from the current support scope on 2026-10-08.
Historical validation recorded on 2026-10-08 for #92 (before the ARM-only scope update):
| Environment | Verified |
| --- | --- |
| API 37 arm64 emulator, 4 KB | Generated API fixture/error/redaction tests; 25 UI states; release APK and AAB split launch; update-state preservation |
| API 37 arm64 emulator, 16 KB, compatibility disabled | Same native/UI tests, release APK/AAB launch and update-state preservation |
| Fresh source export | Three ABI debug builds with pinned tools, locked dependencies and strict artifact verification |
| All three ABI release outputs | ELF alignment and identical UniFFI metadata; stripped APK libraries and separate FULL Rust symbols; nine bundletool ABI split APKs audited |
| Physical devices | Excluded from milestone testing by user instruction |
| x86_64 runtime | System image installed; QEMU2 rejects x86_64 on this aarch64 Mac, including with acceleration disabled |
The historical x86_64 runtime gap is no longer a required check under the current
ARM-only support decision.