From cc34f4cb526c2bac20b2a6c08b15907380b3f352 Mon Sep 17 00:00:00 2001 From: Zach Klippenstein Date: Tue, 3 Nov 2026 17:41:45 -0800 Subject: [PATCH] Migrate to AGP 9 built-in Kotlin and new DSL Removes the temporary android.builtInKotlin=false / android.newDsl=false opt-outs added with the Compose 1.12.1 upgrade. AGP marks both deprecated and removes them in AGP 10. - Drop the standalone org.jetbrains.kotlin.android plugin from every module; AGP now compiles Kotlin itself. The Kotlin Gradle plugin stays on the buildscript classpath to pin the Kotlin version (AGP depends on KGP at runtime and a higher classpath version wins), and the org.jetbrains.kotlin.plugin.compose plugin is still the Compose compiler wiring for the Compose modules. - Move compiler options from tasks.withType to the kotlin {} extension AGP registers. jvmTarget is no longer set explicitly: built-in Kotlin defaults it to compileOptions.targetCompatibility, so radiography stays on JVM 1.8 and the Compose test modules on JVM 11. - Use kotlin { explicitApi() } in radiography; AGP applies it to production sources only, resolving the old TODO. - binary-compatibility-validator 0.16.3 only configures Android modules when it sees the kotlin-android plugin id, so it registered no api tasks at all under built-in Kotlin. radiography/build.gradle.kts now registers BCV's public apiBuild/apiCheck/apiDump task types itself, mirroring BCV's kotlin-android path, and adds kotlin-metadata-jvm to BCV's worker classpath. api/radiography.api is unchanged. - Drop the explicit kotlin("stdlib") dependency in stoic-plugin; built-in Kotlin adds it. Verified: ./gradlew build --warning-mode all is green with no deprecation warnings, apiCheck passes with no .api changes (and fails on an added public class), and connectedCheck passes on a Samsung SM-A256U1 (43 tests). Co-Authored-By: Claude Fable 5.1 --- build.gradle.kts | 20 +++--- compose-tests/build.gradle.kts | 11 +--- compose-unsupported-tests/build.gradle.kts | 11 +--- gradle.properties | 4 -- radiography/build.gradle.kts | 72 +++++++++++++++++----- sample-compose/build.gradle.kts | 11 +--- sample/build.gradle.kts | 5 +- stoic-plugin/build.gradle.kts | 6 +- 8 files changed, 72 insertions(+), 68 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index af9d2d5..6774a2f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,8 +17,7 @@ import com.ncorti.ktfmt.gradle.KtfmtExtension import com.vanniktech.maven.publish.AndroidSingleVariantLibrary import com.vanniktech.maven.publish.MavenPublishBaseExtension import kotlinx.validation.ApiValidationExtension -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidExtension println("Building with Kotlin compiler version ${libs.versions.kotlin.get()}") @@ -106,14 +105,17 @@ subprojects { } } - tasks.withType { - compilerOptions { - // Allow warnings when running from IDE, makes it easier to experiment. - if (!isRunningFromIde) { - allWarningsAsErrors.set(true) + // AGP's built-in Kotlin registers the `kotlin` extension for every Android module. + // jvmTarget is not set here: with built-in Kotlin it defaults to each module's + // android.compileOptions.targetCompatibility. + plugins.withId("com.android.base") { + configure { + compilerOptions { + // Allow warnings when running from IDE, makes it easier to experiment. + if (!isRunningFromIde) { + allWarningsAsErrors.set(true) + } } - - jvmTarget.set(JvmTarget.JVM_1_8) } } } diff --git a/compose-tests/build.gradle.kts b/compose-tests/build.gradle.kts index 7c7e8eb..4c68ce0 100644 --- a/compose-tests/build.gradle.kts +++ b/compose-tests/build.gradle.kts @@ -1,9 +1,5 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - plugins { id("com.android.library") - kotlin("android") id("org.jetbrains.kotlin.plugin.compose") } @@ -32,12 +28,7 @@ android { testNamespace = "com.squareup.radiography.test.compose" } -tasks.withType { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - freeCompilerArgs.addAll("-opt-in=kotlin.RequiresOptIn") - } -} +kotlin { compilerOptions { freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn") } } dependencies { androidTestImplementation(project(":radiography")) diff --git a/compose-unsupported-tests/build.gradle.kts b/compose-unsupported-tests/build.gradle.kts index 3dd9d13..999d98e 100644 --- a/compose-unsupported-tests/build.gradle.kts +++ b/compose-unsupported-tests/build.gradle.kts @@ -1,9 +1,5 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - plugins { id("com.android.library") - kotlin("android") id("org.jetbrains.kotlin.plugin.compose") } @@ -32,12 +28,7 @@ android { testNamespace = "com.squareup.radiography.test.compose.unsupported" } -tasks.withType { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - freeCompilerArgs.addAll("-opt-in=kotlin.RequiresOptIn") - } -} +kotlin { compilerOptions { freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn") } } dependencies { androidTestImplementation(project(":radiography")) diff --git a/gradle.properties b/gradle.properties index 9adac4f..48b109b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -35,7 +35,3 @@ POM_LICENCE_DIST=repo POM_DEVELOPER_ID=square POM_DEVELOPER_NAME=Square, Inc. - -# AGP 9 enables built-in Kotlin by default; keep using the standalone Kotlin Gradle plugin for now. -android.builtInKotlin=false -android.newDsl=false diff --git a/radiography/build.gradle.kts b/radiography/build.gradle.kts index 59c6269..06984c8 100644 --- a/radiography/build.gradle.kts +++ b/radiography/build.gradle.kts @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import kotlinx.validation.KotlinApiBuildTask +import kotlinx.validation.KotlinApiCompareTask import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("com.android.library") - kotlin("android") id("com.vanniktech.maven.publish.base") } @@ -44,25 +45,64 @@ android { testNamespace = "com.squareup.radiography.test" } -tasks.withType { - compilerOptions { - freeCompilerArgs.addAll( - listOfNotNull( - "-opt-in=kotlin.RequiresOptIn", - - // Require explicit public modifiers and types. - // TODO this should be moved to a top-level `kotlin { explicitApi() }` once that's working - // for android projects, see https://youtrack.jetbrains.com/issue/KT-37652. - "-Xexplicit-api=strict" - .takeUnless { - // Tests aren't part of the public API, don't turn explicit API mode on for them. - name.contains("test", ignoreCase = true) - }, - ) +kotlin { + // Require explicit public modifiers and types. AGP's built-in Kotlin only applies this to + // production sources; tests aren't part of the public API. + explicitApi() + + compilerOptions { freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn") } +} + +// The binary-compatibility-validator plugin (applied in the root build script) only configures +// Android modules when the standalone org.jetbrains.kotlin.android plugin is applied, which AGP's +// built-in Kotlin support replaces. Until BCV (or KGP's own abiValidation) supports built-in +// Kotlin, reproduce BCV's kotlin-android wiring here using its public task types: apiBuild dumps +// the public API of the release compilation, apiCheck compares it with api/radiography.api, and +// apiDump updates that file. +val apiDir = layout.projectDirectory.dir("api") +val apiFileName = "${project.name}.api" + +// BCV only adds kotlin-metadata-jvm to its worker classpath when it sees a Kotlin plugin id. +dependencies.add( + "bcv-rt-jvm-cp", + "org.jetbrains.kotlin:kotlin-metadata-jvm:${libs.versions.kotlin.get()}", +) + +val apiBuild = + tasks.register("apiBuild") { + description = "Builds Kotlin API for the release compilation of ${project.name}." + // AGP's compilations don't populate output.classesDirs, so read the compile tasks' outputs. + inputClassesDirs.from( + tasks.named("compileReleaseKotlin").flatMap { it.destinationDirectory }, + tasks.named("compileReleaseJavaWithJavac").flatMap { it.destinationDirectory }, ) + inputDependencies.from( + kotlin.target.compilations.named("release").map { it.compileDependencyFiles } + ) + outputApiFile.set(layout.buildDirectory.file("api/$apiFileName")) + runtimeClasspath.from(configurations.named("bcv-rt-jvm-cp-resolver")) + } + +val apiCheck = + tasks.register("apiCheck") { + group = "verification" + description = + "Checks signatures of public API against the golden value in API folder for ${project.name}" + projectApiFile.set(apiDir.file(apiFileName)) + generatedApiFile.set(apiBuild.flatMap { it.outputApiFile }) } + +tasks.register("apiDump") { + description = "Syncs the API file for ${project.name}" + val from = apiBuild.flatMap { it.outputApiFile } + val to = apiDir.file(apiFileName) + inputs.file(from) + outputs.file(to) + doLast { from.get().asFile.copyTo(to.asFile, overwrite = true) } } +tasks.named("check") { dependsOn(apiCheck) } + dependencies { implementation(libs.curtains) // We don't want to bring any Compose dependencies in unless the consumer of this library is diff --git a/sample-compose/build.gradle.kts b/sample-compose/build.gradle.kts index a0b4776..8e1eb94 100644 --- a/sample-compose/build.gradle.kts +++ b/sample-compose/build.gradle.kts @@ -1,9 +1,5 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - plugins { id("com.android.application") - kotlin("android") id("org.jetbrains.kotlin.plugin.compose") } @@ -29,12 +25,7 @@ android { testNamespace = "com.squareup.radiography.sample.compose.test" } -tasks.withType { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_1_8) - freeCompilerArgs.addAll("-opt-in=kotlin.RequiresOptIn") - } -} +kotlin { compilerOptions { freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn") } } dependencies { implementation(project(":radiography")) diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index b989b4e..3a9677e 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -14,10 +14,7 @@ * limitations under the License. */ -plugins { - id("com.android.application") - kotlin("android") -} +plugins { id("com.android.application") } android { compileSdk = 35 diff --git a/stoic-plugin/build.gradle.kts b/stoic-plugin/build.gradle.kts index 6a1e884..52dd427 100644 --- a/stoic-plugin/build.gradle.kts +++ b/stoic-plugin/build.gradle.kts @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -plugins { - id("com.android.application") - kotlin("android") -} +plugins { id("com.android.application") } android { namespace = "radiography.stoic" @@ -38,7 +35,6 @@ android { dependencies { compileOnly("com.squareup.stoic:plugin-sdk:0.7.0") - implementation(kotlin("stdlib")) implementation(project(":radiography")) }