From dfcf9c2de44e72bf089c12e362771eda7848690c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Quenaudon?= Date: Fri, 28 Aug 2026 14:27:34 +0100 Subject: [PATCH] Generate protos into commonMain for multiplatform projects with an Android target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since 6.0.0-alpha02, a Kotlin multiplatform project with an Android target gets no generateCommonMainProtos task. Wire generates into the Android variants only, and commonMain plus every non-Android target see no generated code. Two causes, both from the #3503 rewrite. First, forEachWireSource checks hasAndroid before the Kotlin multiplatform arm, so any project with an Android plugin takes the Android path. Second, the rewrite removed the afterEvaluate deferral for Android projects. The setup now runs at android-plugin-apply time, before the build script evaluates the wire {} block. The Android path tolerates this because onVariants defers its callbacks. The multiplatform path reads extension.outputs eagerly, so it would wire nothing into the compilations. Fix both: the multiplatform arm now precedes the hasAndroid arm, and the android handler defers applyWirePlugin to afterEvaluate when the KotlinMultiplatformExtension is present. That path does not use the Android variant API, so the deferral is safe. Pure Android projects keep the synchronous call that onVariants requires. Add the kotlin-multiplatform-android fixture. The only multiplatform fixture had no Android target, which is why CI never caught this. Fixes #3688 Co-authored-by: Benoît Quenaudon Signed-off-by: Benoît Quenaudon --- CHANGELOG.md | 7 +++ .../com/squareup/wire/gradle/WirePlugin.kt | 11 +++- .../wire/gradle/kotlin/SourceRoots.kt | 11 ++-- .../squareup/wire/gradle/WirePluginTest.kt | 25 +++++++++ .../kotlin-multiplatform-android/build.gradle | 55 +++++++++++++++++++ .../gradle.properties | 2 + .../settings.gradle | 19 +++++++ .../proto/squareup/dinosaurs/dinosaur.proto | 19 +++++++ .../proto/squareup/geology/period.proto | 16 ++++++ 9 files changed, 160 insertions(+), 5 deletions(-) create mode 100644 wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/build.gradle create mode 100644 wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/gradle.properties create mode 100644 wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/settings.gradle create mode 100644 wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/src/commonMain/proto/squareup/dinosaurs/dinosaur.proto create mode 100644 wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/src/commonMain/proto/squareup/geology/period.proto diff --git a/CHANGELOG.md b/CHANGELOG.md index 21266ed160..d937d44e7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ Change Log ========== +Unreleased +---------- + +### Gradle plugin + +* Fix: generate protos into commonMain again for Kotlin multiplatform projects with an Android target (#3688) + Version 7.0.0-alpha09 --------------------- diff --git a/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/WirePlugin.kt b/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/WirePlugin.kt index 250b7bee1c..a5df5abeb3 100644 --- a/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/WirePlugin.kt +++ b/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/WirePlugin.kt @@ -56,7 +56,16 @@ class WirePlugin : Plugin { // When `android.builtInKotlin` property is enabled, AGP provides Kotlin support for all projects without // requiring users to apply the `org.jetbrains.kotlin.android` plugin. project.extensions.findByName("kotlin")?.let { kotlin.set(true) } - applyWirePlugin() + if (project.extensions.findByType(KotlinMultiplatformExtension::class.java) != null) { + // Multiplatform project with an Android target. Wire generates into commonMain and reads + // the wire {} extension eagerly, so the setup must wait for the build script to be + // evaluated. This path does not use the Android variant API, so afterEvaluate is safe. + project.afterEvaluate { applyWirePlugin() } + } else { + // The Android setup must run now: the variant API used in forEachWireSource requires + // onVariants to be registered before AGP computes its variants. + applyWirePlugin() + } } project.plugins.withId("com.android.application", androidPluginHandler) project.plugins.withId("com.android.library", androidPluginHandler) diff --git a/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/kotlin/SourceRoots.kt b/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/kotlin/SourceRoots.kt index d25772d980..d561534bd9 100644 --- a/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/kotlin/SourceRoots.kt +++ b/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/kotlin/SourceRoots.kt @@ -41,6 +41,13 @@ internal fun forEachWireSource( sourceHandler: (WireSource) -> Unit, ) { when { + // A multiplatform project can also have an Android target which applies an Android plugin. + // The Kotlin Multiplatform check has to come first: Wire generates once into commonMain, and + // the Android target consumes commonMain like every other target. + hasKotlin && project.extensions.findByType(KotlinMultiplatformExtension::class.java) != null -> { + val extension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + extension.sourceRoots().forEach(sourceHandler) + } hasAndroid -> { val extension = project.extensions.getByType(AndroidComponentsExtension::class.java) extension.onVariants { variant -> @@ -57,10 +64,6 @@ internal fun forEachWireSource( sourceHandler(source) } } - hasKotlin && project.extensions.findByType(KotlinMultiplatformExtension::class.java) != null -> { - val extension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) - extension.sourceRoots().forEach(sourceHandler) - } hasKotlin -> { val kotlinSourceSets = project.extensions.findByType(KotlinProjectExtension::class.java)?.sourceSets val javaSourceSets = project.extensions.findByType(SourceSetContainer::class.java) diff --git a/wire-gradle-plugin/src/test/kotlin/com/squareup/wire/gradle/WirePluginTest.kt b/wire-gradle-plugin/src/test/kotlin/com/squareup/wire/gradle/WirePluginTest.kt index 7797cf43f5..770091e1d1 100644 --- a/wire-gradle-plugin/src/test/kotlin/com/squareup/wire/gradle/WirePluginTest.kt +++ b/wire-gradle-plugin/src/test/kotlin/com/squareup/wire/gradle/WirePluginTest.kt @@ -1057,6 +1057,31 @@ class WirePluginTest { assertThat(generatedProto2).exists() } + @Test + fun kotlinMultiplatformWithAndroidTarget() { + val fixtureRoot = File("src/test/projects/kotlin-multiplatform-android") + + val result = fixtureGradleRunner( + fixtureRoot, + "assemble", + "--info", + "--no-build-cache", + ).build() + + // Wire generates once into commonMain. The Android target consumes commonMain like every + // other target, so there must be no per-variant generation task. + assertThat(result.task(":generateCommonMainProtos")).isNotNull() + assertThat(result.task(":generateDebugProtos")).isNull() + assertThat(result.task(":generateReleaseProtos")).isNull() + + val generatedProto1 = + File(fixtureRoot, "build/generated/source/wire/com/squareup/dinosaurs/Dinosaur.kt") + val generatedProto2 = + File(fixtureRoot, "build/generated/source/wire/com/squareup/geology/Period.kt") + assertThat(generatedProto1).exists() + assertThat(generatedProto2).exists() + } + private fun fieldsFromProtoSource(generatedProtoSource: String): List { val protoFieldPattern = "@field:WireField.*?(val .*?):" val matchedFields = protoFieldPattern.toRegex(setOf(MULTILINE, DOT_MATCHES_ALL)) diff --git a/wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/build.gradle b/wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/build.gradle new file mode 100644 index 0000000000..104ef7dc2d --- /dev/null +++ b/wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/build.gradle @@ -0,0 +1,55 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +buildscript { + dependencies { + classpath "com.squareup.wire:wire-gradle-plugin:$wireVersion" + classpath libs.pluginz.kotlin + classpath libs.pluginz.android + } + + repositories { + maven { + url new File(rootDir, "../../../../../build/localMaven").toURI().toString() + } + mavenCentral() + google() + } +} + +apply plugin: 'org.jetbrains.kotlin.multiplatform' +apply plugin: 'com.android.library' +apply plugin: 'com.squareup.wire' + +repositories { + maven { + url new File(rootDir, "../../../../../build/localMaven").toURI().toString() + } + mavenCentral() + google() +} + +android { + namespace = 'com.squareup.wire.kmpandroid' + compileSdk = 36 +} + +kotlin { + jvm() + androidTarget() +} + +wire { + kotlin { + } +} + +tasks.withType(JavaCompile).configureEach { + sourceCompatibility = JavaVersion.VERSION_11.toString() + targetCompatibility = JavaVersion.VERSION_11.toString() +} + +tasks.withType(KotlinCompile).configureEach { + kotlinOptions { + jvmTarget = "11" + } +} diff --git a/wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/gradle.properties b/wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/gradle.properties new file mode 100644 index 0000000000..63ae174737 --- /dev/null +++ b/wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/gradle.properties @@ -0,0 +1,2 @@ +android.builtInKotlin=false +android.newDsl=false diff --git a/wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/settings.gradle b/wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/settings.gradle new file mode 100644 index 0000000000..7af53a5e7a --- /dev/null +++ b/wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/settings.gradle @@ -0,0 +1,19 @@ +pluginManagement { + repositories { + maven { + url new File(rootDir, "../../../../../build/localMaven").toURI().toString() + } + google() + mavenCentral() + } +} + +include ':' + +dependencyResolutionManagement { + versionCatalogs { + libs { + from(files('../../../../../gradle/libs.versions.toml')) + } + } +} diff --git a/wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/src/commonMain/proto/squareup/dinosaurs/dinosaur.proto b/wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/src/commonMain/proto/squareup/dinosaurs/dinosaur.proto new file mode 100644 index 0000000000..9218b789bb --- /dev/null +++ b/wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/src/commonMain/proto/squareup/dinosaurs/dinosaur.proto @@ -0,0 +1,19 @@ +syntax = "proto2"; + +package squareup.dinosaurs; + +option java_package = "com.squareup.dinosaurs"; + +import "squareup/geology/period.proto"; + +message Dinosaur { + /** Common name of this dinosaur, like "Stegosaurus". */ + optional string name = 1; + + /** URLs with images of this dinosaur. */ + repeated string picture_urls = 2; + + optional double length_meters = 3; + optional double mass_kilograms = 4; + optional squareup.geology.Period period = 5; +} diff --git a/wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/src/commonMain/proto/squareup/geology/period.proto b/wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/src/commonMain/proto/squareup/geology/period.proto new file mode 100644 index 0000000000..fc96fbeb73 --- /dev/null +++ b/wire-gradle-plugin/src/test/projects/kotlin-multiplatform-android/src/commonMain/proto/squareup/geology/period.proto @@ -0,0 +1,16 @@ +syntax = "proto2"; + +package squareup.geology; + +option java_package = "com.squareup.geology"; + +enum Period { + /** 145.5 million years ago — 66.0 million years ago. */ + CRETACEOUS = 1; + + /** 201.3 million years ago — 145.0 million years ago. */ + JURASSIC = 2; + + /** 252.17 million years ago — 201.3 million years ago. */ + TRIASSIC = 3; +}