From 40e4e8389f86b4f6183a72153cd8a2c8f0c9a62d Mon Sep 17 00:00:00 2001 From: skdas20 Date: Wed, 12 Aug 2026 07:11:18 +0000 Subject: [PATCH] Support provided-scope libraries when extracting an executable war 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 gh-51367 Signed-off-by: Sumit Kumar Das <151006536+skdas20@users.noreply.github.com> --- .../tasks/bundling/BootArchiveSupport.java | 7 +- .../boot/gradle/tasks/bundling/BootJar.java | 2 +- .../boot/gradle/tasks/bundling/BootWar.java | 2 +- .../jarmode/tools/IndexedJarStructure.java | 22 +++++- .../tools/IndexedJarStructureTests.java | 71 +++++++++++++++++++ .../boot/loader/tools/Packager.java | 10 ++- 6 files changed, 106 insertions(+), 8 deletions(-) diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java index 12b68384e91f..ea0877b10911 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java @@ -84,14 +84,17 @@ class BootArchiveSupport { } void configureManifest(Manifest manifest, String mainClass, String classes, String lib, - @Nullable String classPathIndex, @Nullable String layersIndex, String jdkVersion, - String implementationTitle, @Nullable Object implementationVersion) { + @Nullable String libProvided, @Nullable String classPathIndex, @Nullable String layersIndex, + String jdkVersion, String implementationTitle, @Nullable Object implementationVersion) { Attributes attributes = manifest.getAttributes(); attributes.putIfAbsent("Main-Class", this.loaderMainClass); attributes.putIfAbsent("Start-Class", mainClass); attributes.computeIfAbsent("Spring-Boot-Version", (name) -> determineSpringBootVersion()); attributes.putIfAbsent("Spring-Boot-Classes", classes); attributes.putIfAbsent("Spring-Boot-Lib", lib); + if (libProvided != null) { + attributes.putIfAbsent("Spring-Boot-Lib-Provided", libProvided); + } if (classPathIndex != null) { attributes.putIfAbsent("Spring-Boot-Classpath-Index", classPathIndex); } diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java index b4a2143995af..e11a1e090286 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java @@ -131,7 +131,7 @@ ResolvedDependencies getResolvedDependencies() { @Override public void copy() { - this.support.configureManifest(getManifest(), getMainClass().get(), CLASSES_DIRECTORY, LIB_DIRECTORY, + this.support.configureManifest(getManifest(), getMainClass().get(), CLASSES_DIRECTORY, LIB_DIRECTORY, null, CLASSPATH_INDEX, (isLayeredDisabled()) ? null : LAYERS_INDEX, this.getTargetJavaVersion().get().getMajorVersion(), this.projectName.get(), this.projectVersion.get()); super.copy(); diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java index 46a3520ac22c..b8f5d6ec756b 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java @@ -106,7 +106,7 @@ ResolvedDependencies getResolvedDependencies() { @Override public void copy() { this.support.configureManifest(getManifest(), getMainClass().get(), CLASSES_DIRECTORY, LIB_DIRECTORY, - CLASSPATH_INDEX, (isLayeredDisabled()) ? null : LAYERS_INDEX, + LIB_PROVIDED_DIRECTORY, CLASSPATH_INDEX, (isLayeredDisabled()) ? null : LAYERS_INDEX, this.getTargetJavaVersion().get().getMajorVersion(), this.projectName.get(), this.projectVersion.get()); super.copy(); } diff --git a/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java b/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java index c4c7dc266ffe..a90f0b03ce8d 100644 --- a/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java +++ b/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java @@ -49,7 +49,7 @@ class IndexedJarStructure implements JarStructure { private static final List MANIFEST_DENY_LIST = List.of("Start-Class", "Spring-Boot-Classes", - "Spring-Boot-Lib", "Spring-Boot-Classpath-Index", "Spring-Boot-Layers-Index"); + "Spring-Boot-Lib", "Spring-Boot-Lib-Provided", "Spring-Boot-Classpath-Index", "Spring-Boot-Layers-Index"); private static final Set ENTRY_IGNORE_LIST = Set.of("META-INF/", "META-INF/MANIFEST.MF", "META-INF/services/java.nio.file.spi.FileSystemProvider"); @@ -58,6 +58,8 @@ class IndexedJarStructure implements JarStructure { private final String libLocation; + private final @Nullable String providedLibLocation; + private final String classesLocation; private final List classpathEntries; @@ -65,6 +67,7 @@ class IndexedJarStructure implements JarStructure { IndexedJarStructure(Manifest originalManifest, String indexFile) { this.originalManifest = originalManifest; this.libLocation = getLocation(originalManifest, "Spring-Boot-Lib"); + this.providedLibLocation = getOptionalLocation(originalManifest, "Spring-Boot-Lib-Provided"); this.classesLocation = getLocation(originalManifest, "Spring-Boot-Classes"); this.classpathEntries = readIndexFile(indexFile); } @@ -74,6 +77,14 @@ private static String getLocation(Manifest manifest, String attribute) { return (!location.endsWith("/")) ? location + "/" : location; } + private static @Nullable String getOptionalLocation(Manifest manifest, String attribute) { + String location = manifest.getMainAttributes().getValue(attribute); + if (!StringUtils.hasLength(location)) { + return null; + } + return (!location.endsWith("/")) ? location + "/" : location; + } + private static List readIndexFile(String indexFile) { String[] lines = Arrays.stream(indexFile.split("\n")) .map((line) -> line.replace("\r", "")) @@ -130,8 +141,13 @@ public Manifest createLauncherManifest(UnaryOperator libraryTransformer) } private String toStructureDependency(String libEntryName) { - Assert.state(libEntryName.startsWith(this.libLocation), () -> "Invalid library location " + libEntryName); - return libEntryName.substring(this.libLocation.length()); + if (libEntryName.startsWith(this.libLocation)) { + return libEntryName.substring(this.libLocation.length()); + } + if (this.providedLibLocation != null && libEntryName.startsWith(this.providedLibLocation)) { + return libEntryName.substring(this.providedLibLocation.length()); + } + throw new IllegalStateException("Invalid library location " + libEntryName); } private static String getMandatoryAttribute(Manifest manifest, String attribute) { diff --git a/loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java b/loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java index 0585b824254b..7f48232280d3 100644 --- a/loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java +++ b/loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java @@ -35,6 +35,7 @@ import org.springframework.boot.jarmode.tools.JarStructure.Entry.Type; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link IndexedJarStructure}. @@ -53,6 +54,24 @@ void shouldResolveLibraryEntry() throws IOException { assertThat(entry.type()).isEqualTo(Type.LIBRARY); } + @Test + void shouldResolveLibraryEntryFromWarProvidedLocation() throws IOException { + IndexedJarStructure structure = createWarStructure(); + Entry entry = structure.resolve("WEB-INF/lib-provided/tomcat-embed-core-10.1.19.jar"); + assertThat(entry).isNotNull(); + assertThat(entry.location()).isEqualTo("tomcat-embed-core-10.1.19.jar"); + assertThat(entry.originalLocation()).isEqualTo("WEB-INF/lib-provided/tomcat-embed-core-10.1.19.jar"); + assertThat(entry.type()).isEqualTo(Type.LIBRARY); + } + + @Test + void shouldCreateLauncherManifestForWarWithProvidedLibraries() throws IOException { + IndexedJarStructure structure = createWarStructure(); + Manifest manifest = structure.createLauncherManifest(UnaryOperator.identity()); + assertThat(getAttributes(manifest)).containsEntry("Class-Path", + "spring-webmvc-6.1.4.jar tomcat-embed-core-10.1.19.jar"); + } + @Test void shouldResolveApplicationEntry() throws IOException { IndexedJarStructure structure = createStructure(); @@ -119,10 +138,62 @@ private Map getAttributes(Manifest manifest) { return result; } + @Test + void shouldFailToResolveProvidedLibraryWhenAttributeIsMissing() throws IOException { + // an archive whose index references lib-provided but whose manifest does not + // record the location has nothing to flatten the entry against + IndexedJarStructure structure = createWarStructureWithoutProvidedLibAttribute(); + assertThatIllegalStateException() + .isThrownBy(() -> structure.resolve("WEB-INF/lib-provided/tomcat-embed-core-10.1.19.jar")) + .withMessageContaining("Invalid library location"); + } + + @Test + void launcherManifestShouldNotContainProvidedLibAttribute() throws IOException { + IndexedJarStructure structure = createWarStructure(); + Manifest manifest = structure.createLauncherManifest(UnaryOperator.identity()); + assertThat(getAttributes(manifest)).doesNotContainKey("Spring-Boot-Lib-Provided"); + } + private IndexedJarStructure createStructure() throws IOException { return new IndexedJarStructure(createManifest(), createIndexFile()); } + private IndexedJarStructure createWarStructure() throws IOException { + Manifest manifest = new Manifest(new ByteArrayInputStream(""" + Manifest-Version: 1.0 + Main-Class: org.springframework.boot.loader.launch.WarLauncher + Start-Class: org.springframework.boot.jarmode.tools.IndexedJarStructureTests + Spring-Boot-Version: 3.3.0-SNAPSHOT + Spring-Boot-Classes: WEB-INF/classes/ + Spring-Boot-Lib: WEB-INF/lib/ + Spring-Boot-Lib-Provided: WEB-INF/lib-provided/ + Spring-Boot-Classpath-Index: WEB-INF/classpath.idx + """.getBytes(StandardCharsets.UTF_8))); + String indexFile = """ + - "WEB-INF/lib/spring-webmvc-6.1.4.jar" + - "WEB-INF/lib-provided/tomcat-embed-core-10.1.19.jar" + """; + return new IndexedJarStructure(manifest, indexFile); + } + + private IndexedJarStructure createWarStructureWithoutProvidedLibAttribute() throws IOException { + Manifest manifest = new Manifest(new ByteArrayInputStream(""" + Manifest-Version: 1.0 + Main-Class: org.springframework.boot.loader.launch.WarLauncher + Start-Class: org.springframework.boot.jarmode.tools.IndexedJarStructureTests + Spring-Boot-Version: 3.3.0-SNAPSHOT + Spring-Boot-Classes: WEB-INF/classes/ + Spring-Boot-Lib: WEB-INF/lib/ + Spring-Boot-Classpath-Index: WEB-INF/classpath.idx + """.getBytes(StandardCharsets.UTF_8))); + String indexFile = """ + - "WEB-INF/lib/spring-webmvc-6.1.4.jar" + - "WEB-INF/lib-provided/tomcat-embed-core-10.1.19.jar" + """; + return new IndexedJarStructure(manifest, indexFile); + } + private String createIndexFile() { return """ - "BOOT-INF/lib/spring-webmvc-6.1.4.jar" diff --git a/loader/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java b/loader/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java index 50754fa09c9a..109a0acd40bb 100644 --- a/loader/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java +++ b/loader/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java @@ -45,6 +45,7 @@ import org.springframework.boot.loader.tools.AbstractJarWriter.UnpackHandler; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -69,6 +70,8 @@ public abstract class Packager { private static final String BOOT_LIB_ATTRIBUTE = "Spring-Boot-Lib"; + private static final String BOOT_LIB_PROVIDED_ATTRIBUTE = "Spring-Boot-Lib-Provided"; + private static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index"; private static final String BOOT_LAYERS_INDEX_ATTRIBUTE = "Spring-Boot-Layers-Index"; @@ -406,7 +409,12 @@ private void addBootAttributesForLayout(Attributes attributes) { else { attributes.putValue(BOOT_CLASSES_ATTRIBUTE, layout.getClassesLocation()); } - putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, getLayout().getLibraryLocation("", LibraryScope.COMPILE)); + 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); + } putIfHasLength(attributes, BOOT_CLASSPATH_INDEX_ATTRIBUTE, layout.getClasspathIndexFileLocation()); if (isLayered()) { putIfHasLength(attributes, BOOT_LAYERS_INDEX_ATTRIBUTE, layout.getLayersIndexFileLocation());