Skip to content

fix: Reproducible Builds not working when using modular jar #205

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/main/java/org/codehaus/plexus/archiver/jar/JarArchiver.java
Original file line number Diff line number Diff line change
Expand Up @@ -825,4 +825,22 @@ else if ( !name.contains( "/" ) )
}
}

/**
* Override the behavior of the Zip Archiver to match the output of the JAR tool.
*
* @param zipEntry to set the last modified time
* @param lastModified to set in the zip entry only if the {@link #getLastModifiedTime()} returns null.
*/
@Override
protected void setTime( ZipArchiveEntry zipEntry, long lastModified )
{
if ( getLastModifiedTime() != null )
{
lastModified = getLastModifiedTime().toMillis();
}

// The JAR tool does not round up, so we keep that behavior here (JDK-8277755).
zipEntry.setTime( lastModified );
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,30 @@
*/
package org.codehaus.plexus.archiver.jar;

import org.apache.commons.compress.parallel.InputStreamSupplier;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.zip.ConcurrentJarCreator;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

import org.apache.commons.compress.parallel.InputStreamSupplier;
import org.apache.commons.io.output.NullOutputStream;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.zip.ConcurrentJarCreator;
import org.codehaus.plexus.util.IOUtil;

/**
* A {@link ModularJarArchiver} implementation that uses
Expand Down Expand Up @@ -58,6 +71,8 @@ public class JarToolModularJarArchiver

private boolean moduleDescriptorFound;

private boolean hasJarDateOption;

public JarToolModularJarArchiver()
{
try
Expand Down Expand Up @@ -111,18 +126,30 @@ protected void postCreateArchive()
getLogger().debug( "Using the jar tool to " +
"update the archive to modular JAR." );

Integer result = (Integer) jarTool.getClass()
.getMethod( "run",
PrintStream.class, PrintStream.class, String[].class )
.invoke( jarTool,
System.out, System.err,
getJarToolArguments() );
final Method jarRun = jarTool.getClass()
.getMethod( "run", PrintStream.class, PrintStream.class, String[].class );

if ( getLastModifiedTime() != null )
{
hasJarDateOption = isJarDateOptionSupported( jarRun );
getLogger().debug( "jar tool --date option is supported: " + hasJarDateOption );
}

Integer result = (Integer) jarRun.invoke( jarTool, System.out, System.err, getJarToolArguments() );

if ( result != null && result != 0 )
{
throw new ArchiverException( "Could not create modular JAR file. " +
"The JDK jar tool exited with " + result );
}

if ( !hasJarDateOption && getLastModifiedTime() != null )
{
getLogger().debug( "Fix last modified time zip entries." );
// --date option not supported, fallback to rewrite the JAR file
// https://github.com/codehaus-plexus/plexus-archiver/issues/164
fixLastModifiedTimeZipEntries();
}
}
catch ( IOException | ReflectiveOperationException | SecurityException e )
{
Expand All @@ -131,6 +158,36 @@ protected void postCreateArchive()
}
}

/**
* Fallback to rewrite the JAR file with the correct timestamp if the {@code --date} option is not available.
*/
private void fixLastModifiedTimeZipEntries()
throws IOException
{
long timeMillis = getLastModifiedTime().toMillis();
Path destFile = getDestFile().toPath();
Path tmpZip = Files.createTempFile( destFile.getParent(), null, null );
try ( ZipFile zipFile = new ZipFile( getDestFile() );
ZipOutputStream out = new ZipOutputStream( Files.newOutputStream( tmpZip ) ) )
{
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while ( entries.hasMoreElements() )
{
ZipEntry entry = entries.nextElement();
// Not using setLastModifiedTime(FileTime) as it sets the extended timestamp
// which is not compatible with the jar tool output.
entry.setTime( timeMillis );
out.putNextEntry( entry );
if ( !entry.isDirectory() )
{
IOUtil.copy( zipFile.getInputStream( entry ), out );
}
out.closeEntry();
}
}
Files.move( tmpZip, destFile, StandardCopyOption.REPLACE_EXISTING );
}

/**
* Returns {@code true} if {@code path}
* is a module descriptor.
Expand Down Expand Up @@ -201,11 +258,51 @@ private String[] getJarToolArguments()
args.add( "--no-compress" );
}

if ( hasJarDateOption )
{
// The --date option already normalize the time, so revert to the local time
FileTime localTime = revertToLocalTime( getLastModifiedTime() );
args.add( "--date" );
args.add( localTime.toString() );
}

args.add( "-C" );
args.add( tempEmptyDir.getAbsolutePath() );
args.add( "." );

return args.toArray( new String[0] );
}

private static FileTime revertToLocalTime( FileTime time )
{
long restoreToLocalTime = time.toMillis();
Calendar cal = Calendar.getInstance( TimeZone.getDefault(), Locale.ROOT );
cal.setTimeInMillis( restoreToLocalTime );
restoreToLocalTime = restoreToLocalTime + ( cal.get( Calendar.ZONE_OFFSET ) + cal.get( Calendar.DST_OFFSET ) );
return FileTime.fromMillis( restoreToLocalTime );
}

/**
* Check support for {@code --date} option introduced since Java 17.0.3 (JDK-8279925).
*
* @return true if the JAR tool supports the {@code --date} option
*/
private boolean isJarDateOptionSupported( Method runMethod )
{
try
{
// Test the output code validating the --date option.
String[] args = { "--date", "2099-12-31T23:59:59Z", "--version" };

PrintStream nullPrintStream = new PrintStream( NullOutputStream.NULL_OUTPUT_STREAM );
Integer result = (Integer) runMethod.invoke( jarTool, nullPrintStream, nullPrintStream, args );

return result != null && result.intValue() == 0;
}
catch ( ReflectiveOperationException | SecurityException e )
{
return false;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ public InputStream get()
}
}

private void setTime( java.util.zip.ZipEntry zipEntry, long lastModified )
protected void setTime( ZipArchiveEntry zipEntry, long lastModified )
{
if ( getLastModifiedTime() != null )
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,16 @@
*/
package org.codehaus.plexus.archiver.jar;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;

import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.file.attribute.FileTime;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
Expand All @@ -30,10 +37,6 @@
import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.*;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;

public class JarToolModularJarArchiverTest
extends BaseJarArchiverTest
{
Expand Down Expand Up @@ -252,25 +255,57 @@ public void testModularMultiReleaseJar()
{
assumeTrue( modulesAreSupported() );

// Add two module-info.class, one on the root and one on the multi-release dir.
archiver.addFile( new File( "src/test/resources/java-module-descriptor/module-info.class" ),
"META-INF/versions/9/module-info.class" );
archiver.addFile( new File( "src/test/resources/java-module-descriptor/module-info.class" ),
"META-INF/versions/9/module-info.class" );
"module-info.class" );

Manifest manifest = new Manifest();
manifest.addConfiguredAttribute( new Manifest.Attribute( "Main-Class", "com.example.app.Main2" ) );
manifest.addConfiguredAttribute( new Manifest.Attribute( "Multi-Release", "true" ) );
archiver.addConfiguredManifest( manifest );

archiver.setModuleVersion( "1.0.0" );
// This attribute overwrites the one from the manifest.
archiver.setModuleMainClass( "com.example.app.Main" );

SimpleDateFormat isoFormat = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ssXXX" );
long dateTimeMillis = isoFormat.parse( "2020-02-29T23:59:59Z" ).getTime();
FileTime lastModTime = FileTime.fromMillis( dateTimeMillis );

archiver.configureReproducibleBuild( lastModTime );
archiver.createArchive();

// Round-down two seconds precision
long roundedDown = lastModTime.toMillis() - ( lastModTime.toMillis() % 2000 );
// Normalize to UTC
long expectedLastModifiedTime = normalizeLastModifiedTime( roundedDown );

// verify that the resulting modular jar has the proper version and main class set
try ( ZipFile resultingArchive = new ZipFile( archiver.getDestFile() ) )
{
ZipEntry moduleDescriptorEntry =
resultingArchive.getEntry( "META-INF/versions/9/module-info.class" );
ZipEntry moduleDescriptorEntry = resultingArchive.getEntry( "META-INF/versions/9/module-info.class" );
InputStream resultingModuleDescriptor = resultingArchive.getInputStream( moduleDescriptorEntry );
assertModuleDescriptor( resultingModuleDescriptor, "1.0.0", "com.example.app.Main", "com.example.app",
"com.example.resources" );

assertModuleDescriptor( resultingModuleDescriptor,
"1.0.0", "com.example.app.Main", "com.example.app", "com.example.resources" );
ZipEntry rootModuleDescriptorEntry = resultingArchive.getEntry( "module-info.class" );
InputStream rootResultingModuleDescriptor = resultingArchive.getInputStream( rootModuleDescriptorEntry );
assertModuleDescriptor( rootResultingModuleDescriptor, "1.0.0", "com.example.app.Main", "com.example.app",
"com.example.resources" );

// verify every entry has the correct last modified time
Enumeration<? extends ZipEntry> entries = resultingArchive.entries();
while ( entries.hasMoreElements() )
{
ZipEntry element = entries.nextElement();
assertEquals( "Last Modified Time does not match with expected", expectedLastModifiedTime,
element.getTime() );
FileTime expectedFileTime = FileTime.fromMillis( expectedLastModifiedTime );
assertEquals( "Last Modified Time does not match with expected", expectedFileTime,
element.getLastModifiedTime() );
}
}
}

Expand Down