Skip to content

Stop bundling third-party code in the IntelliJ uber JARs - #188

Merged
alexander-yevsyukov merged 15 commits into
masterfrom
unshade-intellij-platform-third-party
Aug 13, 2026
Merged

Stop bundling third-party code in the IntelliJ uber JARs#188
alexander-yevsyukov merged 15 commits into
masterfrom
unshade-intellij-platform-third-party

Conversation

@alexander-yevsyukov

@alexander-yevsyukov alexander-yevsyukov commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Why

In delivery-server, jibDockerBuild failed with a NoSuchMethodError in commons-compress: the intellij-platform fat JAR bundles 575 packages of unrelocated third-party classes (Guava 31, commons-compress 1.21, Caffeine, Netty, …) that reach consumers' buildscript classpaths and shadow genuine artifacts by classloader order. No version constraint or BOM can fix it — the shaded copies are not dependencies, they are bytecode inside another artifact. See .agents/tasks/unshade-intellij-platform-third-party.md for the full incident analysis.

What

A single shading policy (buildSrc/.../shade/IntelliJUberJar.kt) applied identically to intellij-platform and intellij-platform-java (they form a layered pair — the java JAR subtracts the platform JAR's contents at build time):

  • Include filter: only JetBrains groups (com.jetbrains.intellij*, org.jetbrains.intellij*) are shaded.
  • Central artifacts → POM dependencies: everything else is declared in the published POM instead of bundled — intellij-platform 20 runtime entries, intellij-platform-java 84 (83 runtime third-party plus the sibling intellij-platform at compile scope, since the java JAR's classes extend platform types it deliberately does not contain). Consumers now upgrade e.g. commons-compress by normal Gradle conflict resolution — precisely the fix for the Jib incident.
  • JetBrains forks → relocated under io.spine.tools.ij: jdom, log4j, trove4j, fastutil, Batik, ion-java, commons-imaging. Exceptions: JNA is excluded outright and jcef stays unrelocated (their natives bind JNI entry points to literal class names); self-namespaced forks (asm-all, jb-jdi, …) are shaded as is.
  • Dropped entirely — machinery never exercised by headless PSI, so neither shaded nor declared: pty4j, purejavacomm, jediterm, winp (OS/terminal integration, replacing the incomplete per-file exclude patterns), and the BouncyCastle stack plus marketplace-zip-signer (remote-development and plugin-signing crypto — see below).
  • The java module's subtraction filter reverse-maps Shadow's transforms (relocated paths, renamed service files, .shadow.kotlin_module) via sourceFormOf, so nothing duplicates across the pair.

Result: intellij-platform 25.8 → 17.4 MB, intellij-platform-java 115.1 → 78.6 MB.

The published POM resolves conflict-free

Replacing a dependency-less POM with an 84-entry one moves work onto consumers unless the entries are curated, so three problems found downstream were fixed in this PR after the first review round:

1. Entries are published non-transitive. Each declared entry initially carried its own transitive graph, re-requesting commons-*, kotlin-*, annotations, and objenesis at the version that entry was built against rather than the resolved version pinned here — 10 conflicting modules for a bare consumer under failOnVersionConflict(), forcing boilerplate the old dependency-less POM never required. Every entry now carries a wildcard <exclusions> block. The declared list is already the complete flattened runtime closure (each entry's transitives are themselves entries), so nothing is lost, every module is requested at exactly one version, and the POM resolves with zero conflicts and no consumer-side forces. The curation also becomes authoritative — Slf4J, the Kotlin runtime, and the dropped groups cannot re-enter through an entry's own POM. Consumers still upgrade any entry by ordinary resolution, so the commons-compress fix is preserved.

2. Pins align with the Spine stack. With entries non-transitive, the remaining conflict source is a pin that disagrees with the consumer's own graph. core-jvm-compiler/tests (strict resolution) surfaced Jackson 2.13.0 and Caffeine 3.0.4 — the versions the IJ-213 POMs request, which stood because this repo never forced these families. uber-jar-module now forces the Jackson family and Caffeine in the uber modules' resolution, so the published pins are the versions this repository is built and tested with (2.22.1/2.22, 3.2.4), the same treatment Guava already receives from forceVersions().

3. The BouncyCastle clash. core-jvm-compiler's integration tests hit Conflict found for module 'org.bouncycastle:bcprov-jdk15on': between versions 1.69 and 1.64 against the .410 POM: ide-impl reaches BouncyCastle by two disagreeing paths — bcpg-jdk15on:1.69 (via remote-dev-util) wants bcprov:1.69, while marketplace-zip-signer:0.1.3 wants bcpkix:1.64bcprov:1.64. Neither bcprov nor bcpkix is on this repo's runtime classpath, so the clash was invisible here and surfaced only in consumers, which re-resolve each declared entry's own POM. Both entry points are remote-dev and plugin-signing machinery, so dropping them removes the cryptography surface entirely rather than merely aligning versions. POM entries 86 → 84; org.jetbrains:annotations and annotations-java5 verified to survive the artifact-level drop.

Verification

  • Full ./gradlew build dokkaGenerate green; psi / psi-java suites: 148 tests pass against the relocated JARs.
  • Both POMs regenerated and inspected: 20 and 84 entries, every one carrying the wildcard exclusion; aligned pins (Jackson 2.22.1, Caffeine 3.2.4, Guava 33.6.0-jre) present as published values.
  • All 83 distinct third-party POM entries across both modules verified present on Maven Central (blockmap, which is absent from Central, is shaded instead).
  • JAR entry listings diffed at every step and byte-identical across the dependency-alignment change; cross-JAR duplicates reduced to META-INF/MANIFEST.MF only. The two non-IJ uber modules (jvm-tool-plugins, protobuf-setup-plugins) are unaffected by the shared forcing block.
  • IntelliJUberJarSpec (8 tests) covers the pure policy logic — group filtering, per-artifact drops, and all three sourceFormOf reversals.
  • End-to-end consumer proof: a scratch Gradle project resolving io.spine.tools:intellij-platform-java:2.0.0-SNAPSHOT.410 from Maven Local gets 85 artifacts (uber JAR + the sibling at compile scope + all 83 third-party, with commons-compress:1.21, guava:33.6.0-jre, jackson-databind:2.22.1 as pinned), and resolves conflict-free under failOnVersionConflict() with no consumer-side forces.
  • Pre-PR reviewer round: spine-code-review, kotlin-engineer, review-docs, dependency-audit, gradle-review — all APPROVE after fixes (tests added, KDoc link repaired, CC-hardening of the subtraction task wiring). Codex's compile-scope finding is fixed and resolved.

Reviewer notes

  • The fork relocation is why this bump is breaking (version .404.410): previously bundled classes disappear or move to io.spine.tools.ij.*. No Spine source imports the affected packages (verified by grep).
  • Known transition-window skew: Jackson pins still disagree with artifacts published before the stack moved to 2.22.1 (e.g. compiler-jvm .066 requests 2.21.1 via palantir-java-format). The consumer conventions' JacksonV2 forces cover it until the stack republishes, after which it disappears.
  • The POM is the only metadata these artifacts publish, and that is load-bearing. Because fatJar is an artifact-only publication (artifact(tasks.shadowJar), no software component), Gradle emits no .module file — verified: publishToMavenLocal yields exactly .jar + .pom for both IJ modules, while component-based siblings like psi-java do get a .module. Gradle consumers therefore fall back to the POM this PR populates. If anyone later switches fatJar to from(components[...]), the generated Module Metadata would take precedence and silently bypass declareUnshadedDependencies — disable metadata generation for the publication in that case. (Raised by Copilot; the two threads carry the measurements.)
  • Two unrelated one-line edits ride along in Line.java and ExternalModules.java: parenthesising instanceof patterns to clear an ErrorProne OperatorPrecedence warning surfaced by these builds.
  • A --configuration-cache probe is recorded in the task log: the POM-generation tasks execute under CC, but the entry is discarded (the withXml action's captured Configuration is not serializable; the root script's gcloud call is a pre-existing blocker). Full CC readiness is out of scope — the repo does not enable it.
  • Remaining follow-up (tracked in the task file): consumer proof in delivery-server — drop the CommonsCompress classpath-ordering workaround once these artifacts are published.

🤖 Generated with Claude Code

alexander-yevsyukov and others added 7 commits August 12, 2026 17:15
Despite the `com.sun.jna` package, JNA is not part of the JDK: the classes
come from the JetBrains fork (`org.jetbrains.intellij.deps.jna`) and would
shadow the genuine `net.java.dev.jna` artifacts on a consumer's classpath.
The headless PSI code never uses this OS-integration layer.

The exclusion lives in the shared `uber-jar-module` list because the two IJ
fat JARs are a layered pair: `intellij-platform-java` excludes whatever the
`intellij-platform` JAR already contains, so an exclusion made in only one
module resurfaces the files in the other module's JAR.

Also:
 - Delete the verbatim copy of `excludeFiles()` from
   `intellij-platform/build.gradle.kts` — the shared list is its single home.
 - Fix the dead `resources/com/pti4j/**` typo -> `resources/com/pty4j/**`,
   so the pty4j native binaries are actually excluded.
 - Add the task file for this branch.

Verified: `intellij-platform`, `jvm-tool-plugins`, and
`protobuf-setup-plugins` JAR entry listings are identical before vs after;
the `intellij-platform-java` delta is exactly the JNA entries and the pty4j
natives. `:psi:test` and `:psi-java:test` pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduce the `IntelliJUberJar` shading policy (`buildSrc`, package
`io.spine.gradle.shade`) and apply it to both `intellij-platform` and
`intellij-platform-java` — the layered pair demands identical treatment:

 - Only JetBrains groups (`com.jetbrains.intellij*`, `org.jetbrains.intellij*`)
   enter the shade. Third-party artifacts published on Maven Central become
   `runtime` dependencies of the published POMs instead (platform: 20 deps,
   java: 86 deps incl. the sibling `intellij-platform` JAR), pinned at the
   versions resolved in this repository. Consumers now upgrade components
   such as `commons-compress` by normal conflict resolution — the fix for
   the `delivery-server` Jib incident.
 - JetBrains forks claiming public package names — jdom, log4j, trove4j,
   fastutil, Batik, ion-java, commons-imaging — are relocated under
   `io.spine.tools.ij`. JNA stays excluded and jcef unrelocated: their
   natives bind JNI entry points to literal class names. Self-namespaced
   forks (asm-all, jb-jdi, sa-jdwp, agents, blockmap) are shaded as is.
 - Terminal and Windows process management machinery (pty4j, purejavacomm,
   jediterm, winp) is dropped entirely, superseding the per-file exclusion
   patterns, which are now removed from `uber-jar-module`.
 - The subtraction filter of `intellij-platform-java` reverse-maps Shadow
   transforms (`IntelliJUberJar.sourceFormOf()`): relocated paths, renamed
   service files, and `.shadow.kotlin_module` renames, so nothing from the
   sibling JAR duplicates.

JAR sizes drop 25.8 -> 17.4 MB (platform) and 115.1 -> 78.6 MB (java).
Every declared POM dependency is verified to exist on Maven Central.
Full build passes; `psi` and `psi-java` suites: 148 tests green.

Dependency reports are regenerated for `2.0.0-SNAPSHOT.410`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pre-PR reviewer round (spine-code-review, kotlin-engineer, review-docs,
dependency-audit, gradle-review) requested:

 - Cover the pure logic of `IntelliJUberJar` with unit tests. The new
   `IntelliJUberJarSpec` (7 tests) exercises `isShaded` (incl. regex
   anchoring and the bare `org.jetbrains` group), `isPomDependency`
   (all dropped groups), and the three `sourceFormOf` reversals with
   their `null` cases.
 - Repair the dangling KDoc link left by a rename
   (`unrelocatedFormOf` -> `sourceFormOf`), and use a restrictive "that".
 - Keep the `pom.withXml` action from capturing the `Project`: the
   configuration is looked up eagerly now.
 - Track the sibling JAR as a proper task input of
   `intellij-platform-java:shadowJar` (`inputs.file` on the archive
   provider) instead of a bare `dependsOn`, closing an up-to-date and
   build-cache staleness gap; read it via an injected `ArchiveOperations`
   instead of the ambient `Project.zipTree`; collect the subtracted paths
   into a `Set` cleared per execution.

Both JAR entry listings are byte-identical before and after these fixes;
`:buildSrc:test` passes. A `--configuration-cache` probe of the POM
generation is recorded in the task log: the tasks execute, but full CC
readiness remains out of scope for this repository.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4645845dcf

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread intellij-platform-java/build.gradle.kts
@alexander-yevsyukov alexander-yevsyukov self-assigned this Aug 12, 2026
@alexander-yevsyukov alexander-yevsyukov moved this to 🏗 In progress in v2.0 Aug 12, 2026
The classes of `intellij-platform-java` compile against the classes of
`intellij-platform`, which the layered JAR deliberately does not contain.
With the sibling flattened to the `runtime` scope, a consumer compiling
directly against the java artifact would miss core IntelliJ types on the
compile classpath.

`declareUnshadedDependencies()` now maps project (sibling) dependencies to
the `compile` scope and keeps third-party artifacts at `runtime` — the
previously dependency-less POM never offered third-party types at compile
scope, and widening that surface is not the goal of this change.

Addresses the Codex review finding on PR #188.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.73%. Comparing base (cca5d03) to head (31f2213).

Additional details and impacted files
@@            Coverage Diff            @@
##             master     #188   +/-   ##
=========================================
  Coverage     90.73%   90.73%           
  Complexity      547      547           
=========================================
  Files           122      122           
  Lines          2245     2245           
  Branches        313      313           
=========================================
  Hits           2037     2037           
  Misses           86       86           
  Partials        122      122           
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

alexander-yevsyukov and others added 7 commits August 12, 2026 22:40
Resolving the generated POM in a bare consumer with `failOnVersionConflict()`
failed on 10 modules: every declared entry still carried its own transitive
graph, re-requesting `commons-*`, `kotlin-*`, `annotations`, and `objenesis`
at the versions that entry was built against rather than the resolved
versions pinned here. The dependency-less POM this task replaces never
required such forcing, so the task was introducing the boilerplate.

`declareUnshadedDependencies` now publishes every entry with a wildcard
`<exclusions>` block. The declared list is already the complete flattened
runtime closure — each entry's transitives are themselves entries — so the
exclusions lose nothing, every module is requested at exactly one version,
and the POM resolves conflict-free with no consumer-side forces. The
curation also becomes authoritative: Slf4J, the Kotlin runtime, and the
dropped groups and modules can no longer re-enter through an entry's
own POM.

With the entries non-transitive, the remaining conflict source is a pin
disagreeing with the consumer's own graph. `uber-jar-module` therefore
forces the Jackson family and Caffeine in the uber modules' resolution, so
the published pins become the versions this repository is built and tested
with (`2.22.1`/`2.22`, `3.2.4`) instead of those the IntelliJ Platform POMs
request (`2.13.0`, `3.0.4`) — the treatment Guava already receives from
`forceVersions()`.

Version -> `2.0.0-SNAPSHOT.412`, as the `.410` artifacts are already
published and consumed by the downstream verification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alexander-yevsyukov
alexander-yevsyukov requested review from armiol and a lite review from Copilot August 12, 2026 22:27
@alexander-yevsyukov alexander-yevsyukov moved this from 🏗 In progress to In Review in v2.0 Aug 12, 2026
@alexander-yevsyukov
alexander-yevsyukov enabled auto-merge (squash) August 12, 2026 22:28

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.

Pull request overview

This PR changes how the intellij-platform and intellij-platform-java uber JARs are produced so they no longer bundle (and thereby shadow) large sets of unrelocated third-party classes; instead, only JetBrains artifacts are shaded into the JARs, JetBrains forks are relocated, and the remaining third-party artifacts are published as normal dependencies.

Changes:

  • Introduce a shared IntelliJ uber-JAR shading policy (IntelliJUberJar) and apply it to both IntelliJ uber modules (shade only JetBrains artifacts + relocation + POM dependency declaration).
  • Align selected dependency versions (Jackson family + Caffeine) within uber-jar modules so the published dependency pins match what this repo builds/tests with.
  • Bump published version and refresh dependency documentation / reports accordingly.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
version.gradle.kts Bumps the version published by this repository to .410.
tool-base/src/main/java/io/spine/tools/fs/ExternalModules.java Adds parentheses to equals expression to clarify instanceof/operator precedence.
tool-base/src/main/java/io/spine/tools/code/Line.java Adds parentheses to equals expression to clarify instanceof/operator precedence.
intellij-platform/build.gradle.kts Switches Shadow configuration to the shared IntelliJ shading policy and adds POM dependency declaration hook.
intellij-platform-java/build.gradle.kts Applies the shared IntelliJ shading policy, hardens the subtraction/exclusion logic against Shadow transforms, and adds POM dependency declaration hook.
docs/dependencies/pom.xml Updates documented dependency versions (including tool-base snapshot and spine-base snapshot) and adds missing Kotlin entries.
docs/dependencies/dependencies.md Regenerates the dependency/license report to reflect the new version and updated resolved dependency set.
buildSrc/src/test/kotlin/io/spine/gradle/shade/IntelliJUberJarSpec.kt Adds tests for the IntelliJ shading/POM policy logic (group filters, drops, reverse-mapping).
buildSrc/src/main/kotlin/uber-jar-module.gradle.kts Forces Jackson/Caffeine alignment in uber-jar modules and updates shared Shadow excludes (notably JNA path exclusion).
buildSrc/src/main/kotlin/io/spine/gradle/shade/IntelliJUberJar.kt Adds the central policy for what gets shaded/relocated vs declared in the published metadata, plus helper extensions.
buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt Updates the pinned spine-base snapshot version used by this repo (.425.440).
.idea/kotlinc.xml Removes Kotlin language/api version options from IDE config (IDE-only change).
.agents/tasks/unshade-intellij-platform-third-party.md Adds a task log/analysis document describing the incident and the implemented approach.
Files not reviewed (1)
  • .idea/kotlinc.xml: Generated file

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

Comment thread intellij-platform-java/build.gradle.kts
Comment thread intellij-platform/build.gradle.kts
@alexander-yevsyukov
alexander-yevsyukov merged commit 5dac873 into master Aug 13, 2026
12 checks passed
@alexander-yevsyukov
alexander-yevsyukov deleted the unshade-intellij-platform-third-party branch August 13, 2026 11:20
@github-project-automation github-project-automation Bot moved this from In Review to ✅ Done in v2.0 Aug 13, 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.

3 participants