From d015714cba4e696471a0ec8f12c9a2ad6a0a689f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 21 Mar 2017 16:16:30 +0000 Subject: [PATCH] Add support for reproducible archives to BootJar and BootWar Closes gh-8391 --- .../gradle/bundling/BootArchiveSupport.java | 34 ++++++++++++++- .../gradle/bundling/BootZipCopyAction.java | 31 +++++++++++--- .../bundling/AbstractBootArchiveTests.java | 42 +++++++++++++++++++ 3 files changed, 100 insertions(+), 7 deletions(-) diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootArchiveSupport.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootArchiveSupport.java index 7f24622081..2c40538c0b 100644 --- a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootArchiveSupport.java +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootArchiveSupport.java @@ -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 detailsByPath = new TreeMap<>(); + stream.process((details) -> { + detailsByPath.put(details.getRelativePath(), details); + }); + detailsByPath.values().stream().forEach(action::processFile); + }); + } + + } + } diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootZipCopyAction.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootZipCopyAction.java index 652c12ad6d..b29e224c86 100644 --- a/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootZipCopyAction.java +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/bundling/BootZipCopyAction.java @@ -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 requiresUnpack; private final LaunchScriptConfiguration launchScript; private final Set storedPathPrefixes; - BootZipCopyAction(File output, Spec requiresUnpack, - LaunchScriptConfiguration launchScript, Set storedPathPrefixes) { + BootZipCopyAction(File output, boolean preserveFileTimestamps, + Spec requiresUnpack, LaunchScriptConfiguration launchScript, + Set 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 requiresUnpack; private final Set storedPathPrefixes; private ZipStreamAction(ZipOutputStream zipStream, File output, - Spec requiresUnpack, Set storedPathPrefixes) { + boolean preserveFileTimestamps, Spec requiresUnpack, + Set 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; + } + } /** diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/AbstractBootArchiveTests.java b/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/AbstractBootArchiveTests.java index 000691d54d..6a1c2cc7fd 100644 --- a/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/AbstractBootArchiveTests.java +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/bundling/AbstractBootArchiveTests.java @@ -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 { } } + @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 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 textFiles = new ArrayList<>(); + try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + Enumeration 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");