From cecb1522fed493999ebd46d6ade97e55de345f2a Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Thu, 4 Jun 2020 00:56:24 +0800 Subject: [PATCH] refactor build clash scripts --- build.gradle.kts | 2 +- core/build.gradle.kts | 35 ++++- core/clash.gradle.kts | 254 +++++++++++++------------------ core/src/main/cpp/CMakeLists.txt | 10 ++ core/src/main/cpp/bridge.c | 8 + core/src/main/golang/main.go | 7 + 6 files changed, 156 insertions(+), 160 deletions(-) create mode 100644 core/src/main/cpp/CMakeLists.txt create mode 100644 core/src/main/cpp/bridge.c diff --git a/build.gradle.kts b/build.gradle.kts index f68b8ed..3d6fa83 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,7 +30,7 @@ buildscript { jcenter() } dependencies { - classpath("com.android.tools.build:gradle:4.0.0-rc01") + classpath("com.android.tools.build:gradle:4.0.0") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") classpath("org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion") } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 44ae59e..6733a09 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,4 +1,4 @@ -apply(from = "clash.gradle.kts") +import android.databinding.tool.ext.toCamelCase plugins { id("com.android.library") @@ -7,6 +7,8 @@ plugins { id("kotlinx-serialization") } +apply(from = "clash.gradle.kts") + val rootExtra = rootProject.extra val gCompileSdkVersion: Int by rootExtra @@ -23,7 +25,10 @@ val gKotlinCoroutineVersion: String by rootExtra val gKotlinSerializationVersion: String by rootExtra val gAndroidKtxVersion: String by rootExtra -val clashCoreOutput = buildDir.resolve("extraSources") +val geoipOutput = buildDir.resolve("outputs/geoip") +val golangSource = file("src/main/golang") +val golangOutput = buildDir.resolve("outputs/golang") +val nativeAbis = listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") android { compileSdkVersion(gCompileSdkVersion) @@ -37,6 +42,13 @@ android { versionName = gVersionName consumerProguardFiles("consumer-rules.pro") + + externalNativeBuild { + cmake { + abiFilters(*nativeAbis.toTypedArray()) + arguments("-DCLASH_OUTPUT=$golangOutput", "-DCLASH_SOURCE=$golangSource") + } + } } buildTypes { @@ -48,9 +60,8 @@ android { sourceSets { maybeCreate("main").apply { - assets.srcDir(clashCoreOutput.resolve("assets")) - jniLibs.srcDir(clashCoreOutput.resolve("jniLibs")) - java.srcDir(clashCoreOutput.resolve("classes")) + assets.srcDir(geoipOutput) + jniLibs.srcDir(golangOutput) } } @@ -62,6 +73,12 @@ android { kotlinOptions { jvmTarget = "1.8" } + + externalNativeBuild { + cmake { + path = file("src/main/cpp/CMakeLists.txt") + } + } } dependencies { @@ -77,6 +94,10 @@ repositories { } afterEvaluate { - tasks["clean"].dependsOn(tasks["resetGolangPathMode"]) - tasks["preBuild"].dependsOn(tasks["extractSources"], tasks["downloadGeoipDatabase"]) + android.buildTypes.forEach { + val cName = it.name.toCamelCase() + + tasks["externalNativeBuild${cName}"].dependsOn(tasks["compileClashCore"]) + tasks["package${cName}Assets"].dependsOn(tasks["downloadGeoipDatabase"]) + } } \ No newline at end of file diff --git a/core/clash.gradle.kts b/core/clash.gradle.kts index 91bfe49..3604868 100644 --- a/core/clash.gradle.kts +++ b/core/clash.gradle.kts @@ -2,85 +2,99 @@ import org.apache.tools.ant.taskdefs.condition.Os import java.io.* import java.util.* import java.net.* +import java.time.* -object Constants { - const val GEOIP_DATABASE_URL = "https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb" - const val GEOIP_INVALID_INTERVAL = 1000L * 60 * 60 * 24 * 7 +val gMinSdkVersion: Int by rootProject.extra - const val SOURCE_PATH = "src/main/golang" - const val OUTPUT_PATH = "extraSources" +val geoipDatabaseUrl = "https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb" +val geoipInvalidate = Duration.ofDays(7) +val geoipOutput = buildDir.resolve("outputs/geoip") +val golangSource = file("src/main/golang") +val golangOutput = buildDir.resolve("outputs/golang") +val nativeAbis = listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") - const val GOLANG_BASE = "intermediates/golang" - const val GOLANG_PATH = "$GOLANG_BASE/path" - const val GOLANG_BIND = "$GOLANG_BASE/bind" - const val GOLANG_BINARY = "$GOLANG_PATH/bin" - const val GOLANG_OUTPUT = "$GOLANG_BASE/bridge.aar" - const val GOLANG_OUTPUT_SOURCES = "$GOLANG_BASE/bridge-sources.jar" +fun generateGolangBuildEnvironment(abi: String): Map { + val properties = Properties().apply { + load(FileInputStream(rootProject.file("local.properties"))) + } - val STUB_GO_FILE_CONTENT = """ - package main + val ndk = properties.getProperty("ndk.dir") + ?: throw GradleScriptException("ndk.dir not found in local.properties", + FileNotFoundException("ndk.dir not found in local.properties")) - import "github.com/kr328/cfa/bridge" - - func main() {} - """.trimIndent() - val STUB_GO_MOD_CONTENT = """ - module github.com/kr328/cfa-bind - - require github.com/kr328/cfa v0.0.0 // redirect - - replace github.com/kr328/cfa => {SOURCE_PATH} - - """.trimIndent() + val host = when { + Os.isFamily(Os.FAMILY_WINDOWS) -> + "windows" + Os.isFamily(Os.FAMILY_MAC) -> + "darwin" + Os.isFamily(Os.FAMILY_UNIX) -> + "linux" + else -> + throw GradleScriptException("Unsupported host", FileNotFoundException("Unsupported host")) + } - val REGEX_REPLACE = Regex("replace\\s+(\\S+)\\s+(\\S*)\\s*=>\\s*(\\S+)\\s*(\\S*)\\s*") - val REGEX_JNI = Regex("^jni/") -} + val compilerBase = rootProject.file(ndk).resolve("toolchains/llvm/prebuilt/$host-x86_64/bin") -fun generateGolangBuildEnvironment(vararg pathAppend: String): Map { - val environment = TreeMap(String.CASE_INSENSITIVE_ORDER).apply { putAll(System.getenv()) } - val properties = Properties().apply { load(rootProject.file("local.properties").inputStream()) } + val cCompiler = when(abi) { + "armeabi-v7a" -> + "armv7a-$host-androideabi$gMinSdkVersion-clang" + "arm64-v8a" -> + "aarch64-$host-android$gMinSdkVersion-clang" + "x86" -> + "i686-$host-android$gMinSdkVersion-clang" + "x86_64" -> + "x86_64-$host-android$gMinSdkVersion-clang" + else -> + throw GradleScriptException("Unsupported abi $abi", FileNotFoundException("Unsupported abi $abi")) + } - val sdkPath = properties.getProperty("sdk.dir") - ?: throw GradleScriptException("sdk.dir not found", FileNotFoundException()) - val ndkPath = properties.getProperty("ndk.dir") - ?: throw GradleScriptException("ndk.dir not found", FileNotFoundException()) + val cppCompiler = when(abi) { + "armeabi-v7a" -> + "armv7a-$host-androideabi$gMinSdkVersion-clang++" + "arm64-v8a" -> + "aarch64-$host-android$gMinSdkVersion-clang++" + "x86" -> + "i686-$host-android$gMinSdkVersion-clang++" + "x86_64" -> + "x86_64-$host-android$gMinSdkVersion-clang++" + else -> + throw GradleScriptException("Unsupported abi $abi", FileNotFoundException("Unsupported abi $abi")) + } - val pathSeparator = if ( Os.isFamily(Os.FAMILY_WINDOWS) ) ";" else ":" + val linker = when(abi) { + "armeabi-v7a" -> + "arm-$host-androideabi-ld" + "arm64-v8a" -> + "aarch64-$host-android-ld" + "x86" -> + "i686-$host-android-ld" + "x86_64" -> + "x86_64-$host-android-ld" + else -> + throw GradleScriptException("Unsupported abi $abi", FileNotFoundException("Unsupported abi $abi")) + } - environment["GOPATH"] = listOf(buildDir.resolve(Constants.GOLANG_PATH).absolutePath, *pathAppend) - .joinToString(separator = pathSeparator) - environment["ANDROID_HOME"] = sdkPath - environment["ANDROID_NDK_HOME"] = ndkPath - environment["PATH"] += "$pathSeparator${buildDir.resolve(Constants.GOLANG_BINARY)}" + val golangArch = when(abi) { + "armeabi-v7a" -> + "arm" + "arm64-v8a" -> + "arm64" + "x86" -> + "386" + "x86_64" -> + "amd64" + else -> + throw GradleScriptException("Unsupported abi $abi", FileNotFoundException("Unsupported abi $abi")) + } - return environment -} - -fun generateGolangModule(): String { - val moduleFile = file(Constants.SOURCE_PATH).resolve("go.mod") - - val replaces = moduleFile - .readLines() - .asSequence() - .map { line -> Constants.REGEX_REPLACE.matchEntire(line) } - .filterNotNull() - .map { match -> - val source = match.groupValues[1].trim() - val sVersion = match.groupValues[2].trim() - val target = match.groupValues[3].trim() - val tVersion = match.groupValues[4].trim() - - val resolvedTarget = if ( target.startsWith("./") ) - moduleFile.parentFile!!.resolve(target).canonicalPath - else - target - - "replace $source $sVersion => $resolvedTarget $tVersion" - }.joinToString(separator = "\n") - - return Constants.STUB_GO_MOD_CONTENT - .replace("{SOURCE_PATH}", file(Constants.SOURCE_PATH).absolutePath) + replaces + return mapOf( + "CC" to compilerBase.resolve(cCompiler).absolutePath, + "CXX" to compilerBase.resolve(cppCompiler).absolutePath, + "LD" to compilerBase.resolve(linker).absolutePath, + "GOOS" to "android", + "GOARCH" to golangArch, + "CGO_ENABLED" to "1" + ) } fun String.exec(pwd: File = buildDir, env: Map = System.getenv()): String { @@ -109,110 +123,46 @@ fun String.exec(pwd: File = buildDir, env: Map = System.getenv() return outputStream.toString("utf-8") } -task("generateClashBindSources") { +task("compileClashCore") { onlyIf { - val lastModified = file(Constants.SOURCE_PATH).walk() - .filter { it.extension == "go" || it.extension == "mod" } + val sourceModified = golangSource.walk() + .filter { it.extension == "go" || it.name == "go.mod" } .map { it.lastModified() } - .max() ?: 0L + .max() ?: Long.MAX_VALUE + val targetModified = golangOutput.walk() + .filter { it.extension == "so" } + .map { it.lastModified() } + .min() ?: Long.MIN_VALUE - return@onlyIf lastModified > buildDir.resolve(Constants.GOLANG_OUTPUT).lastModified() - } - - doFirst { - buildDir.resolve(Constants.GOLANG_BIND).apply { - deleteRecursively() - mkdirs() - } + sourceModified > targetModified } doLast { - val environment = generateGolangBuildEnvironment() + nativeAbis.parallelStream().forEach { + val env = generateGolangBuildEnvironment(it) + val out = golangOutput.resolve(it).apply { + mkdirs() + }.resolve("libclash.so") - val bind = buildDir.resolve(Constants.GOLANG_BIND).apply { - resolve("main.go").writeText(Constants.STUB_GO_FILE_CONTENT) - resolve("go.mod").writeText(generateGolangModule()) - } - - "go mod vendor".exec(pwd = bind, env = environment) - - buildDir.resolve(Constants.GOLANG_BIND).apply { - resolve("vendor").renameTo(resolve("src")) - resolve("go.mod").delete() - resolve("main.go").delete() - resolve("go.sum").delete() + "go build --buildmode=c-shared -o \"$out\"".exec(pwd = golangSource, env = env) } } } -task("assembleClashCore") { - dependsOn(tasks["generateClashBindSources"]) - - onlyIf { - !tasks["generateClashBindSources"].state.skipped - } - - doFirst { - val environment = generateGolangBuildEnvironment() - - "go get golang.org/x/mobile/cmd/gomobile".exec(env = environment) - } - - doLast { - val bind = buildDir.resolve(Constants.GOLANG_BIND) - val environment = generateGolangBuildEnvironment(bind.absolutePath) - - "gomobile init".exec(pwd = bind, env = environment) - "gomobile bind -target=android -trimpath github.com/kr328/cfa/bridge" - .exec(pwd = buildDir.resolve(Constants.GOLANG_BASE), env = environment) - } -} - -task("extractSources", type = Copy::class) { - dependsOn(tasks["assembleClashCore"]) - - from(zipTree(buildDir.resolve(Constants.GOLANG_OUTPUT))) { - include("**/*.so") - eachFile { - path = path.replace(Constants.REGEX_JNI, "jniLibs/") - } - } - from(zipTree(buildDir.resolve(Constants.GOLANG_OUTPUT_SOURCES))) { - include("**/*.java") - into("classes") - } - - destinationDir = buildDir.resolve(Constants.OUTPUT_PATH) -} - task("downloadGeoipDatabase") { - dependsOn(tasks["extractSources"]) + val geoipFile = geoipOutput.resolve("Country.mmdb") onlyIf { - val file = buildDir.resolve(Constants.OUTPUT_PATH).resolve("assets/Country.mmdb") - - System.currentTimeMillis() - file.lastModified() > Constants.GEOIP_INVALID_INTERVAL + System.currentTimeMillis() - geoipFile.lastModified() > geoipInvalidate.toMillis() } doLast { - val assets = buildDir.resolve(Constants.OUTPUT_PATH).resolve("assets") + geoipOutput.mkdirs() - assets.mkdirs() - - URL(Constants.GEOIP_DATABASE_URL).openConnection().getInputStream().use { input -> - FileOutputStream(assets.resolve("Country.mmdb")).use { output -> + URL(geoipDatabaseUrl).openConnection().getInputStream().use { input -> + FileOutputStream(geoipFile).use { output -> input.copyTo(output) } } } } - -task("resetGolangPathMode", type = Exec::class) { - onlyIf { - !Os.isFamily(Os.FAMILY_WINDOWS) - } - - commandLine("chmod", "-R", "777", buildDir.resolve(Constants.GOLANG_PATH)) - - isIgnoreExitValue = true -} \ No newline at end of file diff --git a/core/src/main/cpp/CMakeLists.txt b/core/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000..31eb2b0 --- /dev/null +++ b/core/src/main/cpp/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.0) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) + +include_directories(${CLASH_OUTPUT}/${ANDROID_ABI} ${CLASH_SOURCE}) +link_directories(${CLASH_OUTPUT}/${CMAKE_ANDROID_ARCH_ABI}) +link_libraries(log clash) + +add_library(bridge SHARED bridge.c) \ No newline at end of file diff --git a/core/src/main/cpp/bridge.c b/core/src/main/cpp/bridge.c new file mode 100644 index 0000000..2365ecc --- /dev/null +++ b/core/src/main/cpp/bridge.c @@ -0,0 +1,8 @@ +#include + +#include "libclash.h" + +JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *unused) { + initialize(); + return JNI_VERSION_1_6; +} \ No newline at end of file diff --git a/core/src/main/golang/main.go b/core/src/main/golang/main.go index 38dd16d..e4a12e0 100644 --- a/core/src/main/golang/main.go +++ b/core/src/main/golang/main.go @@ -1,3 +1,10 @@ package main +import "C" + func main() {} + +//export initialize +func initialize() { + +} \ No newline at end of file