Add support for reproducible archives to BootJar and BootWar

Closes gh-8391
This commit is contained in:
Andy Wilkinson
2017-03-21 16:16:30 +00:00
parent bc543ef08a
commit d015714cba
3 changed files with 100 additions and 7 deletions

View File

@@ -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);
});
}
}
}

View File

@@ -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;
}
}
/**

View File

@@ -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");