diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java index 5cda8cf720f..a2cce86f048 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java @@ -93,12 +93,8 @@ class BootArchiveSupport { attributes.putIfAbsent("Main-Class", this.loaderMainClass); attributes.putIfAbsent("Start-Class", mainClass); attributes.computeIfAbsent("Spring-Boot-Version", (name) -> determineSpringBootVersion()); - if (classes != null) { - attributes.putIfAbsent("Spring-Boot-Classes", classes); - } - if (lib != null) { - attributes.putIfAbsent("Spring-Boot-Lib", lib); - } + attributes.putIfAbsent("Spring-Boot-Classes", classes); + attributes.putIfAbsent("Spring-Boot-Lib", lib); if (classPathIndex != null) { attributes.putIfAbsent("Spring-Boot-Classpath-Index", classPathIndex); } @@ -113,10 +109,10 @@ class BootArchiveSupport { } CopyAction createCopyAction(Jar jar) { - return createCopyAction(jar, null, false); + return createCopyAction(jar, null, null); } - CopyAction createCopyAction(Jar jar, LayerResolver layerResolver, boolean includeLayerTools) { + CopyAction createCopyAction(Jar jar, LayerResolver layerResolver, String layerToolsLocation) { File output = jar.getArchiveFile().get().getAsFile(); Manifest manifest = jar.getManifest(); boolean preserveFileTimestamps = jar.isPreserveFileTimestamps(); @@ -128,8 +124,8 @@ class BootArchiveSupport { Function compressionResolver = this.compressionResolver; String encoding = jar.getMetadataCharset(); CopyAction action = new BootZipCopyAction(output, manifest, preserveFileTimestamps, includeDefaultLoader, - includeLayerTools, requiresUnpack, exclusions, launchScript, librarySpec, compressionResolver, encoding, - layerResolver); + layerToolsLocation, requiresUnpack, exclusions, launchScript, librarySpec, compressionResolver, + encoding, layerResolver); return jar.isReproducibleFileOrder() ? new ReproducibleOrderingCopyAction(action) : action; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java index 2ea8349fb3e..d6dd6ae6d13 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java @@ -97,14 +97,8 @@ public class BootJar extends Jar implements BootArchive { @Override public void copy() { - if (this.layered != null) { - this.support.configureManifest(getManifest(), getMainClassName(), null, null, CLASSPATH_INDEX, - LAYERS_INDEX); - } - else { - this.support.configureManifest(getManifest(), getMainClassName(), CLASSES_FOLDER, LIB_FOLDER, - CLASSPATH_INDEX, null); - } + this.support.configureManifest(getManifest(), getMainClassName(), CLASSES_FOLDER, LIB_FOLDER, CLASSPATH_INDEX, + (this.layered != null) ? LAYERS_INDEX : null); super.copy(); } @@ -112,8 +106,8 @@ public class BootJar extends Jar implements BootArchive { protected CopyAction createCopyAction() { if (this.layered != null) { LayerResolver layerResolver = new LayerResolver(getConfigurations(), this.layered, this::isLibrary); - boolean includeLayerTools = this.layered.isIncludeLayerTools(); - return this.support.createCopyAction(this, layerResolver, includeLayerTools); + String layerToolsLocation = this.layered.isIncludeLayerTools() ? LIB_FOLDER : null; + return this.support.createCopyAction(this, layerResolver, layerToolsLocation); } return this.support.createCopyAction(this); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java index 0267e6f81bd..10eb611628b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java @@ -48,7 +48,12 @@ import org.gradle.api.tasks.WorkResults; import org.springframework.boot.loader.tools.DefaultLaunchScript; import org.springframework.boot.loader.tools.FileUtils; import org.springframework.boot.loader.tools.JarModeLibrary; +import org.springframework.boot.loader.tools.Layer; +import org.springframework.boot.loader.tools.LayersIndex; +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; import org.springframework.util.StreamUtils; +import org.springframework.util.StringUtils; /** * A {@link CopyAction} for creating a Spring Boot zip archive (typically a jar or war). @@ -70,7 +75,7 @@ class BootZipCopyAction implements CopyAction { private final boolean includeDefaultLoader; - private final boolean includeLayerTools; + private final String layerToolsLocation; private final Spec requiresUnpack; @@ -87,7 +92,7 @@ class BootZipCopyAction implements CopyAction { private final LayerResolver layerResolver; BootZipCopyAction(File output, Manifest manifest, boolean preserveFileTimestamps, boolean includeDefaultLoader, - boolean includeLayerTools, Spec requiresUnpack, Spec exclusions, + String layerToolsLocation, Spec requiresUnpack, Spec exclusions, LaunchScriptConfiguration launchScript, Spec librarySpec, Function compressionResolver, String encoding, LayerResolver layerResolver) { @@ -95,7 +100,7 @@ class BootZipCopyAction implements CopyAction { this.manifest = manifest; this.preserveFileTimestamps = preserveFileTimestamps; this.includeDefaultLoader = includeDefaultLoader; - this.includeLayerTools = includeLayerTools; + this.layerToolsLocation = layerToolsLocation; this.requiresUnpack = requiresUnpack; this.exclusions = exclusions; this.launchScript = launchScript; @@ -177,7 +182,9 @@ class BootZipCopyAction implements CopyAction { private ZipArchiveOutputStream out; - private Spec writtenLoaderEntries; + private final LayersIndex layerIndex; + + private LoaderZipEntries.WrittenEntries writtenLoaderEntries; private Set writtenDirectories = new LinkedHashSet<>(); @@ -185,6 +192,8 @@ class BootZipCopyAction implements CopyAction { Processor(ZipArchiveOutputStream out) { this.out = out; + this.layerIndex = (BootZipCopyAction.this.layerResolver != null) + ? new LayersIndex(BootZipCopyAction.this.layerResolver.getLayers()) : null; } void process(FileCopyDetails details) { @@ -207,11 +216,11 @@ class BootZipCopyAction implements CopyAction { private boolean skipProcessing(FileCopyDetails details) { return BootZipCopyAction.this.exclusions.isSatisfiedBy(details) - || (this.writtenLoaderEntries != null && this.writtenLoaderEntries.isSatisfiedBy(details)); + || (this.writtenLoaderEntries != null && this.writtenLoaderEntries.isWrittenDirectory(details)); } private void processDirectory(FileCopyDetails details) throws IOException { - String name = getEntryName(details); + String name = details.getRelativePath().getPathString(); long time = getTime(details); writeParentDirectoriesIfNecessary(name, time); ZipArchiveEntry entry = new ZipArchiveEntry(name + '/'); @@ -223,7 +232,7 @@ class BootZipCopyAction implements CopyAction { } private void processFile(FileCopyDetails details) throws IOException { - String name = getEntryName(details); + String name = details.getRelativePath().getPathString(); long time = getTime(details); writeParentDirectoriesIfNecessary(name, time); ZipArchiveEntry entry = new ZipArchiveEntry(name); @@ -239,6 +248,10 @@ class BootZipCopyAction implements CopyAction { if (BootZipCopyAction.this.librarySpec.isSatisfiedBy(details)) { this.writtenLibraries.add(name.substring(name.lastIndexOf('/') + 1)); } + if (BootZipCopyAction.this.layerResolver != null) { + Layer layer = BootZipCopyAction.this.layerResolver.getLayer(details); + this.layerIndex.add(layer, name); + } } private void writeParentDirectoriesIfNecessary(String name, long time) throws IOException { @@ -261,34 +274,12 @@ class BootZipCopyAction implements CopyAction { return name.substring(0, lastSlash); } - private String getEntryName(FileCopyDetails details) { - if (BootZipCopyAction.this.layerResolver == null) { - return details.getRelativePath().getPathString(); - } - return BootZipCopyAction.this.layerResolver.getPath(details); - } - - private void prepareStoredEntry(FileCopyDetails details, ZipArchiveEntry archiveEntry) throws IOException { - archiveEntry.setMethod(java.util.zip.ZipEntry.STORED); - archiveEntry.setSize(details.getSize()); - archiveEntry.setCompressedSize(details.getSize()); - archiveEntry.setCrc(getCrc(details)); - if (BootZipCopyAction.this.requiresUnpack.isSatisfiedBy(details)) { - archiveEntry.setComment("UNPACK:" + FileUtils.sha1Hash(details.getFile())); - } - } - - private long getCrc(FileCopyDetails details) { - Crc32OutputStream crcStream = new Crc32OutputStream(); - details.copyTo(crcStream); - return crcStream.getCrc(); - } - void finish() throws IOException { writeLoaderEntriesIfNecessary(null); writeJarToolsIfNecessary(); - writeLayersIndexIfNecessary(); writeClassPathIndexIfNecessary(); + // We must write the layer index last + writeLayersIndexIfNecessary(); } private void writeLoaderEntriesIfNecessary(FileCopyDetails details) throws IOException { @@ -299,9 +290,15 @@ class BootZipCopyAction implements CopyAction { // Don't write loader entries until after META-INF folder (see gh-16698) return; } - LoaderZipEntries entries = new LoaderZipEntries( + LoaderZipEntries loaderEntries = new LoaderZipEntries( BootZipCopyAction.this.preserveFileTimestamps ? null : CONSTANT_TIME_FOR_ZIP_ENTRIES); - this.writtenLoaderEntries = entries.writeTo(this.out); + this.writtenLoaderEntries = loaderEntries.writeTo(this.out); + if (BootZipCopyAction.this.layerResolver != null) { + for (String name : this.writtenLoaderEntries.getFiles()) { + Layer layer = BootZipCopyAction.this.layerResolver.getLayer(name); + this.layerIndex.add(layer, name); + } + } } private boolean isInMetaInf(FileCopyDetails details) { @@ -313,44 +310,75 @@ class BootZipCopyAction implements CopyAction { } private void writeJarToolsIfNecessary() throws IOException { - if (BootZipCopyAction.this.layerResolver == null || !BootZipCopyAction.this.includeLayerTools) { - return; + if (BootZipCopyAction.this.layerToolsLocation != null) { + writeJarModeLibrary(BootZipCopyAction.this.layerToolsLocation, JarModeLibrary.LAYER_TOOLS); } - writeJarModeLibrary(JarModeLibrary.LAYER_TOOLS); } - private void writeJarModeLibrary(JarModeLibrary jarModeLibrary) throws IOException { - String name = BootZipCopyAction.this.layerResolver.getPath(jarModeLibrary); - writeFile(name, ZipEntryWriter.fromInputStream(jarModeLibrary.openStream())); + private void writeJarModeLibrary(String location, JarModeLibrary library) throws IOException { + String name = location + library.getName(); + writeEntry(name, ZipEntryWriter.fromInputStream(library.openStream()), false, + (entry) -> prepareStoredEntry(library.openStream(), entry)); + if (BootZipCopyAction.this.layerResolver != null) { + Layer layer = BootZipCopyAction.this.layerResolver.getLayer(library); + this.layerIndex.add(layer, name); + } } - private void writeLayersIndexIfNecessary() throws IOException { - Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes(); - String layersIndex = (String) manifestAttributes.get("Spring-Boot-Layers-Index"); - if (layersIndex != null && BootZipCopyAction.this.layerResolver != null) { - writeFile(layersIndex, ZipEntryWriter.fromLines(BootZipCopyAction.this.encoding, - BootZipCopyAction.this.layerResolver.getLayerNames())); + private void prepareStoredEntry(FileCopyDetails details, ZipArchiveEntry archiveEntry) throws IOException { + prepareStoredEntry(details.open(), archiveEntry); + if (BootZipCopyAction.this.requiresUnpack.isSatisfiedBy(details)) { + archiveEntry.setComment("UNPACK:" + FileUtils.sha1Hash(details.getFile())); } } + private void prepareStoredEntry(InputStream input, ZipArchiveEntry archiveEntry) throws IOException { + archiveEntry.setMethod(java.util.zip.ZipEntry.STORED); + Crc32OutputStream crcStream = new Crc32OutputStream(); + int size = FileCopyUtils.copy(input, crcStream); + archiveEntry.setSize(size); + archiveEntry.setCompressedSize(size); + archiveEntry.setCrc(crcStream.getCrc()); + } + private void writeClassPathIndexIfNecessary() throws IOException { Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes(); String classPathIndex = (String) manifestAttributes.get("Spring-Boot-Classpath-Index"); if (classPathIndex != null) { - writeFile(classPathIndex, - ZipEntryWriter.fromLines(BootZipCopyAction.this.encoding, this.writtenLibraries)); + writeEntry(classPathIndex, + ZipEntryWriter.fromLines(BootZipCopyAction.this.encoding, this.writtenLibraries), true); + } + } + + private void writeLayersIndexIfNecessary() throws IOException { + if (BootZipCopyAction.this.layerResolver != null) { + Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes(); + String name = (String) manifestAttributes.get("Spring-Boot-Layers-Index"); + Assert.state(StringUtils.hasText(name), "Missing layer index manifest attribute"); + Layer layer = BootZipCopyAction.this.layerResolver.getLayer(name); + this.layerIndex.add(layer, name); + writeEntry(name, (entry, out) -> this.layerIndex.writeTo(out), false); } } - private void writeFile(String name, ZipEntryWriter entryWriter) throws IOException { + private void writeEntry(String name, ZipEntryWriter entryWriter, boolean addToLayerIndex) throws IOException { + writeEntry(name, entryWriter, addToLayerIndex, ZipEntryCustomizer.NONE); + } + + private void writeEntry(String name, ZipEntryWriter entryWriter, boolean addToLayerIndex, + ZipEntryCustomizer entryCustomizer) throws IOException { writeParentDirectoriesIfNecessary(name, CONSTANT_TIME_FOR_ZIP_ENTRIES); ZipArchiveEntry entry = new ZipArchiveEntry(name); entry.setUnixMode(UnixStat.FILE_FLAG); entry.setTime(CONSTANT_TIME_FOR_ZIP_ENTRIES); + entryCustomizer.customize(entry); this.out.putArchiveEntry(entry); entryWriter.writeTo(entry, this.out); this.out.closeArchiveEntry(); - + if (addToLayerIndex && BootZipCopyAction.this.layerResolver != null) { + Layer layer = BootZipCopyAction.this.layerResolver.getLayer(name); + this.layerIndex.add(layer, name); + } } private long getTime(FileCopyDetails details) { @@ -360,6 +388,24 @@ class BootZipCopyAction implements CopyAction { } + /** + * Callback interface used to customize a {@link ZipArchiveEntry}. + */ + @FunctionalInterface + private interface ZipEntryCustomizer { + + ZipEntryCustomizer NONE = (entry) -> { + }; + + /** + * Customize the entry. + * @param entry the entry to customize + * @throws IOException on IO error + */ + void customize(ZipArchiveEntry entry) throws IOException; + + } + /** * Callback used to write a zip entry data. */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerResolver.java index cd6cf66b993..fad76ac3647 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerResolver.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerResolver.java @@ -18,9 +18,7 @@ package org.springframework.boot.gradle.tasks.bundling; import java.io.File; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import org.gradle.api.artifacts.ArtifactCollection; import org.gradle.api.artifacts.Configuration; @@ -30,9 +28,7 @@ import org.gradle.api.artifacts.result.ResolvedArtifactResult; import org.gradle.api.file.FileCopyDetails; import org.gradle.api.specs.Spec; -import org.springframework.boot.loader.tools.JarModeLibrary; import org.springframework.boot.loader.tools.Layer; -import org.springframework.boot.loader.tools.Layers; import org.springframework.boot.loader.tools.Library; import org.springframework.boot.loader.tools.LibraryCoordinates; @@ -47,8 +43,6 @@ import org.springframework.boot.loader.tools.LibraryCoordinates; */ class LayerResolver { - private static final String BOOT_INF_FOLDER = "BOOT-INF/"; - private final ResolvedDependencies resolvedDependencies; private final LayeredSpec layeredConfiguration; @@ -62,40 +56,28 @@ class LayerResolver { this.librarySpec = librarySpec; } - String getPath(JarModeLibrary jarModeLibrary) { - Layers layers = this.layeredConfiguration.asLayers(); - Layer layer = layers.getLayer(jarModeLibrary); - if (layer != null) { - return BOOT_INF_FOLDER + "layers/" + layer + "/lib/" + jarModeLibrary.getName(); - } - return BOOT_INF_FOLDER + "lib/" + jarModeLibrary.getName(); - } - - String getPath(FileCopyDetails details) { - String path = details.getRelativePath().getPathString(); - Layer layer = getLayer(details); - if (layer == null || !path.startsWith(BOOT_INF_FOLDER)) { - return path; - } - path = path.substring(BOOT_INF_FOLDER.length()); - return BOOT_INF_FOLDER + "layers/" + layer + "/" + path; - } - Layer getLayer(FileCopyDetails details) { - Layers layers = this.layeredConfiguration.asLayers(); try { if (this.librarySpec.isSatisfiedBy(details)) { - return layers.getLayer(asLibrary(details)); + return getLayer(asLibrary(details)); } - return layers.getLayer(details.getSourcePath()); + return getLayer(details.getSourcePath()); } catch (UnsupportedOperationException ex) { return null; } } - List getLayerNames() { - return this.layeredConfiguration.asLayers().stream().map(Layer::toString).collect(Collectors.toList()); + Layer getLayer(Library library) { + return this.layeredConfiguration.asLayers().getLayer(library); + } + + Layer getLayer(String applicationResource) { + return this.layeredConfiguration.asLayers().getLayer(applicationResource); + } + + Iterable getLayers() { + return this.layeredConfiguration.asLayers(); } private Library asLibrary(FileCopyDetails details) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java index a6fcd39612d..b7caad06386 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java @@ -19,7 +19,7 @@ package org.springframework.boot.gradle.tasks.bundling; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -28,7 +28,6 @@ import org.apache.commons.compress.archivers.zip.UnixStat; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.gradle.api.file.FileTreeElement; -import org.gradle.api.specs.Spec; /** * Internal utility used to copy entries from the {@code spring-boot-loader.jar}. @@ -38,29 +37,30 @@ import org.gradle.api.specs.Spec; */ class LoaderZipEntries { - private Long entryTime; + private final Long entryTime; LoaderZipEntries(Long entryTime) { this.entryTime = entryTime; } - Spec writeTo(ZipArchiveOutputStream out) throws IOException { - WrittenDirectoriesSpec writtenDirectoriesSpec = new WrittenDirectoriesSpec(); + WrittenEntries writeTo(ZipArchiveOutputStream out) throws IOException { + WrittenEntries written = new WrittenEntries(); try (ZipInputStream loaderJar = new ZipInputStream( getClass().getResourceAsStream("/META-INF/loader/spring-boot-loader.jar"))) { java.util.zip.ZipEntry entry = loaderJar.getNextEntry(); while (entry != null) { if (entry.isDirectory() && !entry.getName().equals("META-INF/")) { writeDirectory(new ZipArchiveEntry(entry), out); - writtenDirectoriesSpec.add(entry); + written.addDirectory(entry); } else if (entry.getName().endsWith(".class")) { writeClass(new ZipArchiveEntry(entry), loaderJar, out); + written.addFile(entry); } entry = loaderJar.getNextEntry(); } } - return writtenDirectoriesSpec; + return written; } private void writeDirectory(ZipArchiveEntry entry, ZipArchiveOutputStream out) throws IOException { @@ -92,23 +92,32 @@ class LoaderZipEntries { } /** - * Spec to track directories that have been written. + * Tracks entries that have been written. */ - private static class WrittenDirectoriesSpec implements Spec { + static class WrittenEntries { - private final Set entries = new HashSet<>(); + private final Set directories = new LinkedHashSet<>(); - @Override - public boolean isSatisfiedBy(FileTreeElement element) { + private final Set files = new LinkedHashSet<>(); + + private void addDirectory(ZipEntry entry) { + this.directories.add(entry.getName()); + } + + private void addFile(ZipEntry entry) { + this.files.add(entry.getName()); + } + + boolean isWrittenDirectory(FileTreeElement element) { String path = element.getRelativePath().getPathString(); if (element.isDirectory() && !path.endsWith(("/"))) { path += "/"; } - return this.entries.contains(path); + return this.directories.contains(path); } - void add(ZipEntry entry) { - this.entries.add(entry.getName()); + Set getFiles() { + return this.files; } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java index ccc150c8ca2..527d2becf47 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java @@ -437,12 +437,16 @@ abstract class AbstractBootArchiveTests { } protected List getEntryNames(File file) throws IOException { - List entryNames = new ArrayList<>(); try (JarFile jarFile = new JarFile(file)) { - Enumeration entries = jarFile.entries(); - while (entries.hasMoreElements()) { - entryNames.add(entries.nextElement().getName()); - } + return getEntryNames(jarFile); + } + } + + protected List getEntryNames(JarFile jarFile) { + List entryNames = new ArrayList<>(); + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + entryNames.add(entries.nextElement().getName()); } return entryNames; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java index a8e7b6eeeb4..66923903fc3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java @@ -16,14 +16,25 @@ package org.springframework.boot.gradle.tasks.bundling; +import java.io.BufferedReader; import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.io.InputStreamReader; import java.io.PrintWriter; +import java.io.StringReader; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.jar.JarFile; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.InvalidRunnerConfigurationException; @@ -77,14 +88,45 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { writeMainClass(); writeResource(); assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + Map> indexedLayers; + String layerToolsJar = "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName(); try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { - assertThat(jarFile.getEntry(jarModeLayerTools())).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/commons-lang3-3.9.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/layers/snapshot-dependencies/lib/commons-io-2.7-SNAPSHOT.jar")) - .isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/layers/application/classes/example/Main.class")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/layers/application/classes/static/file.txt")).isNotNull(); + assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/lib/commons-lang3-3.9.jar")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/lib/commons-io-2.7-SNAPSHOT.jar")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/classes/example/Main.class")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/classes/static/file.txt")).isNotNull(); + indexedLayers = readLayerIndex(jarFile); } + List layerNames = Arrays.asList("dependencies", "spring-boot-loader", "snapshot-dependencies", + "application"); + assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames); + List expectedDependencies = new ArrayList<>(); + expectedDependencies.add("BOOT-INF/lib/commons-lang3-3.9.jar"); + expectedDependencies.add("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar"); + expectedDependencies.add("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar"); + List expectedSnapshotDependencies = new ArrayList<>(); + expectedSnapshotDependencies.add("BOOT-INF/lib/commons-io-2.7-SNAPSHOT.jar"); + (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); + assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); + assertThat(indexedLayers.get("spring-boot-loader")) + .allMatch(Pattern.compile("org/springframework/boot/loader/.+\\.class").asPredicate()); + assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); + assertThat(indexedLayers.get("application")).containsExactly("META-INF/MANIFEST.MF", + "BOOT-INF/classes/example/Main.class", "BOOT-INF/classes/static/file.txt", "BOOT-INF/classpath.idx", + "BOOT-INF/layers.idx"); + BuildResult listLayers = this.gradleBuild.build("listLayers"); + assertThat(listLayers.task(":listLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + String listLayersOutput = listLayers.getOutput(); + assertThat(new BufferedReader(new StringReader(listLayersOutput)).lines()).containsSequence(layerNames); + BuildResult extractLayers = this.gradleBuild.build("extractLayers"); + assertThat(extractLayers.task(":extractLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + Map> extractedLayers = readExtractedLayers(this.gradleBuild.getProjectDir(), layerNames); + assertThat(extractedLayers.keySet()).isEqualTo(indexedLayers.keySet()); + extractedLayers.forEach( + (name, contents) -> assertThat(contents).containsExactlyInAnyOrderElementsOf(indexedLayers.get(name))); } @TestTemplate @@ -92,23 +134,49 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { writeMainClass(); writeResource(); BuildResult build = this.gradleBuild.build("bootJar"); - System.out.println(build.getOutput()); assertThat(build.task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + Map> indexedLayers; + String layerToolsJar = "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName(); try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { - assertThat(jarFile.getEntry(jarModeLayerTools())).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/layers/commons-dependencies/lib/commons-lang3-3.9.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/layers/snapshot-dependencies/lib/commons-io-2.7-SNAPSHOT.jar")) - .isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/layers/app/classes/example/Main.class")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/layers/static/classes/static/file.txt")).isNotNull(); + assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/lib/commons-lang3-3.9.jar")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/lib/commons-io-2.7-SNAPSHOT.jar")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/classes/example/Main.class")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/classes/static/file.txt")).isNotNull(); + assertThat(jarFile.getEntry("BOOT-INF/layers.idx")).isNotNull(); + indexedLayers = readLayerIndex(jarFile); } - } - - private String jarModeLayerTools() { - JarModeLibrary library = JarModeLibrary.LAYER_TOOLS; - String version = library.getCoordinates().getVersion(); - String layer = (version == null || !version.contains("SNAPSHOT")) ? "dependencies" : "snapshot-dependencies"; - return "BOOT-INF/layers/" + layer + "/lib/" + library.getName(); + List layerNames = Arrays.asList("dependencies", "commons-dependencies", "snapshot-dependencies", + "static", "app"); + assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames); + List expectedDependencies = new ArrayList<>(); + expectedDependencies.add("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar"); + expectedDependencies.add("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar"); + List expectedSnapshotDependencies = new ArrayList<>(); + expectedSnapshotDependencies.add("BOOT-INF/lib/commons-io-2.7-SNAPSHOT.jar"); + (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); + assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); + assertThat(indexedLayers.get("commons-dependencies")).containsExactly("BOOT-INF/lib/commons-lang3-3.9.jar"); + assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); + assertThat(indexedLayers.get("static")).containsExactly("BOOT-INF/classes/static/file.txt"); + List appLayer = new ArrayList<>(indexedLayers.get("app")); + List nonLoaderEntries = Arrays.asList("META-INF/MANIFEST.MF", "BOOT-INF/classes/example/Main.class", + "BOOT-INF/classpath.idx", "BOOT-INF/layers.idx"); + assertThat(appLayer).containsSubsequence(nonLoaderEntries); + appLayer.removeAll(nonLoaderEntries); + assertThat(appLayer).allMatch(Pattern.compile("org/springframework/boot/loader/.+\\.class").asPredicate()); + BuildResult listLayers = this.gradleBuild.build("listLayers"); + assertThat(listLayers.task(":listLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + String listLayersOutput = listLayers.getOutput(); + assertThat(new BufferedReader(new StringReader(listLayersOutput)).lines()).containsSequence(layerNames); + BuildResult extractLayers = this.gradleBuild.build("extractLayers"); + assertThat(extractLayers.task(":extractLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + Map> extractedLayers = readExtractedLayers(this.gradleBuild.getProjectDir(), layerNames); + assertThat(extractedLayers.keySet()).isEqualTo(indexedLayers.keySet()); + extractedLayers.forEach( + (name, contents) -> assertThat(contents).containsExactlyInAnyOrderElementsOf(indexedLayers.get(name))); } private void writeMainClass() { @@ -144,4 +212,24 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { } } + private Map> readLayerIndex(JarFile jarFile) throws IOException { + ZipEntry indexEntry = jarFile.getEntry("BOOT-INF/layers.idx"); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(jarFile.getInputStream(indexEntry)))) { + return reader.lines().map((line) -> line.split(" ")) + .collect(Collectors.groupingBy((layerAndPath) -> layerAndPath[0], LinkedHashMap::new, + Collectors.mapping((layerAndPath) -> layerAndPath[1], Collectors.toList()))); + } + } + + private Map> readExtractedLayers(File root, List layerNames) throws IOException { + Map> extractedLayers = new LinkedHashMap<>(); + for (String layerName : layerNames) { + File layer = new File(root, layerName); + assertThat(layer).isDirectory(); + extractedLayers.put(layerName, Files.walk(layer.toPath()).filter((path) -> path.toFile().isFile()) + .map(layer.toPath()::relativize).map(Path::toString).collect(Collectors.toList())); + } + return extractedLayers; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java index 710ab6a0a4d..058d3b1d9a3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java @@ -79,46 +79,41 @@ class BootJarTests extends AbstractBootArchiveTests { } } - @Test - void whenJarIsLayeredThenBootInfContainsOnlyLayersAndIndexFiles() throws IOException { - List entryNames = getEntryNames(createLayeredJar()); - assertThat(entryNames.stream().filter((name) -> name.startsWith("BOOT-INF/")) - .filter((name) -> !name.startsWith("BOOT-INF/layers/"))).contains("BOOT-INF/layers.idx", - "BOOT-INF/classpath.idx"); - } - @Test void whenJarIsLayeredThenManifestContainsEntryForLayersIndexInPlaceOfClassesAndLib() throws IOException { try (JarFile jarFile = new JarFile(createLayeredJar())) { - assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classes")).isEqualTo(null); - assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isEqualTo(null); + assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classes")) + .isEqualTo("BOOT-INF/classes/"); + assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib")) + .isEqualTo("BOOT-INF/lib/"); + assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classpath-Index")) + .isEqualTo("BOOT-INF/classpath.idx"); assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Layers-Index")) .isEqualTo("BOOT-INF/layers.idx"); } } @Test - void whenJarIsLayeredThenLayersIndexIsPresentAndListsLayersInOrder() throws IOException { + void whenJarIsLayeredThenLayersIndexIsPresentAndCorrect() throws IOException { try (JarFile jarFile = new JarFile(createLayeredJar())) { - assertThat(entryLines(jarFile, "BOOT-INF/layers.idx")).containsExactly("dependencies", "spring-boot-loader", + List entryNames = getEntryNames(jarFile); + assertThat(entryNames).contains("BOOT-INF/lib/first-library.jar", "BOOT-INF/lib/second-library.jar", + "BOOT-INF/lib/third-library-SNAPSHOT.jar", "BOOT-INF/classes/com/example/Application.class", + "BOOT-INF/classes/application.properties", "BOOT-INF/classes/static/test.css"); + List index = entryLines(jarFile, "BOOT-INF/layers.idx"); + assertThat(getLayerNames(index)).containsExactly("dependencies", "spring-boot-loader", "snapshot-dependencies", "application"); + assertThat(index).contains("dependencies BOOT-INF/lib/first-library.jar", + "dependencies BOOT-INF/lib/second-library.jar", + "snapshot-dependencies BOOT-INF/lib/third-library-SNAPSHOT.jar", + "application BOOT-INF/classes/com/example/Application.class", + "application BOOT-INF/classes/application.properties", + "application BOOT-INF/classes/static/test.css"); } } @Test - void whenJarIsLayeredThenContentsAreMovedToLayerDirectories() throws IOException { - List entryNames = getEntryNames(createLayeredJar()); - assertThat(entryNames) - .containsSubsequence("BOOT-INF/layers/dependencies/lib/first-library.jar", - "BOOT-INF/layers/dependencies/lib/second-library.jar") - .contains("BOOT-INF/layers/snapshot-dependencies/lib/third-library-SNAPSHOT.jar") - .containsSubsequence("BOOT-INF/layers/application/classes/com/example/Application.class", - "BOOT-INF/layers/application/classes/application.properties") - .contains("BOOT-INF/layers/application/classes/static/test.css"); - } - - @Test - void whenJarIsLayeredWithCustomStrategiesThenContentsAreMovedToLayerDirectories() throws IOException { + void whenJarIsLayeredWithCustomStrategiesThenLayersIndexIsPresentAndCorrent() throws IOException { File jar = createLayeredJar((layered) -> { layered.application((application) -> { application.intoLayer("resources", (spec) -> spec.include("static/**")); @@ -131,25 +126,30 @@ class BootJarTests extends AbstractBootArchiveTests { }); layered.layerOrder("my-deps", "my-internal-deps", "my-snapshot-deps", "resources", "application"); }); - List entryNames = getEntryNames(jar); - assertThat(entryNames) - .containsSubsequence("BOOT-INF/layers/my-internal-deps/lib/first-library.jar", - "BOOT-INF/layers/my-internal-deps/lib/second-library.jar") - .contains("BOOT-INF/layers/my-snapshot-deps/lib/third-library-SNAPSHOT.jar") - .containsSubsequence("BOOT-INF/layers/application/classes/com/example/Application.class", - "BOOT-INF/layers/application/classes/application.properties") - .contains("BOOT-INF/layers/resources/classes/static/test.css"); + try (JarFile jarFile = new JarFile(jar)) { + List entryNames = getEntryNames(jar); + assertThat(entryNames).contains("BOOT-INF/lib/first-library.jar", "BOOT-INF/lib/second-library.jar", + "BOOT-INF/lib/third-library-SNAPSHOT.jar", "BOOT-INF/classes/com/example/Application.class", + "BOOT-INF/classes/application.properties", "BOOT-INF/classes/static/test.css"); + List index = entryLines(jarFile, "BOOT-INF/layers.idx"); + assertThat(getLayerNames(index)).containsExactly("my-deps", "my-internal-deps", "my-snapshot-deps", + "resources", "application"); + assertThat(index).contains("my-internal-deps BOOT-INF/lib/first-library.jar", + "my-internal-deps BOOT-INF/lib/second-library.jar", + "my-snapshot-deps BOOT-INF/lib/third-library-SNAPSHOT.jar", + "application BOOT-INF/classes/com/example/Application.class", + "application BOOT-INF/classes/application.properties", + "resources BOOT-INF/classes/static/test.css"); + } } @Test - void whenJarIsLayeredJarsInLibAreStored() throws IOException { + void jarsInLibAreStored() throws IOException { try (JarFile jarFile = new JarFile(createLayeredJar())) { - assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/first-library.jar").getMethod()) - .isEqualTo(ZipEntry.STORED); - assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/second-library.jar").getMethod()) + assertThat(jarFile.getEntry("BOOT-INF/lib/first-library.jar").getMethod()).isEqualTo(ZipEntry.STORED); + assertThat(jarFile.getEntry("BOOT-INF/lib/second-library.jar").getMethod()).isEqualTo(ZipEntry.STORED); + assertThat(jarFile.getEntry("BOOT-INF/lib/third-library-SNAPSHOT.jar").getMethod()) .isEqualTo(ZipEntry.STORED); - assertThat(jarFile.getEntry("BOOT-INF/layers/snapshot-dependencies/lib/third-library-SNAPSHOT.jar") - .getMethod()).isEqualTo(ZipEntry.STORED); } } @@ -164,14 +164,7 @@ class BootJarTests extends AbstractBootArchiveTests { @Test void whenJarIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException { List entryNames = getEntryNames(createLayeredJar()); - assertThat(entryNames).contains(jarModeLayerTools()); - } - - private String jarModeLayerTools() { - JarModeLibrary library = JarModeLibrary.LAYER_TOOLS; - String version = library.getCoordinates().getVersion(); - String layer = (version == null || !version.contains("SNAPSHOT")) ? "dependencies" : "snapshot-dependencies"; - return "BOOT-INF/layers/" + layer + "/lib/" + library.getName(); + assertThat(entryNames).contains("BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName()); } @Test @@ -267,6 +260,14 @@ class BootJarTests extends AbstractBootArchiveTests { } } + private Set getLayerNames(List index) { + return index.stream().map(this::getLayerName).collect(Collectors.toCollection(LinkedHashSet::new)); + } + + private String getLayerName(String indexLine) { + return indexLine.substring(0, indexLine.indexOf(" ")); + } + @Override protected void executeTask() { getTask().copy(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle index f3bec81b597..15cce1e6393 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle @@ -31,6 +31,19 @@ repositories { } dependencies { - implementation("org.apache.commons:commons-lang3:3.9") implementation("commons-io:commons-io:2.7-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") + implementation("org.springframework:spring-core:5.2.5.RELEASE") +} + +task listLayers(type: JavaExec) { + classpath = bootJar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "list" +} + +task extractLayers(type: JavaExec) { + classpath = bootJar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "extract" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle index 1cd9de76bf4..22dc55ae655 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle @@ -14,6 +14,19 @@ repositories { } dependencies { - implementation("org.apache.commons:commons-lang3:3.9") implementation("commons-io:commons-io:2.7-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") + implementation("org.springframework:spring-core:5.2.5.RELEASE") } + +task listLayers(type: JavaExec) { + classpath = bootJar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "list" +} + +task extractLayers(type: JavaExec) { + classpath = bootJar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "extract" +} \ No newline at end of file