From 5be8e575e611f2dd5d34ff6978bc3fd2bb567c51 Mon Sep 17 00:00:00 2001 From: Steven Ruppert Date: Fri, 9 Sep 2016 14:31:13 -0600 Subject: [PATCH] Add "zeroEntryTimestamps()" option See javadoc on the method for details on why and how. This is basically the half-assed deterministic build from #229, which is to say still extremely helpful when dealing with rsync. --- .../shadow/tasks/ShadowCopyAction.groovy | 26 +++++++++++---- .../plugins/shadow/tasks/ShadowJar.java | 33 +++++++++++++++++-- .../plugins/shadow/ShadowPluginSpec.groovy | 33 ++++++++++++++++++- .../shadow/util/PluginSpecification.groovy | 6 ++++ 4 files changed, 88 insertions(+), 10 deletions(-) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy index 5aad966f6..4cbe5c531 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy @@ -39,6 +39,10 @@ import java.util.zip.ZipException @Slf4j public class ShadowCopyAction implements CopyAction { + /** + * Jan 1st, 1980. The day the zip timestamps stood still. + */ + public static final int DOS_TIMESTAMP_ZERO = 315558000000L private final File zipFile private final ZipCompressor compressor @@ -47,11 +51,11 @@ public class ShadowCopyAction implements CopyAction { private final List relocators private final PatternSet patternSet private final ShadowStats stats + private final boolean zeroEntryTimestamps public ShadowCopyAction(File zipFile, ZipCompressor compressor, DocumentationRegistry documentationRegistry, List transformers, List relocators, PatternSet patternSet, - ShadowStats stats) { - + ShadowStats stats, boolean zeroEntryTimestamps) { this.zipFile = zipFile this.compressor = compressor this.documentationRegistry = documentationRegistry @@ -59,6 +63,7 @@ public class ShadowCopyAction implements CopyAction { this.relocators = relocators this.patternSet = patternSet this.stats = stats + this.zeroEntryTimestamps = zeroEntryTimestamps } @Override @@ -170,7 +175,7 @@ public class ShadowCopyAction implements CopyAction { ZipEntry archiveEntry = new ZipEntry(mappedPath) archiveEntry.setTime(fileDetails.lastModified) archiveEntry.unixMode = (UnixStat.FILE_FLAG | fileDetails.mode) - zipOutStr.putNextEntry(archiveEntry) + putNextEntry(archiveEntry) fileDetails.copyTo(zipOutStr) zipOutStr.closeEntry() } else { @@ -209,7 +214,7 @@ public class ShadowCopyAction implements CopyAction { private void visitArchiveDirectory(RelativeArchivePath archiveDir) { if (recordVisit(archiveDir)) { - zipOutStr.putNextEntry(archiveDir.entry) + putNextEntry(archiveDir.entry) zipOutStr.closeEntry() } } @@ -277,7 +282,7 @@ public class ShadowCopyAction implements CopyAction { try { // Now we put it back on so the class file is written out with the right extension. - zipOutStr.putNextEntry(new ZipEntry(mappedName + ".class")) + putNextEntry(new ZipEntry(mappedName + ".class")) IOUtils.copyLarge(new ByteArrayInputStream(renamedClass), zipOutStr) zipOutStr.closeEntry() } catch (ZipException e) { @@ -289,7 +294,7 @@ public class ShadowCopyAction implements CopyAction { String mappedPath = remapper.map(archiveFile.entry.name) RelativeArchivePath mappedFile = new RelativeArchivePath(new ZipEntry(mappedPath), archiveFile.details) addParentDirectories(mappedFile) - zipOutStr.putNextEntry(mappedFile.entry) + putNextEntry(mappedFile.entry) IOUtils.copyLarge(archive.getInputStream(archiveFile.entry), zipOutStr) zipOutStr.closeEntry() } @@ -301,7 +306,7 @@ public class ShadowCopyAction implements CopyAction { ZipEntry archiveEntry = new ZipEntry(path) archiveEntry.setTime(dirDetails.lastModified) archiveEntry.unixMode = (UnixStat.DIR_FLAG | dirDetails.mode) - zipOutStr.putNextEntry(archiveEntry) + putNextEntry(archiveEntry) zipOutStr.closeEntry() recordVisit(dirDetails.relativePath) } catch (Exception e) { @@ -326,6 +331,13 @@ public class ShadowCopyAction implements CopyAction { return transformers.any { it.canTransformResource(element) } } + // manually intercept `putNextEntry` to zero out timestamps if requested. + private void putNextEntry(ZipEntry entry) { + if (zeroEntryTimestamps) + entry.setTime(DOS_TIMESTAMP_ZERO) + zipOutStr.putNextEntry(entry) + } + } class RelativeArchivePath extends RelativePath { diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java index 64332430a..440fea5ae 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java @@ -16,7 +16,6 @@ import org.gradle.api.internal.DocumentationRegistry; import org.gradle.api.internal.file.FileResolver; import org.gradle.api.internal.file.copy.CopyAction; -import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.TaskAction; @@ -36,6 +35,8 @@ public class ShadowJar extends Jar implements ShadowSpec { private final ShadowStats shadowStats = new ShadowStats(); private final GradleVersionUtil versionUtil; + private boolean zeroEntryTimestamps = false; + public ShadowJar() { versionUtil = new GradleVersionUtil(getProject().getGradle().getGradleVersion()); dependencyFilter = new DefaultDependencyFilter(getProject()); @@ -54,7 +55,27 @@ public InheritManifest getManifest() { protected CopyAction createCopyAction() { DocumentationRegistry documentationRegistry = getServices().get(DocumentationRegistry.class); return new ShadowCopyAction(getArchivePath(), getInternalCompressor(), documentationRegistry, - transformers, relocators, getRootPatternSet(), shadowStats); + transformers, relocators, getRootPatternSet(), shadowStats, zeroEntryTimestamps); + } + + /** + * Configure the shadow jar such that all timestamps on entries + * inside are fixed to Jan 1st 1980. + * + * Why would you want to do this? The timestamps usually are just set to the + * time the jar was built. However, those timestamps are repeated across every + * file inside the jar, which scatters byte-level changes throughout the + * shadow jar, which breaks any efforts to calculate efficient deltas between + * shadow jar builds (e.g. rsync). With static timestamps though, rsync just works. + * + * This is generally safe to turn on because nothing cares about + * internal timestamps, but it's off by default. + * + * @return this + */ + public ShadowJar zeroEntryTimestamps() { + this.zeroEntryTimestamps = true; + return this; } protected ZipCompressor getInternalCompressor() { @@ -278,6 +299,14 @@ public ShadowJar relocate(Class relocatorClass, Action< return this; } + public boolean isZeroEntryTimestamps() { + return zeroEntryTimestamps; + } + + public void setZeroEntryTimestamps(boolean zeroEntryTimestamps) { + this.zeroEntryTimestamps = zeroEntryTimestamps; + } + public List getTransformers() { return this.transformers; } diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy index 92a2eac20..49c559c33 100644 --- a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy @@ -1,5 +1,6 @@ package com.github.jengelman.gradle.plugins.shadow +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowCopyAction import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.util.AppendableMavenFileRepository import com.github.jengelman.gradle.plugins.shadow.util.PluginSpecification @@ -61,7 +62,7 @@ class ShadowPluginSpec extends PluginSpecification { .withArguments('--stacktrace') .withProjectDir(dir.root) .forwardOutput() - .withDebug(true) +// .withDebug(true) .withTestKitDir(getTestKitDir()) @@ -484,6 +485,36 @@ class ShadowPluginSpec extends PluginSpecification { } + def 'zeroes zip entry timestamps if requested'() { + given: + file('src/main/java/shadow/Passed.java') << ''' + package shadow; + public class Passed {} + '''.stripIndent() + + buildFile << """ + dependencies { compile 'junit:junit:3.8.2' } + + // tag::rename[] + shadowJar { + baseName = 'shadow' + classifier = null + version = null + + zeroEntryTimestamps() + } + // end::rename[] + """.stripIndent() + + when: + runner.withArguments('-S', 'shadowJar').build() + + then: + getZipEntries(output("shadow.jar")).each { + assert it.getTime() == ShadowCopyAction.DOS_TIMESTAMP_ZERO + } + } + private String escapedPath(File file) { file.path.replaceAll('\\\\', '\\\\\\\\') } diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/PluginSpecification.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/PluginSpecification.groovy index a51b54654..00c61035e 100644 --- a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/PluginSpecification.groovy +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/PluginSpecification.groovy @@ -2,6 +2,8 @@ package com.github.jengelman.gradle.plugins.shadow.util import com.github.jengelman.gradle.plugins.shadow.util.file.TestFile import com.google.common.base.StandardSystemProperty +import org.apache.tools.zip.ZipEntry +import org.apache.tools.zip.ZipFile import org.codehaus.plexus.util.IOUtil import org.gradle.testkit.runner.GradleRunner import org.junit.Rule @@ -100,6 +102,10 @@ class PluginSpecification extends Specification { return sw.toString() } + List getZipEntries(File f) { + new ZipFile(f).entries.toList() + } + void contains(File f, List paths) { JarFile jar = new JarFile(f) paths.each { path ->