Skip to content

Commit 33688bf

Browse files
authored
Merge pull request #3665 from square/bquenaudon.2026-07-22.ksp-generated-kotlin
Gradle plugin: don't register generated sources via generatedKotlin on KSP projects
2 parents 69dc7ba + 07d283f commit 33688bf

8 files changed

Lines changed: 171 additions & 6 deletions

File tree

wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/kotlin/SourceRoots.kt

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,12 @@ private class JvmOrKmpSource(
141141
javaSourceDirectorySet?.srcDir(outputDirectory)
142142
}
143143
is KotlinOutput -> {
144-
registerKotlinGeneratedSources(kotlinSourceSet, outputDirectory)
144+
registerKotlinGeneratedSources(project, kotlinSourceSet, outputDirectory)
145145
}
146146
else -> {
147147
// Custom and third-party outputs are wildcards, so we add all output directories.
148148
javaSourceDirectorySet?.srcDir(outputDirectory)
149-
registerKotlinGeneratedSources(kotlinSourceSet, outputDirectory)
149+
registerKotlinGeneratedSources(project, kotlinSourceSet, outputDirectory)
150150
}
151151
}
152152
}
@@ -185,23 +185,34 @@ private class AndroidSource(
185185
}
186186
}
187187

188+
private const val KSP_PLUGIN_ID = "com.google.devtools.ksp"
189+
188190
/**
189191
* Registers [outputDirectory] as a generated Kotlin source directory on [kotlinSourceSet].
190192
*
191193
* On Kotlin 2.3+, uses the [KotlinSourceSet.generatedKotlin] API so that IDEs can distinguish
192194
* generated sources from handwritten ones. Falls back to [KotlinSourceSet.kotlin] on older
193-
* versions of the Kotlin Gradle Plugin where the API is not available.
195+
* versions of the Kotlin Gradle Plugin where the API is not available, and on projects using
196+
* KSP, which doesn't know about `generatedKotlin`: sources registered there are invisible to
197+
* symbol processors, and KSP tasks lose the dependency on the Wire task they used to get from
198+
* the `kotlin.srcDir()` registration, failing Gradle's execution-time dependency validation in
199+
* builds which also reference Wire's output directory from another source set.
194200
*/
195201
private fun registerKotlinGeneratedSources(
202+
project: Project,
196203
kotlinSourceSet: KotlinSourceSet?,
197204
outputDirectory: Provider<Directory>,
198205
) {
199206
if (kotlinSourceSet == null) return
200207
// generatedKotlin was introduced experimentally in Kotlin 2.3. Detect it reflectively so that
201208
// Wire remains compatible with earlier Kotlin Gradle Plugin versions.
202-
val generatedKotlinMethod = runCatching {
203-
kotlinSourceSet.javaClass.getMethod("getGeneratedKotlin")
204-
}.getOrNull()
209+
val generatedKotlinMethod = if (project.pluginManager.hasPlugin(KSP_PLUGIN_ID)) {
210+
null
211+
} else {
212+
runCatching {
213+
kotlinSourceSet.javaClass.getMethod("getGeneratedKotlin")
214+
}.getOrNull()
215+
}
205216
if (generatedKotlinMethod != null) {
206217
val generatedKotlin = generatedKotlinMethod.invoke(kotlinSourceSet) as SourceDirectorySet
207218
generatedKotlin.srcDir(outputDirectory)

wire-gradle-plugin/src/test/kotlin/com/squareup/wire/gradle/WirePluginTest.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,26 @@ class WirePluginTest {
713713
assertThat(result.task(":compileKotlin")).isNotNull()
714714
}
715715

716+
/**
717+
* Verifies that projects using KSP build even though `KotlinSourceSet.generatedKotlin` is
718+
* available. KSP doesn't know about `generatedKotlin`, so registering Wire's output there
719+
* hides it from symbol processors and drops the task dependency KSP builds used to get from
720+
* the `kotlin.srcDir()` registration, failing Gradle's dependency validation ("Task
721+
* ':kspKotlin' uses this output of task ':generateMainProtos' without declaring an explicit
722+
* or implicit dependency") when the output directory is also referenced from another source
723+
* set. Wire falls back to `kotlin.srcDir()` on such projects.
724+
*/
725+
@Test
726+
fun kotlinProjectKotlinProtosWithGeneratedKotlinApiAndKsp() {
727+
val fixtureRoot = File("src/test/projects/kotlin-project-kotlin-protos-kgp23-ksp")
728+
729+
val result = fixtureGradleRunner(fixtureRoot, "clean", "build").build()
730+
731+
assertThat(result.task(":generateMainProtos")).isNotNull()
732+
assertThat(result.task(":kspKotlin")).isNotNull()
733+
assertThat(result.task(":compileKotlin")).isNotNull()
734+
}
735+
716736
@Test
717737
fun protoLibrary() {
718738
val fixtureRoot = File("src/test/projects/proto-library")
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2+
3+
buildscript {
4+
dependencies {
5+
classpath "com.squareup.wire:wire-gradle-plugin:$wireVersion"
6+
// Hardcoded to a Kotlin version where the KotlinSourceSet.generatedKotlin API is available.
7+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10"
8+
classpath "com.google.devtools.ksp:symbol-processing-gradle-plugin:2.3.10"
9+
}
10+
11+
repositories {
12+
maven {
13+
url new File(rootDir, "../../../../../build/localMaven").toURI().toString()
14+
}
15+
mavenCentral()
16+
google()
17+
}
18+
}
19+
20+
apply plugin: 'application'
21+
apply plugin: 'org.jetbrains.kotlin.jvm'
22+
apply plugin: 'com.google.devtools.ksp'
23+
apply plugin: 'com.squareup.wire'
24+
25+
application.mainClass = "com.squareup.dinosaurs.Sample"
26+
27+
repositories {
28+
maven {
29+
url new File(rootDir, "../../../../../build/localMaven").toURI().toString()
30+
}
31+
mavenCentral()
32+
}
33+
34+
dependencies {
35+
implementation "com.squareup.wire:wire-runtime:$wireVersion"
36+
// Any processor will do: its presence creates the kspKotlin task, which consumes the
37+
// main source set's Kotlin directories including Wire's generated sources.
38+
ksp "com.squareup.moshi:moshi-kotlin-codegen:1.15.2"
39+
}
40+
41+
// Mimics real-world builds which add Wire's output directory to a source set as a plain path,
42+
// with no task dependency. Wire's own registration of the same directory on the Kotlin source
43+
// set is what gives the task graph the edge from KSP to the Wire task.
44+
sourceSets {
45+
main {
46+
java.srcDir(layout.buildDirectory.dir("generated/source/wire"))
47+
}
48+
}
49+
50+
wire {
51+
kotlin {}
52+
}
53+
54+
tasks.withType(JavaCompile).configureEach {
55+
sourceCompatibility = JavaVersion.VERSION_11.toString()
56+
targetCompatibility = JavaVersion.VERSION_11.toString()
57+
}
58+
59+
tasks.withType(KotlinCompile).configureEach {
60+
kotlinOptions {
61+
jvmTarget = "11"
62+
}
63+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# The dependency-validation regression only manifests with KSP2's KspAATask.
2+
ksp.useKSP2=true
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include ':'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (C) 2026 Square, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.squareup.dinosaurs
17+
18+
import com.squareup.geology.Period
19+
import java.io.IOException
20+
import okio.ByteString.Companion.toByteString
21+
22+
/* Note that we do not execute this class in tests. We're only testing that it compiles. */
23+
fun main(args: Array<String>) {
24+
val stegosaurus = Dinosaur(
25+
name = "Stegosaurus",
26+
period = Period.JURASSIC,
27+
length_meters = 9.0,
28+
mass_kilograms = 5_000.0,
29+
picture_urls = listOf("http://goo.gl/LD5KY5", "http://goo.gl/VYRM67"),
30+
)
31+
val stegosaurusEncoded = Dinosaur.ADAPTER.encode(stegosaurus)
32+
println(stegosaurusEncoded.toByteString().base64())
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
syntax = "proto2";
2+
3+
package squareup.dinosaurs;
4+
5+
option java_package = "com.squareup.dinosaurs";
6+
7+
import "squareup/geology/period.proto";
8+
9+
message Dinosaur {
10+
/** Common name of this dinosaur, like "Stegosaurus". */
11+
optional string name = 1;
12+
13+
/** URLs with images of this dinosaur. */
14+
repeated string picture_urls = 2;
15+
16+
optional double length_meters = 3;
17+
optional double mass_kilograms = 4;
18+
optional squareup.geology.Period period = 5;
19+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
syntax = "proto2";
2+
3+
package squareup.geology;
4+
5+
option java_package = "com.squareup.geology";
6+
7+
enum Period {
8+
/** 145.5 million years ago — 66.0 million years ago. */
9+
CRETACEOUS = 1;
10+
11+
/** 201.3 million years ago — 145.0 million years ago. */
12+
JURASSIC = 2;
13+
14+
/** 252.17 million years ago — 201.3 million years ago. */
15+
TRIASSIC = 3;
16+
}

0 commit comments

Comments
 (0)