packageRelease 任务在 ARM64 Proot 环境下丢失 manifest, 新增 fixReleaseManifest 后处理任务:从 .ap_ 提取 binary AndroidManifest.xml 注入 APK 并用 apksigner 重新签名。 仅 aarch64 环境触发,CI x86_64 不受影响。
216 lines
8.6 KiB
Plaintext
216 lines
8.6 KiB
Plaintext
import java.io.File
|
||
|
||
plugins {
|
||
alias(libs.plugins.android.application)
|
||
alias(libs.plugins.kotlin.compose)
|
||
alias(libs.plugins.kotlin.serialization)
|
||
alias(libs.plugins.ksp)
|
||
alias(libs.plugins.hilt.android)
|
||
}
|
||
|
||
android {
|
||
namespace = "com.rainy.token"
|
||
compileSdk = 35
|
||
|
||
defaultConfig {
|
||
applicationId = "com.rainy.token"
|
||
minSdk = 31
|
||
targetSdk = 35
|
||
versionCode = 5
|
||
versionName = "1.5"
|
||
|
||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||
vectorDrawables {
|
||
useSupportLibrary = true
|
||
}
|
||
}
|
||
|
||
signingConfigs {
|
||
create("release") {
|
||
storeFile = rootProject.file("release.jks")
|
||
storePassword = System.getenv("KEYSTORE_PASSWORD") ?: "RainyToken2026!"
|
||
keyAlias = System.getenv("KEYSTORE_ALIAS") ?: "rainy"
|
||
keyPassword = System.getenv("KEY_PASSWORD") ?: "RainyToken2026!"
|
||
}
|
||
}
|
||
|
||
buildTypes {
|
||
debug {
|
||
isMinifyEnabled = false
|
||
}
|
||
release {
|
||
isMinifyEnabled = false
|
||
signingConfig = signingConfigs.getByName("release")
|
||
proguardFiles(
|
||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||
"proguard-rules.pro"
|
||
)
|
||
}
|
||
}
|
||
androidResources {
|
||
// AGP 9.0 ARM64 Proot: noCompress workaround (manifest still needs post-process)
|
||
noCompress += "xml"
|
||
}
|
||
compileOptions {
|
||
sourceCompatibility = JavaVersion.VERSION_17
|
||
targetCompatibility = JavaVersion.VERSION_17
|
||
}
|
||
buildFeatures {
|
||
compose = true
|
||
}
|
||
packaging {
|
||
resources {
|
||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||
}
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// AGP 9.0 ARM64 Proot Workaround: packageRelease 丢失
|
||
// AndroidManifest.xml
|
||
//
|
||
// 修复:assembleRelease 后检查 APK 是否含 manifest,
|
||
// 缺失则注入并重新签名。仅 aarch64 环境触发。
|
||
// ═══════════════════════════════════════════════════════
|
||
if (System.getProperty("os.arch") == "aarch64") {
|
||
tasks.register("fixReleaseManifest") {
|
||
dependsOn("assembleRelease")
|
||
doLast {
|
||
val apk = layout.buildDirectory.file("outputs/apk/release/app-release.apk").get().asFile
|
||
val manifest = layout.buildDirectory
|
||
.file("intermediates/linked_resources_binary_format/release/processReleaseResources/linked-resources-binary-format-release.ap_")
|
||
.get().asFile
|
||
if (!apk.exists() || !manifest.exists()) return@doLast
|
||
|
||
// 检查 APK 是否已包含 manifest
|
||
val checkResult = providers.exec {
|
||
commandLine("unzip", "-l", apk.absolutePath)
|
||
isIgnoreExitValue = true
|
||
}.standardOutput.asText.get()
|
||
|
||
if (!checkResult.contains("AndroidManifest.xml")) {
|
||
logger.lifecycle("FixManifestTask: injecting AndroidManifest.xml into release APK")
|
||
// 从 .ap_ 提取 binary AndroidManifest.xml 到临时文件
|
||
val tmpManifest = File(apk.parentFile, "AndroidManifest.xml")
|
||
val extractProcess = Runtime.getRuntime().exec(
|
||
arrayOf("unzip", "-o", manifest.absolutePath, "AndroidManifest.xml", "-d", apk.parentFile!!.absolutePath)
|
||
)
|
||
extractProcess.waitFor()
|
||
logger.lifecycle("FixManifestTask: extract from .ap_ exit code = ${extractProcess.exitValue()}")
|
||
// 用 zip 命令注入 binary manifest
|
||
val zipProcess = Runtime.getRuntime().exec(arrayOf("zip", "-j", apk.absolutePath, tmpManifest.absolutePath))
|
||
zipProcess.waitFor()
|
||
logger.lifecycle("FixManifestTask: zip exit code = ${zipProcess.exitValue()}")
|
||
// 找 apksigner 并重新签名
|
||
val androidHome = System.getenv("ANDROID_HOME") ?: System.getenv("ANDROID_SDK_ROOT")
|
||
var signerFile: File? = null
|
||
if (androidHome != null) {
|
||
val btDir = File(androidHome, "build-tools")
|
||
val latest = btDir.listFiles()?.filter { it.isDirectory }?.maxByOrNull { it.name }
|
||
if (latest != null) {
|
||
val signer = File(latest, "apksigner")
|
||
if (signer.exists()) signerFile = signer
|
||
}
|
||
}
|
||
val sf = signerFile
|
||
if (sf != null && sf.exists()) {
|
||
// 从 android extension 拿签名配置
|
||
val androidExt = project.extensions.findByName("android")
|
||
val signingConfig = (androidExt as? com.android.build.api.dsl.ApplicationExtension)
|
||
?.signingConfigs?.findByName("release")
|
||
val ksFile = signingConfig?.storeFile?.absolutePath ?: ""
|
||
val ksPass = signingConfig?.storePassword ?: ""
|
||
val ksAlias = signingConfig?.keyAlias ?: ""
|
||
val ksKeyPass = signingConfig?.keyPassword ?: ""
|
||
val signCmd = arrayOf(
|
||
sf.absolutePath, "sign",
|
||
"--ks", ksFile,
|
||
"--ks-pass", "pass:$ksPass",
|
||
"--ks-key-alias", ksAlias,
|
||
"--key-pass", "pass:$ksKeyPass",
|
||
"--v2-signing-enabled", "true",
|
||
"--v3-signing-enabled", "false",
|
||
"--min-sdk-version", "31",
|
||
apk.absolutePath
|
||
)
|
||
val signProcess = Runtime.getRuntime().exec(signCmd)
|
||
val errorText = signProcess.errorStream.bufferedReader().readText()
|
||
signProcess.waitFor()
|
||
logger.lifecycle("FixManifestTask: apksigner exit code = ${signProcess.exitValue()}")
|
||
if (errorText.isNotBlank()) logger.lifecycle("FixManifestTask: apksigner stderr = $errorText")
|
||
logger.lifecycle("FixManifestTask: APK re-signed successfully")
|
||
} else {
|
||
logger.warn("FixManifestTask: apksigner not found, APK not re-signed")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
project.tasks.matching { it.name == "assembleRelease" }.configureEach {
|
||
finalizedBy("fixReleaseManifest")
|
||
}
|
||
}
|
||
|
||
// Force ARM64 AAPT2 in Proot environment (local only; GitHub Actions x86_64 uses default)
|
||
if (System.getProperty("os.arch") == "aarch64") {
|
||
configurations.all {
|
||
resolutionStrategy.eachDependency {
|
||
if (requested.group == "com.android.tools.build" && requested.name == "aapt2") {
|
||
useTarget("com.android.tools.build:aapt2:${'$'}{requested.version}:linux-aarch64")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
dependencies {
|
||
|
||
// Core
|
||
implementation(libs.androidx.core.ktx)
|
||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||
implementation(libs.androidx.activity.compose)
|
||
|
||
// Compose
|
||
implementation(platform(libs.androidx.compose.bom))
|
||
implementation(libs.androidx.ui)
|
||
implementation(libs.androidx.ui.graphics)
|
||
implementation(libs.androidx.ui.tooling.preview)
|
||
implementation(libs.androidx.material3)
|
||
implementation(libs.androidx.material3.window.size)
|
||
|
||
// Navigation
|
||
implementation(libs.androidx.navigation.compose)
|
||
|
||
// Network
|
||
implementation(libs.retrofit)
|
||
implementation(libs.okhttp)
|
||
implementation(libs.okhttp.logging.interceptor)
|
||
implementation(libs.kotlinx.serialization.json)
|
||
implementation(libs.retrofit.kotlinx.serialization.converter)
|
||
|
||
// DataStore
|
||
implementation(libs.androidx.datastore.preferences)
|
||
|
||
// WebKit
|
||
implementation(libs.androidx.webkit)
|
||
|
||
// DI (Hilt + KSP)
|
||
implementation(libs.hilt.android)
|
||
ksp(libs.hilt.compiler)
|
||
implementation(libs.androidx.hilt.navigation.compose)
|
||
|
||
// Room
|
||
implementation(libs.room.runtime)
|
||
implementation(libs.room.ktx)
|
||
ksp(libs.room.compiler)
|
||
|
||
// Test
|
||
testImplementation(libs.junit)
|
||
androidTestImplementation(libs.androidx.junit)
|
||
androidTestImplementation(libs.androidx.espresso.core)
|
||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||
debugImplementation(libs.androidx.ui.tooling)
|
||
debugImplementation(libs.androidx.ui.test.manifest)
|
||
}
|