Skip to content

Add Wire Binary Compatibility plugin #3306

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

HLGreer
Copy link
Collaborator

@HLGreer HLGreer commented Mar 31, 2025

This change adds a Kotlin compiler plugin that rewrites Wire-generated callsites to be more resilient to schema changes. This first PR only rewrites uses of the generated constructor.

Why:

When a new field is added to a proto Message, existing usages of the Wire-generated class constructor can cause class conflicts. This is often encountered this when there are duplicate or transitive dependencies pulling in different versions of the same proto, resulting in errors shaped as java.lang.NoSuchMethod at runtime.

When faced with this situation, a frequent recommendation is to refactor the codebase to use the class' Builder. Rather than requiring individuals to update their codebases, we can adapt their Kotlin code instead. Wrapping up this solution as a plugin allows for individual opt-in and rollout without influencing the core Wire implementation.

Next:

I will follow up with a second PR that will rewrite the Wire-generated copy() method. The copy method currently uses the generated constructor.

Co-authored-by: Jesse Wilson <[email protected]>
Co-authored-by: Hannah Greer <[email protected]>
@HLGreer HLGreer requested a review from oldergod March 31, 2025 14:48
@@ -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
Member

Choose a reason for hiding this comment

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

oops

Copy link
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

Copy link
Member

Choose a reason for hiding this comment

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

Do we need this file and the next?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes. They hook into the Kotlin compiler to register this plugin

import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.ir.IrStatement
import org.jetbrains.kotlin.ir.backend.js.utils.valueArguments
import org.jetbrains.kotlin.ir.builders.*
Copy link
Contributor

Choose a reason for hiding this comment

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

supernit: our style wants full imports here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes. They hook into the Kotlin compiler to register this plugin

override fun getPluginArtifact(): SubpluginArtifact = SubpluginArtifact(
groupId = "com.squareup.wire.binarycompatibility",
artifactId = "wire-binary-compatibility-kotlin-plugin",
version = "2.6.0-SNAPSHOT",
Copy link
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
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!

dependencies {
implementation(kotlin("gradle-plugin-api"))
implementation(project(":wire-binary-compatibility-kotlin-plugin"))
implementation(libs.kotlin.gradlePlugin)
Copy link
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
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
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
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
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
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
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
Collaborator Author

Choose a reason for hiding this comment

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

Fixed!

@@ -0,0 +1,42 @@
/*
Copy link
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

@HLGreer HLGreer force-pushed the wire-binary-compatibility branch from ecc42a4 to b868c71 Compare April 1, 2025 14:45
@HLGreer HLGreer force-pushed the wire-binary-compatibility branch from b868c71 to af75cfc Compare April 1, 2025 15:04
Copy link
Collaborator

@swankjesse swankjesse left a comment

Choose a reason for hiding this comment

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

So rad

When a new field is added to the Dinosaur schema, if there are competing versions of the compiled class and the new
version is resolved at runtime, the usage above may encounter an error:
```
java.lang.NoSuchMethodError: 'void com.squareup.dinosaurs.Dinosaur<init>(java.lang.String, java.lang.Double,
Copy link
Collaborator

Choose a reason for hiding this comment

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

YASS!

*/
package com.squareup.wire.binarycompatibility.gradle

object BuildConfig {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Usually this file can be generated from gradle properties.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I was looking at some prior art! I figured that is a good follow up unless someone felt strongly to have it generated here now

Copy link
Collaborator

Choose a reason for hiding this comment

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

It must be generated, at least the version component, or it will not work properly in the destination project. It's only a few lines in the Gradle build and you can copy it from Burst or Zipline probably without any changes.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think maybe this PR also demonstrates this: https://github.com/square/gradle-dependencies-sorter/pull/123/files

@HLGreer HLGreer requested a review from staktrace April 1, 2025 17:25
configure<MavenPublishBaseExtension> {
configure(
GradlePlugin(
javadocJar = JavadocJar.Empty()
Copy link
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
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants