alef 0.80.0

Opinionated polyglot binding generator for Rust libraries
Documentation
// 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")
}