Support Maven's outputTimestamp when repackaging jars and wars

Closes gh-20176
This commit is contained in:
Andy Wilkinson
2020-03-16 16:59:34 +00:00
parent df8c25e213
commit ca202ad59f
11 changed files with 319 additions and 6 deletions

View File

@@ -16,10 +16,18 @@
package org.springframework.boot.maven;
import java.io.File;
import java.io.IOException;
import java.util.List;
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.util.FileSystemUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
@@ -316,4 +324,35 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests {
});
}
@TestTemplate
void whenJarIsRepackagedWithOutputTimestampConfiguredThenJarIsReproducible(MavenBuild mavenBuild)
throws InterruptedException {
String firstHash = buildJarWithOutputTimestamp(mavenBuild);
Thread.sleep(1500);
String secondHash = buildJarWithOutputTimestamp(mavenBuild);
assertThat(firstHash).isEqualTo(secondHash);
}
private String buildJarWithOutputTimestamp(MavenBuild mavenBuild) {
AtomicReference<String> jarHash = new AtomicReference<>();
mavenBuild.project("jar-output-timestamp").execute((project) -> {
File repackaged = new File(project, "target/jar-output-timestamp-0.0.1.BUILD-SNAPSHOT.jar");
assertThat(repackaged).isFile();
assertThat(repackaged.lastModified()).isEqualTo(1584352800000L);
try (JarFile jar = new JarFile(repackaged)) {
List<String> unreproducibleEntries = jar.stream()
.filter((entry) -> entry.getLastModifiedTime().toMillis() != 1584352800000L)
.map((entry) -> entry.getName() + ": " + entry.getLastModifiedTime())
.collect(Collectors.toList());
assertThat(unreproducibleEntries).isEmpty();
jarHash.set(FileUtils.sha1Hash(repackaged));
FileSystemUtils.deleteRecursively(project);
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
});
return jarHash.get();
}
}

View File

@@ -17,10 +17,18 @@
package org.springframework.boot.maven;
import java.io.File;
import java.io.IOException;
import java.util.List;
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.util.FileSystemUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
@@ -63,4 +71,35 @@ class WarIntegrationTests extends AbstractArchiveIntegrationTests {
.hasEntryWithNameStartingWith("WEB-INF/lib/spring-jcl-"));
}
@TestTemplate
void whenWarIsRepackagedWithOutputTimestampConfiguredThenWarIsReproducible(MavenBuild mavenBuild)
throws InterruptedException {
String firstHash = buildWarWithOutputTimestamp(mavenBuild);
Thread.sleep(1500);
String secondHash = buildWarWithOutputTimestamp(mavenBuild);
assertThat(firstHash).isEqualTo(secondHash);
}
private String buildWarWithOutputTimestamp(MavenBuild mavenBuild) {
AtomicReference<String> 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<String> 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 (IOException ex) {
throw new RuntimeException(ex);
}
});
return warHash.get();
}
}

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-output-timestamp</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.build.outputTimestamp>2020-03-16T02:00:00-08:00</project.build.outputTimestamp>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>@maven-jar-plugin.version@</version>
<configuration>
<archive>
<manifest>
<mainClass>some.random.Main</mainClass>
</manifest>
<manifestEntries>
<Not-Used>Foo</Not-Used>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>@spring-framework.version@</version>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>@jakarta-servlet.version@</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.test;
public class SampleApplication {
public static void main(String[] args) {
}
}

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>war-output-timestamp</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.build.outputTimestamp>2020-03-16T02:00:00-08:00</project.build.outputTimestamp>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>@maven-war-plugin.version@</version>
<configuration>
<archive>
<manifestEntries>
<Not-Used>Foo</Not-Used>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>@spring-framework.version@</version>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>@jakarta-servlet.version@</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.test;
public class SampleApplication {
public static void main(String[] args) {
}
}

View File

@@ -18,8 +18,11 @@ package org.springframework.boot.maven;
import java.io.File;
import java.io.IOException;
import java.nio.file.attribute.FileTime;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import org.apache.maven.artifact.Artifact;
@@ -140,6 +143,15 @@ public class RepackageMojo extends AbstractPackagerMojo {
@Parameter
private Properties embeddedLaunchScriptProperties;
/**
* Timestamp for reproducible output archive entries, either formatted as ISO 8601
* (<code>yyyy-MM-dd'T'HH:mm:ssXXX</code>) or an {@code int} representing seconds
* since the epoch.
* @since 2.3.0
*/
@Parameter(defaultValue = "${project.build.outputTimestamp}")
private String outputTimestamp;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (this.project.getPackaging().equals("pom")) {
@@ -160,7 +172,7 @@ public class RepackageMojo extends AbstractPackagerMojo {
Libraries libraries = getLibraries(this.requiresUnpack);
try {
LaunchScript launchScript = getLaunchScript();
repackager.repackage(target, libraries, launchScript);
repackager.repackage(target, libraries, launchScript, parseOutputTimestamp());
}
catch (IOException ex) {
throw new MojoExecutionException(ex.getMessage(), ex);
@@ -168,6 +180,22 @@ public class RepackageMojo extends AbstractPackagerMojo {
updateArtifact(source, target, repackager.getBackupFile());
}
private FileTime parseOutputTimestamp() {
// Maven ignore a single-character timestamp as it is "useful to override a full
// value during pom inheritance"
if (this.outputTimestamp == null || this.outputTimestamp.length() < 2) {
return null;
}
long epochSeconds;
try {
epochSeconds = Long.parseLong(this.outputTimestamp);
}
catch (NumberFormatException ex) {
epochSeconds = OffsetDateTime.parse(this.outputTimestamp).toInstant().getEpochSecond();
}
return FileTime.from(epochSeconds, TimeUnit.SECONDS);
}
/**
* Return the source {@link Artifact} to repackage. If a classifier is specified and
* an artifact with that classifier exists, it is used. Otherwise, the main artifact