Automatically add jarmode jars when packaging
Update the `Packager` to automatically add the layertools jarmode jar when producing a layered jar. Closes gh-19865
This commit is contained in:
@@ -11,6 +11,7 @@ def generatedResources = "${buildDir}/generated-resources/main"
|
||||
|
||||
configurations {
|
||||
loader
|
||||
jarmode
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -22,6 +23,8 @@ dependencies {
|
||||
|
||||
loader(project(":spring-boot-project:spring-boot-tools:spring-boot-loader"))
|
||||
|
||||
jarmode(project(":spring-boot-project:spring-boot-tools:spring-boot-jarmode-layertools"))
|
||||
|
||||
testImplementation("org.assertj:assertj-core")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||
testImplementation("org.mockito:mockito-core")
|
||||
@@ -45,6 +48,18 @@ task reproducibleLoaderJar(type: Jar) {
|
||||
destinationDirectory = file("${generatedResources}/META-INF/loader")
|
||||
}
|
||||
|
||||
task reproducibleJarModeLayerToolsJar(type: Jar) {
|
||||
dependsOn configurations.jarmode
|
||||
from {
|
||||
zipTree(configurations.jarmode.incoming.files.filter {it.name.startsWith "spring-boot-jarmode-layertools" }.singleFile)
|
||||
}
|
||||
reproducibleFileOrder = true
|
||||
preserveFileTimestamps = false
|
||||
archiveFileName = "spring-boot-jarmode-layertools.jar"
|
||||
destinationDirectory = file("${generatedResources}/META-INF/jarmode")
|
||||
}
|
||||
|
||||
processResources {
|
||||
dependsOn reproducibleLoaderJar
|
||||
dependsOn reproducibleJarModeLayerToolsJar
|
||||
}
|
||||
|
||||
@@ -20,17 +20,15 @@ import java.io.BufferedInputStream;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
@@ -127,17 +125,16 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
|
||||
|
||||
/**
|
||||
* Write a nested library.
|
||||
* @param destination the destination of the library
|
||||
* @param location the destination of the library
|
||||
* @param library the library
|
||||
* @throws IOException if the write fails
|
||||
*/
|
||||
public void writeNestedLibrary(String destination, Library library) throws IOException {
|
||||
File file = library.getFile();
|
||||
JarArchiveEntry entry = new JarArchiveEntry(destination + library.getName());
|
||||
entry.setTime(getNestedLibraryTime(file));
|
||||
new CrcAndSize(file).setupStoredEntry(entry);
|
||||
try (FileInputStream input = new FileInputStream(file)) {
|
||||
writeEntry(entry, new InputStreamEntryWriter(input), new LibraryUnpackHandler(library));
|
||||
public void writeNestedLibrary(String location, Library library) throws IOException {
|
||||
JarArchiveEntry entry = new JarArchiveEntry(location + library.getName());
|
||||
entry.setTime(getNestedLibraryTime(library));
|
||||
new CrcAndSize(library::openStream).setupStoredEntry(entry);
|
||||
try (InputStream inputStream = library.openStream()) {
|
||||
writeEntry(entry, new InputStreamEntryWriter(inputStream), new LibraryUnpackHandler(library));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +145,7 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
|
||||
* @throws IOException if the write fails
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public void writeIndexFile(String location, List<String> lines) throws IOException {
|
||||
public void writeIndexFile(String location, Collection<String> lines) throws IOException {
|
||||
if (location != null) {
|
||||
JarArchiveEntry entry = new JarArchiveEntry(location);
|
||||
writeEntry(entry, (outputStream) -> {
|
||||
@@ -163,22 +160,22 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
|
||||
}
|
||||
}
|
||||
|
||||
private long getNestedLibraryTime(File file) {
|
||||
private long getNestedLibraryTime(Library library) {
|
||||
try {
|
||||
try (JarFile jarFile = new JarFile(file)) {
|
||||
Enumeration<JarEntry> entries = jarFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry entry = entries.nextElement();
|
||||
try (JarInputStream jarStream = new JarInputStream(library.openStream())) {
|
||||
JarEntry entry = jarStream.getNextJarEntry();
|
||||
while (entry != null) {
|
||||
if (!entry.isDirectory()) {
|
||||
return entry.getTime();
|
||||
}
|
||||
entry = jarStream.getNextJarEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Ignore and just use the source file timestamp
|
||||
// Ignore and just use the library timestamp
|
||||
}
|
||||
return file.lastModified();
|
||||
return library.getLastModified();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -292,8 +289,8 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
|
||||
|
||||
private long size;
|
||||
|
||||
CrcAndSize(File file) throws IOException {
|
||||
try (FileInputStream inputStream = new FileInputStream(file)) {
|
||||
CrcAndSize(InputStreamSupplier supplier) throws IOException {
|
||||
try (InputStream inputStream = supplier.openStream()) {
|
||||
load(inputStream);
|
||||
}
|
||||
}
|
||||
@@ -380,7 +377,7 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
|
||||
|
||||
@Override
|
||||
public String sha1Hash(String name) throws IOException {
|
||||
return FileUtils.sha1Hash(this.library.getFile());
|
||||
return Digest.sha1(this.library::openStream);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* Utility class used to calculate digests.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class Digest {
|
||||
|
||||
private Digest() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the SHA1 digest from the supplied stream.
|
||||
* @param supplier the stream supplier
|
||||
* @return the SHA1 digest
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
static String sha1(InputStreamSupplier supplier) throws IOException {
|
||||
try {
|
||||
try (DigestInputStream inputStream = new DigestInputStream(supplier.openStream(),
|
||||
MessageDigest.getInstance("SHA-1"))) {
|
||||
byte[] buffer = new byte[4098];
|
||||
while (inputStream.read(buffer) != -1) {
|
||||
// Read the entire stream
|
||||
}
|
||||
return bytesToHex(inputStream.getMessageDigest().digest());
|
||||
}
|
||||
}
|
||||
catch (NoSuchAlgorithmException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder hex = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
hex.append(String.format("%02x", b));
|
||||
}
|
||||
return hex.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,11 +17,7 @@
|
||||
package org.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* Utilities for manipulating files and directories in Spring Boot tooling.
|
||||
@@ -62,27 +58,7 @@ public abstract class FileUtils {
|
||||
* @throws IOException if the file cannot be read
|
||||
*/
|
||||
public static String sha1Hash(File file) throws IOException {
|
||||
try {
|
||||
try (DigestInputStream inputStream = new DigestInputStream(new FileInputStream(file),
|
||||
MessageDigest.getInstance("SHA-1"))) {
|
||||
byte[] buffer = new byte[4098];
|
||||
while (inputStream.read(buffer) != -1) {
|
||||
// Read the entire stream
|
||||
}
|
||||
return bytesToHex(inputStream.getMessageDigest().digest());
|
||||
}
|
||||
}
|
||||
catch (NoSuchAlgorithmException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder hex = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
hex.append(String.format("%02x", b));
|
||||
}
|
||||
return hex.toString();
|
||||
return Digest.sha1(InputStreamSupplier.forFile(file));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Supplier to provide an {@link InputStream}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface InputStreamSupplier {
|
||||
|
||||
/**
|
||||
* Returns a new open {@link InputStream} at the begining of the content.
|
||||
* @return a new {@link InputStream}
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
InputStream openStream() throws IOException;
|
||||
|
||||
/**
|
||||
* Factory method to create an {@link InputStreamSupplier} for the given {@link File}.
|
||||
* @param file the source file
|
||||
* @return a new {@link InputStreamSupplier} instance
|
||||
*/
|
||||
static InputStreamSupplier forFile(File file) {
|
||||
return () -> new FileInputStream(file);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link Library} implementation for internal jarmode jars.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class JarModeLibrary extends Library {
|
||||
|
||||
static final JarModeLibrary LAYER_TOOLS = new JarModeLibrary("spring-boot-jarmode-layertools.jar");
|
||||
|
||||
JarModeLibrary(String name) {
|
||||
super(name, null, LibraryScope.RUNTIME, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
InputStream openStream() throws IOException {
|
||||
String path = "META-INF/jarmode/" + getName();
|
||||
URL resource = getClass().getClassLoader().getResource(path);
|
||||
Assert.state(resource != null, "Unable to find resource " + path);
|
||||
return resource.openStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
long getLastModified() {
|
||||
return 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFile() {
|
||||
throw new UnsupportedOperationException("Unable to access jar mode library file");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,6 +17,9 @@
|
||||
package org.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Encapsulates information about a single library that may be packed into the archive.
|
||||
@@ -85,6 +88,15 @@ public class Library {
|
||||
return this.file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a stream that provides the content of the source file.
|
||||
* @return the file content
|
||||
* @throws IOException on error
|
||||
*/
|
||||
InputStream openStream() throws IOException {
|
||||
return new FileInputStream(this.file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the scope of the library.
|
||||
* @return the scope
|
||||
@@ -102,4 +114,8 @@ public class Library {
|
||||
return this.unpackRequired;
|
||||
}
|
||||
|
||||
long getLastModified() {
|
||||
return this.file.lastModified();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package org.springframework.boot.loader.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
@@ -81,6 +80,8 @@ public abstract class Packager {
|
||||
|
||||
private Layers layers = Layers.IMPLICIT;
|
||||
|
||||
private boolean includeRelevantJarModeJars = true;
|
||||
|
||||
/**
|
||||
* Create a new {@link Packager} instance.
|
||||
* @param source the source JAR file to package
|
||||
@@ -141,6 +142,14 @@ public abstract class Packager {
|
||||
this.layers = layers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets if jarmode jars relevant for the packaging should be automatcially included.
|
||||
* @param includeRelevantJarModeJars if relevant jars are included
|
||||
*/
|
||||
public void setIncludeRelevantJarModeJars(boolean includeRelevantJarModeJars) {
|
||||
this.includeRelevantJarModeJars = includeRelevantJarModeJars;
|
||||
}
|
||||
|
||||
protected final boolean isAlreadyPackaged() throws IOException {
|
||||
return isAlreadyPackaged(this.source);
|
||||
}
|
||||
@@ -191,9 +200,9 @@ public abstract class Packager {
|
||||
return EntryTransformer.NONE;
|
||||
}
|
||||
|
||||
private boolean isZip(File file) {
|
||||
private boolean isZip(InputStreamSupplier supplier) {
|
||||
try {
|
||||
try (FileInputStream inputStream = new FileInputStream(file)) {
|
||||
try (InputStream inputStream = supplier.openStream()) {
|
||||
return isZip(inputStream);
|
||||
}
|
||||
}
|
||||
@@ -430,14 +439,24 @@ public abstract class Packager {
|
||||
|
||||
WritableLibraries(Libraries libraries) throws IOException {
|
||||
libraries.doWithLibraries((library) -> {
|
||||
if (isZip(library.getFile())) {
|
||||
String location = getLocation(library);
|
||||
if (location != null) {
|
||||
Library existing = this.libraries.putIfAbsent(location + library.getName(), library);
|
||||
Assert.state(existing == null, "Duplicate library " + library.getName());
|
||||
}
|
||||
if (isZip(library::openStream)) {
|
||||
addLibrary(library);
|
||||
}
|
||||
});
|
||||
if (Packager.this.includeRelevantJarModeJars) {
|
||||
if (getLayout() instanceof LayeredLayout) {
|
||||
addLibrary(JarModeLibrary.LAYER_TOOLS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addLibrary(Library library) {
|
||||
String location = getLocation(library);
|
||||
if (location != null) {
|
||||
String path = location + library.getName();
|
||||
Library existing = this.libraries.putIfAbsent(path, library);
|
||||
Assert.state(existing == null, "Duplicate library " + library.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private String getLocation(Library library) {
|
||||
@@ -461,17 +480,19 @@ public abstract class Packager {
|
||||
public String sha1Hash(String name) throws IOException {
|
||||
Library library = this.libraries.get(name);
|
||||
Assert.notNull(library, "No library found for entry name '" + name + "'");
|
||||
return FileUtils.sha1Hash(library.getFile());
|
||||
return Digest.sha1(library::openStream);
|
||||
}
|
||||
|
||||
private void write(AbstractJarWriter writer) throws IOException {
|
||||
for (Entry<String, Library> entry : this.libraries.entrySet()) {
|
||||
writer.writeNestedLibrary(entry.getKey().substring(0, entry.getKey().lastIndexOf('/') + 1),
|
||||
entry.getValue());
|
||||
String path = entry.getKey();
|
||||
Library library = entry.getValue();
|
||||
String location = path.substring(0, path.lastIndexOf('/') + 1);
|
||||
writer.writeNestedLibrary(location, library);
|
||||
}
|
||||
if (getLayout() instanceof RepackagingLayout) {
|
||||
String location = ((RepackagingLayout) getLayout()).getClasspathIndexFileLocation();
|
||||
writer.writeIndexFile(location, new ArrayList<>(this.libraries.keySet()));
|
||||
writer.writeIndexFile(location, this.libraries.keySet());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -254,6 +254,7 @@ abstract class AbstractPackagerTests<P extends Packager> {
|
||||
layers.addLibrary(libJarFile2, "0002");
|
||||
layers.addLibrary(libJarFile3, "0003");
|
||||
packager.setLayers(layers);
|
||||
packager.setIncludeRelevantJarModeJars(false);
|
||||
packager.setLayout(new Layouts.LayeredJar());
|
||||
execute(packager, (callback) -> {
|
||||
callback.library(new Library(libJarFile1, LibraryScope.COMPILE));
|
||||
@@ -277,6 +278,23 @@ abstract class AbstractPackagerTests<P extends Packager> {
|
||||
assertThat(Arrays.asList(layersIndex.split("\\n"))).containsExactly(expectedLayers.toArray(new String[0]));
|
||||
}
|
||||
|
||||
@Test
|
||||
void layeredLayoutAddJarModeJar() throws Exception {
|
||||
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
|
||||
P packager = createPackager();
|
||||
TestLayers layers = new TestLayers();
|
||||
packager.setLayers(layers);
|
||||
packager.setLayout(new Layouts.LayeredJar());
|
||||
execute(packager, Libraries.NONE);
|
||||
assertThat(hasPackagedEntry("BOOT-INF/classpath.idx")).isTrue();
|
||||
String classpathIndex = getPackagedEntryContent("BOOT-INF/classpath.idx");
|
||||
assertThat(Arrays.asList(classpathIndex.split("\\n")))
|
||||
.containsExactly("BOOT-INF/layers/default/lib/spring-boot-jarmode-layertools.jar");
|
||||
assertThat(hasPackagedEntry("BOOT-INF/layers.idx")).isTrue();
|
||||
String layersIndex = getPackagedEntryContent("BOOT-INF/layers.idx");
|
||||
assertThat(Arrays.asList(layersIndex.split("\\n"))).containsExactly("default");
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateLibraries() throws Exception {
|
||||
TestJarFile libJar = new TestJarFile(this.tempDir);
|
||||
|
||||
Reference in New Issue
Block a user