Skip to content

Commit

Permalink
#185 Add outputTimestamp property for reproducible builds
Browse files Browse the repository at this point in the history
  • Loading branch information
aalmiray committed Mar 20, 2023
1 parent a1bd562 commit 3e2b18e
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 9 deletions.
6 changes: 6 additions & 0 deletions README.md
Expand Up @@ -212,6 +212,7 @@ the _add-module-info_ goal as follows:
<configuration>
<jvmVersion>11</jvmVersion>
<failOnWarning>false</failOnWarning>
<outputTimestamp>1980-01-01T00:00:02Z</outputTimestamp>
<module>
<moduleInfo>
<name>com.example</name>
Expand Down Expand Up @@ -242,6 +243,11 @@ The special value `base` (the default) can be used to add the descriptor to the
Putting the descriptor under `META-INF/versions` can help to increase compatibility with older libraries scanning class files that may fail when encountering the `module-info.class` file
(as chances are lower that such tool will look for class files under `META-INF/versions/...`).

The optional `outputTimestamp` element may be used to create reproducible output archive entries, either formatted as
ISO 8601 extended offset date-time (e.g. in UTC such as '2011-12-03T10:15:30Z' or with an offset '2019-10-05T20:37:42+06:00'),
or as an int representing seconds since the epoch. As an alternative you may set `${project.build.outputTimestamp}` which also
matches the user property used by other Maven plugins such as `maven-jar-plugin`.

The optional `failOnWarning` option prevents the build from failing when set to `false`. The default is to fail.

The optional `exclusions` option may be used to filter out any `compile` or `runtime` dependencies that should not be used, as it might be the case when shading internal dependencies.
Expand Down
59 changes: 58 additions & 1 deletion core/src/main/java/org/moditect/Moditect.java
Expand Up @@ -17,6 +17,12 @@

import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.NumberFormat;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;

import org.moditect.commands.AddModuleInfo;

Expand All @@ -31,7 +37,7 @@ public static void main(String[] args) throws Exception {
CliArgs cliArgs = new CliArgs();
new JCommander( cliArgs, args );

new AddModuleInfo( null, null, null, null, cliArgs.outputDirecory, cliArgs.jvmVersion, cliArgs.overwriteExistingFiles ).run();
new AddModuleInfo( null, null, null, null, cliArgs.outputDirecory, cliArgs.jvmVersion, cliArgs.overwriteExistingFiles, cliArgs.timestamp ).run();
}

@Parameters(separators = "=")
Expand Down Expand Up @@ -67,6 +73,14 @@ private static class CliArgs {
description = "Whether to overwrite existing files or not"
)
private boolean overwriteExistingFiles;

@Parameter(
names = "--timestamp",
required = false,
description = "Timestamp used when writing archive entries",
converter = InstantConverter.class
)
private Instant timestamp;
}

private static class PathConverter implements IStringConverter<Path> {
Expand All @@ -76,4 +90,47 @@ public Path convert(String value) {
return Paths.get( value );
}
}

private static class InstantConverter implements IStringConverter<Instant> {

private static final Instant DATE_MIN = Instant.parse( "1980-01-01T00:00:02Z" );
private static final Instant DATE_MAX = Instant.parse( "2099-12-31T23:59:59Z" );

@Override
public Instant convert(String value) {
if ( value == null ) {
return null;
}

// Number representing seconds since the epoch
if ( !value.isEmpty() && isNumeric( value ) ) {
return Instant.ofEpochSecond( Long.parseLong( value.trim() ) );
}

try {
// Parse the date in UTC such as '2011-12-03T10:15:30Z' or with an offset '2019-10-05T20:37:42+06:00'.
final Instant date = OffsetDateTime.parse( value )
.withOffsetSameInstant( ZoneOffset.UTC ).truncatedTo( ChronoUnit.SECONDS ).toInstant();

if ( date.isBefore( DATE_MIN ) || date.isAfter( DATE_MAX ) ) {
throw new IllegalArgumentException( "'" + date + "' is not within the valid range "
+ DATE_MIN + " to " + DATE_MAX );
}
return date;
}
catch ( DateTimeParseException pe ) {
throw new IllegalArgumentException( "Invalid project.build.outputTimestamp value '" + value + "'",
pe );
}
}

private boolean isNumeric( String str ) {
try {
Long.parseLong( str.trim() );
return true;
} catch( NumberFormatException e ) {
return false;
}
}
}
}
26 changes: 22 additions & 4 deletions core/src/main/java/org/moditect/commands/AddModuleInfo.java
Expand Up @@ -24,6 +24,8 @@
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.Attributes;
Expand All @@ -49,16 +51,17 @@ public class AddModuleInfo {
private final Path outputDirectory;
private final Integer jvmVersion;
private final boolean overwriteExistingFiles;
private final Instant timestamp;

public AddModuleInfo(String moduleInfoSource, String mainClass, String version, Path inputJar, Path outputDirectory, String jvmVersion, boolean overwriteExistingFiles) {
public AddModuleInfo(String moduleInfoSource, String mainClass, String version, Path inputJar, Path outputDirectory, String jvmVersion, boolean overwriteExistingFiles, Instant timestamp) {
this.moduleInfoSource = moduleInfoSource;
this.mainClass = mainClass;
this.version = version;
this.inputJar = inputJar;
this.outputDirectory = outputDirectory;

// #67 It'd be nice to use META-INF/services/9 by default to avoid conflicts with legacy
// classpath scanners, but this causes isses with subsequent jdeps invocations if there
// classpath scanners, but this causes issues with subsequent jdeps invocations if there
// are MR-JARs and non-MR JARs passed to it due to https://bugs.openjdk.java.net/browse/JDK-8207162
if (jvmVersion == null || jvmVersion.equals(NO_JVM_VERSION)) {
this.jvmVersion = null;
Expand All @@ -75,6 +78,7 @@ public AddModuleInfo(String moduleInfoSource, String mainClass, String version,
}
}
this.overwriteExistingFiles = overwriteExistingFiles;
this.timestamp = timestamp;
}

public void run() {
Expand Down Expand Up @@ -105,14 +109,16 @@ public void run() {

Map<String, String> env = new HashMap<>();
env.put( "create", "true" );
URI uri = URI.create( "jar:" + outputJar.toUri().toString() );
URI uri = URI.create( "jar:" + outputJar.toUri() );

try (FileSystem zipfs = FileSystems.newFileSystem( uri, env ) ) {
if (jvmVersion == null) {
Files.write( zipfs.getPath( "module-info.class" ), clazz,
Path path = zipfs.getPath("module-info.class");
Files.write(path, clazz,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING );
Files.setLastModifiedTime( path, toFileTime(timestamp) );
}
else {
Path path = zipfs.getPath( "META-INF/versions", jvmVersion.toString(), "module-info.class" );
Expand All @@ -121,6 +127,13 @@ public void run() {
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING );
FileTime lastModifiedTime = toFileTime( timestamp );
// module-info.class
Files.setLastModifiedTime( path, lastModifiedTime );
// jvmVersion
Files.setLastModifiedTime( path.getParent(), lastModifiedTime );
// versions
Files.setLastModifiedTime( path.getParent().getParent(), lastModifiedTime );

Path manifestPath = zipfs.getPath( "META-INF/MANIFEST.MF" );
Manifest manifest;
Expand All @@ -136,10 +149,15 @@ public void run() {
try (OutputStream manifestOs = Files.newOutputStream( manifestPath, StandardOpenOption.TRUNCATE_EXISTING )) {
manifest.write( manifestOs );
}
Files.setLastModifiedTime( manifestPath, lastModifiedTime );
}
}
catch(IOException e) {
throw new RuntimeException( "Couldn't add module-info.class to JAR", e );
}
}

private FileTime toFileTime( Instant timestamp ) {
return FileTime.from( timestamp != null ? timestamp : Instant.now() );
}
}
6 changes: 4 additions & 2 deletions core/src/test/java/org/moditect/test/AddModuleInfoTest.java
Expand Up @@ -99,7 +99,8 @@ public void addJvmVersionModuleInfoAndRunModular() throws Exception {
Paths.get( "target", "generated-test-resources", "example.jar" ),
Paths.get( "target", "generated-test-modules" ),
"9",
false
false,
null
)
.run();

Expand Down Expand Up @@ -144,7 +145,8 @@ public void addModuleInfoAndRunModular() throws Exception {
Paths.get( "target", "generated-test-resources", "example.jar" ),
Paths.get( "target", "generated-test-modules" ),
null,
false
false,
null
)
.run();

Expand Down
Expand Up @@ -20,6 +20,11 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
Expand Down Expand Up @@ -108,6 +113,15 @@ public class AddModuleInfoMojo extends AbstractMojo {
@Parameter(property = "overwriteExistingFiles", defaultValue = "false")
private boolean overwriteExistingFiles;

/**
* Timestamp for reproducible output archive entries, either formatted as ISO 8601 extended offset date-time
* (e.g. in UTC such as '2011-12-03T10:15:30Z' or with an offset '2019-10-05T20:37:42+06:00'),
* or as an int representing seconds since the epoch
* (like <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
*/
@Parameter(defaultValue = "${project.build.outputTimestamp}")
private String outputTimestamp;

@Parameter(property = "moditect.skip", defaultValue = "false")
private boolean skip;

Expand Down Expand Up @@ -183,7 +197,8 @@ project, repoSystem, repoSession, remoteRepos, artifactResolutionHelper, jdepsEx
inputFile,
outputPath,
jvmVersion,
overwriteExistingFiles
overwriteExistingFiles,
InstantConverter.convert(outputTimestamp)
);

addModuleInfo.run();
Expand Down Expand Up @@ -220,7 +235,8 @@ project, repoSystem, repoSession, remoteRepos, artifactResolutionHelper, jdepsEx
inputJar,
outputPath,
jvmVersion,
overwriteExistingFiles
overwriteExistingFiles,
InstantConverter.convert(outputTimestamp)
);
addModuleInfo.run();

Expand Down Expand Up @@ -485,4 +501,47 @@ private String getAssignedModuleName(Map<ArtifactIdentifier, String> assignedNam

return null;
}


private static class InstantConverter {

private static final Instant DATE_MIN = Instant.parse( "1980-01-01T00:00:02Z" );
private static final Instant DATE_MAX = Instant.parse( "2099-12-31T23:59:59Z" );

public static Instant convert(String value) {
if ( value == null ) {
return null;
}

// Number representing seconds since the epoch
if ( !value.isEmpty() && isNumeric( value ) ) {
return Instant.ofEpochSecond( Long.parseLong( value.trim() ) );
}

try {
// Parse the date in UTC such as '2011-12-03T10:15:30Z' or with an offset '2019-10-05T20:37:42+06:00'.
final Instant date = OffsetDateTime.parse( value )
.withOffsetSameInstant( ZoneOffset.UTC ).truncatedTo( ChronoUnit.SECONDS ).toInstant();

if ( date.isBefore( DATE_MIN ) || date.isAfter( DATE_MAX ) ) {
throw new IllegalArgumentException( "'" + date + "' is not within the valid range "
+ DATE_MIN + " to " + DATE_MAX );
}
return date;
}
catch ( DateTimeParseException pe ) {
throw new IllegalArgumentException( "Invalid project.build.outputTimestamp value '" + value + "'",
pe );
}
}

private static boolean isNumeric( String str ) {
try {
Long.parseLong( str.trim() );
return true;
} catch( NumberFormatException e ) {
return false;
}
}
}
}

0 comments on commit 3e2b18e

Please sign in to comment.