Skip to content

Support provided-scope libraries when extracting an executable war - #51372

Open
skdas20 wants to merge 1 commit into
spring-projects:mainfrom
skdas20:fix-51367-war-provided-libs
Open

Support provided-scope libraries when extracting an executable war#51372
skdas20 wants to merge 1 commit into
spring-projects:mainfrom
skdas20:fix-51367-war-provided-libs

Conversation

@skdas20

@skdas20 skdas20 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

See gh-51367.

Problem

IndexedJarStructure.toStructureDependency flattens every classpath index entry relative to the single Spring-Boot-Lib location:

Assert.state(libEntryName.startsWith(this.libLocation), () -> "Invalid library location " + libEntryName);
return libEntryName.substring(this.libLocation.length());

For an executable war Spring-Boot-Lib is WEB-INF/lib/, but provided-scope dependencies (the embedded container among them) are packaged under WEB-INF/lib-provided/ by Layouts.War and are listed in WEB-INF/classpath.idx alongside the rest. The first such entry trips the assertion, so -Djarmode=tools extract fails with Invalid library location WEB-INF/lib-provided/... on any war built from a web starter.

Only Spring-Boot-Lib is recorded in the manifest, so the second location can't be read from there. WarLauncher has the same need and hardcodes both:

return name.startsWith("WEB-INF/lib/") || name.startsWith("WEB-INF/lib-provided/");

Change

Accept entries from either location when flattening, mirroring WarLauncher, and keep the failure for anything genuinely outside a library root (now an explicit IllegalStateException, since the two-location check no longer fits Assert.state).

Tests

Added to IndexedJarStructureTests, using a war-shaped manifest and an index containing both a WEB-INF/lib/ and a WEB-INF/lib-provided/ entry:

  • shouldResolveLibraryEntryFromWarProvidedLocation — the provided entry resolves as a LIBRARY flattened to its bare jar name
  • shouldCreateLauncherManifestForWarWithProvidedLibraries — both jars appear in the generated Class-Path

Both fail on main:

IndexedJarStructureTests > shouldCreateLauncherManifestForWarWithProvidedLibraries() FAILED
IndexedJarStructureTests > shouldResolveLibraryEntryFromWarProvidedLocation() FAILED

With the change the module's tests and checkFormatMain/checkFormatTest all pass.

One thing worth your call: I hardcoded the location as a constant to match WarLauncher. If you'd rather it be derived (or carried in the manifest as a new attribute so the tools jarmode doesn't need to know war layout at all), happy to redo it that way.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Aug 12, 2026
@skdas20

skdas20 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Note on the red build: the failure is :platform:spring-boot-dependencies:bomrCheck, not this change.

> Task :platform:spring-boot-dependencies:bomrCheck FAILED

MongoDB
    - Version 5.9.1 is misaligned. It should be 5.9.2.

MongoDB 5.9.2 has been published since #51339 bumped the BOM to 5.9.1, so the check flags the drift on any build until that's bumped again. This PR only touches IndexedJarStructure and its test in spring-boot-jarmode-tools, nothing under platform/ — I've left the BOM alone since dependency upgrades look like they're handled through your own tracked issues.

Happy to rebase once that lands if you'd like a green run before reviewing.

@mhalbritter

Copy link
Copy Markdown
Contributor

@philwebb What do you think about adding a new manifest attribite for the lib-provided dependencies, which the jartools can then read? Instead of hardcoding WEB-INF/lib-provided?

@skdas20

skdas20 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@mhalbritter that reads better to me than the constant, and I'm happy to redo it that way — it would let the jarmode stop knowing war layout at all, which is really the underlying smell here.

One fact that may make the decision easier: the tools jarmode is packaged into the archive it operates on — JarModeLibrary.TOOLS writes it to META-INF/jarmode/spring-boot-jarmode-tools.jar, so the reader and the manifest are always produced by the same Boot version. That means a new attribute wouldn't need a fallback for archives built by older versions the way a normally-consumed API would; there's no mixed-version case to support.

Shape I'd propose unless you'd rather something else:

  • Packager writes the location when the layout declares one — the war layout already knows it from Layouts.War.SCOPE_LOCATION for PROVIDED, so it isn't a new piece of knowledge, just newly recorded.
  • IndexedJarStructure reads it as optional and adds it to the set of library roots it will flatten against; jars simply won't have it.
  • Naming: Spring-Boot-Lib-Provided alongside the existing Spring-Boot-Lib, and added to MANIFEST_DENY_LIST so it's stripped from the generated launcher manifest like its siblings.

That's a wider change than this PR, touching loader-tools and both build plugins' expected-manifest tests. Say the word and I'll push it here, or if you'd rather keep this one as the narrow fix and track the attribute separately, that works too — I don't mind which, and I'd rather not guess and hand you the wrong scope.

@philwebb

Copy link
Copy Markdown
Member

I like the proposal. I agree with @mhalbritter that it's better than hardcoding the location.

@mhalbritter

Copy link
Copy Markdown
Contributor

@skdas20 you can go ahead, please implement adding a new manifest attribute for the lib-provided dependencies, which the jartools can then read.

@mhalbritter mhalbritter self-assigned this Aug 26, 2026
@mhalbritter mhalbritter added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels Aug 26, 2026
@mhalbritter mhalbritter added this to the 4.x milestone Aug 26, 2026
Extracting an executable war with the tools jarmode failed with
"Invalid library location WEB-INF/lib-provided/..." because
IndexedJarStructure only knew about the single library location recorded
in Spring-Boot-Lib, while the classpath index of a war also references
the provided-scope directory that WarLauncher adds to the classpath.

Record the provided-scope library location in a new
Spring-Boot-Lib-Provided manifest attribute and have the jarmode read it,
rather than hardcoding the war layout in the jarmode. The attribute is
written only when the layout reports a different location for
LibraryScope.PROVIDED than for COMPILE, so jars are unaffected, and it is
added to the deny list so it is stripped from the generated launcher
manifest like its siblings.

Closes spring-projectsgh-51367

Signed-off-by: Sumit Kumar Das <151006536+skdas20@users.noreply.github.com>
@skdas20
skdas20 force-pushed the fix-51367-war-provided-libs branch from 5c14f66 to 40e4e83 Compare August 26, 2026 20:15
@skdas20

skdas20 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @mhalbritter, @philwebb — pushed, and the hardcoded WEB-INF/lib-provided/ constant is gone.

Packager records the location alongside the existing one:

String libraryLocation = getLayout().getLibraryLocation("", LibraryScope.COMPILE);
putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, libraryLocation);
String providedLibraryLocation = getLayout().getLibraryLocation("", LibraryScope.PROVIDED);
if (!ObjectUtils.nullSafeEquals(providedLibraryLocation, libraryLocation)) {
    putIfHasLength(attributes, BOOT_LIB_PROVIDED_ATTRIBUTE, providedLibraryLocation);
}

The comparison against the compile location is doing real work rather than being defensive: Layouts.Jar.getLibraryLocation returns BOOT-INF/lib/ for every scope, so writing it unconditionally would stamp a redundant Spring-Boot-Lib-Provided: BOOT-INF/lib/ onto every jar. Keying off "the layout reports somewhere different for PROVIDED" keeps it to wars without Packager needing to know what a war is, and a custom layout that separates provided dependencies gets it for free.

IndexedJarStructure reads it as optional and treats it as a second root to flatten against, so the jarmode no longer knows anything about war layout. It's in MANIFEST_DENY_LIST so it's stripped from the generated launcher manifest like its siblings.

Gradle plugin: configureManifest takes a nullable libProvided; BootWar passes LIB_PROVIDED_DIRECTORY, BootJar passes null.

As mentioned, no fallback for older archives is needed since JarModeLibrary.TOOLS packages the jarmode into the archive it operates on, so reader and manifest always come from the same Boot version. There's a test pinning the failure mode anyway (shouldFailToResolveProvidedLibraryWhenAttributeIsMissing) so the behaviour is at least explicit if that ever stops holding.

Green locally: loader-tools 181, jarmode-tools 84, gradle-plugin 709, maven-plugin tests, and checkFormat across all touched modules.

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

Labels

type: enhancement A general enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants