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 releaseKeyAlias = resolveSigningProp("CHANORA_RELEASE_KEY_ALIAS")
|
||||||
val releaseKeyPassword = resolveSigningProp("CHANORA_RELEASE_KEY_PASSWORD")
|
val releaseKeyPassword = resolveSigningProp("CHANORA_RELEASE_KEY_PASSWORD")
|
||||||
val hasReleaseSigning = listOf(releaseStoreFile, releaseStorePassword, releaseKeyAlias, releaseKeyPassword).all { !it.isNullOrBlank() }
|
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 {
|
android {
|
||||||
namespace = "app.chanora.chanora_flutter"
|
namespace = "app.chanora.chanora_flutter"
|
||||||
@@ -77,7 +95,7 @@ android {
|
|||||||
// ANDROID_PLATFORM as -D variables to the child cmake
|
// ANDROID_PLATFORM as -D variables to the child cmake
|
||||||
// invocation). See Cargo.toml [patch.crates-io] block and
|
// invocation). See Cargo.toml [patch.crates-io] block and
|
||||||
// docs/governance/product-decision-register.md DEC-032.
|
// 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.
|
// SDD-109 item 2: Android App Bundle (.aab) split configuration.
|
||||||
// Combined with the SDD-073 abiFilters set, this yields two
|
// Combined with the SDD-073 abiFilters set, this yields two
|
||||||
// native splits (arm64-v8a, x86_64), per-language resource
|
// 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
|
// The ort crate (Rust) loads libonnxruntime.so via dlopen at runtime
|
||||||
// (`load-dynamic` feature). The AAR ships the .so for arm64-v8a,
|
// (`load-dynamic` feature). The AAR ships the .so for arm64-v8a,
|
||||||
// armeabi-v7a, x86_64, x86. AGP merges these into the APK/AAB.
|
// 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
|
// `android.defaultConfig.ndk.abiFilters` declaration owned by SDD-073 item 4
|
||||||
// so that the build-automation list and the AGP packaging list are guaranteed
|
// so that the build-automation list and the AGP packaging list are guaranteed
|
||||||
// in sync. Any unsupported ABI fails fast.
|
// 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 ->
|
configuredAbis.forEach { abi ->
|
||||||
require(abiToRustTriple.containsKey(abi)) {
|
require(abiToRustTriple.containsKey(abi)) {
|
||||||
"[SDD-118] ABI '$abi' is not supported by SDD-118; see SDD-073 item 4. " +
|
"[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",
|
"arm64-v8a" to "aarch64-linux-android",
|
||||||
"x86_64" to "x86_64-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 capitalized = profile.replaceFirstChar { it.uppercase() }
|
||||||
val buildTask = if (profile == "release") buildRustBridgeRelease else buildRustBridgeDebug
|
val buildTask = if (profile == "release") buildRustBridgeRelease else buildRustBridgeDebug
|
||||||
|
|
||||||
@@ -468,16 +500,30 @@ fun registerJniLibsCopyTask(profile: String): TaskProvider<Copy> {
|
|||||||
.takeUnless { it.isNullOrBlank() }
|
.takeUnless { it.isNullOrBlank() }
|
||||||
?: android.ndkDirectory.absolutePath
|
?: android.ndkDirectory.absolutePath
|
||||||
|
|
||||||
return tasks.register<Copy>("copyRustBridgeJniLibs$capitalized") {
|
return tasks.register("copyRustBridgeJniLibs$capitalized") {
|
||||||
group = "chanora_bridge"
|
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)
|
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 ->
|
configuredAbis.forEach { abi ->
|
||||||
val triple = abiToRustTriple.getValue(abi)
|
val triple = abiToRustTriple.getValue(abi)
|
||||||
val sysrootTriple = abiToNdkSysrootTriple.getValue(abi)
|
val sysrootTriple = abiToNdkSysrootTriple.getValue(abi)
|
||||||
val bridgeSo = cargoWorkspaceRoot.resolve("target/$triple/$profile/libchanora_bridge.so")
|
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
|
// SDD-118 item 6 (extended): also stage libc++_shared.so from the
|
||||||
// NDK sysroot. libchanora_bridge.so is dynamically linked against
|
// NDK sysroot. libchanora_bridge.so is dynamically linked against
|
||||||
// the NDK's shared C++ runtime (via audiopus_sys / opus-cpp and
|
// 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(
|
val cxxSharedSo = file(
|
||||||
"$effectiveNdkPath/toolchains/llvm/prebuilt/$ndkHostTag/sysroot/usr/lib/$sysrootTriple/libc++_shared.so"
|
"$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
|
// SDD-118 item 8: input tracking for both staged sources so
|
||||||
// Gradle correctly invalidates when either changes (e.g. NDK
|
// Gradle correctly invalidates when either changes (e.g. NDK
|
||||||
// version bump replacing libc++_shared.so).
|
// version bump replacing libc++_shared.so).
|
||||||
inputs.file(bridgeSo).withPropertyName("bridgeSo_$abi")
|
inputs.file(bridgeSo).withPropertyName("bridgeSo_$abi")
|
||||||
inputs.file(cxxSharedSo).withPropertyName("cxxSharedSo_$abi")
|
inputs.file(cxxSharedSo).withPropertyName("cxxSharedSo_$abi")
|
||||||
}
|
inputs.file(llvmStrip).withPropertyName("llvmStrip")
|
||||||
into(layout.projectDirectory.dir("src/main/jniLibs"))
|
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.
|
doLast {
|
||||||
configuredAbis.forEach { abi ->
|
val abiDir = File(jniLibsDir, abi)
|
||||||
outputs.file(
|
abiDir.mkdirs()
|
||||||
layout.projectDirectory.file("src/main/jniLibs/$abi/libchanora_bridge.so")
|
strippedBridgeSo.parentFile.mkdirs()
|
||||||
)
|
bridgeSo.copyTo(strippedBridgeSo, overwrite = true)
|
||||||
outputs.file(
|
project.exec {
|
||||||
layout.projectDirectory.file("src/main/jniLibs/$abi/libc++_shared.so")
|
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