Skip to content

Resolve archive project coordinates from dependency graphs - #52391

Open
gregjotau wants to merge 1 commit into
spring-projects:mainfrom
beint-no:support-isolated-boot-archives
Open

gregjotau wants to merge 1 commit into
spring-projects:mainfrom
beint-no:support-isolated-boot-archives

Conversation

@gregjotau

Copy link
Copy Markdown

BootJar and BootWar currently construct a map of every local project's coordinates by reading other projects' mutable group and version. This fails while the tasks are being created under strict Isolated Projects, before any packaging occurs. The path-only map also assigns the wrong coordinates when a main-build project and an included-build project share a path, affecting custom layers and reachability metadata even without isolation.

This PR resolves project coordinates lazily from the dependency graph that produced the archive's artifacts. It also proposes an explicit compatibility boundary for the public artifact-only registration API. Addresses gh-43755.

Why the Gradle prerequisite is now actionable

Gradle's maintainer confirmation establishes that, for a ProjectComponentIdentifier, ResolvedComponentResult.getModuleVersion() reliably supplies the producer project's group/name/version under isolation. These are project coordinates, not Maven publication coordinates. Synthetic graph roots are different and must not be used as project-coordinate substitutes; Gradle 10 will also use anonymous roots for ordinary configurations.

The implementation uses that contract. It does not infer coordinates from capabilities or request a new Gradle API. This is a different approach from the earlier capability-based #51311.

Proposed API and compatibility decision

Add this overload to BootArchive:

void resolvedArtifacts(Provider<Set<ResolvedArtifactResult>> artifacts,
        Provider<ResolvedComponentResult> rootComponent);

The artifacts and root must come from the same configuration. An artifact view can supply the artifacts while its owning configuration supplies the root.

The interface method has a default implementation that calls the existing artifact-only overload, preserving compatibility with independent implementations of BootArchive. BootJar and BootWar override the new method to consume the graph. Their built-in runtime-classpath wiring uses it automatically.

The existing single-provider method retains local-project coordinate lookup for ordinary builds, but the lookup runs only if that method is called, rather than during every archive task's construction. Existing custom archive callers keep their method signature and local-project lookup. The legacy snapshot is taken at explicit registration rather than construction; it is no longer collected for tasks that never use that overload. They must move to the graph overload to use Isolated Projects. The legacy overload is not silently redirected to the runtime classpath: its artifacts may originate from an entirely different configuration.

This is the central policy choice for review: preserve the legacy API's ordinary-build behavior and give callers an explicit migration path, rather than guess project coordinates, silently stop matching custom layers, or break existing implementations by adding an abstract interface method. The new default preserves independent implementations; it does not imply that an arbitrary third-party implementation itself becomes isolation-compatible.

Why each implementation mechanism is present

Change Reason
Wire each artifact provider together with its root-component provider The artifact-only provider exposes component identity but cannot supply authoritative project GAV for an arbitrary configuration. Both providers remain lazy.
Traverse resolved components transitively Project dependencies can be reached through another project or a substituted module; examining only direct dependencies misses them.
Key coordinates and the visited set by full ComponentIdentifier A project path is not unique across composite builds. The visited set also bounds traversal of cyclic graphs; an iterative queue avoids recursion depth limits.
Read moduleVersion only for project identifiers This follows the confirmed contract and does not mistake a synthetic root's anonymous GAV for producer coordinates. External module GAV comes directly from its public ModuleComponentIdentifier.
Ignore unresolved graph edges while traversing A lenient artifact view can legitimately omit unresolved components. Resolution errors are not caught or suppressed.
Fail when a project artifact has no matching graph coordinates A mismatched graph, or a project component with missing coordinates, must not silently change layer or native-metadata selection. The error identifies the provider-pair requirement.
Snapshot each artifact's file, identifier and coordinate strings together Coordinates become declared task inputs, and metadata cannot drift between independent parallel lists. No Project or dependency graph is retained in the snapshot used for packaging and configuration-cache restoration.
Finalize that internal list on first read Registration remains lazy, while repeated archive metadata lookups share a consistent snapshot and do not repeatedly traverse the graph.
Keep file/identifier inputs and nullable coordinates for opaque artifacts Preserve artifact content/identity tracking and the existing unmapped behavior for files without dependency coordinates.
Keep registrations additive and lookup order unchanged Existing callers can register multiple sources; the first registration for a file continues to win.

All new production logic is confined to archive metadata resolution, the two archive implementations, their Java/WAR wiring, and the additive interface method. The documentation explains the custom-archive migration rather than enabling isolation globally.

Alternatives considered

  • Capabilities: explicit producer capabilities need not encode project GAV and may replace the implicit capability. Selecting one by name cannot establish authoritative coordinates.
  • Archive names or Maven publication GAV: renamed archives and multiple/custom publications make these different identities. The existing layer coordinate convention is project GAV.
  • Move allprojects into a provider: delaying cross-project model access does not make it isolated and risks retaining a Project in task state.
  • Always use runtimeClasspath's graph behind the old API: this changes semantics for custom configurations and artifact views.
  • Set a single graph separately from artifact registration: existing callers may register several providers from different configurations. Keeping a graph attached to each registration avoids ordering-dependent or mismatched global state.
  • Accept only a Configuration in the new API: convenient for the built-in case, but prevents callers from registering a filtered or transformed artifact view alongside the correct graph. A provider pair preserves that flexibility without retaining the mutable configuration.
  • Use only project paths or direct dependency components: breaks composite identities or misses transitive project artifacts.
  • Disable layering or suppress isolation violations: neither addresses authoritative reachability metadata, task construction, or incorrect coordinate matching.
  • Use the graph only when isolation is enabled: that leaves the ordinary-build composite bug unfixed and creates two built-in metadata implementations. The same authoritative metadata should be used in both modes.
  • Remove or deprecate the old API immediately: unnecessary compatibility cost for a feature that can be added explicitly. The old method's limitations are documented instead.

Validation

Executed against upstream main 11778bdc24f:

  • Baseline: the new isolated bootJar test fails at Project.group during task construction; the normal composite test assigns included-lib.jar to the main project's custom layer instead of the included project's layer.
  • JDK 27 / Gradle 9.8.1: 104 targeted archive/metadata tests passed, with no failures or skips. Plugin validation, main/test Checkstyle, and Javadoc passed.
  • Existing BootJarIntegrationTests and BootWarIntegrationTests: 106 cases, zero failures, six existing version-specific skips (100 executed).
  • JDK 17: 122 targeted cases across Gradle 8.14.6, 9.0.0, and 9.8.1 passed with no failures or skips.
  • Kotlin DSL: custom-archive registration, strict isolation, and forced execution after configuration-cache reuse passed on Gradle 8.14.6, 9.0.0, and 9.8.1 (three additional cases).
  • Tooling API: task models exposing bootJar, bootWar, and bootBuildImage passed under strict isolation on all three Gradle versions (three additional cases).

Commands:

./gradlew :build-plugin:spring-boot-gradle-plugin:test \
  --tests '*IsolatedProjectsIntegrationTests' --tests '*ResolvedDependenciesTests' \
  --tests '*BootJarTests' --tests '*BootWarTests' \
  :build-plugin:spring-boot-gradle-plugin:validatePlugins \
  :build-plugin:spring-boot-gradle-plugin:checkstyleMain \
  :build-plugin:spring-boot-gradle-plugin:checkstyleTest \
  :build-plugin:spring-boot-gradle-plugin:javadoc --no-build-cache
./gradlew :build-plugin:spring-boot-gradle-plugin:test \
  --tests '*BootJarIntegrationTests' --tests '*BootWarIntegrationTests' --no-build-cache
./gradlew :build-plugin:spring-boot-gradle-plugin:test -PtoolchainVersion=17 \
  --tests '*IsolatedProjectsIntegrationTests' --tests '*ResolvedDependenciesTests' \
  --tests '*BootJarTests' --tests '*BootWarTests' --no-build-cache

The Tooling API and Kotlin DSL cases were added after the 122-case matrix and run separately with -PtoolchainVersion=17 and the matching test filters (six additional cases; 128 targeted executions in total, no failures/skips).

A packaged patch was also tested against a real multi-project Kotlin application on Gradle 9.8.2. An external init script substituted only the locally built plugin artifact; the application source/configuration was not changed, and a reflection assertion verified the new overload was actually loaded. :web-app:build -x test --isolated-projects passed (448 tasks), then the identical command reused the configuration cache. All 3,474 entry names and uncompressed file contents in its executable archive matched the normal-build baseline exactly; ZIP metadata was not compared. This consumer uses native BOM platforms. No production deployment was performed.

The committed integration fixture deliberately combines two :library projects in different builds, a transitive included-build dependency, a late producer version, a renamed archive, an unrelated selected capability, and publication coordinates different from project coordinates. These are independent ways a shortcut can appear to work while selecting the wrong metadata.

Separate tests cover normal/isolated JAR and WAR packaging, forced archive execution after configuration-cache reuse, coordinate-only input invalidation with byte-identical library JARs, the legacy custom-archive overload, the graph overload with a filtered artifact view, image-task realization without Docker execution, project reachability-metadata overrides with layers disabled, Tooling API task-model construction, and direct Kotlin DSL use of the new overload. The Tooling API test supplies the same unpublished plugin classpath explicitly because model requests do not use TestKit’s classpath injection. Focused tests cover lazy snapshotting, synthetic roots, cyclic/transitive graphs, mismatched providers, missing coordinates, external/opaque components, and compatibility of the interface default.

Scope and limitations

This changes Boot's post-resolution archive metadata path. It does not fix io.spring.dependency-management's separate pre-resolution local-GA exclusion rule (#380). Resolving that rule cannot consume the completed graph of the resolution it is currently constructing. Tests here use native Gradle dependency handling and do not suppress violations.

The legacy artifact-only path remains incompatible with strict isolation for multi-project builds, and retains its historical path-based lookup. Custom callers opt into the graph overload for composite correctness and isolation. This is intentional compatibility behavior, not a claim of complete support for every build plugin or custom archive implementation.

AI assistance: this implementation, its tests, and the analysis were prepared with Codex. Validation results describe commands actually run locally.

Signed-off-by: GT <gregjotau@gmail.com>
Copilot AI balanced review requested due to automatic review settings October 23, 2026 18:41

This comment was marked as low quality.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue that has not yet been triaged. Contributions will not be accepted. label Oct 23, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: waiting-for-triage An issue that has not yet been triaged. Contributions will not be accepted.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants