Skip to content

Commit ae29b75

Browse files
jorsolhboutemy
authored andcommitted
fix: Reproducible Builds not working when using modular jar
Signed-off-by: Jorge Solórzano <[email protected]>
1 parent 1f557b2 commit ae29b75

File tree

4 files changed

+170
-20
lines changed

4 files changed

+170
-20
lines changed

src/main/java/org/codehaus/plexus/archiver/jar/JarArchiver.java

+18
Original file line numberDiff line numberDiff line change
@@ -825,4 +825,22 @@ else if ( !name.contains( "/" ) )
825825
}
826826
}
827827

828+
/**
829+
* Override the behavior of the Zip Archiver to match the output of the JAR tool.
830+
*
831+
* @param zipEntry to set the last modified time
832+
* @param lastModified to set in the zip entry only if the {@link #getLastModifiedTime()} returns null.
833+
*/
834+
@Override
835+
protected void setTime( ZipArchiveEntry zipEntry, long lastModified )
836+
{
837+
if ( getLastModifiedTime() != null )
838+
{
839+
lastModified = getLastModifiedTime().toMillis();
840+
}
841+
842+
// The JAR tool does not round up, so we keep that behavior here (JDK-8277755).
843+
zipEntry.setTime( lastModified );
844+
}
845+
828846
}

src/main/java/org/codehaus/plexus/archiver/jar/JarToolModularJarArchiver.java

+107-10
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,30 @@
1616
*/
1717
package org.codehaus.plexus.archiver.jar;
1818

19-
import org.apache.commons.compress.parallel.InputStreamSupplier;
20-
import org.codehaus.plexus.archiver.ArchiverException;
21-
import org.codehaus.plexus.archiver.zip.ConcurrentJarCreator;
22-
2319
import java.io.File;
2420
import java.io.IOException;
2521
import java.io.PrintStream;
22+
import java.lang.reflect.Method;
2623
import java.nio.file.Files;
24+
import java.nio.file.Path;
25+
import java.nio.file.StandardCopyOption;
26+
import java.nio.file.attribute.FileTime;
2727
import java.util.ArrayList;
28+
import java.util.Calendar;
29+
import java.util.Enumeration;
2830
import java.util.List;
31+
import java.util.Locale;
32+
import java.util.TimeZone;
2933
import java.util.regex.Pattern;
34+
import java.util.zip.ZipEntry;
35+
import java.util.zip.ZipFile;
36+
import java.util.zip.ZipOutputStream;
37+
38+
import org.apache.commons.compress.parallel.InputStreamSupplier;
39+
import org.apache.commons.io.output.NullOutputStream;
40+
import org.codehaus.plexus.archiver.ArchiverException;
41+
import org.codehaus.plexus.archiver.zip.ConcurrentJarCreator;
42+
import org.codehaus.plexus.util.IOUtil;
3043

3144
/**
3245
* A {@link ModularJarArchiver} implementation that uses
@@ -58,6 +71,8 @@ public class JarToolModularJarArchiver
5871

5972
private boolean moduleDescriptorFound;
6073

74+
private boolean hasJarDateOption;
75+
6176
public JarToolModularJarArchiver()
6277
{
6378
try
@@ -111,18 +126,30 @@ protected void postCreateArchive()
111126
getLogger().debug( "Using the jar tool to " +
112127
"update the archive to modular JAR." );
113128

114-
Integer result = (Integer) jarTool.getClass()
115-
.getMethod( "run",
116-
PrintStream.class, PrintStream.class, String[].class )
117-
.invoke( jarTool,
118-
System.out, System.err,
119-
getJarToolArguments() );
129+
final Method jarRun = jarTool.getClass()
130+
.getMethod( "run", PrintStream.class, PrintStream.class, String[].class );
131+
132+
if ( getLastModifiedTime() != null )
133+
{
134+
hasJarDateOption = isJarDateOptionSupported( jarRun );
135+
getLogger().debug( "jar tool --date option is supported: " + hasJarDateOption );
136+
}
137+
138+
Integer result = (Integer) jarRun.invoke( jarTool, System.out, System.err, getJarToolArguments() );
120139

121140
if ( result != null && result != 0 )
122141
{
123142
throw new ArchiverException( "Could not create modular JAR file. " +
124143
"The JDK jar tool exited with " + result );
125144
}
145+
146+
if ( !hasJarDateOption && getLastModifiedTime() != null )
147+
{
148+
getLogger().debug( "Fix last modified time zip entries." );
149+
// --date option not supported, fallback to rewrite the JAR file
150+
// https://github.com/codehaus-plexus/plexus-archiver/issues/164
151+
fixLastModifiedTimeZipEntries();
152+
}
126153
}
127154
catch ( IOException | ReflectiveOperationException | SecurityException e )
128155
{
@@ -131,6 +158,36 @@ protected void postCreateArchive()
131158
}
132159
}
133160

161+
/**
162+
* Fallback to rewrite the JAR file with the correct timestamp if the {@code --date} option is not available.
163+
*/
164+
private void fixLastModifiedTimeZipEntries()
165+
throws IOException
166+
{
167+
long timeMillis = getLastModifiedTime().toMillis();
168+
Path destFile = getDestFile().toPath();
169+
Path tmpZip = Files.createTempFile( destFile.getParent(), null, null );
170+
try ( ZipFile zipFile = new ZipFile( getDestFile() );
171+
ZipOutputStream out = new ZipOutputStream( Files.newOutputStream( tmpZip ) ) )
172+
{
173+
Enumeration<? extends ZipEntry> entries = zipFile.entries();
174+
while ( entries.hasMoreElements() )
175+
{
176+
ZipEntry entry = entries.nextElement();
177+
// Not using setLastModifiedTime(FileTime) as it sets the extended timestamp
178+
// which is not compatible with the jar tool output.
179+
entry.setTime( timeMillis );
180+
out.putNextEntry( entry );
181+
if ( !entry.isDirectory() )
182+
{
183+
IOUtil.copy( zipFile.getInputStream( entry ), out );
184+
}
185+
out.closeEntry();
186+
}
187+
}
188+
Files.move( tmpZip, destFile, StandardCopyOption.REPLACE_EXISTING );
189+
}
190+
134191
/**
135192
* Returns {@code true} if {@code path}
136193
* is a module descriptor.
@@ -201,11 +258,51 @@ private String[] getJarToolArguments()
201258
args.add( "--no-compress" );
202259
}
203260

261+
if ( hasJarDateOption )
262+
{
263+
// The --date option already normalize the time, so revert to the local time
264+
FileTime localTime = revertToLocalTime( getLastModifiedTime() );
265+
args.add( "--date" );
266+
args.add( localTime.toString() );
267+
}
268+
204269
args.add( "-C" );
205270
args.add( tempEmptyDir.getAbsolutePath() );
206271
args.add( "." );
207272

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

276+
private static FileTime revertToLocalTime( FileTime time )
277+
{
278+
long restoreToLocalTime = time.toMillis();
279+
Calendar cal = Calendar.getInstance( TimeZone.getDefault(), Locale.ROOT );
280+
cal.setTimeInMillis( restoreToLocalTime );
281+
restoreToLocalTime = restoreToLocalTime + ( cal.get( Calendar.ZONE_OFFSET ) + cal.get( Calendar.DST_OFFSET ) );
282+
return FileTime.fromMillis( restoreToLocalTime );
283+
}
284+
285+
/**
286+
* Check support for {@code --date} option introduced since Java 17.0.3 (JDK-8279925).
287+
*
288+
* @return true if the JAR tool supports the {@code --date} option
289+
*/
290+
private boolean isJarDateOptionSupported( Method runMethod )
291+
{
292+
try
293+
{
294+
// Test the output code validating the --date option.
295+
String[] args = { "--date", "2099-12-31T23:59:59Z", "--version" };
296+
297+
PrintStream nullPrintStream = new PrintStream( NullOutputStream.NULL_OUTPUT_STREAM );
298+
Integer result = (Integer) runMethod.invoke( jarTool, nullPrintStream, nullPrintStream, args );
299+
300+
return result != null && result.intValue() == 0;
301+
}
302+
catch ( ReflectiveOperationException | SecurityException e )
303+
{
304+
return false;
305+
}
306+
}
307+
211308
}

src/main/java/org/codehaus/plexus/archiver/zip/AbstractZipArchiver.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ public InputStream get()
534534
}
535535
}
536536

537-
private void setTime( java.util.zip.ZipEntry zipEntry, long lastModified )
537+
protected void setTime( ZipArchiveEntry zipEntry, long lastModified )
538538
{
539539
if ( getLastModifiedTime() != null )
540540
{

src/test/java/org/codehaus/plexus/archiver/jar/JarToolModularJarArchiverTest.java

+44-9
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,16 @@
1616
*/
1717
package org.codehaus.plexus.archiver.jar;
1818

19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertNotNull;
21+
import static org.junit.Assume.assumeFalse;
22+
import static org.junit.Assume.assumeTrue;
23+
1924
import java.io.File;
2025
import java.io.InputStream;
2126
import java.lang.reflect.Method;
27+
import java.nio.file.attribute.FileTime;
28+
import java.text.SimpleDateFormat;
2229
import java.util.Arrays;
2330
import java.util.Enumeration;
2431
import java.util.HashSet;
@@ -30,10 +37,6 @@
3037
import org.junit.Before;
3138
import org.junit.Test;
3239

33-
import static org.junit.Assert.*;
34-
import static org.junit.Assume.assumeFalse;
35-
import static org.junit.Assume.assumeTrue;
36-
3740
public class JarToolModularJarArchiverTest
3841
extends BaseJarArchiverTest
3942
{
@@ -252,25 +255,57 @@ public void testModularMultiReleaseJar()
252255
{
253256
assumeTrue( modulesAreSupported() );
254257

258+
// Add two module-info.class, one on the root and one on the multi-release dir.
259+
archiver.addFile( new File( "src/test/resources/java-module-descriptor/module-info.class" ),
260+
"META-INF/versions/9/module-info.class" );
255261
archiver.addFile( new File( "src/test/resources/java-module-descriptor/module-info.class" ),
256-
"META-INF/versions/9/module-info.class" );
262+
"module-info.class" );
263+
257264
Manifest manifest = new Manifest();
265+
manifest.addConfiguredAttribute( new Manifest.Attribute( "Main-Class", "com.example.app.Main2" ) );
258266
manifest.addConfiguredAttribute( new Manifest.Attribute( "Multi-Release", "true" ) );
259267
archiver.addConfiguredManifest( manifest );
268+
260269
archiver.setModuleVersion( "1.0.0" );
270+
// This attribute overwrites the one from the manifest.
261271
archiver.setModuleMainClass( "com.example.app.Main" );
262272

273+
SimpleDateFormat isoFormat = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ssXXX" );
274+
long dateTimeMillis = isoFormat.parse( "2020-02-29T23:59:59Z" ).getTime();
275+
FileTime lastModTime = FileTime.fromMillis( dateTimeMillis );
276+
277+
archiver.configureReproducibleBuild( lastModTime );
263278
archiver.createArchive();
264279

280+
// Round-down two seconds precision
281+
long roundedDown = lastModTime.toMillis() - ( lastModTime.toMillis() % 2000 );
282+
// Normalize to UTC
283+
long expectedLastModifiedTime = normalizeLastModifiedTime( roundedDown );
284+
265285
// verify that the resulting modular jar has the proper version and main class set
266286
try ( ZipFile resultingArchive = new ZipFile( archiver.getDestFile() ) )
267287
{
268-
ZipEntry moduleDescriptorEntry =
269-
resultingArchive.getEntry( "META-INF/versions/9/module-info.class" );
288+
ZipEntry moduleDescriptorEntry = resultingArchive.getEntry( "META-INF/versions/9/module-info.class" );
270289
InputStream resultingModuleDescriptor = resultingArchive.getInputStream( moduleDescriptorEntry );
290+
assertModuleDescriptor( resultingModuleDescriptor, "1.0.0", "com.example.app.Main", "com.example.app",
291+
"com.example.resources" );
271292

272-
assertModuleDescriptor( resultingModuleDescriptor,
273-
"1.0.0", "com.example.app.Main", "com.example.app", "com.example.resources" );
293+
ZipEntry rootModuleDescriptorEntry = resultingArchive.getEntry( "module-info.class" );
294+
InputStream rootResultingModuleDescriptor = resultingArchive.getInputStream( rootModuleDescriptorEntry );
295+
assertModuleDescriptor( rootResultingModuleDescriptor, "1.0.0", "com.example.app.Main", "com.example.app",
296+
"com.example.resources" );
297+
298+
// verify every entry has the correct last modified time
299+
Enumeration<? extends ZipEntry> entries = resultingArchive.entries();
300+
while ( entries.hasMoreElements() )
301+
{
302+
ZipEntry element = entries.nextElement();
303+
assertEquals( "Last Modified Time does not match with expected", expectedLastModifiedTime,
304+
element.getTime() );
305+
FileTime expectedFileTime = FileTime.fromMillis( expectedLastModifiedTime );
306+
assertEquals( "Last Modified Time does not match with expected", expectedFileTime,
307+
element.getLastModifiedTime() );
308+
}
274309
}
275310
}
276311

0 commit comments

Comments
 (0)