Add support for reproducible archives to BootJar and BootWar
Closes gh-8391
This commit is contained in:
@@ -18,13 +18,19 @@ package org.springframework.boot.gradle.bundling;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.gradle.api.file.FileTreeElement;
|
||||
import org.gradle.api.file.RelativePath;
|
||||
import org.gradle.api.internal.file.copy.CopyAction;
|
||||
import org.gradle.api.internal.file.copy.CopyActionProcessingStream;
|
||||
import org.gradle.api.internal.file.copy.FileCopyDetailsInternal;
|
||||
import org.gradle.api.java.archives.Attributes;
|
||||
import org.gradle.api.specs.Spec;
|
||||
import org.gradle.api.specs.Specs;
|
||||
import org.gradle.api.tasks.WorkResult;
|
||||
import org.gradle.api.tasks.bundling.Jar;
|
||||
import org.gradle.api.tasks.util.PatternSet;
|
||||
|
||||
@@ -55,8 +61,13 @@ class BootArchiveSupport {
|
||||
}
|
||||
|
||||
CopyAction createCopyAction(Jar jar) {
|
||||
return new BootZipCopyAction(jar.getArchivePath(), this::requiresUnpacking,
|
||||
CopyAction copyAction = new BootZipCopyAction(jar.getArchivePath(),
|
||||
jar.isPreserveFileTimestamps(), this::requiresUnpacking,
|
||||
this.launchScript, this.storedPathPrefixes);
|
||||
if (!jar.isReproducibleFileOrder()) {
|
||||
return copyAction;
|
||||
}
|
||||
return new ReproducibleOrderingCopyAction(copyAction);
|
||||
}
|
||||
|
||||
private boolean requiresUnpacking(FileTreeElement fileTreeElement) {
|
||||
@@ -87,4 +98,25 @@ class BootArchiveSupport {
|
||||
this.requiresUnpack.include(spec);
|
||||
}
|
||||
|
||||
private static final class ReproducibleOrderingCopyAction implements CopyAction {
|
||||
|
||||
private final CopyAction delegate;
|
||||
|
||||
private ReproducibleOrderingCopyAction(CopyAction delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorkResult execute(CopyActionProcessingStream stream) {
|
||||
return this.delegate.execute((action) -> {
|
||||
Map<RelativePath, FileCopyDetailsInternal> detailsByPath = new TreeMap<>();
|
||||
stream.process((details) -> {
|
||||
detailsByPath.put(details.getRelativePath(), details);
|
||||
});
|
||||
detailsByPath.values().stream().forEach(action::processFile);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.file.FileCopyDetails;
|
||||
import org.gradle.api.file.FileTreeElement;
|
||||
import org.gradle.api.internal.file.CopyActionProcessingStreamAction;
|
||||
import org.gradle.api.internal.file.copy.CopyAction;
|
||||
@@ -35,6 +36,7 @@ import org.gradle.api.internal.file.copy.CopyActionProcessingStream;
|
||||
import org.gradle.api.internal.file.copy.FileCopyDetailsInternal;
|
||||
import org.gradle.api.specs.Spec;
|
||||
import org.gradle.api.tasks.WorkResult;
|
||||
import org.gradle.util.GUtil;
|
||||
|
||||
import org.springframework.boot.loader.tools.DefaultLaunchScript;
|
||||
import org.springframework.boot.loader.tools.FileUtils;
|
||||
@@ -49,15 +51,19 @@ class BootZipCopyAction implements CopyAction {
|
||||
|
||||
private final File output;
|
||||
|
||||
private final boolean preserveFileTimestamps;
|
||||
|
||||
private final Spec<FileTreeElement> requiresUnpack;
|
||||
|
||||
private final LaunchScriptConfiguration launchScript;
|
||||
|
||||
private final Set<String> storedPathPrefixes;
|
||||
|
||||
BootZipCopyAction(File output, Spec<FileTreeElement> requiresUnpack,
|
||||
LaunchScriptConfiguration launchScript, Set<String> storedPathPrefixes) {
|
||||
BootZipCopyAction(File output, boolean preserveFileTimestamps,
|
||||
Spec<FileTreeElement> requiresUnpack, LaunchScriptConfiguration launchScript,
|
||||
Set<String> storedPathPrefixes) {
|
||||
this.output = output;
|
||||
this.preserveFileTimestamps = preserveFileTimestamps;
|
||||
this.requiresUnpack = requiresUnpack;
|
||||
this.launchScript = launchScript;
|
||||
this.storedPathPrefixes = storedPathPrefixes;
|
||||
@@ -77,7 +83,8 @@ class BootZipCopyAction implements CopyAction {
|
||||
}
|
||||
try {
|
||||
stream.process(new ZipStreamAction(zipStream, this.output,
|
||||
this.requiresUnpack, this.storedPathPrefixes));
|
||||
this.preserveFileTimestamps, this.requiresUnpack,
|
||||
this.storedPathPrefixes));
|
||||
}
|
||||
finally {
|
||||
try {
|
||||
@@ -100,6 +107,9 @@ class BootZipCopyAction implements CopyAction {
|
||||
byte[] buffer = new byte[4096];
|
||||
while ((entry = in.getNextEntry()) != null) {
|
||||
if (entry.getName().endsWith((".class"))) {
|
||||
if (!this.preserveFileTimestamps) {
|
||||
entry.setTime(GUtil.CONSTANT_TIME_FOR_ZIP_ENTRIES);
|
||||
}
|
||||
out.putNextEntry(entry);
|
||||
int read;
|
||||
while ((read = in.read(buffer)) > 0) {
|
||||
@@ -134,14 +144,18 @@ class BootZipCopyAction implements CopyAction {
|
||||
|
||||
private final File output;
|
||||
|
||||
private final boolean preserveFileTimestamps;
|
||||
|
||||
private final Spec<FileTreeElement> requiresUnpack;
|
||||
|
||||
private final Set<String> storedPathPrefixes;
|
||||
|
||||
private ZipStreamAction(ZipOutputStream zipStream, File output,
|
||||
Spec<FileTreeElement> requiresUnpack, Set<String> storedPathPrefixes) {
|
||||
boolean preserveFileTimestamps, Spec<FileTreeElement> requiresUnpack,
|
||||
Set<String> storedPathPrefixes) {
|
||||
this.zipStream = zipStream;
|
||||
this.output = output;
|
||||
this.preserveFileTimestamps = preserveFileTimestamps;
|
||||
this.requiresUnpack = requiresUnpack;
|
||||
this.storedPathPrefixes = storedPathPrefixes;
|
||||
}
|
||||
@@ -165,7 +179,7 @@ class BootZipCopyAction implements CopyAction {
|
||||
private void createDirectory(FileCopyDetailsInternal details) throws IOException {
|
||||
ZipEntry archiveEntry = new ZipEntry(
|
||||
details.getRelativePath().getPathString() + '/');
|
||||
archiveEntry.setTime(details.getLastModified());
|
||||
archiveEntry.setTime(getTime(details));
|
||||
this.zipStream.putNextEntry(archiveEntry);
|
||||
this.zipStream.closeEntry();
|
||||
}
|
||||
@@ -173,7 +187,7 @@ class BootZipCopyAction implements CopyAction {
|
||||
private void createFile(FileCopyDetailsInternal details) throws IOException {
|
||||
String relativePath = details.getRelativePath().getPathString();
|
||||
ZipEntry archiveEntry = new ZipEntry(relativePath);
|
||||
archiveEntry.setTime(details.getLastModified());
|
||||
archiveEntry.setTime(getTime(details));
|
||||
this.zipStream.putNextEntry(archiveEntry);
|
||||
if (isStoredEntry(relativePath)) {
|
||||
archiveEntry.setMethod(ZipEntry.STORED);
|
||||
@@ -202,6 +216,11 @@ class BootZipCopyAction implements CopyAction {
|
||||
return false;
|
||||
}
|
||||
|
||||
private long getTime(FileCopyDetails details) {
|
||||
return this.preserveFileTimestamps ? details.getLastModified()
|
||||
: GUtil.CONSTANT_TIME_FOR_ZIP_ENTRIES;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,13 +20,18 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.tasks.bundling.AbstractArchiveTask;
|
||||
import org.gradle.api.tasks.bundling.Jar;
|
||||
import org.gradle.testfixtures.ProjectBuilder;
|
||||
import org.gradle.util.GUtil;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
@@ -220,6 +225,43 @@ public abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fileTimestampPreservationCanBeDisabled() throws IOException {
|
||||
this.task.setMainClass("com.example.Main");
|
||||
this.task.setPreserveFileTimestamps(false);
|
||||
this.task.execute();
|
||||
assertThat(this.task.getArchivePath().exists());
|
||||
try (JarFile jarFile = new JarFile(this.task.getArchivePath())) {
|
||||
Enumeration<JarEntry> entries = jarFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry entry = entries.nextElement();
|
||||
assertThat(entry.getTime())
|
||||
.isEqualTo(GUtil.CONSTANT_TIME_FOR_ZIP_ENTRIES);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reproducibleOrderingCanBeEnabled() throws IOException {
|
||||
this.task.setMainClass("com.example.Main");
|
||||
this.task.from(this.temp.newFile("bravo.txt"), this.temp.newFile("alpha.txt"),
|
||||
this.temp.newFile("charlie.txt"));
|
||||
this.task.setReproducibleFileOrder(true);
|
||||
this.task.execute();
|
||||
assertThat(this.task.getArchivePath().exists());
|
||||
List<String> textFiles = new ArrayList<>();
|
||||
try (JarFile jarFile = new JarFile(this.task.getArchivePath())) {
|
||||
Enumeration<JarEntry> entries = jarFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry entry = entries.nextElement();
|
||||
if (entry.getName().endsWith(".txt")) {
|
||||
textFiles.add(entry.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
assertThat(textFiles).containsExactly("alpha.txt", "bravo.txt", "charlie.txt");
|
||||
}
|
||||
|
||||
private T configure(T task) throws IOException {
|
||||
AbstractArchiveTask archiveTask = task;
|
||||
archiveTask.setBaseName("test");
|
||||
|
||||
Reference in New Issue
Block a user