Establish native Linux ARM64 Android CI and verification gates (#94)
This commit is contained in:
1 parent
1abd75282e
commit
8fa187ab8e
29 files changed
+1862
-56
No files matched your search
+18
-17
@@ -1,9 +1,11 @@
|
||||
# Native Android build
|
||||
|
||||
Architecture and issue ownership: [ADR](../docs/android/architecture.md) and
|
||||
[parity matrix](../docs/android/parity.md). The native foundation implements #89 and #92. Onboarding and secure storage
|
||||
are implemented by their milestone issues. Both variants now invoke Rust through
|
||||
the generated UniFFI API.
|
||||
[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
|
||||
|
||||
@@ -63,10 +65,11 @@ 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 three ABIs are packaged. Native runtime packaging is described
|
||||
and the selected two ARM ABIs are packaged. Native runtime packaging is described
|
||||
below; no development network endpoint is included.
|
||||
|
||||
The manifest has only the exported launcher activity. Backup/cloud/device-transfer
|
||||
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.
|
||||
@@ -93,14 +96,14 @@ 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 x86_64-linux-android
|
||||
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 all three ABIs. A packaging step selects only the Android library
|
||||
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.
|
||||
@@ -126,15 +129,14 @@ 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.
|
||||
|
||||
No Rust-to-Kotlin platform callbacks are installed by #92. When #93 adds them,
|
||||
use generated UniFFI/JNA callback adapters only: JNA attaches a calling native
|
||||
#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 and
|
||||
x86_64. The release set may only be narrowed by changing the support decision in
|
||||
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
|
||||
@@ -144,17 +146,16 @@ 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 all three ABI directories and both Rust/JNA libraries, and rejects
|
||||
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. Keep x86_64 emulator evidence separate
|
||||
from the Apple Silicon arm64 emulator run.
|
||||
outside the test scope for every issue. The user removed x86_64 Android from the current support scope on 2026-10-08.
|
||||
|
||||
Validation recorded on 2026-10-08 for #92:
|
||||
Historical validation recorded on 2026-10-08 for #92 (before the ARM-only scope update):
|
||||
|
||||
| Environment | Verified |
|
||||
| --- | --- |
|
||||
@@ -165,5 +166,5 @@ Validation recorded on 2026-10-08 for #92:
|
||||
| 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 outstanding x86_64 emulator check needs a compatible host; cross-compilation
|
||||
and static package checks do not establish x86_64 runtime coverage.
|
||||
The historical x86_64 runtime gap is no longer a required check under the current
|
||||
ARM-only support decision.
|
||||
@@ -18,7 +18,7 @@ android {
|
||||
versionCode = 1
|
||||
versionName = "1.0.1"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64") }
|
||||
ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a") }
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
@@ -41,12 +41,17 @@ android {
|
||||
lint {
|
||||
abortOnError = true
|
||||
warningsAsErrors = true
|
||||
// The user scoped this milestone to ARM Android; x86 ChromeOS is excluded.
|
||||
disable += "ChromeOsAbiSupport"
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(25)
|
||||
compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
allWarningsAsErrors.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -69,7 +74,6 @@ dependencies {
|
||||
androidTestImplementation(libs.compose.ui.test)
|
||||
androidTestImplementation(libs.android.test.runner)
|
||||
androidTestImplementation(libs.android.test.espresso)
|
||||
debugImplementation(libs.compose.ui.test.manifest)
|
||||
implementation(libs.coroutines.android)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.junit)
|
||||
|
||||
@@ -51,7 +51,6 @@ androidx.compose.ui:ui-graphics:1.12.1=debugAndroidTestCompileClasspath,debugAnd
|
||||
androidx.compose.ui:ui-test-android:1.12.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
|
||||
androidx.compose.ui:ui-test-junit4-android:1.12.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
|
||||
androidx.compose.ui:ui-test-junit4:1.12.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
|
||||
androidx.compose.ui:ui-test-manifest:1.12.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
|
||||
androidx.compose.ui:ui-test:1.12.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
|
||||
androidx.compose.ui:ui-text-android:1.12.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.compose.ui:ui-text:1.12.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import groovy.json.JsonOutput
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
|
||||
import org.gradle.api.artifacts.ExternalModuleDependency
|
||||
import org.gradle.api.artifacts.ProjectDependency
|
||||
|
||||
@@ -39,8 +42,15 @@ subprojects {
|
||||
listOf("org.eclipse.jgit", "org.bouncycastle", "androidx.room", "com.squareup.retrofit2", "com.squareup.okhttp3").none { group.startsWith(it) }
|
||||
}) { "Domain libraries must stay in crates/storage: $group:$name" }
|
||||
}
|
||||
val enforceDomain = name.endsWith("RuntimeClasspath") || name.endsWith("CompileClasspath")
|
||||
resolutionStrategy.eachDependency {
|
||||
require(!requested.version.orEmpty().contains('+')) { "Dynamic versions are forbidden" }
|
||||
require(requested.version.orEmpty().let { version ->
|
||||
!version.contains('+') && !version.contains("SNAPSHOT") &&
|
||||
!version.startsWith("latest.") && version.none { it in "[]()," }
|
||||
}) { "Dynamic versions are forbidden" }
|
||||
require(!enforceDomain || listOf("org.eclipse.jgit", "org.bouncycastle", "androidx.room", "com.squareup.retrofit2", "com.squareup.okhttp3", "org.pgpainless", "com.google.mlkit").none {
|
||||
requested.group == it || requested.group.startsWith("$it.")
|
||||
}) { "Transitive domain libraries must stay in crates/storage" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,3 +89,51 @@ tasks.register<Exec>("auditNativePackages") {
|
||||
) + packages.map { it.asFile.absolutePath }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Each project resolves its own configurations under Gradle's project lock.
|
||||
subprojects {
|
||||
val owner = project
|
||||
tasks.register("dependencyInventory") {
|
||||
val output = layout.buildDirectory.file("reports/android-dependencies.json")
|
||||
outputs.file(output)
|
||||
outputs.upToDateWhen { false }
|
||||
doLast {
|
||||
val rows = listOf("debugRuntimeClasspath", "releaseRuntimeClasspath", "debugAndroidTestRuntimeClasspath", "debugUnitTestRuntimeClasspath").flatMap { name ->
|
||||
val configuration = owner.configurations.findByName(name)
|
||||
if (configuration == null || !configuration.isCanBeResolved) emptyList() else {
|
||||
configuration.incoming.artifactView {
|
||||
componentFilter { it is ModuleComponentIdentifier }
|
||||
}.artifacts.artifacts.mapNotNull { artifact ->
|
||||
val id = artifact.id.componentIdentifier as? ModuleComponentIdentifier
|
||||
id?.let {
|
||||
mapOf(
|
||||
"group" to it.group, "name" to it.module, "version" to it.version,
|
||||
"sha256" to MessageDigest.getInstance("SHA-256").digest(artifact.file.readBytes())
|
||||
.joinToString("") { byte -> "%02x".format(byte) },
|
||||
"scope" to "${owner.path}:$name"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.sortedWith(compareBy({ it.getValue("scope") }, { it.getValue("group") }, { it.getValue("name") }, { it.getValue("version") }, { it.getValue("sha256") }))
|
||||
output.get().asFile.apply {
|
||||
parentFile.mkdirs()
|
||||
writeText(JsonOutput.prettyPrint(JsonOutput.toJson(rows)) + "\n")
|
||||
}
|
||||
}
|
||||
notCompatibleWithConfigurationCache("Resolves reviewed artifact graphs")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("dependencyInventory") {
|
||||
dependsOn(subprojects.map { "${it.path}:dependencyInventory" })
|
||||
}
|
||||
|
||||
tasks.register("androidVerification") {
|
||||
dependsOn("checkArchitecture", "dependencyInventory", "auditNativePackages")
|
||||
dependsOn(subprojects.map { "${it.path}:ktlintCheck" })
|
||||
dependsOn(subprojects.map { "${it.path}:lintDebug" })
|
||||
dependsOn(subprojects.map { "${it.path}:testDebugUnitTest" })
|
||||
dependsOn(":app:assembleDebugAndroidTest", ":rust-bridge:assembleDebugAndroidTest", ":platform:assembleDebugAndroidTest")
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"sdk_cli": {
|
||||
"url": "https://dl.google.com/android/repository/commandlinetools-linux-14742923_latest.zip",
|
||||
"sha256": "04453066b540409d975c676d781da1477479dde3761310f1a7eb92a1dfb15af7"
|
||||
},
|
||||
"ndk": {
|
||||
"url": "https://github.com/HomuHomu833/android-ndk-custom/releases/download/r30/android-ndk-r30-aarch64-linux-gnu.tar.xz",
|
||||
"sha256": "f8eeb06b7c97415d883a8ecdd0a725e759406edab4502e13b1a6a8561461a0c8"
|
||||
},
|
||||
"sdk_native": {
|
||||
"url": "https://github.com/lzhiyong/android-sdk-tools/releases/download/35.0.2/android-sdk-tools-static-aarch64.zip",
|
||||
"sha256": "db1cea2c4454d5f9c5a802646b2d1cf560b4ee7badbe23e51ab8e1881bb50fc2"
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,10 @@ android {
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(25)
|
||||
compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
allWarningsAsErrors.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
{
|
||||
"android": {
|
||||
"androidx.activity:activity-compose:1.12.0": "Apache-2.0",
|
||||
"androidx.activity:activity-compose:1.13.0": "Apache-2.0",
|
||||
"androidx.activity:activity-ktx:1.12.0": "Apache-2.0",
|
||||
"androidx.activity:activity-ktx:1.13.0": "Apache-2.0",
|
||||
"androidx.activity:activity-ktx:1.8.1": "Apache-2.0",
|
||||
"androidx.activity:activity:1.12.0": "Apache-2.0",
|
||||
"androidx.activity:activity:1.13.0": "Apache-2.0",
|
||||
"androidx.activity:activity:1.8.1": "Apache-2.0",
|
||||
"androidx.annotation:annotation-experimental:1.4.1": "Apache-2.0",
|
||||
"androidx.annotation:annotation-experimental:1.5.1": "Apache-2.0",
|
||||
"androidx.annotation:annotation-jvm:1.11.0": "Apache-2.0",
|
||||
"androidx.appcompat:appcompat-resources:1.2.0": "Apache-2.0",
|
||||
"androidx.appcompat:appcompat:1.2.0": "Apache-2.0",
|
||||
"androidx.arch.core:core-common:2.2.0": "Apache-2.0",
|
||||
"androidx.arch.core:core-runtime:2.2.0": "Apache-2.0",
|
||||
"androidx.autofill:autofill:1.0.0": "Apache-2.0",
|
||||
"androidx.biometric:biometric:1.1.0": "Apache-2.0",
|
||||
"androidx.collection:collection-jvm:1.5.0": "Apache-2.0",
|
||||
"androidx.collection:collection-jvm:1.6.0": "Apache-2.0",
|
||||
"androidx.collection:collection-ktx:1.5.0": "Apache-2.0",
|
||||
"androidx.collection:collection-ktx:1.6.0": "Apache-2.0",
|
||||
"androidx.compose.animation:animation-android:1.12.1": "Apache-2.0",
|
||||
"androidx.compose.animation:animation-core-android:1.12.1": "Apache-2.0",
|
||||
"androidx.compose.foundation:foundation-android:1.12.1": "Apache-2.0",
|
||||
"androidx.compose.foundation:foundation-layout-android:1.12.1": "Apache-2.0",
|
||||
"androidx.compose.material3.adaptive:adaptive-android:1.3.0": "Apache-2.0",
|
||||
"androidx.compose.material3:material3-android:1.4.0": "Apache-2.0",
|
||||
"androidx.compose.material:material-ripple-android:1.12.1": "Apache-2.0",
|
||||
"androidx.compose.runtime:runtime-android:1.12.1": "Apache-2.0",
|
||||
"androidx.compose.runtime:runtime-annotation-android:1.12.1": "Apache-2.0",
|
||||
"androidx.compose.runtime:runtime-retain-android:1.12.1": "Apache-2.0",
|
||||
"androidx.compose.runtime:runtime-saveable-android:1.12.1": "Apache-2.0",
|
||||
"androidx.compose.ui:ui-android:1.12.1": "Apache-2.0",
|
||||
"androidx.compose.ui:ui-geometry-android:1.12.1": "Apache-2.0",
|
||||
"androidx.compose.ui:ui-graphics-android:1.12.1": "Apache-2.0",
|
||||
"androidx.compose.ui:ui-test-android:1.12.1": "Apache-2.0",
|
||||
"androidx.compose.ui:ui-test-junit4-android:1.12.1": "Apache-2.0",
|
||||
"androidx.compose.ui:ui-text-android:1.12.1": "Apache-2.0",
|
||||
"androidx.compose.ui:ui-unit-android:1.12.1": "Apache-2.0",
|
||||
"androidx.compose.ui:ui-util-android:1.12.1": "Apache-2.0",
|
||||
"androidx.concurrent:concurrent-futures-ktx:1.2.0": "Apache-2.0",
|
||||
"androidx.concurrent:concurrent-futures:1.1.0": "Apache-2.0",
|
||||
"androidx.concurrent:concurrent-futures:1.2.0": "Apache-2.0",
|
||||
"androidx.core:core-ktx:1.13.1": "Apache-2.0",
|
||||
"androidx.core:core-ktx:1.16.0": "Apache-2.0",
|
||||
"androidx.core:core-ktx:1.18.0": "Apache-2.0",
|
||||
"androidx.core:core-viewtree:1.0.0": "Apache-2.0",
|
||||
"androidx.core:core:1.13.1": "Apache-2.0",
|
||||
"androidx.core:core:1.16.0": "Apache-2.0",
|
||||
"androidx.core:core:1.18.0": "Apache-2.0",
|
||||
"androidx.cursoradapter:cursoradapter:1.0.0": "Apache-2.0",
|
||||
"androidx.customview:customview-poolingcontainer:1.0.0": "Apache-2.0",
|
||||
"androidx.customview:customview:1.0.0": "Apache-2.0",
|
||||
"androidx.drawerlayout:drawerlayout:1.0.0": "Apache-2.0",
|
||||
"androidx.emoji2:emoji2:1.4.0": "Apache-2.0",
|
||||
"androidx.fragment:fragment:1.5.1": "Apache-2.0",
|
||||
"androidx.fragment:fragment:1.9.1": "Apache-2.0",
|
||||
"androidx.graphics:graphics-path:1.0.1": "Apache-2.0",
|
||||
"androidx.interpolator:interpolator:1.0.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-common-java8:2.11.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-common-jvm:2.10.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-common-jvm:2.11.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-livedata-core-ktx:2.10.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-livedata-core-ktx:2.11.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-livedata-core:2.10.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-livedata-core:2.11.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-livedata:2.10.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-livedata:2.11.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-process:2.11.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-runtime-android:2.10.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-runtime-android:2.11.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-runtime-compose-android:2.11.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-runtime-ktx-android:2.10.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-runtime-ktx-android:2.11.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-viewmodel-android:2.10.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-viewmodel-android:2.11.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-viewmodel-compose-android:2.11.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-viewmodel-ktx:2.11.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.10.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.11.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-viewmodel:2.10.0": "Apache-2.0",
|
||||
"androidx.lifecycle:lifecycle-viewmodel:2.11.0": "Apache-2.0",
|
||||
"androidx.loader:loader:1.0.0": "Apache-2.0",
|
||||
"androidx.navigation3:navigation3-runtime-android:1.2.0": "Apache-2.0",
|
||||
"androidx.navigation3:navigation3-ui-android:1.2.0": "Apache-2.0",
|
||||
"androidx.navigationevent:navigationevent-android:1.1.1": "Apache-2.0",
|
||||
"androidx.navigationevent:navigationevent-compose-android:1.1.1": "Apache-2.0",
|
||||
"androidx.profileinstaller:profileinstaller:1.4.0": "Apache-2.0",
|
||||
"androidx.savedstate:savedstate-android:1.4.0": "Apache-2.0",
|
||||
"androidx.savedstate:savedstate-compose-android:1.4.0": "Apache-2.0",
|
||||
"androidx.savedstate:savedstate-ktx:1.4.0": "Apache-2.0",
|
||||
"androidx.startup:startup-runtime:1.1.1": "Apache-2.0",
|
||||
"androidx.test.espresso:espresso-core:3.7.0": "Apache-2.0",
|
||||
"androidx.test.espresso:espresso-idling-resource:3.7.0": "Apache-2.0",
|
||||
"androidx.test.ext:junit:1.1.5": "Apache-2.0",
|
||||
"androidx.test.services:storage:1.6.0": "Apache-2.0",
|
||||
"androidx.test:core:1.7.0": "Apache-2.0",
|
||||
"androidx.test:monitor:1.8.0": "Apache-2.0",
|
||||
"androidx.test:runner:1.7.0": "Apache-2.0",
|
||||
"androidx.tracing:tracing-android:2.0.0": "Apache-2.0",
|
||||
"androidx.tracing:tracing:1.1.0": "Apache-2.0",
|
||||
"androidx.tracing:tracing:1.2.0": "Apache-2.0",
|
||||
"androidx.vectordrawable:vectordrawable-animated:1.1.0": "Apache-2.0",
|
||||
"androidx.vectordrawable:vectordrawable:1.1.0": "Apache-2.0",
|
||||
"androidx.versionedparcelable:versionedparcelable:1.1.1": "Apache-2.0",
|
||||
"androidx.viewpager:viewpager:1.0.0": "Apache-2.0",
|
||||
"androidx.window:window-core-android:1.5.0": "Apache-2.0",
|
||||
"androidx.window:window:1.5.0": "Apache-2.0",
|
||||
"com.google.code.findbugs:jsr305:3.0.2": "Apache-2.0",
|
||||
"com.google.dagger:dagger-lint-aar:2.60.1": "Apache-2.0",
|
||||
"com.google.dagger:dagger:2.60.1": "Apache-2.0",
|
||||
"com.google.dagger:hilt-android:2.60.1": "Apache-2.0",
|
||||
"com.google.dagger:hilt-core:2.60.1": "Apache-2.0",
|
||||
"com.google.errorprone:error_prone_annotations:2.30.0": "Apache-2.0",
|
||||
"com.google.guava:listenablefuture:1.0": "Apache-2.0",
|
||||
"jakarta.inject:jakarta.inject-api:2.0.1": "Apache-2.0",
|
||||
"javax.inject:javax.inject:1": "Apache-2.0",
|
||||
"junit:junit:4.13.2": "EPL-1.0",
|
||||
"net.java.dev.jna:jna:5.19.1": "Apache-2.0 OR LGPL-2.1-or-later",
|
||||
"org.hamcrest:hamcrest-core:1.3": "BSD-3-Clause",
|
||||
"org.hamcrest:hamcrest-library:1.3": "BSD-3-Clause",
|
||||
"org.jetbrains.kotlin:kotlin-stdlib:2.4.21": "Apache-2.0",
|
||||
"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0": "Apache-2.0",
|
||||
"org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.11.0": "Apache-2.0",
|
||||
"org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.11.0": "Apache-2.0",
|
||||
"org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3": "Apache-2.0",
|
||||
"org.jetbrains:annotations:13.0": "Apache-2.0",
|
||||
"org.jetbrains:annotations:23.0.0": "Apache-2.0",
|
||||
"org.jspecify:jspecify:1.0.0": "Apache-2.0"
|
||||
},
|
||||
"rust_licenses": [
|
||||
"(Apache-2.0 OR MIT) AND BSD-3-Clause",
|
||||
"(MIT OR Apache-2.0) AND ISC",
|
||||
"(MIT OR Apache-2.0) AND Unicode-3.0",
|
||||
"0BSD OR MIT OR Apache-2.0",
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 / MIT",
|
||||
"Apache-2.0 OR ISC OR MIT",
|
||||
"Apache-2.0 OR MIT",
|
||||
"Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT",
|
||||
"BSD-2-Clause",
|
||||
"BSD-2-Clause OR Apache-2.0 OR MIT",
|
||||
"BSD-3-Clause",
|
||||
"BSD-3-Clause OR Apache-2.0",
|
||||
"BSD-3-Clause OR MIT OR Apache-2.0",
|
||||
"CC0-1.0 OR MIT-0 OR Apache-2.0",
|
||||
"ISC",
|
||||
"ISC AND (Apache-2.0 OR ISC)",
|
||||
"ISC AND (Apache-2.0 OR ISC) AND Apache-2.0 AND MIT AND BSD-3-Clause AND (Apache-2.0 OR ISC OR MIT) AND (Apache-2.0 OR ISC OR MIT-0)",
|
||||
"MIT",
|
||||
"MIT OR Apache-2.0",
|
||||
"MIT OR Apache-2.0 OR Zlib",
|
||||
"MIT OR Zlib OR Apache-2.0",
|
||||
"MIT/Apache-2.0",
|
||||
"MPL-2.0",
|
||||
"Unicode-3.0",
|
||||
"Unlicense OR MIT",
|
||||
"Unlicense/MIT",
|
||||
"Zlib",
|
||||
"Zlib OR Apache-2.0 OR MIT"
|
||||
]
|
||||
}
|
||||
@@ -28,7 +28,10 @@ android {
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(25)
|
||||
compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
allWarningsAsErrors.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -31,7 +31,6 @@ compose-ui = { module = "androidx.compose.ui:ui" }
|
||||
compose-material3 = { module = "androidx.compose.material3:material3" }
|
||||
compose-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "adaptive" }
|
||||
compose-ui-test = { module = "androidx.compose.ui:ui-test-junit4" }
|
||||
compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
|
||||
activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
|
||||
lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
|
||||
lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
|
||||
|
||||
@@ -3946,11 +3946,17 @@
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.junit" name="junit-bom" version="5.10.2">
|
||||
<artifact name="junit-bom-5.10.2.module">
|
||||
<sha256 value="de23b114b3e4119a8fe6eb17bed5a3852816698bace67071579d6d927ebb080a" origin="Reviewed against Maven Central for Linux ARM64 CI"/>
|
||||
</artifact>
|
||||
<artifact name="junit-bom-5.10.2.pom">
|
||||
<sha256 value="169dd904a4b0f6520cffe658cc62292bfe9f3c14a989fa92120724cde43a9968" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.junit" name="junit-bom" version="5.11.0-M2">
|
||||
<artifact name="junit-bom-5.11.0-M2.module">
|
||||
<sha256 value="86477abcf490d6ca059aa9973cb108d22a506f49d1a5569bb32cc6cbf43c2cce" origin="Reviewed against Maven Central for Linux ARM64 CI"/>
|
||||
</artifact>
|
||||
<artifact name="junit-bom-5.11.0-M2.pom">
|
||||
<sha256 value="4a3ffc4a4edcfec2cb5d619922706a9407161798571939f854dfda73e4e17cc8" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
@@ -4108,5 +4114,15 @@
|
||||
<sha256 value="0fe31326e83b7622cbcd9c75d467b291f01ffe3cc6e3e76651ac05a1c1c7a360" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.google.guava" name="guava-parent" version="33.4.0-jre">
|
||||
<artifact name="guava-parent-33.4.0-jre.pom">
|
||||
<sha256 value="3a499ed34a0d9ee0f1bcc39230021a1cd4e2f7dd0426ab6844f585465d41dcd7" origin="Reviewed against Maven Central for Linux ARM64 CI"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-bom" version="1.8.0">
|
||||
<artifact name="kotlinx-coroutines-bom-1.8.0.pom">
|
||||
<sha256 value="1239e9dbe1397cd5971342956b2511bc3ace7b641842e4372a088dcfa8b9ad55" origin="Reviewed against Maven Central for Linux ARM64 CI"/>
|
||||
</artifact>
|
||||
</component>
|
||||
</components>
|
||||
</verification-metadata>
|
||||
+3
-3
@@ -2,9 +2,9 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
networkTimeout=60000
|
||||
retries=3
|
||||
retryBackOffMs=2000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -32,7 +32,10 @@ android {
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(25)
|
||||
compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
allWarningsAsErrors.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -12,7 +12,7 @@ android {
|
||||
defaultConfig {
|
||||
minSdk = libs.versions.minSdk.get().toInt()
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64") }
|
||||
ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a") }
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
@@ -22,12 +22,17 @@ android {
|
||||
lint {
|
||||
abortOnError = true
|
||||
warningsAsErrors = true
|
||||
// The user scoped this milestone to ARM Android; x86 ChromeOS is excluded.
|
||||
disable += "ChromeOsAbiSupport"
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(25)
|
||||
compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
allWarningsAsErrors.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -73,6 +78,12 @@ androidComponents.onVariants { variant ->
|
||||
val nativeOutput = layout.buildDirectory.dir("generated/native/$name/jniLibs")
|
||||
val rawBindings = layout.buildDirectory.dir("generated/bindings-raw/$name")
|
||||
val bindings = layout.buildDirectory.dir("generated/bindings/$name")
|
||||
val rustFlags = listOf(
|
||||
"-D warnings",
|
||||
"-C link-arg=-Wl,-z,max-page-size=16384",
|
||||
"-C link-arg=-Wl,-z,common-page-size=16384",
|
||||
"-C link-arg=-Wl,--build-id=sha1"
|
||||
).joinToString(" ")
|
||||
val native = tasks.register<Exec>("build${title}Rust") {
|
||||
dependsOn(checkRustToolchain)
|
||||
workingDir = workspace
|
||||
@@ -81,22 +92,33 @@ androidComponents.onVariants { variant ->
|
||||
inputs.property("cargoNdk", libs.versions.cargoNdk.get())
|
||||
inputs.property("ndkVersion", libs.versions.ndk.get())
|
||||
inputs.property("minSdk", libs.versions.minSdk.get())
|
||||
inputs.property("rustFlags", rustFlags)
|
||||
inputs.property("abis", listOf("arm64-v8a", "armeabi-v7a"))
|
||||
inputs.property("releaseDebug", "2")
|
||||
inputs.property(
|
||||
"devDebug",
|
||||
providers.environmentVariable("CARGO_PROFILE_DEV_DEBUG").orElse("cargo-default")
|
||||
)
|
||||
inputs.property(
|
||||
"incremental",
|
||||
providers.environmentVariable("CARGO_INCREMENTAL").orElse("cargo-default")
|
||||
)
|
||||
inputs.property(
|
||||
"cargoTargetDirectory",
|
||||
providers.environmentVariable("CARGO_TARGET_DIR").orElse("workspace-default")
|
||||
)
|
||||
outputs.dir(rawNative)
|
||||
environment("RUSTUP_TOOLCHAIN", rustVersion)
|
||||
environment("RUSTC", rustcExecutable.get())
|
||||
environment("ANDROID_NDK_HOME", ndkDirectory.get().asFile.absolutePath)
|
||||
environment("ANDROID_HOME", sdkDirectory.get().asFile.absolutePath)
|
||||
environment(
|
||||
"RUSTFLAGS",
|
||||
"-D warnings -C link-arg=-Wl,-z,max-page-size=16384 -C link-arg=-Wl,-z,common-page-size=16384"
|
||||
)
|
||||
environment("RUSTFLAGS", rustFlags)
|
||||
environment("CARGO_PROFILE_RELEASE_DEBUG", "2")
|
||||
commandLine(
|
||||
listOf(
|
||||
cargoExecutable.get(), "+$rustVersion", "ndk",
|
||||
"--target", "arm64-v8a",
|
||||
"--target", "armeabi-v7a",
|
||||
"--target", "x86_64",
|
||||
"--platform", libs.versions.minSdk.get(),
|
||||
"--output-dir", rawNative.get().asFile.absolutePath,
|
||||
"build", "--locked", "--package", "ironstorage-android", "--lib"
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package de.rfc1437.ironstorage.bridge
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import de.rfc1437.ironstorage.bridge.generated.MobileShellState
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class NativeLoadPerformanceTest {
|
||||
@Test
|
||||
fun reportsNativeLoadAndFirstRustFixtureWithoutSecretData() {
|
||||
val instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||
val runtime = NativeRuntime(instrumentation.targetContext)
|
||||
val started = SystemClock.elapsedRealtimeNanos()
|
||||
val shell = runtime.fixture(MobileShellState.EMPTY)
|
||||
val elapsed = SystemClock.elapsedRealtimeNanos() - started
|
||||
assertTrue(elapsed > 0)
|
||||
assertEquals(5, shell.pages.size)
|
||||
instrumentation.sendStatus(
|
||||
0,
|
||||
Bundle().apply { putLong("ironstorage_native_load_ns", elapsed) }
|
||||
)
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -14,6 +14,7 @@ import de.rfc1437.ironstorage.bridge.generated.MobileTab
|
||||
import de.rfc1437.ironstorage.bridge.generated.UniffiRustCallStatus
|
||||
import de.rfc1437.ironstorage.bridge.generated.uniffiTraitInterfaceCall
|
||||
import java.io.File
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
@@ -102,12 +103,17 @@ class NativeRuntimeTest {
|
||||
@Test
|
||||
fun missingLibraryAndUnsupportedAbiFailBeforeEnteringRust() {
|
||||
val abiFailure = assertThrows(NativeBridgeException::class.java) {
|
||||
NativeRuntime.supportedAbi(listOf("unsupported-fixture-abi"))
|
||||
NativeRuntime.supportedAbi(listOf("x86_64", "unsupported-fixture-abi"))
|
||||
}
|
||||
assertEquals(NativeFailureKind.UnsupportedAbi, abiFailure.kind)
|
||||
assertEquals("arm64-v8a", NativeRuntime.supportedAbi(listOf("x86_64", "arm64-v8a")))
|
||||
val packageFile = File.createTempFile("native-empty-package", ".apk", context.cacheDir)
|
||||
try {
|
||||
ZipOutputStream(packageFile.outputStream()).close()
|
||||
ZipOutputStream(packageFile.outputStream()).use { archive ->
|
||||
// API 26 rejects a ZIP with no entries; keep this valid package free of native code.
|
||||
archive.putNextEntry(ZipEntry("non-native-fixture"))
|
||||
archive.closeEntry()
|
||||
}
|
||||
val info = ApplicationInfo(context.applicationInfo).apply {
|
||||
sourceDir = packageFile.absolutePath
|
||||
splitSourceDirs = null
|
||||
|
||||
@@ -85,7 +85,7 @@ class NativeRuntime(context: Context) {
|
||||
private const val LIBRARY_FILE = "libironstorage_android.so"
|
||||
|
||||
internal fun supportedAbi(deviceAbis: List<String>): String =
|
||||
deviceAbis.firstOrNull { it in setOf("arm64-v8a", "armeabi-v7a", "x86_64") }
|
||||
deviceAbis.firstOrNull { it in setOf("arm64-v8a", "armeabi-v7a") }
|
||||
?: throw NativeBridgeException(NativeFailureKind.UnsupportedAbi)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "1.97.1"
|
||||
profile = "minimal"
|
||||
targets = ["aarch64-linux-android", "armv7-linux-androideabi", "x86_64-linux-android"]
|
||||
targets = ["aarch64-linux-android", "armv7-linux-androideabi"]
|
||||
Reference in new issue
Block a user