Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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<Directory>,
) {
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# The dependency-validation regression only manifests with KSP2's KspAATask.
ksp.useKSP2=true
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ':'
Original file line number Diff line number Diff line change
@@ -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<String>) {
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())
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Loading