Skip to content

Add unbundleProtobufRuntime() for fat JARs - #796

Merged
alexander-yevsyukov merged 3 commits into
masterfrom
declare-protobuf-runtime
Oct 16, 2026
Merged

alexander-yevsyukov merged 3 commits into
masterfrom
declare-protobuf-runtime

Conversation

@alexander-yevsyukov

@alexander-yevsyukov alexander-yevsyukov commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Fat JARs that bundle the Protobuf runtime unrelocated pin the runtime of the build that made them on their consumers' classpath. Gradle cannot see the copy, because it is bytecode inside another artifact rather than a dependency, so force(...) cannot replace it. Code generated by a newer Protobuf then fails with ProtobufRuntimeVersionException. During the dependency refresh (Protobuf 4.36.0 → 4.36.2) this broke core-jvm-compiler, core-jvm, and jdbc-storage.

Relocation is not an option, because the Protobuf runtime depends on the names of its classes. SpineEventEngine/tool-base#196 and SpineEventEngine/compiler#89 fix their fat JARs with two copies of the same code. This PR moves that code here, so both can call it.

Change

buildSrc/…/io/spine/gradle/publish/ProtobufRuntime.kt adds Project.unbundleProtobufRuntime(publication: String = "fatJar"). For each library of Protobuf.libs, it:

  • excludes the library from shadowJar through the dependency filter, not by path. This keeps the SBOM accurate (SbomContent.bundled(task));
  • declares the library, non-transitively, in a new protobufRuntime configuration. Shadow's shadowRuntimeElements extends it, so the shadowed variant, such as a Gradle plugin's publication, lists the library in its metadata;
  • writes the library into the POM of the given artifact-only publication: fatJar of uber-jar-module by default, or cliFatJar in compiler. If the POM already has a <dependencies> element, the entries go into it.

Why some alternatives were not used:

  • The shadow configuration. Shadow would also write the library into the manifest Class-Path (Class-Path: protobuf-java-4.36.2.jar …). In a directory that holds several Protobuf JARs, that entry would put the build-time version first again.
  • Wiring it into uber-jar-module. The function is opt-in, because the IntelliJ Platform uber JARs use that script too, bundle no Protobuf, and must not declare it.
  • Transitive declarations. The libraries' own dependencies are covered elsewhere: Guava and Gson stay bundled, protobuf-java is declared itself, and Gradle provides Kotlin.

Tests

ProtobufRuntimeIgTest runs an offline TestKit build with the configuration cache. The repository holds stand-in Protobuf JARs; protobuf-java-util depends on a Gson stand-in. The test checks:

  • The fat JAR has no com/google/protobuf/ entries, while Gson and the other libraries stay bundled.
  • Shadow's includedDependencies no longer lists Protobuf.
  • The manifest has no Class-Path.
  • The publication POM and the shadowed variant's .module declare the three libraries, runtime, with *:* exclusions.
  • When the POM already has a <dependencies> element, written by Gradle or by hand, the entries are added to it and the POM keeps a single one.
  • Two calls in one module work, one with the default publication name and one with a custom name.

./gradlew :buildSrc:test --tests '*ProtobufRuntimeIgTest' detekt passes: 9 of 9 tests, and Detekt is clean.

After merge

SpineEventEngine/tool-base#196 and SpineEventEngine/compiler#89 run ./config/pull and replace their copies with:

  • tool-base: unbundleProtobufRuntime()
  • compiler: unbundleProtobufRuntime(publication = "cliFatJar")

🤖 Generated with Claude Code

alexander-yevsyukov and others added 2 commits October 16, 2026 18:14
A fat JAR bundling the unrelocated Protobuf runtime pins the runtime of
the build that made it on the classpath of its consumers, and code generated
by a newer Protobuf fails with `ProtobufRuntimeVersionException`. The runtime
cannot be relocated, as it depends on the names of its classes.

`Project.unbundleProtobufRuntime()` excludes `Protobuf.libs` from `shadowJar`
by the dependency filter, and declares them as non-transitive `runtime`
dependencies: in a `protobufRuntime` configuration extended by
`shadowRuntimeElements`, and in the POM of the given artifact-only
publication. It replaces the copies of this code in tool-base and compiler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Allow a call per publication of the fat JAR, use the name constants of
Shadow and the `publications` accessor, and state in the KDoc the order
of calls and the effect on the compile classpath of consumers.

Extend the test: merging into the `dependencies` element of a POM, whether
Gradle or a build script wrote it, two calls in one module, the default and
a custom publication, the dependencies the Shadow filter leaves in, and
a run with the configuration cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alexander-yevsyukov
alexander-yevsyukov requested a balanced review from Copilot October 16, 2026 17:50
@alexander-yevsyukov alexander-yevsyukov self-assigned this Oct 16, 2026
@alexander-yevsyukov alexander-yevsyukov moved this to 🏗 In progress in v2.0 Oct 16, 2026
@alexander-yevsyukov
alexander-yevsyukov marked this pull request as ready for review October 16, 2026 17:51
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T17:54:04.753709Z 148ddcd Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alexander-yevsyukov
alexander-yevsyukov enabled auto-merge (squash) October 16, 2026 17:52

Copilot AI left a comment

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.

🔵 Needs a closer look

The expanded integration tests still await verification on the reviewed revision.

0 open findings

What changed in this PR

Adds an opt-in publishing helper so fat JAR consumers can resolve Protobuf versions instead of using a bundled runtime.

Changes:

  • Excludes Protobuf from Shadow JARs and declares non-transitive runtime dependencies in publication metadata.
  • Adds integration coverage for JAR contents, manifests, POMs, and shadowed variants.
  • Records implementation progress and downstream adoption steps.
File Description
buildSrc/​src/​test/​kotlin/​io/​spine/​gradle/​publish/​ProtobufRuntimeIgTest.kt Tests runtime exclusion and publication metadata.
buildSrc/​src/​main/​kotlin/​io/​spine/​gradle/​publish/​ProtobufRuntime.kt Implements unbundleProtobufRuntime().
.agents/​tasks/​unbundle-protobuf-runtime.md Tracks verification and downstream adoption.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Copilot AI left a comment

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.

🟢 Approved

No blocking issues were identified, and focused integration coverage addresses the intended bundling and publishing behavior.

0 open findings

🧠 Review effort: Balanced

@alexander-yevsyukov
alexander-yevsyukov merged commit 455c1d3 into master Oct 16, 2026
3 checks passed
@alexander-yevsyukov
alexander-yevsyukov deleted the declare-protobuf-runtime branch October 16, 2026 18:00
@github-project-automation github-project-automation Bot moved this from 🏗 In progress to ✅ Done in v2.0 Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

2 participants