Files
FlClash/android/core/build.gradle.kts
T
chen08209 fefbcf4598 feat(android): drop Firebase analytics and crash reporting
Changelog: No more Firebase analytics or crash reporting
2026-10-17 09:54:29 +08:00

276 lines
10 KiB
Kotlin

import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.io.ByteArrayOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
import javax.inject.Inject
plugins {
id("com.android.library")
}
android {
namespace = "com.follow.clash.core"
compileSdk = libs.versions.compileSdk.get().toInt()
ndkVersion = libs.versions.ndkVersion.get()
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
consumerProguardFiles("consumer-rules.pro")
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
val gomobileTargetByPlatform =
linkedMapOf("android-arm" to "android/arm", "android-arm64" to "android/arm64", "android-x64" to "android/amd64")
val gomobileTargets =
(rootProject.findProperty("target-platform") as String?)
?.split(",")
?.map { gomobileTargetByPlatform[it.trim()] ?: throw GradleException("No Core for Flutter target platform $it") }
?: gomobileTargetByPlatform.values.toList()
val isWindows = System.getProperty("os.name").startsWith("Windows")
// Gradle started from Android Studio sees the PATH of the GUI session, which leaves out Homebrew and the Go installer.
val goExecutablePath =
providers.provider {
val name = if (isWindows) "go.exe" else "go"
val home = System.getProperty("user.home")
val searchPath =
System.getenv("PATH").orEmpty().split(File.pathSeparator) +
listOf("/opt/homebrew/bin", "/usr/local/bin", "/usr/local/go/bin", "$home/go/bin")
searchPath
.filter { it.isNotEmpty() }
.map { File(it, name) }
.firstOrNull { it.canExecute() }
?.path
?: throw GradleException("The Android Core is built with gomobile, which needs Go on PATH")
}
val goModule = rootProject.layout.projectDirectory.dir("../core")
// gomobile names the library gojni, both as the file it builds and in go.Seq's System.loadLibrary, and has no option to change it.
val coreLibraryName = "clash"
val buildConfig =
providers.fileContents(rootProject.layout.projectDirectory.file("../build_config.yaml")).asText.map { text ->
text.lines()
.mapNotNull { Regex("""^(\w+):\s*(.*?)\s*$""").find(it)?.destructured }
.associate { (key, value) -> key to value.trim('"', '\'') }
}
abstract class GomobileBind : DefaultTask() {
@get:Inject
abstract val execOperations: ExecOperations
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val sources: ConfigurableFileCollection
@get:Internal
abstract val moduleDirectory: DirectoryProperty
@get:Internal
abstract val goExecutable: Property<String>
@get:Input
abstract val goVersion: Property<String>
@get:Input
abstract val targets: ListProperty<String>
@get:Input
abstract val androidApi: Property<Int>
@get:Input
abstract val tags: Property<String>
@get:Input
abstract val ldflags: Property<String>
@get:Internal
abstract val sdkDirectory: DirectoryProperty
@get:Internal
abstract val ndkDirectory: DirectoryProperty
@get:Input
abstract val ndkVersion: Property<String>
@get:Input
abstract val libraryName: Property<String>
@get:OutputFile
abstract val aar: RegularFileProperty
@get:OutputFile
abstract val classesJar: RegularFileProperty
@TaskAction
fun bind() {
val go = goExecutable.get()
val tools = temporaryDir.resolve("bin")
val executableSuffix = if (System.getProperty("os.name").startsWith("Windows")) ".exe" else ""
tools.mkdirs()
val path =
listOf(tools, File(System.getProperty("java.home"), "bin"), File(go).parentFile)
.joinToString(File.pathSeparator) { it.path } + File.pathSeparator + System.getenv("PATH").orEmpty()
execOperations.exec {
workingDir(moduleDirectory)
environment("PATH", path)
commandLine(go, "build", "-o", tools.path + File.separator, "golang.org/x/mobile/cmd/gomobile", "golang.org/x/mobile/cmd/gobind")
}
// gomobile links the library as the main package of a module it generates, whose go line is the toolchain's,
// so the GODEBUG defaults that the go line of core/go.mod sets for the desktop Core would not reach it on their own.
val godebug = ByteArrayOutputStream()
execOperations.exec {
workingDir(moduleDirectory)
environment("PATH", path)
commandLine(go, "list", "-f", "{{.DefaultGODEBUG}}", "./cmd/core")
standardOutput = godebug
}
execOperations.exec {
workingDir(moduleDirectory)
environment("PATH", path)
environment("ANDROID_HOME", sdkDirectory.get().asFile.path)
environment("ANDROID_NDK_HOME", ndkDirectory.get().asFile.path)
commandLine(
tools.resolve("gomobile$executableSuffix").path,
"bind",
"-target=${targets.get().joinToString(",")}",
"-androidapi=${androidApi.get()}",
"-javapkg=com.follow.clash.core",
"-tags=${tags.get()}",
"-ldflags=${ldflags.get()} -X=runtime.godebugDefault=${godebug.toString().trim()}",
"-o",
aar.get().asFile.path,
"./mobile",
)
}
ZipFile(aar.get().asFile).use { aarFile ->
ZipInputStream(aarFile.getInputStream(aarFile.getEntry("classes.jar"))).use { jar ->
ZipOutputStream(classesJar.get().asFile.outputStream()).use { out ->
var seqPatched = false
generateSequence { jar.nextEntry }.forEach { entry ->
var bytes = jar.readBytes()
if (entry.name == "go/Seq.class") {
bytes = bytes.replaceUtf8Constant("gojni", libraryName.get())
seqPatched = true
}
out.putNextEntry(ZipEntry(entry.name).apply { time = entry.time })
out.write(bytes)
}
if (!seqPatched) throw GradleException("gomobile's classes.jar has no go/Seq.class to load lib${libraryName.get()}.so")
}
}
}
}
private fun ByteArray.replaceUtf8Constant(old: String, new: String): ByteArray {
fun constant(value: String) = value.toByteArray().let { byteArrayOf(1, (it.size shr 8).toByte(), it.size.toByte()) + it }
val target = constant(old)
val matches = (0..size - target.size).filter { start -> target.indices.all { this[start + it] == target[it] } }
if (matches.size != 1) throw GradleException("Expected one \"$old\" constant in go/Seq.class, found ${matches.size}")
val start = matches.single()
return copyOfRange(0, start) + constant(new) + copyOfRange(start + target.size, size)
}
}
abstract class UnpackJniLibs : DefaultTask() {
@get:Inject
abstract val archiveOperations: ArchiveOperations
@get:Inject
abstract val fileSystemOperations: FileSystemOperations
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val aar: RegularFileProperty
@get:Input
abstract val libraryName: Property<String>
@get:OutputDirectory
abstract val outputDirectory: DirectoryProperty
@TaskAction
fun unpack() {
var renamed = 0
fileSystemOperations.sync {
from(archiveOperations.zipTree(aar)) {
include("jni/**")
eachFile {
path = path.removePrefix("jni/")
if (name == "libgojni.so") {
name = "lib${libraryName.get()}.so"
renamed++
}
}
includeEmptyDirs = false
}
into(outputDirectory)
}
if (renamed == 0) throw GradleException("gomobile's AAR has no libgojni.so to rename")
}
}
val bindCore =
tasks.register<GomobileBind>("bindCore") {
sources.from(
fileTree(goModule) {
exclude("**/*_test.go", "**/testdata/**", "cmd/**", "Clash.Meta/.github/**")
},
)
moduleDirectory.set(goModule)
goExecutable.set(goExecutablePath)
goVersion.set(
goExecutablePath
.flatMap { go -> providers.exec { commandLine(go, "env", "GOVERSION") }.standardOutput.asText }
.map { it.trim() },
)
targets.set(gomobileTargets)
androidApi.set(libs.versions.minSdk.get().toInt())
tags.set(buildConfig.map { it["tags"].orEmpty() })
ldflags.set(
buildConfig.map { config ->
val flags = config["go_ldflags"].orEmpty().split(" ").filter { it.isNotEmpty() }
val version = config["core_version"].orEmpty()
(flags + listOfNotNull(version.takeIf { it.isNotEmpty() }?.let { "-X=github.com/metacubex/mihomo/constant.Version=$it" }))
.joinToString(" ")
},
)
sdkDirectory.set(androidComponents.sdkComponents.sdkDirectory)
ndkDirectory.set(androidComponents.sdkComponents.ndkDirectory)
ndkVersion.set(libs.versions.ndkVersion)
libraryName.set(coreLibraryName)
aar.set(layout.buildDirectory.file("gomobile/core.aar"))
classesJar.set(layout.buildDirectory.file("gomobile/classes.jar"))
}
// A library cannot bundle a local AAR into its own, so the gomobile AAR reaches AGP as a classes jar and generated jniLibs.
androidComponents {
onVariants { variant ->
val unpack =
tasks.register<UnpackJniLibs>("unpack${variant.name.replaceFirstChar { it.uppercase() }}CoreJniLibs") {
aar.set(bindCore.flatMap { it.aar })
libraryName.set(coreLibraryName)
}
variant.sources.jniLibs?.addGeneratedSourceDirectory(unpack, UnpackJniLibs::outputDirectory)
}
}
dependencies {
implementation(files(bindCore.flatMap { it.classesJar }))
}