Files
XSnap/native/build.gradle.kts
particle-box 4aba9c815f multiple fixes
2026-02-24 12:51:25 +05:30

364 lines
13 KiB
Kotlin

import java.io.File
import java.util.Locale
import java.util.Properties
import java.util.zip.CRC32
import org.apache.tools.ant.filters.FixCrLfFilter
import org.gradle.api.tasks.Exec
import org.gradle.api.tasks.Sync
data class CargoTarget(
val triple: String,
val abi: String,
val toolchainPrefix: String,
val apiLevel: Int
) {
val envSuffix: String = triple.replace('-', '_')
val envSuffixUpper: String = envSuffix.uppercase(Locale.ROOT)
val taskSuffix: String = abi.split('-', '_').joinToString("") { fragment ->
fragment.replaceFirstChar { ch -> ch.titlecase(Locale.ROOT) }
}
}
plugins {
alias(libs.plugins.androidLibrary)
}
val nativeBuildHash = rootProject.ext.get("buildHash").toString()
val nativeLibFileName = "lib${nativeBuildHash}.so"
val localProperties = Properties().apply {
val file = rootProject.file("local.properties")
if (file.exists()) {
file.inputStream().use { load(it) }
}
}
val desiredNdkVersion = (findProperty("androidNdkVersion") as? String)
?: "28.2.13676358"
val sdkDirPath = localProperties.getProperty("sdk.dir")?.trimEnd('/', '\\')
?: System.getenv("ANDROID_SDK_ROOT")?.trimEnd('/', '\\')
?: System.getenv("ANDROID_HOME")?.trimEnd('/', '\\')
val ndkHomePath = (System.getenv("ANDROID_NDK_HOME")?.trimEnd('/', '\\')
?: sdkDirPath?.let { "$it${File.separator}ndk${File.separator}$desiredNdkVersion" }
?: error("Unable to locate the Android NDK. Set ANDROID_NDK_HOME or define sdk.dir in local.properties"))
.replace("\\:", ":")
val ndkHome = File(ndkHomePath)
require(ndkHome.exists()) { "Configured NDK directory $ndkHome does not exist" }
val osName = System.getProperty("os.name").lowercase(Locale.ROOT)
val osArch = System.getProperty("os.arch").lowercase(Locale.ROOT)
val hostTag = when {
osName.contains("windows") -> "windows-x86_64"
osName.contains("mac") && osArch.contains("arm") -> "darwin-arm64"
osName.contains("mac") -> "darwin-x86_64"
else -> "linux-x86_64"
}
val toolchainBin = File(ndkHome, "toolchains/llvm/prebuilt/$hostTag/bin")
require(toolchainBin.exists()) { "Unable to locate NDK toolchain under $toolchainBin" }
val isWindowsHost = hostTag.startsWith("windows")
val clangSuffix = if (isWindowsHost) ".cmd" else ""
val llvmArExecutable = File(toolchainBin, if (isWindowsHost) "llvm-ar.exe" else "llvm-ar")
require(llvmArExecutable.exists()) { "Unable to find llvm-ar executable at ${llvmArExecutable.absolutePath}" }
val toolchainPath = listOf(toolchainBin.absolutePath, System.getenv("PATH") ?: "")
.filter { it.isNotBlank() }
.joinToString(File.pathSeparator)
fun File.toWslPath(): String {
val normalized = absolutePath.replace("\\", "/")
return if (normalized.length >= 2 && normalized[1] == ':') {
val drive = normalized[0].lowercaseChar()
"/mnt/$drive${normalized.substring(2)}"
} else normalized
}
fun File.toUnixLikePath(): String = absolutePath.replace("\\", "/")
// In this environment, WSL doesn't mount all Windows drives (e.g. /mnt/d may be missing).
// When using WSL's bash.exe, stage sources into a C:-backed temp directory and build from there.
val wslStagingDir = File(System.getProperty("java.io.tmpdir"), "purrfectsnap-wsl-native").apply { mkdirs() }
val explicitBash = System.getenv("BASH_PATH")?.takeIf { it.isNotBlank() }?.let { File(it) }
val bashCandidates = mutableListOf<File>()
explicitBash?.let { bashCandidates.add(it) }
if (isWindowsHost) {
val systemRoot = System.getenv("WINDIR") ?: "C:\\Windows"
bashCandidates.add(File(systemRoot, "System32/bash.exe"))
System.getenv("ProgramFiles")?.let {
bashCandidates.add(File(it, "Git/bin/bash.exe"))
bashCandidates.add(File(it, "Git/usr/bin/bash.exe"))
}
System.getenv("ProgramFiles(x86)")?.let {
bashCandidates.add(File(it, "Git/bin/bash.exe"))
}
}
val resolvedBash = bashCandidates.firstOrNull { it.exists() }
val bashExecutablePath = resolvedBash?.absolutePath ?: "bash"
val requiresWslPath = resolvedBash?.absolutePath?.contains("system32\\bash.exe", ignoreCase = true) == true
val nativeAbisProp = (findProperty("nativeAbis") as? String)
?.takeIf { it.isNotBlank() }
?: System.getenv("NATIVE_ABIS")
?: "arm64-v8a,armeabi-v7a"
var enabledNativeAbis = nativeAbisProp
.split(',', ';')
.map { it.trim() }
.filter { it.isNotEmpty() }
.toSet()
.ifEmpty { setOf("arm64-v8a", "armeabi-v7a") }
val requestedTasks = gradle.startParameter.taskNames.joinToString(" ")
val wantsArmv7 = requestedTasks.contains("armv7", ignoreCase = true)
val wantsArmv8 = requestedTasks.contains("armv8", ignoreCase = true)
if (wantsArmv7 && !wantsArmv8) {
enabledNativeAbis = setOf("armeabi-v7a")
} else if (wantsArmv8 && !wantsArmv7) {
enabledNativeAbis = setOf("arm64-v8a")
} else if (wantsArmv7 && wantsArmv8) {
enabledNativeAbis = enabledNativeAbis + setOf("armeabi-v7a", "arm64-v8a")
}
val cargoTargets = listOf(
CargoTarget(
triple = "aarch64-linux-android",
abi = "arm64-v8a",
toolchainPrefix = "aarch64-linux-android",
apiLevel = 28,
),
CargoTarget(
triple = "armv7-linux-androideabi",
abi = "armeabi-v7a",
toolchainPrefix = "armv7a-linux-androideabi",
apiLevel = 28,
),
).filter { enabledNativeAbis.contains(it.abi) }
require(cargoTargets.isNotEmpty()) {
"No native ABIs enabled. Check the nativeAbis/NATIVE_ABIS configuration. Current value: $nativeAbisProp"
}
val defaultRustToolchain = if (hostTag.startsWith("windows")) {
"stable-x86_64-pc-windows-gnu"
} else {
"stable"
}
val rustToolchain = (findProperty("rustToolchain") as? String)
?: System.getenv("RUST_TOOLCHAIN")
?: defaultRustToolchain
val omvllVersion = (findProperty("omvllVersion") as? String)
?: System.getenv("OMVLL_VERSION")
?: "1.4.1"
val omvllLinuxAsset = (findProperty("omvllLinuxAsset") as? String)
?: System.getenv("OMVLL_LINUX_ASSET")
?: "omvll_v1-4-1_linux_2025-10-01T09.33.59.tar.gz"
val omvllArchiveUrl = (findProperty("omvllArchiveUrl") as? String)
?.takeIf { it.isNotBlank() }
fun clangExecutableFor(target: CargoTarget): File {
val executable = File(toolchainBin, "${target.toolchainPrefix}${target.apiLevel}-clang$clangSuffix")
require(executable.exists()) { "Unable to find clang executable at ${executable.absolutePath}" }
return executable
}
fun clangPlusPlusExecutableFor(target: CargoTarget): File {
val executable = File(toolchainBin, "${target.toolchainPrefix}${target.apiLevel}-clang++$clangSuffix")
require(executable.exists()) { "Unable to find clang++ executable at ${executable.absolutePath}" }
return executable
}
// Register rustup tasks
val rustupTasks = cargoTargets.map { target ->
tasks.register<Exec>("rustup${target.taskSuffix}") {
workingDir = file("rust")
commandLine("rustup", "target", "add", "--toolchain", rustToolchain, target.triple)
}
}
// Ensure rustup task ordering
rustupTasks.forEachIndexed { index, task ->
if (index > 0) {
task.configure {
mustRunAfter(rustupTasks[index - 1])
}
}
}
// Register sync & cargo build tasks
val syncTasks = cargoTargets.mapIndexed { index, target ->
val rustupTask = rustupTasks[index]
val stageWslTask = tasks.register<Sync>("stageWslNative${target.taskSuffix}") {
enabled = requiresWslPath
from(project.layout.projectDirectory.asFile) {
into("native")
include("build-native.sh")
// Ensure LF endings inside WSL (CRLF breaks `set -euo pipefail`).
filter<FixCrLfFilter>("eol" to FixCrLfFilter.CrLf.newInstance("lf"))
}
from(project.layout.projectDirectory.asFile) {
into("native")
include("omvll_config.py")
include("rust/**")
exclude("rust/target/**")
}
from(rootProject.layout.projectDirectory.asFile) {
include("config/config.json")
}
into(wslStagingDir)
}
val cargoTask = tasks.register<Exec>("cargoBuild${target.taskSuffix}") {
group = "build"
dependsOn(rustupTask)
dependsOn(stageWslTask)
workingDir = project.layout.projectDirectory.dir("rust").asFile
val buildScript = project.layout.projectDirectory.file("build-native.sh").asFile
val scriptCommand = if (requiresWslPath) {
val wslDir = File(wslStagingDir, "native").toWslPath()
val escapedDir = wslDir.replace("'", "'\"'\"'")
val command = "cd '$escapedDir' && bash './${buildScript.name}' ${target.triple}"
listOf(
bashExecutablePath,
"-lc",
command
)
} else {
val normalizedPath = buildScript.toUnixLikePath()
listOf(
bashExecutablePath,
normalizedPath,
target.triple
)
}
inputs.file(buildScript)
commandLine = scriptCommand
val ndkPath = if (requiresWslPath) ndkHome.toWslPath() else ndkHome.absolutePath
environment("ANDROID_NDK_HOME", ndkPath)
environment(
"OMVLL_CONFIG",
(if (requiresWslPath) File(wslStagingDir, "native/omvll_config.py") else project.layout.projectDirectory.file("omvll_config.py").asFile).let {
if (requiresWslPath) it.toWslPath() else it.absolutePath
}
)
environment("RUST_TOOLCHAIN", rustToolchain)
environment("OMVLL_VERSION", omvllVersion)
environment("OMVLL_LINUX_ASSET", omvllLinuxAsset)
omvllArchiveUrl?.let { environment("OMVLL_ARCHIVE_URL", it) }
}
tasks.register("syncNative${target.taskSuffix}") {
dependsOn(cargoTask)
val outputLibName = nativeLibFileName
val wslCandidate = File(wslStagingDir, "native/rust/target/${target.triple}/release/libpurrfectsnap.so")
val localCandidate = layout.projectDirectory.file("rust/target/${target.triple}/release/libpurrfectsnap.so").asFile
val outputDir = layout.buildDirectory.dir("rustJniLibs/android/${target.abi}")
inputs.property("outputLibName", outputLibName)
inputs.files(wslCandidate, localCandidate)
outputs.dir(outputDir)
outputs.upToDateWhen { false }
val checksumsDir = layout.buildDirectory.dir("checksums")
doLast {
val sourceFile = listOf(wslCandidate, localCandidate).firstOrNull { it.exists() }
?: error(
"Native library not found for ${target.abi}. Looked in: " +
"${wslCandidate.absolutePath}, ${localCandidate.absolutePath}"
)
val abiDir = outputDir.get().asFile
abiDir.mkdirs()
project.copy {
from(sourceFile)
into(abiDir)
rename { outputLibName }
}
project.copy {
from(sourceFile)
into(abiDir)
rename { "libpurrfectsnap.so" }
}
val file = File(abiDir, outputLibName)
val crc = CRC32()
crc.update(file.readBytes())
val checksumsDirFile = checksumsDir.get().asFile
checksumsDirFile.mkdirs()
File(checksumsDirFile, target.abi).writeText(crc.value.toString())
}
}
}
val generateChecksumsFile = tasks.register("generateChecksumsFile") {
dependsOn(syncTasks)
val generatedDir = layout.buildDirectory.dir("generated/source/checksums/kotlin")
val checksumsFile = generatedDir.get().asFile.resolve("Checksums.kt")
val checksumsDir = layout.buildDirectory.dir("checksums").get().asFile
inputs.dir(checksumsDir)
outputs.file(checksumsFile)
doLast {
val checksums = checksumsDir.listFiles()?.associate {
it.name to it.readText().toLong()
} ?: emptyMap()
checksumsFile.parentFile.mkdirs()
checksumsFile.writeText(
"""
package me.eternal.purrfectsnap.nativelib
object Checksums {
val checksums = mapOf(
${checksums.entries.joinToString(",\n") { (abi, checksum) -> "\"$abi\" to ${checksum}L" }}
)
}
""".trimIndent()
)
}
}
android {
namespace = rootProject.ext["applicationId"].toString() + ".nativelib"
compileSdk = 36
ndkVersion = desiredNdkVersion
buildFeatures {
buildConfig = true
}
defaultConfig {
buildConfigField("String", "NATIVE_NAME", "\"$nativeBuildHash\".toString()")
buildConfigField("String", "MODULE_PACKAGE_NAME", "\"${rootProject.ext["applicationId"]}\"")
minSdk = 28
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
sourceSets {
getByName("main") {
jniLibs.srcDir("build/rustJniLibs/android")
java.srcDir(layout.buildDirectory.dir("generated/source/checksums/kotlin"))
}
}
}
tasks.matching { it.name.startsWith("pre") && it.name.endsWith("Build") }.configureEach {
syncTasks.forEach { dependsOn(it) }
dependsOn(generateChecksumsFile)
}
dependencies {
implementation("androidx.annotation:annotation:1.7.1")
implementation("com.google.code.gson:gson:2.10.1")
}