Stop bundling third-party code in the IntelliJ uber JARs - #188
Conversation
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>
There was a problem hiding this comment.
💡 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".
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 Report✅ All modified and coverable lines are covered by tests. 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:
|
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>
There was a problem hiding this comment.
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.
Why
In
delivery-server,jibDockerBuildfailed with aNoSuchMethodErrorincommons-compress: theintellij-platformfat 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.mdfor the full incident analysis.What
A single shading policy (
buildSrc/.../shade/IntelliJUberJar.kt) applied identically tointellij-platformandintellij-platform-java(they form a layered pair — the java JAR subtracts the platform JAR's contents at build time):com.jetbrains.intellij*,org.jetbrains.intellij*) are shaded.intellij-platform20runtimeentries,intellij-platform-java84 (83runtimethird-party plus the siblingintellij-platformatcompilescope, since the java JAR's classes extend platform types it deliberately does not contain). Consumers now upgrade e.g.commons-compressby normal Gradle conflict resolution — precisely the fix for the Jib incident.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.marketplace-zip-signer(remote-development and plugin-signing crypto — see below)..shadow.kotlin_module) viasourceFormOf, so nothing duplicates across the pair.Result:
intellij-platform25.8 → 17.4 MB,intellij-platform-java115.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, andobjenesisat the version that entry was built against rather than the resolved version pinned here — 10 conflicting modules for a bare consumer underfailOnVersionConflict(), 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 thecommons-compressfix 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 Jackson2.13.0and Caffeine3.0.4— the versions the IJ-213 POMs request, which stood because this repo never forced these families.uber-jar-modulenow 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 fromforceVersions().3. The BouncyCastle clash.
core-jvm-compiler's integration tests hitConflict found for module 'org.bouncycastle:bcprov-jdk15on': between versions 1.69 and 1.64against the.410POM:ide-implreaches BouncyCastle by two disagreeing paths —bcpg-jdk15on:1.69(viaremote-dev-util) wantsbcprov:1.69, whilemarketplace-zip-signer:0.1.3wantsbcpkix:1.64→bcprov:1.64. Neitherbcprovnorbcpkixis 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:annotationsandannotations-java5verified to survive the artifact-level drop.Verification
./gradlew build dokkaGenerategreen;psi/psi-javasuites: 148 tests pass against the relocated JARs.2.22.1, Caffeine3.2.4, Guava33.6.0-jre) present as published values.blockmap, which is absent from Central, is shaded instead).META-INF/MANIFEST.MFonly. 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 threesourceFormOfreversals.io.spine.tools:intellij-platform-java:2.0.0-SNAPSHOT.410from Maven Local gets 85 artifacts (uber JAR + the sibling at compile scope + all 83 third-party, withcommons-compress:1.21,guava:33.6.0-jre,jackson-databind:2.22.1as pinned), and resolves conflict-free underfailOnVersionConflict()with no consumer-side forces.compile-scope finding is fixed and resolved.Reviewer notes
.404→.410): previously bundled classes disappear or move toio.spine.tools.ij.*. No Spine source imports the affected packages (verified by grep).2.22.1(e.g.compiler-jvm .066requests2.21.1viapalantir-java-format). The consumer conventions'JacksonV2forces cover it until the stack republishes, after which it disappears.fatJaris an artifact-only publication (artifact(tasks.shadowJar), no software component), Gradle emits no.modulefile — verified:publishToMavenLocalyields exactly.jar+.pomfor both IJ modules, while component-based siblings likepsi-javado get a.module. Gradle consumers therefore fall back to the POM this PR populates. If anyone later switchesfatJartofrom(components[...]), the generated Module Metadata would take precedence and silently bypassdeclareUnshadedDependencies— disable metadata generation for the publication in that case. (Raised by Copilot; the two threads carry the measurements.)Line.javaandExternalModules.java: parenthesisinginstanceofpatterns to clear an ErrorProneOperatorPrecedencewarning surfaced by these builds.--configuration-cacheprobe is recorded in the task log: the POM-generation tasks execute under CC, but the entry is discarded (thewithXmlaction's capturedConfigurationis not serializable; the root script'sgcloudcall is a pre-existing blocker). Full CC readiness is out of scope — the repo does not enable it.delivery-server— drop theCommonsCompressclasspath-ordering workaround once these artifacts are published.🤖 Generated with Claude Code