Android: build on JDK 25 with Gradle 9.6.1, AGP 9.4.1, Kotlin 2.2.10

Android Studio now bundles JBR 25, which Gradle 8.14's embedded Kotlin
can't parse. Follow Tauri's current template: Gradle 9.6.1, Kotlin 2.2.10,
the builtInKotlin/newDsl/failOnMissingFiles compatibility flags, the
compilerOptions DSL, and ExecOperations in BuildTask (project.exec is gone
in Gradle 9). AGP goes to 9.4.1 rather than the template's 9.3.1.

Release lint vital is turned off: AGP 9.3/9.4's lint crashes analysing the
.gradle.kts scripts (findFirCompiledSymbol), failing every release build.

The env scripts now accept JDK 17 through 25, preferring Studio's JBR.
This commit is contained in:
2026-10-16 22:39:36 -05:00
parent 89378a41fb
commit 1641d9aa47
10 changed files with 96 additions and 61 deletions

View File

@@ -18,6 +18,6 @@ repositories {
dependencies {
compileOnly(gradleApi())
implementation("com.android.tools.build:gradle:8.11.0")
implementation("com.android.tools.build:gradle:9.4.1")
}

View File

@@ -1,16 +1,28 @@
import java.io.File
import java.util.Properties
import javax.inject.Inject
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.logging.LogLevel
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
abstract class BuildTask : DefaultTask() {
@get:Inject
abstract val execOperations: ExecOperations
open class BuildTask : DefaultTask() {
@Input
var rootDirRel: String? = null
@Input
var projectDir: String? = null
// From local.properties; see RustPlugin.
@Input @Optional
var sdkDir: String? = null
@Input @Optional
var ndkDir: String? = null
@Input
var target: String? = null
@Input
var release: Boolean? = null
@@ -51,27 +63,15 @@ open class BuildTask : DefaultTask() {
val release = release ?: throw GradleException("release cannot be null")
val args = listOf("tauri", "android", "android-studio-script");
// `project.exec` inherits whatever environment the Gradle daemon has —
// which is whatever launched Android Studio, not necessarily a shell
// where ANDROID_HOME/NDK_HOME were exported. Without NDK_HOME, cargo's
// build script for `ring` (and rusqlite's bundled SQLite) can't find
// the NDK's clang and rustBuild*Debug/Release fails outright. Read
// local.properties instead — the same file AGP itself reads sdk.dir
// from — so this works regardless of how Studio was started.
val localProps = Properties().apply {
val f = project.rootProject.file("local.properties")
if (f.exists()) f.inputStream().use { load(it) }
}
project.exec {
workingDir(File(project.projectDir, rootDirRel))
execOperations.exec {
workingDir(File(projectDir, rootDirRel))
executable(executable)
localProps.getProperty("sdk.dir")?.let { environment("ANDROID_HOME", it) }
localProps.getProperty("ndk.dir")?.let { environment("NDK_HOME", it) }
sdkDir?.let { environment("ANDROID_HOME", it) }
ndkDir?.let { environment("NDK_HOME", it) }
args(args)
if (project.logger.isEnabled(LogLevel.DEBUG)) {
if (logger.isEnabled(LogLevel.DEBUG)) {
args("-vv")
} else if (project.logger.isEnabled(LogLevel.INFO)) {
} else if (logger.isEnabled(LogLevel.INFO)) {
args("-v")
}
if (release) {

View File

@@ -1,4 +1,5 @@
import com.android.build.api.dsl.ApplicationExtension
import java.util.Properties
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
@@ -46,6 +47,18 @@ open class RustPlugin : Plugin<Project> {
}
}
// The cargo build inherits whatever environment the Gradle daemon has —
// which is whatever launched Android Studio, not necessarily a shell
// where ANDROID_HOME/NDK_HOME were exported. Without NDK_HOME, cargo's
// build script for `ring` (and rusqlite's bundled SQLite) can't find
// the NDK's clang and rustBuild*Debug/Release fails outright. Read
// local.properties instead — the same file AGP itself reads sdk.dir
// from — so this works regardless of how Studio was started.
val localProps = Properties().apply {
val f = rootProject.file("local.properties")
if (f.exists()) f.inputStream().use { load(it) }
}
afterEvaluate {
for (profile in listOf("debug", "release")) {
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
@@ -63,13 +76,16 @@ open class RustPlugin : Plugin<Project> {
val targetName = targetPair.value
val targetArch = archList[targetPair.index]
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
val targetBuildTask = project.tasks.maybeCreate(
val targetBuildTask = project.tasks.register(
"rustBuild$targetArchCapitalized$profileCapitalized",
BuildTask::class.java
).apply {
) {
group = TASK_GROUP
description = "Build dynamic library in $profile mode for $targetArch"
rootDirRel = config.rootDirRel
projectDir = project.projectDir.path
sdkDir = localProps.getProperty("sdk.dir")
ndkDir = localProps.getProperty("ndk.dir")
target = targetName
release = profile == "release"
}