diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java index 5f93420c680..8d48fc79c7a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java @@ -20,6 +20,8 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.util.Calendar; +import java.util.GregorianCalendar; import java.util.HashSet; import java.util.Set; import java.util.function.Function; @@ -39,7 +41,6 @@ import org.gradle.api.internal.file.copy.FileCopyDetailsInternal; import org.gradle.api.specs.Spec; import org.gradle.api.specs.Specs; 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; @@ -52,6 +53,9 @@ import org.springframework.boot.loader.tools.FileUtils; */ class BootZipCopyAction implements CopyAction { + private static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = new GregorianCalendar(1980, + Calendar.FEBRUARY, 1, 0, 0, 0).getTimeInMillis(); + private final File output; private final boolean preserveFileTimestamps; @@ -158,20 +162,14 @@ class BootZipCopyAction implements CopyAction { private void writeDirectory(ZipArchiveEntry entry, ZipArchiveOutputStream out) throws IOException { - if (!this.preserveFileTimestamps) { - entry.setTime(GUtil.CONSTANT_TIME_FOR_ZIP_ENTRIES); - } - entry.setUnixMode(UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM); + prepareEntry(entry, UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM); out.putArchiveEntry(entry); out.closeArchiveEntry(); } private void writeClass(ZipArchiveEntry entry, ZipInputStream in, ZipArchiveOutputStream out) throws IOException { - if (!this.preserveFileTimestamps) { - entry.setTime(GUtil.CONSTANT_TIME_FOR_ZIP_ENTRIES); - } - entry.setUnixMode(UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM); + prepareEntry(entry, UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM); out.putArchiveEntry(entry); byte[] buffer = new byte[4096]; int read; @@ -181,6 +179,13 @@ class BootZipCopyAction implements CopyAction { out.closeArchiveEntry(); } + private void prepareEntry(ZipArchiveEntry entry, int unixMode) { + if (!this.preserveFileTimestamps) { + entry.setTime(CONSTANT_TIME_FOR_ZIP_ENTRIES); + } + entry.setUnixMode(unixMode); + } + private void writeLaunchScriptIfNecessary(FileOutputStream fileStream) { try { if (this.launchScript != null) { @@ -280,7 +285,7 @@ class BootZipCopyAction implements CopyAction { private long getTime(FileCopyDetails details) { return this.preserveFileTimestamps ? details.getLastModified() - : GUtil.CONSTANT_TIME_FOR_ZIP_ENTRIES; + : CONSTANT_TIME_FOR_ZIP_ENTRIES; } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java index bbb67aea71a..419e03c66a3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java @@ -29,6 +29,7 @@ import org.junit.runner.RunWith; import org.springframework.boot.gradle.junit.GradleCompatibilitySuite; import org.springframework.boot.gradle.testkit.GradleBuild; +import org.springframework.boot.loader.tools.FileUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -56,6 +57,21 @@ public abstract class AbstractBootArchiveIntegrationTests { .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); } + @Test + public void reproducibleArchive() throws InvalidRunnerConfigurationException, + UnexpectedBuildFailure, IOException, InterruptedException { + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName) + .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + File jar = new File(this.gradleBuild.getProjectDir(), "build/libs") + .listFiles()[0]; + String firstHash = FileUtils.sha1Hash(jar); + Thread.sleep(1500); + assertThat(this.gradleBuild.build("clean", this.taskName) + .task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + String secondHash = FileUtils.sha1Hash(jar); + assertThat(firstHash).isEqualTo(secondHash); + } + @Test public void upToDateWhenBuiltTwice() throws InvalidRunnerConfigurationException, UnexpectedBuildFailure, IOException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-reproducibleArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-reproducibleArchive.gradle new file mode 100644 index 00000000000..8885cfdb1ec --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-reproducibleArchive.gradle @@ -0,0 +1,14 @@ +buildscript { + dependencies { + classpath files(pluginClasspath.split(',')) + } +} + +apply plugin: 'java' +apply plugin: 'org.springframework.boot' + +bootJar { + mainClassName = 'com.example.Application' + preserveFileTimestamps = false + reproducibleFileOrder = true +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-reproducibleArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-reproducibleArchive.gradle new file mode 100644 index 00000000000..ea3f6e0da70 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-reproducibleArchive.gradle @@ -0,0 +1,14 @@ +buildscript { + dependencies { + classpath files(pluginClasspath.split(',')) + } +} + +apply plugin: 'war' +apply plugin: 'org.springframework.boot' + +bootWar { + mainClassName = 'com.example.Application' + preserveFileTimestamps = false + reproducibleFileOrder = true +}