Skip to content
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

Zip entry actions #333

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,32 @@
package com.github.jengelman.gradle.plugins.shadow.internal

import org.apache.tools.zip.Zip64Mode
import org.apache.tools.zip.ZipEntry
import org.apache.tools.zip.ZipOutputStream
import org.gradle.api.Action
import org.gradle.api.UncheckedIOException

class DefaultZipCompressor implements ZipCompressor {
private final int entryCompressionMethod
private final Zip64Mode zip64Mode
private final List<Action<ZipEntry>> actions

DefaultZipCompressor(boolean allowZip64Mode, int entryCompressionMethod) {
DefaultZipCompressor(boolean allowZip64Mode, int entryCompressionMethod, List<Action<ZipEntry>> actions) {
this.entryCompressionMethod = entryCompressionMethod
zip64Mode = allowZip64Mode ? Zip64Mode.AsNeeded : Zip64Mode.Never
this.actions = actions
}

ZipOutputStream createArchiveOutputStream(File destination) {
try {
ZipOutputStream zipOutputStream = new ZipOutputStream(destination)
// Shim an override into the outputstream so we can intercept all entries and act on them
ZipOutputStream zipOutputStream = new ZipOutputStream(destination) {
@Override
void putNextEntry(ZipEntry archiveEntry) throws IOException {
actions.each { it.execute(archiveEntry) }
super.putNextEntry(archiveEntry)
}
}
zipOutputStream.setUseZip64(zip64Mode)
zipOutputStream.setMethod(entryCompressionMethod)
return zipOutputStream
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.github.jengelman.gradle.plugins.shadow.internal

import org.apache.tools.zip.ZipEntry
import org.apache.tools.zip.ZipOutputStream
import org.gradle.api.Action
import org.gradle.api.internal.file.copy.CopySpecInternal
import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.bundling.ZipEntryCompression
Expand All @@ -19,12 +21,12 @@ class GradleVersionUtil {
return mainSpec.buildRootResolver().getPatternSet()
}

ZipCompressor getInternalCompressor(ZipEntryCompression entryCompression, Jar jar) {
ZipCompressor getInternalCompressor(ZipEntryCompression entryCompression, Jar jar, List<Action<ZipEntry>> actions) {
switch (entryCompression) {
case ZipEntryCompression.DEFLATED:
return new DefaultZipCompressor(jar.zip64, ZipOutputStream.DEFLATED);
return new DefaultZipCompressor(jar.zip64, ZipOutputStream.DEFLATED, actions);
case ZipEntryCompression.STORED:
return new DefaultZipCompressor(jar.zip64, ZipOutputStream.STORED);
return new DefaultZipCompressor(jar.zip64, ZipOutputStream.STORED, actions);
default:
throw new IllegalArgumentException(String.format("Unknown Compression type %s", entryCompression));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer;
import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer;
import groovy.lang.MetaClass;
import org.apache.tools.zip.ZipEntry;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.gradle.api.Action;
import org.gradle.api.artifacts.Configuration;
Expand All @@ -28,6 +29,7 @@
public class ShadowJar extends Jar implements ShadowSpec {

private List<Transformer> transformers;
private List<Action<ZipEntry>> zipEntryActions;
private List<Relocator> relocators;
private List<Configuration> configurations;
private DependencyFilter dependencyFilter;
Expand All @@ -41,6 +43,7 @@ public ShadowJar() {
dependencyFilter = new DefaultDependencyFilter(getProject());
setManifest(new DefaultInheritManifest(getServices().get(FileResolver.class)));
transformers = new ArrayList<>();
zipEntryActions = new ArrayList<>();
relocators = new ArrayList<>();
configurations = new ArrayList<>();
}
Expand All @@ -63,7 +66,7 @@ protected CopyAction createCopyAction() {
}

protected ZipCompressor getInternalCompressor() {
return versionUtil.getInternalCompressor(getEntryCompression(), this);
return versionUtil.getInternalCompressor(getEntryCompression(), this, zipEntryActions);
}

@TaskAction
Expand Down Expand Up @@ -100,6 +103,11 @@ public ShadowJar dependencies(Action<DependencyFilter> c) {
return this;
}

public ShadowJar entryAction(Action<ZipEntry> action) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're gonna need to make our own 1st class citizen for ZipEntry. We can't expose the apache classes as part of the public API for shadow because those classes are relocated during packaging.

But overall this PR looks good. I'll likely merge it this way and then figure out the stuff above.

zipEntryActions.add(action);
return this;
}

/**
* Add a Transformer instance for modifying JAR resources and configure.
*
Expand Down Expand Up @@ -137,6 +145,42 @@ public ShadowJar transform(Transformer transformer) {
return this;
}

/**
* Act on all zipEntries being added to the final jar.
*
* Every {@link ZipEntry} to be added to the final jar will be presented
* to the {@code zipEntryAction}, which can inspect and modify it as desired.
* @param zipEntryAction the action instance to add
* @return this
*/
public ShadowSpec modifyZipEntries(Action<ZipEntry> zipEntryAction) {
zipEntryActions.add(zipEntryAction);
return this;
}

private static final long DOS_TIMESTAMP_ZERO = 315558000000L;
/**
* Zero out timestamps on zip entries to Jan 1st 1980. (DOS epoch)
*
* 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 zeroZipEntryTimestamps() {
zipEntryActions.add(new Action<ZipEntry>() {
@Override
public void execute(ZipEntry zipEntry) {
zipEntry.setTime(DOS_TIMESTAMP_ZERO);
}
});
return this;
}

/**
* Syntactic sugar for merging service files in JARs.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator;
import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer;
import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer;
import org.apache.tools.zip.ZipEntry;
import org.gradle.api.Action;
import org.gradle.api.file.CopySpec;

Expand All @@ -19,6 +20,10 @@ interface ShadowSpec extends CopySpec {

ShadowSpec transform(Transformer transformer);

ShadowSpec modifyZipEntries(Action<ZipEntry> zipEntryAction);

ShadowSpec zeroZipEntryTimestamps();

ShadowSpec mergeServiceFiles();

ShadowSpec mergeServiceFiles(String rootPath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,64 @@ 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
zeroZipEntryTimestamps()
}
// end::rename[]
""".stripIndent()

when:
runner.withArguments('-S', 'shadowJar').build()

then:
getZipEntries(output("shadow.jar")).each {
assert it.getTime() == 315558000000L
}
}

def 'calls provided ZipEntry Actions' () {
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
modifyZipEntries {
ze -> ze.setName("overwritten")
}
}
// end::rename[]
""".stripIndent()

when:
runner.withArguments('-S', 'shadowJar').build()

then:
getZipEntries(output("shadow.jar")).each {
assert it.getName() == "overwritten"
}
}

private String escapedPath(File file) {
file.path.replaceAll('\\\\', '\\\\\\\\')
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -108,6 +110,10 @@ class PluginSpecification extends Specification {
return sw.toString()
}

List<ZipEntry> getZipEntries(File f) {
new ZipFile(f).entries.toList()
}

void contains(File f, List<String> paths) {
JarFile jar = new JarFile(f)
paths.each { path ->
Expand Down