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 1c0505b633..d25772d980 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 @@ -141,12 +141,12 @@ private class JvmOrKmpSource( javaSourceDirectorySet?.srcDir(outputDirectory) } is KotlinOutput -> { - registerKotlinGeneratedSources(kotlinSourceSet, outputDirectory) + registerKotlinGeneratedSources(project, kotlinSourceSet, outputDirectory) } else -> { // Custom and third-party outputs are wildcards, so we add all output directories. javaSourceDirectorySet?.srcDir(outputDirectory) - registerKotlinGeneratedSources(kotlinSourceSet, outputDirectory) + registerKotlinGeneratedSources(project, kotlinSourceSet, outputDirectory) } } } @@ -185,23 +185,34 @@ private class AndroidSource( } } +private const val KSP_PLUGIN_ID = "com.google.devtools.ksp" + /** * Registers [outputDirectory] as a generated Kotlin source directory on [kotlinSourceSet]. * * On Kotlin 2.3+, uses the [KotlinSourceSet.generatedKotlin] API so that IDEs can distinguish * generated sources from handwritten ones. Falls back to [KotlinSourceSet.kotlin] on older - * versions of the Kotlin Gradle Plugin where the API is not available. + * versions of the Kotlin Gradle Plugin where the API is not available, and on projects using + * KSP, which doesn't know about `generatedKotlin`: sources registered there are invisible to + * symbol processors, and KSP tasks lose the dependency on the Wire task they used to get from + * the `kotlin.srcDir()` registration, failing Gradle's execution-time dependency validation in + * builds which also reference Wire's output directory from another source set. */ private fun registerKotlinGeneratedSources( + project: Project, kotlinSourceSet: KotlinSourceSet?, outputDirectory: Provider, ) { if (kotlinSourceSet == null) return // generatedKotlin was introduced experimentally in Kotlin 2.3. Detect it reflectively so that // Wire remains compatible with earlier Kotlin Gradle Plugin versions. - val generatedKotlinMethod = runCatching { - kotlinSourceSet.javaClass.getMethod("getGeneratedKotlin") - }.getOrNull() + val generatedKotlinMethod = if (project.pluginManager.hasPlugin(KSP_PLUGIN_ID)) { + null + } else { + runCatching { + kotlinSourceSet.javaClass.getMethod("getGeneratedKotlin") + }.getOrNull() + } if (generatedKotlinMethod != null) { val generatedKotlin = generatedKotlinMethod.invoke(kotlinSourceSet) as SourceDirectorySet generatedKotlin.srcDir(outputDirectory) 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 b4432765dd..7797cf43f5 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 @@ -713,6 +713,26 @@ class WirePluginTest { assertThat(result.task(":compileKotlin")).isNotNull() } + /** + * Verifies that projects using KSP build even though `KotlinSourceSet.generatedKotlin` is + * available. KSP doesn't know about `generatedKotlin`, so registering Wire's output there + * hides it from symbol processors and drops the task dependency KSP builds used to get from + * the `kotlin.srcDir()` registration, failing Gradle's dependency validation ("Task + * ':kspKotlin' uses this output of task ':generateMainProtos' without declaring an explicit + * or implicit dependency") when the output directory is also referenced from another source + * set. Wire falls back to `kotlin.srcDir()` on such projects. + */ + @Test + fun kotlinProjectKotlinProtosWithGeneratedKotlinApiAndKsp() { + val fixtureRoot = File("src/test/projects/kotlin-project-kotlin-protos-kgp23-ksp") + + val result = fixtureGradleRunner(fixtureRoot, "clean", "build").build() + + assertThat(result.task(":generateMainProtos")).isNotNull() + assertThat(result.task(":kspKotlin")).isNotNull() + assertThat(result.task(":compileKotlin")).isNotNull() + } + @Test fun protoLibrary() { val fixtureRoot = File("src/test/projects/proto-library") diff --git a/wire-gradle-plugin/src/test/projects/kotlin-project-kotlin-protos-kgp23-ksp/build.gradle b/wire-gradle-plugin/src/test/projects/kotlin-project-kotlin-protos-kgp23-ksp/build.gradle new file mode 100644 index 0000000000..bbda97602f --- /dev/null +++ b/wire-gradle-plugin/src/test/projects/kotlin-project-kotlin-protos-kgp23-ksp/build.gradle @@ -0,0 +1,63 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +buildscript { + dependencies { + classpath "com.squareup.wire:wire-gradle-plugin:$wireVersion" + // Hardcoded to a Kotlin version where the KotlinSourceSet.generatedKotlin API is available. + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10" + classpath "com.google.devtools.ksp:symbol-processing-gradle-plugin:2.3.10" + } + + repositories { + maven { + url new File(rootDir, "../../../../../build/localMaven").toURI().toString() + } + mavenCentral() + google() + } +} + +apply plugin: 'application' +apply plugin: 'org.jetbrains.kotlin.jvm' +apply plugin: 'com.google.devtools.ksp' +apply plugin: 'com.squareup.wire' + +application.mainClass = "com.squareup.dinosaurs.Sample" + +repositories { + maven { + url new File(rootDir, "../../../../../build/localMaven").toURI().toString() + } + mavenCentral() +} + +dependencies { + implementation "com.squareup.wire:wire-runtime:$wireVersion" + // Any processor will do: its presence creates the kspKotlin task, which consumes the + // main source set's Kotlin directories including Wire's generated sources. + ksp "com.squareup.moshi:moshi-kotlin-codegen:1.15.2" +} + +// Mimics real-world builds which add Wire's output directory to a source set as a plain path, +// with no task dependency. Wire's own registration of the same directory on the Kotlin source +// set is what gives the task graph the edge from KSP to the Wire task. +sourceSets { + main { + java.srcDir(layout.buildDirectory.dir("generated/source/wire")) + } +} + +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-project-kotlin-protos-kgp23-ksp/gradle.properties b/wire-gradle-plugin/src/test/projects/kotlin-project-kotlin-protos-kgp23-ksp/gradle.properties new file mode 100644 index 0000000000..d485dcca62 --- /dev/null +++ b/wire-gradle-plugin/src/test/projects/kotlin-project-kotlin-protos-kgp23-ksp/gradle.properties @@ -0,0 +1,2 @@ +# The dependency-validation regression only manifests with KSP2's KspAATask. +ksp.useKSP2=true diff --git a/wire-gradle-plugin/src/test/projects/kotlin-project-kotlin-protos-kgp23-ksp/settings.gradle b/wire-gradle-plugin/src/test/projects/kotlin-project-kotlin-protos-kgp23-ksp/settings.gradle new file mode 100644 index 0000000000..fddff8ac90 --- /dev/null +++ b/wire-gradle-plugin/src/test/projects/kotlin-project-kotlin-protos-kgp23-ksp/settings.gradle @@ -0,0 +1 @@ +include ':' diff --git a/wire-gradle-plugin/src/test/projects/kotlin-project-kotlin-protos-kgp23-ksp/src/main/java/com/squareup/dinosaurs/Sample.kt b/wire-gradle-plugin/src/test/projects/kotlin-project-kotlin-protos-kgp23-ksp/src/main/java/com/squareup/dinosaurs/Sample.kt new file mode 100644 index 0000000000..4708a5efee --- /dev/null +++ b/wire-gradle-plugin/src/test/projects/kotlin-project-kotlin-protos-kgp23-ksp/src/main/java/com/squareup/dinosaurs/Sample.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2026 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.dinosaurs + +import com.squareup.geology.Period +import java.io.IOException +import okio.ByteString.Companion.toByteString + +/* Note that we do not execute this class in tests. We're only testing that it compiles. */ +fun main(args: Array) { + val stegosaurus = Dinosaur( + name = "Stegosaurus", + period = Period.JURASSIC, + length_meters = 9.0, + mass_kilograms = 5_000.0, + picture_urls = listOf("http://goo.gl/LD5KY5", "http://goo.gl/VYRM67"), + ) + val stegosaurusEncoded = Dinosaur.ADAPTER.encode(stegosaurus) + println(stegosaurusEncoded.toByteString().base64()) +} diff --git a/wire-gradle-plugin/src/test/projects/kotlin-project-kotlin-protos-kgp23-ksp/src/main/proto/squareup/dinosaurs/dinosaur.proto b/wire-gradle-plugin/src/test/projects/kotlin-project-kotlin-protos-kgp23-ksp/src/main/proto/squareup/dinosaurs/dinosaur.proto new file mode 100644 index 0000000000..9218b789bb --- /dev/null +++ b/wire-gradle-plugin/src/test/projects/kotlin-project-kotlin-protos-kgp23-ksp/src/main/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-project-kotlin-protos-kgp23-ksp/src/main/proto/squareup/geology/period.proto b/wire-gradle-plugin/src/test/projects/kotlin-project-kotlin-protos-kgp23-ksp/src/main/proto/squareup/geology/period.proto new file mode 100644 index 0000000000..fc96fbeb73 --- /dev/null +++ b/wire-gradle-plugin/src/test/projects/kotlin-project-kotlin-protos-kgp23-ksp/src/main/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; +}