Establish native Linux ARM64 Android CI and verification gates (#94)
This commit is contained in:
1 parent
1abd75282e
commit
278f5e9ba7
29 files changed
+1852
-56
No files matched your search
@@ -66,11 +66,12 @@ policy. The installed android-37.0 SDK reports an empty codename and
|
||||
PreviewSdkInt=0 (revision 2); verify the same package metadata in CI before
|
||||
pinning the build, rather than relying on preview wording in older instructions.
|
||||
|
||||
Release ABIs: **arm64-v8a**, **armeabi-v7a**, **x86_64**, corresponding to
|
||||
**aarch64-linux-android**, **armv7-linux-androideabi**,
|
||||
**x86_64-linux-android**. An ABI may be removed only with a dependency audit and
|
||||
an amendment to this decision; emulator success alone proves no arm64 hardware
|
||||
coverage. All shipped native dependencies, including JNA, must pass ELF LOAD
|
||||
Release ABIs: **arm64-v8a** and **armeabi-v7a**, corresponding to
|
||||
**aarch64-linux-android** and **armv7-linux-androideabi**. On 2026-10-08 the user
|
||||
removed x86_64 Android from the support and CI requirements and specified
|
||||
emulator-only testing for every issue. This supersedes the original three-ABI
|
||||
decision in #88; historical test evidence remains historical.
|
||||
All shipped native dependencies, including JNA, must pass ELF LOAD
|
||||
alignment, APK ZIP alignment and actual 16 KB runtime checks. Do not depend on
|
||||
compatibility mode. [Android's page-size guidance](https://developer.android.com/guide/practices/page-sizes)
|
||||
requires auditing dependencies as well as the app library.
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
# Android verification and reproducible ARM builds
|
||||
|
||||
Issue #94 uses the existing Gitea `linux-arm64` runner. No macOS runner, physical
|
||||
Android device, or x86 Android target is required. The release set is
|
||||
`arm64-v8a` and `armeabi-v7a`; the minimum SDK is 26 and compile/target SDK is 37.
|
||||
|
||||
## CI jobs and local commands
|
||||
|
||||
`.gitea/workflows/android.yml` runs for main, Codex branches and pull requests.
|
||||
The Rust job runs the repository formatting, warnings-as-errors workspace
|
||||
check, Clippy and workspace tests with Rust 1.97.1. System build libraries are
|
||||
installed inside the job container. Native Linux LLD and one build worker bound
|
||||
workspace test-linking memory. The CI host test profile uses `line-tables-only`
|
||||
debug information for backtrace file/line diagnostics and disables incremental
|
||||
caches to bound disk use; every test remains enabled. This does not change the
|
||||
Android release profile's required FULL debug-symbol archive.
|
||||
Jobs check available disk space before installing tools: at least 16 GiB for
|
||||
the host Rust gates and 24 GiB for Android, including SDK/dependencies, both
|
||||
native variants and package assembly. Insufficient capacity fails immediately
|
||||
with an explicit diagnostic. Each job schedules an `always()` cleanup step for
|
||||
only this checkout's generated Cargo/Android build directories after its gates/artifact upload;
|
||||
cleanup rejects symlinks and refuses to run outside CI. It does not prune
|
||||
shared runner caches, SDK installations, other repositories or emulator data.
|
||||
Hard runner termination can prevent cleanup; retained job data then needs
|
||||
runner-host administration.
|
||||
The Android job compiles the UniFFI host tool before starting Gradle. Host/debug
|
||||
builds use line-table debug information and no incremental caches.
|
||||
Release Rust libraries retain their explicit FULL debug information. These
|
||||
profile/environment inputs participate in Gradle's native-task cache keys.
|
||||
With `CI=true`, Cargo uses the owned `android/build/native-cargo` scratch
|
||||
directory. Each native variant is compiled, audited and converted to bindings
|
||||
before its Android-target compiler intermediates are removed. Copied libraries,
|
||||
generated bindings and the host audit tool remain for the full Gradle verification
|
||||
and assembly phase. This bounds peak disk use without removing tests or release
|
||||
symbols; ordinary local development retains its Cargo intermediates.
|
||||
The Gradle wrapper retries transient distribution-download failures three times
|
||||
with bounded exponential backoff; its pinned distribution checksum remains required.
|
||||
The Android job has a 150-minute timeout for the measured single-runner build
|
||||
and uses one worker, a 1 GB Gradle heap and a 512 MB Kotlin compiler heap. It uses native ARM64 Java 25,
|
||||
Rust 1.97.1, cargo-ndk 4.1.2, locked Gradle dependencies and strict SHA-256
|
||||
verification. It builds both debug/release Rust ABIs, compiles Kotlin with
|
||||
warnings as errors, runs JVM tests, KtLint and Android Lint, compiles Compose
|
||||
instrumentation, and builds a debug APK, unsigned release APK and unsigned AAB.
|
||||
There are no Play credentials or upload signing keys in this workflow. #112
|
||||
owns protected distribution signing; Gitea's `environment` key alone is not a
|
||||
protection mechanism.
|
||||
|
||||
From the repository root, with `JAVA_HOME` and `ANDROID_HOME` set:
|
||||
|
||||
```sh
|
||||
cargo fmt --all -- --check
|
||||
RUSTFLAGS="-D warnings" cargo check --workspace --all-targets
|
||||
cargo clippy --workspace --all-targets -- -D warnings
|
||||
cargo test --workspace
|
||||
python3 .gitea/scripts/android_ci.py test
|
||||
python3 .gitea/scripts/android_ci.py build
|
||||
python3 .gitea/scripts/android_ci.py emulator emulator-5554
|
||||
```
|
||||
|
||||
`CARGO` can specify the absolute rustup Cargo executable. `androidVerification`
|
||||
is also directly available through `android/gradlew -p android`. The dependency
|
||||
inventory task resolves each module under its own Gradle project lock and
|
||||
requires configuration caching to be disabled. Ordinary build tasks retain
|
||||
the existing incremental/configuration-cache behavior. Linker flags, selected
|
||||
ABIs, SDK/NDK versions and release debug settings are explicit native task inputs.
|
||||
|
||||
## Native Linux ARM64 host tools
|
||||
|
||||
Google's [NDK r30 Linux download](https://developer.android.com/ndk/downloads)
|
||||
contains x86_64 host binaries, independently of the target Android ABI. CI uses
|
||||
an explicitly pinned native ARM64 community rebuild of that NDK, following the
|
||||
approach described by [zongou/android-build](https://github.com/zongou/android-build).
|
||||
`android/ci-toolchains.json` records exact URLs and reviewed SHA-256 hashes for:
|
||||
|
||||
- [HomuHomu833 NDK r30, aarch64-linux-gnu](https://github.com/HomuHomu833/android-ndk-custom/releases/tag/r30):
|
||||
revision 30.0.16248370, Clang/LLD 21, Android compiler base r574158c. The inspected
|
||||
compiler identifies its rebuild source as `a6285403ecfb895b7515518d40da02422ffffb20`.
|
||||
- [lzhiyong native SDK tools 35.0.2](https://github.com/lzhiyong/android-sdk-tools/releases/tag/35.0.2):
|
||||
statically linked ARM64 AAPT2 selected through Gradle's documented
|
||||
`android.aapt2FromMavenOverride`. This host tool is separately pinned; it is
|
||||
not represented as Google's build-tools 36 binary. A small argument adapter
|
||||
splits AGP's `--resource-path-shortening-map=path` into the option and path
|
||||
required by this AAPT2. Optimization remains enabled and exit codes propagate.
|
||||
- Google's Java-based command-line tools 20.0, build 14742923. The newer 23.0
|
||||
SDK manager delegates to a Linux x86_64 executable; the pinned Java manager
|
||||
provisions the exact `platforms;android-37.0` and `build-tools;36.0.0` packages
|
||||
without CPU emulation. APK Analyzer is also Java-based.
|
||||
|
||||
`prepare-linux` verifies every downloaded archive before extraction and runs
|
||||
native compiler/linker/AAPT2 version probes. cargo-ndk 4.1.2 hardcodes the
|
||||
`linux-x86_64` directory name on Linux, so a compatibility directory symlink
|
||||
points at the native `linux-arm64` tools. It does not change executable
|
||||
architecture. No binfmt registration, privileged container or emulator host
|
||||
patch is used. Mac development retains its official installed SDK/NDK.
|
||||
|
||||
The community toolchain is an additional supply-chain input. Changing an asset,
|
||||
compiler base, hash or host tool requires review and a clean build plus ARM
|
||||
emulator validation. A changed release asset fails its committed checksum.
|
||||
Retain the NDK's NOTICE files; toolchains are build inputs, not app runtime code.
|
||||
|
||||
## Artifact gates and reports
|
||||
|
||||
The Rust build tool checks module/source boundaries, subprocess references,
|
||||
matching UniFFI metadata across ABIs and every packaged Rust/JNA ELF. APK native
|
||||
libraries must be stored and aligned to 16 KB; ELF LOAD segments must also be
|
||||
16 KB compatible. Generated bindings preserve contract/function checksums and
|
||||
fail closed on a changed generator hardening template. Only the upstream eager
|
||||
object-initialization function suppresses Kotlin's `UNUSED_EXPRESSION` false
|
||||
positive; application warnings remain errors.
|
||||
|
||||
The packaged APK manifest is decoded and audited for the exact application ID,
|
||||
backup and cleartext flags, reviewed permissions and exported launcher. The reviewed
|
||||
AndroidX profile receiver must retain the privileged `DUMP` permission and exact
|
||||
profile actions. Other components must explicitly remain unexported. The unused
|
||||
Compose test-host manifest dependency is removed so it cannot export another
|
||||
Activity in debug APKs. The packaged manifest's backup resource IDs must match
|
||||
the APK's compiled resource table for the reviewed rules. APK Analyzer resolves their actual
|
||||
paths, including resource-shortened release paths, and Rust audits those decoded
|
||||
APK XML files. Backup rules must exclude
|
||||
every credential/device-protected domain from cloud backup and device transfer;
|
||||
instrumentation verifies the actual installed XML resources as well.
|
||||
The ChromeOS x86 ABI recommendation is disabled because it conflicts with the
|
||||
user's explicit ARM-only Android scope. No general Lint baseline is used.
|
||||
|
||||
Release Rust libraries must have no debug/symbol-table sections, and each FULL
|
||||
`.so.dbg` file must contain debug information and the identical ELF build ID.
|
||||
JNA's supplied symbol table must also match its stripped library's build ID.
|
||||
The only packaged native libraries allowed are Rust, JNA and AndroidX graphics;
|
||||
unreviewed names or duplicate entries fail.
|
||||
AGP can include symbols for JNA's unselected ABIs; the published symbol ZIP
|
||||
selects only both supported ARM directories, then the Rust gate verifies it
|
||||
against the actual release APK. The artifact checksum manifest covers the
|
||||
packages, symbol ZIP, R8 mapping, decoded manifests, test summaries and SBOM.
|
||||
|
||||
The CycloneDX inventory includes actual resolved Android application/test
|
||||
artifacts, their configurations and SHA-256 values, plus the Rust normal feature
|
||||
graph for both ARM targets with Cargo.lock registry hashes. It is a dependency
|
||||
inventory, not a claim that every graph crate survives linker elimination.
|
||||
Exact Android component versions and Rust license expressions require review
|
||||
in `dependency-policy.json`. Runtime/test JGit, Bouncy Castle, Room, Retrofit,
|
||||
OkHttp, PGPainless and ML Kit dependencies fail even transitively. Lint's own
|
||||
build-tool dependencies are separate from the app's domain boundary. Gradle
|
||||
strict verification additionally covers build/plugin/POM/module artifacts;
|
||||
RustSec runs in the existing security-audit workflow.
|
||||
|
||||
Published JVM reports retain only hashed identifiers and pass/fail/error/skip
|
||||
status. Assertion messages, parameter values, stack traces, properties, test
|
||||
stdout/stderr and XML names are omitted. Failures remain failures and make the
|
||||
gate fail; empty/missing/invalid reports are not accepted. Raw instrumentation
|
||||
failures remain in `android/build/local-validation` with private permissions,
|
||||
never in `ci-artifacts`. Do not upload emulator snapshots, app containers,
|
||||
credential files, screenshot dumps or raw logs containing application data.
|
||||
|
||||
## Emulator matrix and deterministic fixtures
|
||||
|
||||
The existing Gitea runner has no supported native ARM Android Emulator package.
|
||||
Google's current Linux emulator archives are x86_64 host binaries. Instrumented
|
||||
validation therefore runs locally on the available ARM emulators; this limitation
|
||||
is explicit rather than a silently skipped/passing CI emulator job. No additional
|
||||
runner or physical device is provisioned.
|
||||
|
||||
Required local matrix for this issue:
|
||||
|
||||
| ARM emulator | Checks |
|
||||
| --- | --- |
|
||||
| API 26, 4 KB | Minimum-SDK native/JNI and Compose tests; installed backup resources; cold starts and native load |
|
||||
| API 37, 4 KB | Current-SDK same tests; update over existing app data |
|
||||
| API 37, strict 16 KB | Same tests with compatibility mode disabled; ELF/ZIP gate is not a substitute |
|
||||
|
||||
ARMv7 binaries are compiled and audited; an ARM64-only system image cannot run
|
||||
an ARMv7-only APK. Record that runtime limit without requiring an x86 or physical
|
||||
device. API 26 lacks `getconf`; the controller reads its kernel page-size facts
|
||||
from `/proc/self/smaps`. ADB validates emulator identity and rejects physical
|
||||
and unsupported-ABI targets. Installs use `-r`; force-stop/relaunch changes only
|
||||
process state. Never uninstall or wipe existing test state.
|
||||
|
||||
Rust's shell fixtures cover all 25 tab/state combinations. Existing fake clocks
|
||||
exercise lease expiry, background/foreground boundaries and activity refresh in
|
||||
workspace tests. Rust secret-backend fixtures cover every typed protection/error
|
||||
result and late completion. Kotlin's controlled coroutine fixture verifies that
|
||||
cancelled work cannot replace newer state. Generated JNI tests cover secret
|
||||
redaction, checksum/contract rejection, callback ownership/cancellation and
|
||||
Keystore lockout. The private-runtime fixture creates a public-only password
|
||||
store through Rust and reopens it, checking durable layout/tab state. Platform
|
||||
Keystore tests from #93 remain available separately for real emulator biometrics;
|
||||
they require synthetic enrollment and active-prompt ADB control, not a real
|
||||
user's passphrase.
|
||||
|
||||
## Performance baseline
|
||||
|
||||
The controller records five process-cold Activity `TotalTime` samples and five
|
||||
fresh-process native-load/first-Rust-fixture samples using monotonic Android
|
||||
nanoseconds. Reports record SDK, ABI and actual page size. These are observations,
|
||||
not flaky latency pass/fail thresholds. Never compare a debug emulator result to
|
||||
a signed physical-device release measurement.
|
||||
|
||||
Initial #94 local debug baseline (five samples, medians):
|
||||
|
||||
| ARM64 image | Activity cold start | Native load and first Rust fixture |
|
||||
| --- | ---: | ---: |
|
||||
| API 26, 4 KB | 161 ms | 25.4 ms |
|
||||
| API 37, 4 KB | 896 ms | 42.7 ms |
|
||||
| API 37, strict 16 KB | 730 ms | 62.0 ms |
|
||||
|
||||
All three images passed ten JNI tests and two app/Compose tests covering all
|
||||
25 Rust shell states and installed backup resources. The community-toolchain
|
||||
Linux debug APK and both test APKs additionally passed those same suites on the
|
||||
strict 16 KB image after re-signing with the existing local debug identity;
|
||||
installation preserved the app data. Original Linux release packages remain
|
||||
unsigned. Emulators ran concurrently with build work, so these observations
|
||||
are a reproducible measurement method, not a calibrated performance budget.
|
||||
|
||||
#105 will add Macrobenchmark/Baseline Profiles once representative Android
|
||||
screens exist. Its case definitions are: first page and scroll in 10,000 synthetic
|
||||
entries; search update after each query edit; warm cached TOTP page with 100
|
||||
synthetic seeds and an injected clock; editor open, field edits, reorder and save.
|
||||
Use generated public fixtures, fixed queries/clocks, ten warm-up iterations and
|
||||
30 measured iterations; record median/p95, allocations and device/API/toolchain.
|
||||
Do not substitute the current shell for measurements of unimplemented features.
|
||||
@@ -64,7 +64,7 @@ milestone. This list gives ownership, not permission to skip acceptance criteria
|
||||
| [#89 Gradle scaffold/module boundaries](https://git.rfc1437.de/hugo/IronStorage/issues/89) | Android build/app/core/platform/bridge modules | Kotlin DSL/catalog/wrapper, Compose/Hilt/KSP, source/dependency gates, all shell fixtures; clean debug APK |
|
||||
| [#90 Private paths/config/cache/backup](https://git.rfc1437.de/hugo/IronStorage/issues/90) | crates/storage plus Android path adapter | Context roots/capabilities, atomic layout/migrations, backup rules; Rust and sandbox/update tests |
|
||||
| [#91 Platform-neutral mobile contract](https://git.rfc1437.de/hugo/IronStorage/issues/91) | crates/storage and shared UniFFI bridge | Typed semantic actions/icons, opt-in Watch data, platform callbacks, shared Swift/Kotlin inventory tests |
|
||||
| [#92 Native Rust/UniFFI packaging](https://git.rfc1437.de/hugo/IronStorage/issues/92) | Shared/Android Rust bridge and Gradle rust-bridge | cargo-ndk, generated immutable Kotlin/JNA Android, three ABIs, checksums/16 KB/loading/debug symbols |
|
||||
| [#92 Native Rust/UniFFI packaging](https://git.rfc1437.de/hugo/IronStorage/issues/92) | Shared/Android Rust bridge and Gradle rust-bridge | cargo-ndk, generated immutable Kotlin/JNA Android, both ARM ABIs, checksums/16 KB/loading/debug symbols |
|
||||
| [#93 Keystore adapter](https://git.rfc1437.de/hugo/IronStorage/issues/93) | Delivered: Rust sealed-record orchestration and Android OS adapter | [Keystore policy and recovery](keystore.md); native AES-GCM/CryptoObject and Rust lifecycle tests |
|
||||
| [#94 CI/static analysis/reproducibility](https://git.rfc1437.de/hugo/IronStorage/issues/94) | Build/CI/test infrastructure | Cargo gates, Gradle build/Lint/ktlint, JVM/Compose/instrumented tests, verification/locks/SBOM/ABI checks |
|
||||
| [#95 Local-first/HTTPS onboarding](https://git.rfc1437.de/hugo/IronStorage/issues/95) | storage mobile_onboarding plus Compose features | Existing Rust initialization/discovery/clone/credentials; no Kotlin transport |
|
||||
|
||||
Reference in new issue
Block a user