Files
IronStorage/android/README.md
T
hugo 8daf875499
Android ARM verification / rust (push) Failing after 26m4s
Android ARM verification / android (push) Failing after 13m39s
Establish native Linux ARM64 Android CI and verification gates (#94)
2026-10-08 17:52:31 +02:00

10 KiB
Raw Blame History

Native Android build

Architecture and issue ownership: ADR and parity matrix. 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 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:

./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:

./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:

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:

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: 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.