diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java index 75987c35029..b37c5b003e4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java @@ -21,7 +21,6 @@ import java.io.IOException; import java.nio.file.attribute.FileTime; import java.util.jar.JarFile; -import org.springframework.boot.loader.tools.Layouts.War; import org.springframework.util.Assert; /** @@ -102,9 +101,6 @@ public class Repackager extends Packager { throws IOException { Assert.isTrue(destination != null && !destination.isDirectory(), "Invalid destination"); Layout layout = getLayout(); // get layout early - if (lastModifiedTime != null && layout instanceof War) { - throw new IllegalStateException("Reproducible repackaging is not supported with war packaging"); - } destination = destination.getAbsoluteFile(); File source = getSource(); if (isAlreadyPackaged() && source.equals(destination)) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java index be81e135663..f8371022368 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java @@ -17,18 +17,20 @@ package org.springframework.boot.maven; import java.io.File; -import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import java.util.jar.JarFile; +import java.util.stream.Collectors; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.loader.tools.FileUtils; import org.springframework.boot.loader.tools.JarModeLibrary; -import org.springframework.util.FileCopyUtils; +import org.springframework.util.FileSystemUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -78,17 +80,34 @@ class WarIntegrationTests extends AbstractArchiveIntegrationTests { } @TestTemplate - void whenWarIsRepackagedWithOutputTimestampTheBuildFailsAsItIsNotSupported(MavenBuild mavenBuild) + void whenWarIsRepackagedWithOutputTimestampConfiguredThenWarIsReproducible(MavenBuild mavenBuild) throws InterruptedException { - mavenBuild.project("war-output-timestamp").executeAndFail((project) -> { - try { - String log = FileCopyUtils.copyToString(new FileReader(new File(project, "target/build.log"))); - assertThat(log).contains("Reproducible repackaging is not supported with war packaging"); + String firstHash = buildWarWithOutputTimestamp(mavenBuild); + Thread.sleep(1500); + String secondHash = buildWarWithOutputTimestamp(mavenBuild); + assertThat(firstHash).isEqualTo(secondHash); + } + + private String buildWarWithOutputTimestamp(MavenBuild mavenBuild) { + AtomicReference warHash = new AtomicReference<>(); + mavenBuild.project("war-output-timestamp").execute((project) -> { + File repackaged = new File(project, "target/war-output-timestamp-0.0.1.BUILD-SNAPSHOT.war"); + assertThat(repackaged).isFile(); + assertThat(repackaged.lastModified()).isEqualTo(1584352800000L); + try (JarFile jar = new JarFile(repackaged)) { + List unreproducibleEntries = jar.stream() + .filter((entry) -> entry.getLastModifiedTime().toMillis() != 1584352800000L) + .map((entry) -> entry.getName() + ": " + entry.getLastModifiedTime()) + .collect(Collectors.toList()); + assertThat(unreproducibleEntries).isEmpty(); + warHash.set(FileUtils.sha1Hash(repackaged)); + FileSystemUtils.deleteRecursively(project); } - catch (Exception ex) { + catch (IOException ex) { throw new RuntimeException(ex); } }); + return warHash.get(); } @TestTemplate diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java index 8d78b0f5f81..94c406f6505 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java @@ -148,7 +148,7 @@ public class RepackageMojo extends AbstractPackagerMojo { /** * Timestamp for reproducible output archive entries, either formatted as ISO 8601 * (yyyy-MM-dd'T'HH:mm:ssXXX) or an {@code int} representing seconds - * since the epoch. Not supported with war packaging. + * since the epoch. * @since 2.3.0 */ @Parameter(defaultValue = "${project.build.outputTimestamp}")