// Cross-compile the JNI crate for each configured Android ABI straight into
// src/main/jniLibs/<abi>/, which is what validateJniLibsForRelease above and the
// packaged AAR both require. The sibling buildHostJni task deliberately builds a
// host-architecture library for JVM unit tests; a host artifact can never satisfy
// an Android ABI directory, so without this task a release AAR was only ever
// buildable from libraries staged out of band.
//
// Three conditions gate execution rather than configuration, so nothing that
// works today starts requiring an NDK:
// * only release/publish task graphs — a debug AAR assembles without ABI
// libraries and must keep doing so on a machine with no Android toolchain;
// * only when an ABI's library is not already staged — a publish workflow that
// unpacks prebuilt libraries into src/main/jniLibs keeps working unchanged;
// * never when -Palef.skipAndroidJni=true.
tasks.register("buildAndroidJniLibs", Exec::class) {
val androidAbis = listOf({{ abi_literals }})
val jniLibsRoot = layout.projectDirectory.dir("src/main/jniLibs").asFile
val expectedJniLib = "lib{{ jni_lib_name }}.so"
if (project.providers.gradleProperty("alef.skipAndroidJni").orNull != "true") {
val configuredManifest = project.findProperty("alef.jniManifestPath") as String?
val jniManifest = configuredManifest?.let(::file) ?: generateSequence(projectDir) { it.parentFile }
.map { it.resolve("{{ jni_manifest_workspace_path }}") }
.firstOrNull { it.isFile }
?: throw GradleException(
"Cannot locate {{ jni_manifest_workspace_path }} from $projectDir; " +
"set -Palef.jniManifestPath=/absolute/path/to/Cargo.toml"
)
description = "Cross-compile $jniManifest for Android ABIs into src/main/jniLibs"
commandLine(
listOf("cargo", "ndk") +
androidAbis.flatMap { abi -> listOf("-t", abi) } +
listOf(
"-o",
jniLibsRoot.absolutePath,
"build",
"--release",
"--manifest-path",
jniManifest.absolutePath,
)
)
errorOutput = System.err
onlyIf {
val releaseGraph = gradle.taskGraph.allTasks.any { task ->
task.name == "assembleRelease" || task.name == "publishAndReleaseToMavenCentral"
}
releaseGraph && androidAbis.any { abi -> !jniLibsRoot.resolve(abi).resolve(expectedJniLib).isFile }
}
doFirst {
// Without this probe a missing cross-compiler surfaces as a bare
// "cargo: command not found"-class Exec failure that names neither the
// tool nor the way out.
val cargoNdkAvailable = runCatching {
ProcessBuilder("cargo", "ndk", "--version")
.redirectErrorStream(true)
.start()
.waitFor() == 0
}.getOrDefault(false)
if (!cargoNdkAvailable) {
throw GradleException(
"FATAL: cargo-ndk is required to cross-compile " + expectedJniLib + " for " +
androidAbis.joinToString(", ") + ". Install it with `cargo install cargo-ndk` " +
"and point ANDROID_NDK_HOME at an installed NDK, or stage prebuilt libraries " +
"into src/main/jniLibs/<abi>/ and pass -Palef.skipAndroidJni=true."
)
}
}
} else {
description = "Cross-compile Android JNI libraries (disabled via alef.skipAndroidJni=true)"
commandLine("true")
}
}
tasks.named("validateJniLibsForRelease") {
dependsOn("buildAndroidJniLibs")
}
// AGP's jniLibs merge consumes what buildAndroidJniLibs writes; without an explicit
// dependency Gradle's task-output validation rejects the implicit one.
tasks.matching { it.name.startsWith("merge") && it.name.endsWith("JniLibFolders") }.configureEach {
dependsOn("buildAndroidJniLibs")
}