Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ jmh-generator = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.r
jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "jsr305" }
junit = { module = "junit:junit", version.ref = "junit" }
kaml = { module = "com.charleskorn.kaml:kaml", version = "0.72.0" }
kotlin-compile-testing = { module = "dev.zacsweers.kctfork:core", version = "0.7.0" }
kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlin-gradleApi = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin-api", version.ref = "kotlin" }
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlin-jsr223 = { module = "org.jetbrains.kotlin:kotlin-scripting-jsr223", version.ref = "kotlin" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version = "1.8.0" }
Expand Down
5 changes: 4 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ include(":wire-schema-tests")
include(":wire-swift-generator")
include(":wire-test-utils")
include(":wire-tests")
if (startParameter.projectProperties.get("swift") != "false") {
if (false && startParameter.projectProperties.get("swift") != "false") {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops indeed! Let me remove that

include(":wire-runtime-swift")
include(":wire-tests-swift")
include(":wire-tests-swift:no-manifest")
Expand All @@ -83,3 +83,6 @@ include(":samples:wire-codegen-sample")
include(":samples:wire-grpc-sample:client")
include(":samples:wire-grpc-sample:protos")
include(":samples:wire-grpc-sample:server")
include(":wire-binary-compatibility-gradle-plugin")
include(":wire-binary-compatibility-kotlin-plugin")
include(":wire-binary-compatibility-kotlin-plugin-tests")
36 changes: 36 additions & 0 deletions wire-binary-compatibility-gradle-plugin/build.gradle.kts
Comment thread
swankjesse marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import com.vanniktech.maven.publish.GradlePlugin
import com.vanniktech.maven.publish.JavadocJar
import com.vanniktech.maven.publish.MavenPublishBaseExtension

plugins {
id("java-gradle-plugin")
`kotlin-dsl`
kotlin("jvm")
id("org.jetbrains.dokka")
id("com.vanniktech.maven.publish.base")
}

dependencies {
implementation(kotlin("gradle-plugin-api"))
implementation(project(":wire-binary-compatibility-kotlin-plugin"))
implementation(libs.kotlin.gradlePlugin)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect this dependency on the kotlin gradle plugin should be compileOnly to avoid polluting downstream consumers classpath. @autonomousapps do you know? Perhaps the gradle-plugin-api dependency as well?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated, thank you!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you got the change slightly backwards - the libs.kotlin.gradlePlugin should be compileOnly and the project("...") one should be `implementation

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with that change made I have no other concerns, thanks for doing this!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree with what Kats has said. While this plugin only makes sense in the context of a Kotlin project, we don't want to force a Kotlin version update on anyone if we can avoid it, so we should use compileOnly(libs.kotlin.gradlePlugin).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming back to this -- thanks for the pointers @staktrace / @autonomousapps! I've swapped around the change.

The "gradle-plugin-api" dependency couldn't be compileOnly, that had to be left as implementation (tests would fail otherwise)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect that you could do it as compileOnly if you also add it to the testImplementation or testRuntimeOnly configuration. I'm still a bit concerned that having this dependency as implementation will infect downstream consumers. Looking at the pom file I think it transitively brings in the kotlin-gradle-plugins-bom which will cause downstream consumers to get upgraded to the version of kotlin being used by wire.

}

gradlePlugin {
plugins {
create("wireBinaryCompatibility") {
id = "com.squareup.wire.binarycompatibility"
displayName = "Wire Binary Compatibility"
description = "Rewrites Wire callsites to be resilient to API changes"
implementationClass = "com.squareup.wire.binarycompatibility.gradle.WireBinaryCompatibility"
}
}
}

configure<MavenPublishBaseExtension> {
configure(
GradlePlugin(
javadocJar = JavadocJar.Empty()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, why publish an empty javadoc jar?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It isn’t a library for external use, but Maven Central requires Javadoc

)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2025 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
*
* http://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.wire.binarycompatibility.gradle

import org.gradle.api.provider.Provider
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin
import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact
import org.jetbrains.kotlin.gradle.plugin.SubpluginOption

@Suppress("unused") // Created reflectively by Gradle.
class WireBinaryCompatibilityPlugin : KotlinCompilerPluginSupportPlugin {
override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean = true

override fun getCompilerPluginId(): String = "com.squareup.wire.binarycompatibility.kotlin"

override fun getPluginArtifact(): SubpluginArtifact = SubpluginArtifact(
groupId = "com.squareup.wire.binarycompatibility",
artifactId = "wire-binary-compatibility-kotlin-plugin",
version = "2.6.0-SNAPSHOT",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hard-coding the version here seems wrong?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved these maven coordinates to a simple BuildConfig object, and fixed the version (another copy-paste miss) -- this is the only place these values are referenced, the intent is to just keep this lightweight!

)

override fun applyToCompilation(
kotlinCompilation: KotlinCompilation<*>,
): Provider<List<SubpluginOption>> {
return kotlinCompilation.target.project.provider {
listOf() // No options.
}
}
}
12 changes: 12 additions & 0 deletions wire-binary-compatibility-kotlin-plugin-tests/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
plugins {
kotlin("jvm")
id("org.jetbrains.dokka")
}

dependencies {
testImplementation(project(":wire-binary-compatibility-kotlin-plugin"))
testImplementation(kotlin("compiler-embeddable"))
testImplementation(kotlin("test-junit"))
testImplementation(libs.assertk)
testImplementation(libs.kotlin.compile.testing)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright (C) 2025 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
*
* http://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.wire.binarycompatibility.kotlin

import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.containsExactly
import assertk.assertions.containsExactlyInAnyOrder
import assertk.assertions.isEmpty
import assertk.assertions.isFalse
import assertk.assertions.isTrue
import com.tschuchort.compiletesting.JvmCompilationResult
import com.tschuchort.compiletesting.KotlinCompilation
import com.tschuchort.compiletesting.SourceFile
import java.lang.reflect.Modifier
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi

@OptIn(ExperimentalCompilerApi::class)
class WireBinaryCompatibilityKotlinPluginTest {
@Test
fun rewriteConstructorCallToBuilderCall() {
val result = compile(
sourceFile = SourceFile.kotlin(
"Sample.kt",
"""
package com.squareup.wire

val log = mutableListOf<String>()

fun callConstructor() {
log += "${'$'}{Money(5, "USD")}"
}

fun callConstructorWithDefaultParameters() {
log += "${'$'}{Money(amount = 5)}"
log += "${'$'}{Money(currencyCode = "USD")}"
}

data class Money(
val amount: Long? = null,
val currencyCode: String? = null,
) : Message {

class Builder : Message.Builder {
var amount: Long? = null
var currencyCode: String? = null

fun amount(amount: Long) : Builder {
log += "calling amount()!"
this.amount = amount
return this
}

fun currencyCode(currencyCode: String) : Builder {
log += "calling currencyCode()!"
this.currencyCode = currencyCode
return this
}
fun build() : Money = Money(amount, currencyCode)
}
}

interface Message {
interface Builder
}
""",
),
)
assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode, result.messages)

val testClass = result.classLoader.loadClass("com.squareup.wire.SampleKt")
val log = testClass.getMethod("getLog")
.invoke(null) as MutableList<String>

testClass.getMethod("callConstructor").invoke(null)
assertThat(log).containsExactly(
"calling amount()!",
"calling currencyCode()!",
"Money(amount=5, currencyCode=USD)",
)
log.clear()

testClass.getMethod("callConstructorWithDefaultParameters").invoke(null)
assertThat(log).containsExactly(
"calling amount()!",
"Money(amount=5, currencyCode=null)",
"calling currencyCode()!",
"Money(amount=null, currencyCode=USD)",
)
log.clear()
}
}

@ExperimentalCompilerApi
fun compile(
sourceFiles: List<SourceFile>,
plugin: CompilerPluginRegistrar = WireBinaryCompatibilityCompilerPluginRegistrar(),
): JvmCompilationResult {
return KotlinCompilation().apply {
sources = sourceFiles
compilerPluginRegistrars = listOf(plugin)
inheritClassPath = true
kotlincArguments += "-Xverify-ir=error"
kotlincArguments += "-Xverify-ir-visibility"
}.compile()
}

@ExperimentalCompilerApi
fun compile(
sourceFile: SourceFile,
plugin: CompilerPluginRegistrar = WireBinaryCompatibilityCompilerPluginRegistrar(),
): JvmCompilationResult {
return compile(listOf(sourceFile), plugin)
}
21 changes: 21 additions & 0 deletions wire-binary-compatibility-kotlin-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import com.vanniktech.maven.publish.JavadocJar
import com.vanniktech.maven.publish.KotlinJvm
import com.vanniktech.maven.publish.MavenPublishBaseExtension

plugins {
kotlin("jvm")
id("com.vanniktech.maven.publish.base")
}

dependencies {
compileOnly(kotlin("compiler-embeddable"))
compileOnly(kotlin("stdlib"))
}

configure<MavenPublishBaseExtension> {
configure(
KotlinJvm(
javadocJar = JavadocJar.Empty()
)
)
}
2 changes: 2 additions & 0 deletions wire-binary-compatibility-kotlin-plugin/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# We want the stdlib as a compileOnly dependency.
kotlin.stdlib.default.dependency=false
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filename is copypasta, should probably be updated

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more name fixup needed here

* Copyright (C) 2025 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
*
* http://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.wire.binarycompatibility.kotlin

import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
import org.jetbrains.kotlin.config.CommonConfigurationKeys
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI
@OptIn(
ExperimentalCompilerApi::class,
UnsafeDuringIrConstructionAPI::class,
)
class WireBinaryCompatibilityCompilerPluginRegistrar : CompilerPluginRegistrar() {
override val supportsK2: Boolean
get() = true

override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) {
val messageCollector = configuration.get(
CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY,
MessageCollector.NONE,
)
IrGenerationExtension.registerExtension(
extension = WireBinaryCompatibilityIrGenerationExtension(messageCollector),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2025 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
*
* http://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.wire.binarycompatibility.kotlin

import org.jetbrains.kotlin.compiler.plugin.CliOption
import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi

@OptIn(ExperimentalCompilerApi::class)
class WireBinaryCompatibilityCommandLineProcessor : CommandLineProcessor {
override val pluginId = "com.squareup.wire.binarycompatibility.kotlin"

override val pluginOptions: List<CliOption> = listOf()
}
Loading
Loading