build: add Android release APK packaging script
This commit is contained in:
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
APP_DIR="$REPO_ROOT/apps/chanora_flutter"
|
||||
OUTPUT_DIR="$APP_DIR/build/app/outputs/flutter-apk"
|
||||
DIST_DIR="$APP_DIR/build/app/outputs/release-dist"
|
||||
PUBSPEC_PATH="$APP_DIR/pubspec.yaml"
|
||||
ANDROID_SDK_ROOT_DEFAULT="$HOME/Library/Android/sdk"
|
||||
|
||||
resolve_apksigner() {
|
||||
local sdk_root="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-$ANDROID_SDK_ROOT_DEFAULT}}"
|
||||
local build_tools_dir="$sdk_root/build-tools"
|
||||
if [[ ! -d "$build_tools_dir" ]]; then
|
||||
echo "Android build-tools directory not found: $build_tools_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local latest_build_tools
|
||||
latest_build_tools="$(find "$build_tools_dir" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -n 1)"
|
||||
if [[ -z "$latest_build_tools" ]]; then
|
||||
echo "No Android build-tools versions found under: $build_tools_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local apksigner_path="$latest_build_tools/apksigner"
|
||||
if [[ ! -x "$apksigner_path" ]]; then
|
||||
echo "apksigner not found or not executable: $apksigner_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$apksigner_path"
|
||||
}
|
||||
|
||||
if ! command -v flutter >/dev/null 2>&1; then
|
||||
echo "flutter not found on PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$PUBSPEC_PATH" ]]; then
|
||||
echo "pubspec.yaml not found at $PUBSPEC_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$APP_DIR/android/key.properties" ]]; then
|
||||
echo "Missing signing config: $APP_DIR/android/key.properties" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
APKSIGNER_BIN="$(resolve_apksigner)"
|
||||
|
||||
VERSION_LINE="$(grep '^version:' "$PUBSPEC_PATH" | head -n 1 | awk '{print $2}')"
|
||||
if [[ -z "${VERSION_LINE:-}" ]]; then
|
||||
echo "Could not parse version from $PUBSPEC_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION_NAME="${VERSION_LINE%%+*}"
|
||||
BUILD_NUMBER="${VERSION_LINE##*+}"
|
||||
APP_NAME="chanora"
|
||||
|
||||
mkdir -p "$DIST_DIR"
|
||||
|
||||
build_one() {
|
||||
local target_platform="$1"
|
||||
local abi_label="$2"
|
||||
local release_path_flutter="$OUTPUT_DIR/app-release.apk"
|
||||
local release_path_gradle="$APP_DIR/build/app/outputs/apk/release/app-release.apk"
|
||||
local named_apk="$DIST_DIR/${APP_NAME}-${VERSION_NAME}+${BUILD_NUMBER}-${abi_label}-release.apk"
|
||||
|
||||
rm -f "$release_path_flutter" "$release_path_gradle"
|
||||
(
|
||||
cd "$APP_DIR"
|
||||
flutter build apk --release --target-platform "$target_platform" --tree-shake-icons >&2
|
||||
)
|
||||
|
||||
local built_apk=""
|
||||
if [[ -f "$release_path_flutter" ]]; then
|
||||
built_apk="$release_path_flutter"
|
||||
elif [[ -f "$release_path_gradle" ]]; then
|
||||
built_apk="$release_path_gradle"
|
||||
fi
|
||||
|
||||
if [[ -z "$built_apk" ]]; then
|
||||
echo "Expected release APK missing: $release_path_flutter" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "$built_apk" "$named_apk"
|
||||
"$APKSIGNER_BIN" verify --print-certs "$named_apk" >/dev/null
|
||||
echo "$named_apk"
|
||||
}
|
||||
|
||||
ARM64_APK="$(build_one android-arm64 arm64-v8a)"
|
||||
echo "Built: $ARM64_APK"
|
||||
|
||||
if rustup target list --installed | grep -qx 'x86_64-linux-android'; then
|
||||
X64_APK="$(build_one android-x64 x86_64)"
|
||||
echo "Built: $X64_APK"
|
||||
else
|
||||
echo "Skipped x86_64 build: rustup target x86_64-linux-android is not installed"
|
||||
fi
|
||||
Reference in New Issue
Block a user