import java.util.Properties import org.gradle.internal.os.OperatingSystem plugins { id("com.android.application") id("org.jetbrains.kotlin.plugin.compose") } val appMinSdk = 28 android { namespace = "net.bluemangoo.indpjsk" compileSdk { version = release(37) } defaultConfig { applicationId = "net.bluemangoo.indpjsk" minSdk = appMinSdk targetSdk = 37 versionCode = 6 versionName = "1.5" } buildTypes { release { isMinifyEnabled = false proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } buildFeatures { compose = true buildConfig = true } sourceSets.getByName("main").assets.directories.add( layout.buildDirectory.dir("generated/readme").get().asFile.absolutePath ) } val copyReadmeToAssets = tasks.register("copyReadmeToAssets") { group = "build" description = "Pack README.md" from(rootProject.file("README.md")) into(layout.buildDirectory.dir("generated/readme").get().asFile) rename { "README.md" } inputs.file(rootProject.file("README.md")).withPropertyName("readme") outputs.dir(layout.buildDirectory.dir("generated/readme")).withPropertyName("readmeAssets") } tasks.named("preBuild") { dependsOn(copyReadmeToAssets) } tasks.matching { it.name.startsWith("merge") && it.name.contains("Assets") }.configureEach { dependsOn(copyReadmeToAssets) } dependencies { implementation("androidx.core:core-ktx:1.19.1") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.11.0") implementation("androidx.activity:activity-compose:1.13.0") implementation(platform("androidx.compose:compose-bom:2026.09.00")) implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.material3:material3") debugImplementation("androidx.compose.ui:ui-tooling") } val cargoTarget = "aarch64-linux-android" val cargoSoName = "libcore_vpn.so" val cargoDir = rootProject.file("core_vpn") val cargoSo = File(cargoDir, "target/$cargoTarget/release/$cargoSoName") val jniLibsArm64 = file("src/main/jniLibs/arm64-v8a") val cargoBin: String = providers.gradleProperty("cargoBin").getOrElse("cargo") fun resolveNdkDirectory(project: Project): File { val localPropertiesFile = project.rootProject.file("local.properties") val properties = Properties() if (localPropertiesFile.exists()) { localPropertiesFile.inputStream().use { properties.load(it) } } properties.getProperty("ndk.dir")?.let { path -> val file = File(path) if (file.exists()) return file } val sdkDirPath = properties.getProperty("sdk.dir") ?: System.getenv("ANDROID_SDK_ROOT") ?: System.getenv("ANDROID_HOME") ?: throw GradleException("Failed to resolve Android SDK path") val ndkParentDir = File(sdkDirPath, "ndk") if (!ndkParentDir.exists() || !ndkParentDir.isDirectory) { throw GradleException("Failed to resolve NDK directory: ${ndkParentDir.absolutePath}") } val installedVersions = ndkParentDir.listFiles { f -> f.isDirectory } ?.sortedWith(compareBy { it.name }) ?: emptyList() if (installedVersions.isEmpty()) { throw GradleException("Failed to find any installed NDK versions in ${ndkParentDir.absolutePath}") } return installedVersions.last() } fun resolveLlvmBinDirectory(ndkDir: File): File { val prebuiltDir = File(ndkDir, "toolchains/llvm/prebuilt") val hostDir = prebuiltDir.listFiles { file -> file.isDirectory }?.firstOrNull() ?: throw IllegalStateException("Ndk llvm not found: ${prebuiltDir.absolutePath}") val binDir = File(hostDir, "bin") if (!binDir.exists()) { throw IllegalStateException("Ndk llvm not found: ${binDir.absolutePath}") } return binDir } val cargoBuildVpnCore = tasks.register("cargoBuildVpnCore") { group = "build" description = "Rust build core_vpn" workingDir = cargoDir doFirst { try { val ndkDir = resolveNdkDirectory(project) val ndkHomePath = ndkDir.absolutePath val ndkBin = resolveLlvmBinDirectory(ndkDir) val scriptExt = if (OperatingSystem.current().isWindows) ".cmd" else "" val execExt = if (OperatingSystem.current().isWindows) ".exe" else "" environment("NDK_HOME", ndkHomePath) environment("PATH", "${ndkBin.absolutePath}${File.pathSeparator}${System.getenv("PATH")}") environment( "CC_aarch64-linux-android", ndkBin.resolve("aarch64-linux-android$appMinSdk-clang$scriptExt").absolutePath ) environment( "CXX_aarch64-linux-android", ndkBin.resolve("aarch64-linux-android$appMinSdk-clang++$scriptExt").absolutePath ) environment("AR_aarch64-linux-android", ndkBin.resolve("llvm-ar$execExt").absolutePath) environment( "CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER", ndkBin.resolve("aarch64-linux-android$appMinSdk-clang$scriptExt").absolutePath ) } catch (e: Exception) { logger.warn("[cargoBuildVpnCore] resolveNdkDirectory failed: ${e.message}") } } commandLine(cargoBin, "build", "--release", "--target", cargoTarget) inputs.files( File(cargoDir, "Cargo.toml"), File(cargoDir, "Cargo.lock"), File(cargoDir, "build.rs"), ).withPropertyName("rustConfig") inputs.dir(File(cargoDir, "src")).withPropertyName("rustSrc") outputs.file(cargoSo).withPropertyName("rustSo") } val copyVpnSo = tasks.register("copyVpnSo") { group = "build" description = "Sync core_vpn.so" dependsOn(cargoBuildVpnCore) inputs.file(cargoSo).withPropertyName("rustSo") outputs.dir(jniLibsArm64).withPropertyName("jniLibsArm64") doLast { check(cargoSo.isFile) { "File $cargoSo not exist" } if (jniLibsArm64.exists()) { jniLibsArm64.deleteRecursively() } jniLibsArm64.mkdirs() val dest = File(jniLibsArm64, cargoSoName) cargoSo.copyTo(dest, overwrite = true) logger.lifecycle("[copyVpnSo] ${cargoSo.absolutePath} -> ${dest.absolutePath}") } } tasks.named("preBuild") { dependsOn(copyVpnSo) }