Skip to content

Stop publishing protobuf-setup-plugins as an uber JAR #200

Closed

Description

@alexander-yevsyukov

What happens

protobuf-setup-plugins applies the uber-jar-module plugin, so io.spine.tools:protobuf-setup-plugins is published as a Shadow JAR, and its classes are not relocated. unbundleProtobufRuntime() keeps the Protobuf runtime out of the JAR, and the POM declares only that runtime. Everything else that the module uses is inside the JAR. In 2.0.0-SNAPSHOT.430, which is 5.4 MB with 3,396 classes, that is:

  • Other ToolBase modules: plugin-base (io.spine.tools.gradle, including gradle.project.Projects and gradle.task), code, fs, archive, java-code, proto-code, and jvm-tools (io.spine.tools.meta, jvm, io, version).
  • Spine libraries: Base and Logging.
  • Third-party libraries: Guava 33.7.2-jre, Gson 2.14.0, and the Protobuf Gradle plugin 0.10.0 with osdetector, including their plugin descriptors META-INF/gradle-plugins/com.google.protobuf.properties and com.google.osdetector.properties. Classes under com/google make up 2,351 of the total.

The Gradle module metadata publishes a single shadowRuntimeElements variant (org.gradle.dependency.bundling = shadowed). The protobuf-setup-plugins-all artifact of the fatJar publication is byte-identical.

The Spine Compiler Gradle plugin depends on this artifact. The POM of compiler-gradle-plugin 2.0.0-SNAPSHOT.082 declares protobuf-setup-plugins .430 next to plugin-base, code, fs, and jvm-tools. So every build that applies io.spine.compiler gets the following on one build-script classloader:

  • the ToolBase artifacts, at the versions that Gradle resolves for the build;
  • copies of the same classes inside protobuf-setup-plugins, at the version that protobuf-setup-plugins was built with;
  • copies of Guava, Gson, and the Protobuf Gradle plugin, next to the versions that the build resolves for itself.

Gradle resolves each module to a single version, but it cannot know that protobuf-setup-plugins carries other copies of the same classes. The JVM loads whichever JAR comes first on the classpath.

Evidence

  • The embedded copies follow protobuf-setup-plugins, not plugin-base. javap on io.spine.tools.gradle.project.Projects shows that the copy in protobuf-setup-plugins-2.0.0-SNAPSHOT.428.jar lacks javaCompileTasksFor() and kotlinCompileTasksFor(), and the copy in .430 has them. They were added in Find tasks lazily in plugin-base and protobuf-setup-plugins #199.
  • The versions can diverge. ToolBase has no BOM, so Gradle resolves each ToolBase module on its own. When another plugin on the classpath requires plugin-base .430 and the Compiler Gradle plugin still requires protobuf-setup-plugins .428, Gradle upgrades plugin-base but keeps protobuf-setup-plugins at .428. Which Projects loads then depends on the classpath order.

Why it matters

  • The same failure as Stop bundling ToolBase classes in the core-jvm-plugins fat JAR core-jvm-compiler#124. There, an unrelocated copy of Projects in the core-jvm-plugins fat JAR shadowed plugin-base .430, and the build failed with a NoSuchMethodError on Projects.javaCompileTasksFor(). CoreJvm Compiler is removing its copy, but protobuf-setup-plugins keeps another one on the same classpath.
  • Bumping a ToolBase module is not enough. A plugin that needs new API or a fix from plugin-base, code, fs, or jvm-tools gets it only if protobuf-setup-plugins is bumped too, or happens to come later on the classpath.
  • Consumers do not control the third-party versions. Constraints and resolutionStrategy do not reach classes inside a JAR. A build that declares another version of Guava, Gson, or the Protobuf Gradle plugin may still run the classes embedded here.
  • Mismatches are silent. The build fails only when a member that is missing from the embedded copy is called. A change in behavior goes unnoticed.

Suggested fix

Publish protobuf-setup-plugins as a plain JAR with ordinary dependencies:

  1. Apply module instead of uber-jar-module, as plugin-base and jvm-tools do.
  2. Let the POM and the Gradle module metadata declare the module's dependencies: the ToolBase modules, Base, Logging, and the Protobuf Gradle plugin.

Relocating the embedded classes instead would break the public API. Functions such as Project.descriptorSetFile(SourceSetName) and GenerateProtoTask.sourceSetName take or extend code and Protobuf Gradle plugin types. Relocated copies of those types would not match the ones that callers pass in.

To check while doing it:

  • The -all publication. protobuf-setup-plugins-all is the same JAR. It can go unless something consumes it.
  • The Gradle Plugin Portal. Release versions are also published there. The declared dependencies must resolve for Portal users.
  • jvm-tool-plugins. It also applies uber-jar-module, and it may have the same problem.

Activity

  1. moved this to 📋 Backlog in v2.0on Oct 29, 2026
  2. moved this from 📋 Backlog to 🏗 In progress in v2.0on Oct 29, 2026
  3. moved this from 🏗 In progress to ✅ Done in v2.0on Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

  • Status
    ✅ Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions