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" ...@@ -11,6 +11,7 @@ def generatedResources = "${buildDir}/generated-resources/main"
configurations { configurations {
loader loader
jarmode
} }
dependencies { dependencies {
...@@ -22,6 +23,8 @@ dependencies { ...@@ -22,6 +23,8 @@ dependencies {
loader(project(":spring-boot-project:spring-boot-tools:spring-boot-loader")) 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.assertj:assertj-core")
testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.mockito:mockito-core") testImplementation("org.mockito:mockito-core")
...@@ -45,6 +48,18 @@ task reproducibleLoaderJar(type: Jar) { ...@@ -45,6 +48,18 @@ task reproducibleLoaderJar(type: Jar) {
destinationDirectory = file("${generatedResources}/META-INF/loader") 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 { processResources {
dependsOn reproducibleLoaderJar dependsOn reproducibleLoaderJar
dependsOn reproducibleJarModeLayerToolsJar
} }
...@@ -20,17 +20,15 @@ import java.io.BufferedInputStream; ...@@ -20,17 +20,15 @@ import java.io.BufferedInputStream;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarFile; import java.util.jar.JarFile;
...@@ -127,17 +125,16 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter { ...@@ -127,17 +125,16 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
/** /**
* Write a nested library. * Write a nested library.
* @param destination the destination of the library * @param location the destination of the library
* @param library the library * @param library the library
* @throws IOException if the write fails * @throws IOException if the write fails
*/ */
public void writeNestedLibrary(String destination, Library library) throws IOException { public void writeNestedLibrary(String location, Library library) throws IOException {
File file = library.getFile(); JarArchiveEntry entry = new JarArchiveEntry(location + library.getName());
JarArchiveEntry entry = new JarArchiveEntry(destination + library.getName()); entry.setTime(getNestedLibraryTime(library));
entry.setTime(getNestedLibraryTime(file)); new CrcAndSize(library::openStream).setupStoredEntry(entry);
new CrcAndSize(file).setupStoredEntry(entry); try (InputStream inputStream = library.openStream()) {
try (FileInputStream input = new FileInputStream(file)) { writeEntry(entry, new InputStreamEntryWriter(inputStream), new LibraryUnpackHandler(library));
writeEntry(entry, new InputStreamEntryWriter(input), new LibraryUnpackHandler(library));
} }
} }
...@@ -148,7 +145,7 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter { ...@@ -148,7 +145,7 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
* @throws IOException if the write fails * @throws IOException if the write fails
* @since 2.3.0 * @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) { if (location != null) {
JarArchiveEntry entry = new JarArchiveEntry(location); JarArchiveEntry entry = new JarArchiveEntry(location);
writeEntry(entry, (outputStream) -> { writeEntry(entry, (outputStream) -> {
...@@ -163,22 +160,22 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter { ...@@ -163,22 +160,22 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
} }
} }
private long getNestedLibraryTime(File file) { private long getNestedLibraryTime(Library library) {
try { try {
try (JarFile jarFile = new JarFile(file)) { try (JarInputStream jarStream = new JarInputStream(library.openStream())) {
Enumeration<JarEntry> entries = jarFile.entries(); JarEntry entry = jarStream.getNextJarEntry();
while (entries.hasMoreElements()) { while (entry != null) {
JarEntry entry = entries.nextElement();
if (!entry.isDirectory()) { if (!entry.isDirectory()) {
return entry.getTime(); return entry.getTime();
} }
entry = jarStream.getNextJarEntry();
} }
} }
} }
catch (Exception ex) { 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 { ...@@ -292,8 +289,8 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
private long size; private long size;
CrcAndSize(File file) throws IOException { CrcAndSize(InputStreamSupplier supplier) throws IOException {
try (FileInputStream inputStream = new FileInputStream(file)) { try (InputStream inputStream = supplier.openStream()) {
load(inputStream); load(inputStream);
} }
} }
...@@ -380,7 +377,7 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter { ...@@ -380,7 +377,7 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
@Override @Override
public String sha1Hash(String name) throws IOException { 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 @@ ...@@ -17,11 +17,7 @@
package org.springframework.boot.loader.tools; package org.springframework.boot.loader.tools;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; 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. * Utilities for manipulating files and directories in Spring Boot tooling.
...@@ -62,27 +58,7 @@ public abstract class FileUtils { ...@@ -62,27 +58,7 @@ public abstract class FileUtils {
* @throws IOException if the file cannot be read * @throws IOException if the file cannot be read
*/ */
public static String sha1Hash(File file) throws IOException { public static String sha1Hash(File file) throws IOException {
try { return Digest.sha1(InputStreamSupplier.forFile(file));
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();
} }
} }
/*
* 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 @@ ...@@ -17,6 +17,9 @@
package org.springframework.boot.loader.tools; package org.springframework.boot.loader.tools;
import java.io.File; 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. * Encapsulates information about a single library that may be packed into the archive.
...@@ -85,6 +88,15 @@ public class Library { ...@@ -85,6 +88,15 @@ public class Library {
return this.file; 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 of the library.
* @return the scope * @return the scope
...@@ -102,4 +114,8 @@ public class Library { ...@@ -102,4 +114,8 @@ public class Library {
return this.unpackRequired; return this.unpackRequired;
} }
long getLastModified() {
return this.file.lastModified();
}
} }
...@@ -17,7 +17,6 @@ ...@@ -17,7 +17,6 @@
package org.springframework.boot.loader.tools; package org.springframework.boot.loader.tools;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -81,6 +80,8 @@ public abstract class Packager { ...@@ -81,6 +80,8 @@ public abstract class Packager {
private Layers layers = Layers.IMPLICIT; private Layers layers = Layers.IMPLICIT;
private boolean includeRelevantJarModeJars = true;
/** /**
* Create a new {@link Packager} instance. * Create a new {@link Packager} instance.
* @param source the source JAR file to package * @param source the source JAR file to package
...@@ -141,6 +142,14 @@ public abstract class Packager { ...@@ -141,6 +142,14 @@ public abstract class Packager {
this.layers = layers; 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 { protected final boolean isAlreadyPackaged() throws IOException {
return isAlreadyPackaged(this.source); return isAlreadyPackaged(this.source);
} }
...@@ -191,9 +200,9 @@ public abstract class Packager { ...@@ -191,9 +200,9 @@ public abstract class Packager {
return EntryTransformer.NONE; return EntryTransformer.NONE;
} }
private boolean isZip(File file) { private boolean isZip(InputStreamSupplier supplier) {
try { try {
try (FileInputStream inputStream = new FileInputStream(file)) { try (InputStream inputStream = supplier.openStream()) {
return isZip(inputStream); return isZip(inputStream);
} }
} }
...@@ -430,14 +439,24 @@ public abstract class Packager { ...@@ -430,14 +439,24 @@ public abstract class Packager {
WritableLibraries(Libraries libraries) throws IOException { WritableLibraries(Libraries libraries) throws IOException {
libraries.doWithLibraries((library) -> { libraries.doWithLibraries((library) -> {
if (isZip(library.getFile())) { if (isZip(library::openStream)) {
String location = getLocation(library); addLibrary(library);
if (location != null) {
Library existing = this.libraries.putIfAbsent(location + library.getName(), library);
Assert.state(existing == null, "Duplicate library " + library.getName());
}
} }
}); });
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) { private String getLocation(Library library) {
...@@ -461,17 +480,19 @@ public abstract class Packager { ...@@ -461,17 +480,19 @@ public abstract class Packager {
public String sha1Hash(String name) throws IOException { public String sha1Hash(String name) throws IOException {
Library library = this.libraries.get(name); Library library = this.libraries.get(name);
Assert.notNull(library, "No library found for entry name '" + 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 { private void write(AbstractJarWriter writer) throws IOException {
for (Entry<String, Library> entry : this.libraries.entrySet()) { for (Entry<String, Library> entry : this.libraries.entrySet()) {
writer.writeNestedLibrary(entry.getKey().substring(0, entry.getKey().lastIndexOf('/') + 1), String path = entry.getKey();
entry.getValue()); Library library = entry.getValue();
String location = path.substring(0, path.lastIndexOf('/') + 1);
writer.writeNestedLibrary(location, library);
} }
if (getLayout() instanceof RepackagingLayout) { if (getLayout() instanceof RepackagingLayout) {
String location = ((RepackagingLayout) getLayout()).getClasspathIndexFileLocation(); 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> { ...@@ -254,6 +254,7 @@ abstract class AbstractPackagerTests<P extends Packager> {
layers.addLibrary(libJarFile2, "0002"); layers.addLibrary(libJarFile2, "0002");
layers.addLibrary(libJarFile3, "0003"); layers.addLibrary(libJarFile3, "0003");
packager.setLayers(layers); packager.setLayers(layers);
packager.setIncludeRelevantJarModeJars(false);
packager.setLayout(new Layouts.LayeredJar()); packager.setLayout(new Layouts.LayeredJar());
execute(packager, (callback) -> { execute(packager, (callback) -> {
callback.library(new Library(libJarFile1, LibraryScope.COMPILE)); callback.library(new Library(libJarFile1, LibraryScope.COMPILE));
...@@ -277,6 +278,23 @@ abstract class AbstractPackagerTests<P extends Packager> { ...@@ -277,6 +278,23 @@ abstract class AbstractPackagerTests<P extends Packager> {
assertThat(Arrays.asList(layersIndex.split("\\n"))).containsExactly(expectedLayers.toArray(new String[0])); 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 @Test
void duplicateLibraries() throws Exception { void duplicateLibraries() throws Exception {
TestJarFile libJar = new TestJarFile(this.tempDir); 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