build: add Android release APK packaging script

This commit is contained in:
Edison Jwa
2026-05-25 01:20:16 +09:00
parent 5515ff6643
commit 1326b03301
2 changed files with 183 additions and 24 deletions
@@ -33,6 +33,24 @@ val releaseStorePassword = resolveSigningProp("CHANORA_RELEASE_STORE_PASSWORD")
val releaseKeyAlias = resolveSigningProp("CHANORA_RELEASE_KEY_ALIAS")
val releaseKeyPassword = resolveSigningProp("CHANORA_RELEASE_KEY_PASSWORD")
val hasReleaseSigning = listOf(releaseStoreFile, releaseStorePassword, releaseKeyAlias, releaseKeyPassword).all { !it.isNullOrBlank() }
val splitPerAbiRequested = gradle.startParameter.taskNames.any { taskName ->
taskName.contains("split-per-abi", ignoreCase = true)
}
val configuredAbiSet = listOf("arm64-v8a", "x86_64")
val flutterTargetPlatformArg = gradle.startParameter.projectProperties["target-platform"]
?.split(',')
?.map { it.trim() }
?.firstOrNull()
val singleAbiFromFlutterTargetPlatform = when (flutterTargetPlatformArg) {
"android-arm64" -> "arm64-v8a"
"android-x64" -> "x86_64"
else -> null
}
val effectivePackagingAbis: List<String> = when {
singleAbiFromFlutterTargetPlatform != null -> listOf(singleAbiFromFlutterTargetPlatform)
splitPerAbiRequested -> configuredAbiSet
else -> configuredAbiSet
}
android {
namespace = "app.chanora.chanora_flutter"
@@ -77,7 +95,7 @@ android {
// ANDROID_PLATFORM as -D variables to the child cmake
// invocation). See Cargo.toml [patch.crates-io] block and
// docs/governance/product-decision-register.md DEC-032.
abiFilters += listOf("arm64-v8a", "x86_64")
abiFilters += effectivePackagingAbis
}
}
@@ -137,6 +155,16 @@ android {
}
}
packaging {
jniLibs {
if (singleAbiFromFlutterTargetPlatform != null) {
val excludedAbis = configuredAbiSet.filter { it != singleAbiFromFlutterTargetPlatform } +
listOf("armeabi-v7a", "x86")
excludes += excludedAbis.map { abi -> "lib/$abi/**" }
}
}
}
// SDD-109 item 2: Android App Bundle (.aab) split configuration.
// Combined with the SDD-073 abiFilters set, this yields two
// native splits (arm64-v8a, x86_64), per-language resource
@@ -155,7 +183,7 @@ android {
}
}
// ONNX Runtime native library for Silero / TEN VAD.
// ONNX Runtime native library for Silero VAD.
// The ort crate (Rust) loads libonnxruntime.so via dlopen at runtime
// (`load-dynamic` feature). The AAR ships the .so for arm64-v8a,
// armeabi-v7a, x86_64, x86. AGP merges these into the APK/AAB.
@@ -229,7 +257,11 @@ val abiToRustTriple: Map<String, String> = mapOf(
// `android.defaultConfig.ndk.abiFilters` declaration owned by SDD-073 item 4
// so that the build-automation list and the AGP packaging list are guaranteed
// in sync. Any unsupported ABI fails fast.
val configuredAbis: List<String> = android.defaultConfig.ndk.abiFilters.toList()
val configuredAbis: List<String> = when {
singleAbiFromFlutterTargetPlatform != null -> listOf(singleAbiFromFlutterTargetPlatform)
splitPerAbiRequested -> configuredAbiSet
else -> android.defaultConfig.ndk.abiFilters.toList()
}
configuredAbis.forEach { abi ->
require(abiToRustTriple.containsKey(abi)) {
"[SDD-118] ABI '$abi' is not supported by SDD-118; see SDD-073 item 4. " +
@@ -458,7 +490,7 @@ val abiToNdkSysrootTriple: Map<String, String> = mapOf(
"arm64-v8a" to "aarch64-linux-android",
"x86_64" to "x86_64-linux-android",
)
fun registerJniLibsCopyTask(profile: String): TaskProvider<Copy> {
fun registerJniLibsCopyTask(profile: String): TaskProvider<Task> {
val capitalized = profile.replaceFirstChar { it.uppercase() }
val buildTask = if (profile == "release") buildRustBridgeRelease else buildRustBridgeDebug
@@ -468,16 +500,30 @@ fun registerJniLibsCopyTask(profile: String): TaskProvider<Copy> {
.takeUnless { it.isNullOrBlank() }
?: android.ndkDirectory.absolutePath
return tasks.register<Copy>("copyRustBridgeJniLibs$capitalized") {
return tasks.register("copyRustBridgeJniLibs$capitalized") {
group = "chanora_bridge"
description = "SDD-118 item 6 (extended): stage per-ABI libchanora_bridge.so + libc++_shared.so (profile=$profile) into src/main/jniLibs/."
description = "SDD-118 item 6 (extended): strip and stage per-ABI libchanora_bridge.so + libc++_shared.so (profile=$profile) into src/main/jniLibs/."
dependsOn(buildTask)
duplicatesStrategy = DuplicatesStrategy.INCLUDE
val jniLibsDir = layout.projectDirectory.dir("src/main/jniLibs").asFile
val llvmStrip = file(
"$effectiveNdkPath/toolchains/llvm/prebuilt/$ndkHostTag/bin/llvm-strip"
)
doFirst {
jniLibsDir
.listFiles()
?.filter { it.isDirectory && !configuredAbis.contains(it.name) }
?.forEach { staleDir ->
staleDir.deleteRecursively()
}
}
configuredAbis.forEach { abi ->
val triple = abiToRustTriple.getValue(abi)
val sysrootTriple = abiToNdkSysrootTriple.getValue(abi)
val bridgeSo = cargoWorkspaceRoot.resolve("target/$triple/$profile/libchanora_bridge.so")
val strippedBridgeSo = layout.buildDirectory
.file("intermediates/stripped_rust_jni/$profile/$abi/libchanora_bridge.so")
.get()
.asFile
// SDD-118 item 6 (extended): also stage libc++_shared.so from the
// NDK sysroot. libchanora_bridge.so is dynamically linked against
// the NDK's shared C++ runtime (via audiopus_sys / opus-cpp and
@@ -487,28 +533,37 @@ fun registerJniLibsCopyTask(profile: String): TaskProvider<Copy> {
val cxxSharedSo = file(
"$effectiveNdkPath/toolchains/llvm/prebuilt/$ndkHostTag/sysroot/usr/lib/$sysrootTriple/libc++_shared.so"
)
from(bridgeSo) {
into(abi)
}
from(cxxSharedSo) {
into(abi)
}
// SDD-118 item 8: input tracking for both staged sources so
// Gradle correctly invalidates when either changes (e.g. NDK
// version bump replacing libc++_shared.so).
inputs.file(bridgeSo).withPropertyName("bridgeSo_$abi")
inputs.file(cxxSharedSo).withPropertyName("cxxSharedSo_$abi")
}
into(layout.projectDirectory.dir("src/main/jniLibs"))
inputs.file(llvmStrip).withPropertyName("llvmStrip")
outputs.file(strippedBridgeSo)
outputs.file(File(jniLibsDir, "$abi/libchanora_bridge.so"))
outputs.file(File(jniLibsDir, "$abi/libc++_shared.so"))
// SDD-118 item 8: declare staged outputs so Gradle can track them.
configuredAbis.forEach { abi ->
outputs.file(
layout.projectDirectory.file("src/main/jniLibs/$abi/libchanora_bridge.so")
)
outputs.file(
layout.projectDirectory.file("src/main/jniLibs/$abi/libc++_shared.so")
)
doLast {
val abiDir = File(jniLibsDir, abi)
abiDir.mkdirs()
strippedBridgeSo.parentFile.mkdirs()
bridgeSo.copyTo(strippedBridgeSo, overwrite = true)
project.exec {
commandLine(
llvmStrip.absolutePath,
"--strip-debug",
strippedBridgeSo.absolutePath,
)
}
strippedBridgeSo.copyTo(
File(abiDir, "libchanora_bridge.so"),
overwrite = true,
)
cxxSharedSo.copyTo(
File(abiDir, "libc++_shared.so"),
overwrite = true,
)
}
}
}
}