Commit b5b4a02c authored by Phillip Webb's avatar Phillip Webb

Automatically add jarmode jars when packaging

Update the `Packager` to automatically add the layertools jarmode jar
when producing a layered jar.

Closes gh-19865
parent 2b83edeb
......@@ -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);
}
}
......
/*
* 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));
}
}
/*
* 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);
}
}
/*
* 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);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment