diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTracker.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTracker.groovy index 326f76917..6963d0fc4 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTracker.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/UnusedTracker.groovy @@ -6,6 +6,7 @@ import org.gradle.api.artifacts.Dependency import org.gradle.api.artifacts.ProjectDependency import org.gradle.api.artifacts.SelfResolvingDependency import org.gradle.api.file.FileCollection +import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.SourceSet import org.vafer.jdependency.Clazz import org.vafer.jdependency.Clazzpath @@ -50,6 +51,11 @@ class UnusedTracker { return new UnusedTracker(classDirs, apiJars, toMinimize) } + @InputFiles + FileCollection getToMinimize() { + return toMinimize + } + private static boolean isProjectDependencyFile(File file, Dependency dep) { def fileName = file.name def dependencyName = dep.name diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/CacheableRelocator.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/CacheableRelocator.groovy new file mode 100644 index 000000000..966e3f524 --- /dev/null +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/CacheableRelocator.groovy @@ -0,0 +1,17 @@ +package com.github.jengelman.gradle.plugins.shadow.relocation + +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +/** + * Marks that a given instance of {@link Relocator} is is compatible with the Gradle build cache. + * In other words, it has its appropriate inputs annotated so that Gradle can consider them when + * determining the cache key. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@interface CacheableRelocator { + +} \ No newline at end of file diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.groovy index b4e4e8d79..90538f508 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.groovy @@ -20,6 +20,7 @@ package com.github.jengelman.gradle.plugins.shadow.relocation import org.codehaus.plexus.util.SelectorUtils +import org.gradle.api.tasks.Input import java.util.regex.Pattern @@ -30,6 +31,7 @@ import java.util.regex.Pattern * @author Mauro Talevi * @author John Engelman */ +@CacheableRelocator class SimpleRelocator implements Relocator { private final String pattern @@ -43,7 +45,7 @@ class SimpleRelocator implements Relocator { private final Set includes private final Set excludes - + private final boolean rawString SimpleRelocator() { @@ -188,4 +190,39 @@ class SimpleRelocator implements Relocator { return sourceContent.replaceAll("\\b" + pattern, shadedPattern) } } + + @Input + String getPattern() { + return pattern + } + + @Input + String getPathPattern() { + return pathPattern + } + + @Input + String getShadedPattern() { + return shadedPattern + } + + @Input + String getShadedPathPattern() { + return shadedPathPattern + } + + @Input + Set getIncludes() { + return includes + } + + @Input + Set getExcludes() { + return excludes + } + + @Input + boolean getRawString() { + return rawString + } } 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 159a6e5a5..804bcd15e 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 @@ -2,23 +2,20 @@ import com.github.jengelman.gradle.plugins.shadow.ShadowStats; import com.github.jengelman.gradle.plugins.shadow.internal.*; +import com.github.jengelman.gradle.plugins.shadow.relocation.CacheableRelocator; import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator; import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator; -import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer; -import com.github.jengelman.gradle.plugins.shadow.transformers.GroovyExtensionModuleTransformer; -import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer; -import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer; +import com.github.jengelman.gradle.plugins.shadow.transformers.*; import org.gradle.api.Action; +import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; import org.gradle.api.file.DuplicatesStrategy; import org.gradle.api.file.FileCollection; 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.InputFiles; -import org.gradle.api.tasks.Internal; -import org.gradle.api.tasks.Optional; -import org.gradle.api.tasks.TaskAction; +import org.gradle.api.specs.Spec; +import org.gradle.api.tasks.*; import org.gradle.api.tasks.bundling.Jar; import org.gradle.api.tasks.util.PatternSet; @@ -26,6 +23,7 @@ import java.util.List; import java.util.concurrent.Callable; +@CacheableTask public class ShadowJar extends Jar implements ShadowSpec { private List transformers; @@ -48,6 +46,29 @@ public ShadowJar() { transformers = new ArrayList<>(); relocators = new ArrayList<>(); configurations = new ArrayList<>(); + + this.getInputs().property("minimize", new Callable() { + @Override + public Boolean call() throws Exception { + return minimizeJar; + } + }); + this.getOutputs().doNotCacheIf("Has one or more transforms or relocators that are not cacheable", new Spec() { + @Override + public boolean isSatisfiedBy(Task task) { + for (Transformer transformer : transformers) { + if (!isCacheableTransform(transformer.getClass())) { + return true; + } + } + for (Relocator relocator : relocators) { + if (!isCacheableRelocator(relocator.getClass())) { + return true; + } + } + return false; + } + }); } public ShadowJar minimize() { @@ -95,7 +116,7 @@ protected void copy() { getLogger().info(shadowStats.toString()); } - @InputFiles + @Classpath public FileCollection getIncludedDependencies() { return getProject().files(new Callable() { @@ -148,13 +169,14 @@ public ShadowJar transform(Class clazz) throws Instantiat */ public ShadowJar transform(Class clazz, Action c) throws InstantiationException, IllegalAccessException { T transformer = clazz.newInstance(); - if (c != null) { - c.execute(transformer); - } - transformers.add(transformer); + addTransform(transformer, c); return this; } + private boolean isCacheableTransform(Class clazz) { + return clazz.isAnnotationPresent(CacheableTransformer.class); + } + /** * Add a preconfigured transformer instance. * @@ -162,10 +184,18 @@ public ShadowJar transform(Class clazz, Action c) * @return this */ public ShadowJar transform(Transformer transformer) { - transformers.add(transformer); + addTransform(transformer, null); return this; } + private void addTransform(T transformer, Action c) { + if (c != null) { + c.execute(transformer); + } + + transformers.add(transformer); + } + /** * Syntactic sugar for merging service files in JARs. * @@ -268,10 +298,7 @@ public ShadowJar relocate(String pattern, String destination) { */ public ShadowJar relocate(String pattern, String destination, Action configure) { SimpleRelocator relocator = new SimpleRelocator(pattern, destination, new ArrayList(), new ArrayList()); - if (configure != null) { - configure.execute(relocator); - } - relocators.add(relocator); + addRelocator(relocator, configure); return this; } @@ -282,7 +309,7 @@ public ShadowJar relocate(String pattern, String destination, Action relocatorClass) throws Inst return relocate(relocatorClass, null); } + private void addRelocator(R relocator, Action configure) { + if (configure != null) { + configure.execute(relocator); + } + + relocators.add(relocator); + } + /** * Add a relocator of the provided class and configure. * @@ -305,14 +340,15 @@ public ShadowJar relocate(Class relocatorClass) throws Inst */ public ShadowJar relocate(Class relocatorClass, Action configure) throws InstantiationException, IllegalAccessException { R relocator = relocatorClass.newInstance(); - if (configure != null) { - configure.execute(relocator); - } - relocators.add(relocator); + addRelocator(relocator, configure); return this; } - @Internal + private boolean isCacheableRelocator(Class relocatorClass) { + return relocatorClass.isAnnotationPresent(CacheableRelocator.class); + } + + @Nested public List getTransformers() { return this.transformers; } @@ -321,7 +357,7 @@ public void setTransformers(List transformers) { this.transformers = transformers; } - @Internal + @Nested public List getRelocators() { return this.relocators; } @@ -330,7 +366,7 @@ public void setRelocators(List relocators) { this.relocators = relocators; } - @InputFiles @Optional + @Classpath @Optional public List getConfigurations() { return this.configurations; } diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer.groovy index c9289329b..c75fe9ab6 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer.groovy @@ -23,6 +23,7 @@ import org.apache.tools.zip.ZipEntry import org.apache.tools.zip.ZipOutputStream import org.codehaus.plexus.util.IOUtil import org.gradle.api.file.FileTreeElement +import org.gradle.api.tasks.Input /** * A resource processor that appends content for a resource, separated by a newline. @@ -32,6 +33,7 @@ import org.gradle.api.file.FileTreeElement * Modifications * @author John Engelman */ +@CacheableTransformer class AppendingTransformer implements Transformer { String resource @@ -65,4 +67,9 @@ class AppendingTransformer implements Transformer { IOUtil.copy(new ByteArrayInputStream(data.toByteArray()), os) data.reset() } + + @Input + String getResource() { + return resource + } } diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/CacheableTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/CacheableTransformer.groovy new file mode 100644 index 000000000..ae3b74e56 --- /dev/null +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/CacheableTransformer.groovy @@ -0,0 +1,17 @@ +package com.github.jengelman.gradle.plugins.shadow.transformers + +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +/** + * Marks that a given instance of {@link Transformer} is is compatible with the Gradle build cache. + * In other words, it has its appropriate inputs annotated so that Gradle can consider them when + * determining the cache key. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@interface CacheableTransformer { + +} \ No newline at end of file diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/GroovyExtensionModuleTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/GroovyExtensionModuleTransformer.groovy index c64aaf781..7a6a3418a 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/GroovyExtensionModuleTransformer.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/GroovyExtensionModuleTransformer.groovy @@ -32,6 +32,7 @@ import org.codehaus.plexus.util.IOUtil * entries will all be merged into a single META-INF/services/org.codehaus.groovy.runtime.ExtensionModule resource * packaged into the resultant JAR produced by the shadowing process. */ +@CacheableTransformer class GroovyExtensionModuleTransformer implements Transformer { private static final GROOVY_EXTENSION_MODULE_DESCRIPTOR_PATH = diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformer.groovy index e91f8fbb2..418360c5a 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformer.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformer.groovy @@ -25,6 +25,7 @@ import org.apache.tools.zip.ZipEntry import org.apache.tools.zip.ZipOutputStream import org.gradle.api.file.FileTreeElement import org.gradle.api.specs.Spec +import org.gradle.api.tasks.Input import org.gradle.api.tasks.util.PatternFilterable import org.gradle.api.tasks.util.PatternSet import org.codehaus.plexus.util.IOUtil @@ -42,6 +43,7 @@ import org.codehaus.plexus.util.IOUtil * @author Charlie Knudsen * @author John Engelman */ +@CacheableTransformer class ServiceFileTransformer implements Transformer, PatternFilterable { private static final String SERVICES_PATTERN = "META-INF/services/**" @@ -193,6 +195,7 @@ class ServiceFileTransformer implements Transformer, PatternFilterable { * {@inheritDoc} */ @Override + @Input Set getIncludes() { return patternSet.includes } @@ -210,6 +213,7 @@ class ServiceFileTransformer implements Transformer, PatternFilterable { * {@inheritDoc} */ @Override + @Input Set getExcludes() { return patternSet.excludes } @@ -222,5 +226,4 @@ class ServiceFileTransformer implements Transformer, PatternFilterable { patternSet.excludes = excludes return this } - } diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/XmlAppendingTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/XmlAppendingTransformer.groovy index 619437125..d971421f5 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/XmlAppendingTransformer.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/XmlAppendingTransformer.groovy @@ -22,6 +22,7 @@ package com.github.jengelman.gradle.plugins.shadow.transformers import org.apache.tools.zip.ZipEntry import org.apache.tools.zip.ZipOutputStream import org.gradle.api.file.FileTreeElement +import org.gradle.api.tasks.Input import org.jdom2.Attribute import org.jdom2.Content import org.jdom2.Document @@ -41,6 +42,7 @@ import org.xml.sax.SAXException * * @author John Engelman */ +@CacheableTransformer class XmlAppendingTransformer implements Transformer { static final String XSI_NS = "http://www.w3.org/2001/XMLSchema-instance" @@ -110,4 +112,9 @@ class XmlAppendingTransformer implements Transformer { doc = null } + + @Input + String getResource() { + return resource + } } diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/caching/AbstractCachingSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/caching/AbstractCachingSpec.groovy new file mode 100644 index 000000000..726ba282e --- /dev/null +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/caching/AbstractCachingSpec.groovy @@ -0,0 +1,98 @@ +package com.github.jengelman.gradle.plugins.shadow.caching + +import com.github.jengelman.gradle.plugins.shadow.util.PluginSpecification +import org.apache.commons.io.FileUtils +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import org.junit.Rule +import org.junit.rules.TemporaryFolder + +import static org.gradle.testkit.runner.TaskOutcome.FROM_CACHE +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS + + +class AbstractCachingSpec extends PluginSpecification { + @Rule TemporaryFolder alternateDir + + def setup() { + // Use a test-specific build cache directory. This ensures that we'll only use cached outputs generated during this + // test and we won't accidentally use cached outputs from a different test or a different build. + settingsFile << """ + buildCache { + local(DirectoryBuildCache) { + directory = new File(rootDir, 'build-cache') + } + } + """ + } + + void changeConfigurationTo(String content) { + buildFile.text = defaultBuildScript + buildFile << content + } + + BuildResult runWithCacheEnabled(String... arguments) { + List cacheArguments = [ '--build-cache' ] + cacheArguments.addAll(arguments) + return runner.withArguments(cacheArguments).build() + } + + BuildResult runInAlternateDirWithCacheEnabled(String... arguments) { + List cacheArguments = [ '--build-cache' ] + cacheArguments.addAll(arguments) + return alternateDirRunner.withArguments(cacheArguments).build() + } + + GradleRunner getAlternateDirRunner() { + GradleRunner.create() + .withProjectDir(alternateDir.root) + .forwardOutput() + .withPluginClasspath() + } + + private String escapedPath(File file) { + file.path.replaceAll('\\\\', '\\\\\\\\') + } + + void assertShadowJarHasResult(TaskOutcome expectedOutcome) { + def result = runWithCacheEnabled(shadowJarTask) + assert result.task(shadowJarTask).outcome == expectedOutcome + } + + void assertShadowJarHasResultInAlternateDir(TaskOutcome expectedOutcome) { + def result = runInAlternateDirWithCacheEnabled(shadowJarTask) + assert result.task(shadowJarTask).outcome == expectedOutcome + } + + void copyToAlternateDir() { + FileUtils.deleteDirectory(alternateDir.root) + FileUtils.forceMkdir(alternateDir.root) + FileUtils.copyDirectory(dir.root, alternateDir.root) + } + + void assertShadowJarIsCachedAndRelocatable() { + deleteOutputs() + copyToAlternateDir() + // check that shadowJar pulls from cache in the original directory + assertShadowJarHasResult(FROM_CACHE) + // check that shadowJar pulls from cache in a different directory + assertShadowJarHasResultInAlternateDir(FROM_CACHE) + } + + void assertShadowJarExecutes() { + deleteOutputs() + // task was executed and not pulled from cache + assertShadowJarHasResult(SUCCESS) + } + + void deleteOutputs() { + if (output.exists()) { + assert output.delete() + } + } + + String getShadowJarTask() { + return ":shadowJar" + } +} diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/caching/MinimizationCachingSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/caching/MinimizationCachingSpec.groovy new file mode 100644 index 000000000..3973efcae --- /dev/null +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/caching/MinimizationCachingSpec.groovy @@ -0,0 +1,92 @@ +package com.github.jengelman.gradle.plugins.shadow.caching + +import org.gradle.testkit.runner.BuildResult + +import static org.gradle.testkit.runner.TaskOutcome.* + +class MinimizationCachingSpec extends AbstractCachingSpec { + File output + String shadowJarTask = ":server:shadowJar" + + /** + * Ensure that we get a cache miss when minimization is added and that caching works with minimization + */ + def 'shadowJar is cached correctly when minimization is added'() { + given: + file('settings.gradle') << """ + include 'client', 'server' + """.stripIndent() + + file('client/src/main/java/client/Client.java') << """ + package client; + public class Client {} + """.stripIndent() + + file('client/build.gradle') << """ + apply plugin: 'java' + repositories { maven { url "${repo.uri}" } } + dependencies { compile 'junit:junit:3.8.2' } + """.stripIndent() + + file('server/src/main/java/server/Server.java') << """ + package server; + public class Server {} + """.stripIndent() + + file('server/build.gradle') << """ + apply plugin: 'java' + apply plugin: 'com.github.johnrengelman.shadow' + + repositories { maven { url "${repo.uri}" } } + dependencies { compile project(':client') } + """.stripIndent() + + output = getFile('server/build/libs/server-all.jar') + + when: + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class', + 'junit/framework/Test.class', + 'client/Client.class' + ]) + + when: + file('server/build.gradle').text = """ + apply plugin: 'java' + apply plugin: 'com.github.johnrengelman.shadow' + + shadowJar { + minimize { + exclude(dependency('junit:junit:.*')) + } + } + + repositories { maven { url "${repo.uri}" } } + dependencies { compile project(':client') } + """.stripIndent() + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class', + 'junit/framework/Test.class' + ]) + doesNotContain(output, ['client/Client.class']) + + when: + assertShadowJarIsCachedAndRelocatable() + + then: + output.exists() + contains(output, [ + 'server/Server.class', + 'junit/framework/Test.class' + ]) + doesNotContain(output, ['client/Client.class']) + } +} diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/caching/RelocationCachingSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/caching/RelocationCachingSpec.groovy new file mode 100644 index 000000000..25243bf52 --- /dev/null +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/caching/RelocationCachingSpec.groovy @@ -0,0 +1,68 @@ +package com.github.jengelman.gradle.plugins.shadow.caching + +class RelocationCachingSpec extends AbstractCachingSpec { + /** + * Ensure that we get a cache miss when relocation changes and that caching works with relocation + */ + def 'shadowJar is cached correctly when relocation is added'() { + given: + buildFile << """ + dependencies { compile 'junit:junit:3.8.2' } + """.stripIndent() + + file('src/main/java/server/Server.java') << """ + package server; + + import junit.framework.Test; + + public class Server {} + """.stripIndent() + + when: + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class', + 'junit/framework/Test.class' + ]) + + when: + changeConfigurationTo """ + dependencies { compile 'junit:junit:3.8.2' } + + shadowJar { + relocate 'junit.framework', 'foo.junit.framework' + } + """ + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class', + 'foo/junit/framework/Test.class' + ]) + + and: + doesNotContain(output, [ + 'junit/framework/Test.class' + ]) + + when: + assertShadowJarIsCachedAndRelocatable() + + then: + output.exists() + contains(output, [ + 'server/Server.class', + 'foo/junit/framework/Test.class' + ]) + + and: + doesNotContain(output, [ + 'junit/framework/Test.class' + ]) + } +} diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/caching/ShadowJarCachingSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/caching/ShadowJarCachingSpec.groovy new file mode 100644 index 000000000..1179e10a1 --- /dev/null +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/caching/ShadowJarCachingSpec.groovy @@ -0,0 +1,222 @@ +package com.github.jengelman.gradle.plugins.shadow.caching + +class ShadowJarCachingSpec extends AbstractCachingSpec { + + /** + * Ensure that a basic usage reuses an output from cache and then gets a cache miss when the content changes. + */ + def "shadowJar is cached correctly when copying"() { + given: + URL artifact = this.class.classLoader.getResource('test-artifact-1.0-SNAPSHOT.jar') + URL project = this.class.classLoader.getResource('test-project-1.0-SNAPSHOT.jar') + + buildFile << """ + shadowJar { + from('${artifact.path}') + from('${project.path}') + } + """.stripIndent() + + when: + assertShadowJarExecutes() + + then: + assert output.exists() + + when: + assertShadowJarIsCachedAndRelocatable() + + then: + assert output.exists() + + when: + changeConfigurationTo """ + shadowJar { + from('${artifact.path}') + } + """ + assertShadowJarExecutes() + + then: + assert output.exists() + } + + /** + * Ensure that an output is reused from the cache if only the output file name is changed. + */ + def "shadowJar is cached correctly when output file is changed"() { + given: + URL artifact = this.class.classLoader.getResource('test-artifact-1.0-SNAPSHOT.jar') + URL project = this.class.classLoader.getResource('test-project-1.0-SNAPSHOT.jar') + + buildFile << """ + shadowJar { + from('${artifact.path}') + from('${project.path}') + } + """.stripIndent() + + when: + assertShadowJarExecutes() + + then: + assert output.exists() + + when: + changeConfigurationTo """ + shadowJar { + baseName = "foo" + from('${artifact.path}') + from('${project.path}') + } + """ + assertShadowJarIsCachedAndRelocatable() + + then: + assert !output.exists() + assert getFile("build/libs/foo-1.0-all.jar").exists() + } + + /** + * Ensure that we get a cache miss when includes/excludes change and that caching works when includes/excludes are present + */ + def 'shadowJar is cached correctly when using includes/excludes'() { + given: + buildFile << """ + dependencies { compile 'junit:junit:3.8.2' } + + shadowJar { + exclude 'junit/*' + } + """.stripIndent() + + file('src/main/java/server/Server.java') << """ + package server; + + import junit.framework.Test; + + public class Server {} + """.stripIndent() + + file('src/main/java/server/Util.java') << """ + package server; + + import junit.framework.Test; + + public class Util {} + """.stripIndent() + + when: + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class', + 'server/Util.class' + ]) + + when: + changeConfigurationTo """ + dependencies { compile 'junit:junit:3.8.2' } + + shadowJar { + include 'server/*' + exclude '*/Util.*' + } + """ + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class' + ]) + + and: + doesNotContain(output, [ + 'server/Util.class', + 'junit/framework/Test.class' + ]) + + when: + assertShadowJarIsCachedAndRelocatable() + + then: + output.exists() + contains(output, [ + 'server/Server.class' + ]) + + and: + doesNotContain(output, [ + 'server/Util.class', + 'junit/framework/Test.class' + ]) + } + + /** + * Ensure that we get a cache miss when dependency includes/excludes are added and caching works when dependency includes/excludes are present + */ + def 'shadowJar is cached correctly when using dependency includes/excludes'() { + given: + buildFile << """ + dependencies { compile 'junit:junit:3.8.2' } + """.stripIndent() + + file('src/main/java/server/Server.java') << """ + package server; + + import junit.framework.Test; + + public class Server {} + """.stripIndent() + + when: + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class', + 'junit/framework/Test.class' + ]) + + when: + changeConfigurationTo """ + dependencies { compile 'junit:junit:3.8.2' } + + shadowJar { + dependencies { + exclude(dependency('junit:junit')) + } + } + """ + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class' + ]) + + and: + doesNotContain(output, [ + 'junit/framework/Test.class' + ]) + + when: + assertShadowJarIsCachedAndRelocatable() + + then: + output.exists() + contains(output, [ + 'server/Server.class' + ]) + + and: + doesNotContain(output, [ + 'junit/framework/Test.class' + ]) + } +} diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/caching/TransformCachingSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/caching/TransformCachingSpec.groovy new file mode 100644 index 000000000..30ad22213 --- /dev/null +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/caching/TransformCachingSpec.groovy @@ -0,0 +1,354 @@ +package com.github.jengelman.gradle.plugins.shadow.caching + +import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer +import com.github.jengelman.gradle.plugins.shadow.transformers.GroovyExtensionModuleTransformer +import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer +import com.github.jengelman.gradle.plugins.shadow.transformers.XmlAppendingTransformer +import spock.lang.Unroll + +class TransformCachingSpec extends AbstractCachingSpec { + /** + * Ensure that that caching is disabled when transforms are used + */ + def 'shadowJar is not cached when custom transforms are used'() { + given: + file('src/main/java/server/Server.java') << """ + package server; + + public class Server {} + """.stripIndent() + + buildFile << """ + import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer + import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext + import org.apache.tools.zip.ZipOutputStream + import org.gradle.api.file.FileTreeElement + + class CustomTransformer implements Transformer { + @Override + boolean canTransformResource(FileTreeElement element) { + return false + } + + @Override + void transform(TransformerContext context) { + + } + + @Override + boolean hasTransformedResource() { + return false + } + + @Override + void modifyOutputStream(ZipOutputStream jos, boolean preserveFileTimestamps) { + + } + } + + shadowJar { + transform(CustomTransformer) + } + """.stripIndent() + + when: + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class' + ]) + + when: + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class' + ]) + } + + /** + * Ensure that we get a cache miss when ServiceFileTransformer transforms are added and caching works when ServiceFileTransformer transforms are present + */ + @Unroll + def 'shadowJar is cached correctly when using ServiceFileTransformer'() { + given: + file('src/main/java/server/Server.java') << """ + package server; + + public class Server {} + """.stripIndent() + + when: + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class' + ]) + + when: + // Add a transform + changeConfigurationTo """ + shadowJar { + transform(${ServiceFileTransformer.name}) { + path = 'META-INF/foo' + } + } + """ + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class' + ]) + + when: + assertShadowJarIsCachedAndRelocatable() + + then: + output.exists() + contains(output, [ + 'server/Server.class' + ]) + + when: + // Change the transform configuration + changeConfigurationTo """ + shadowJar { + transform(${ServiceFileTransformer.name}) { + path = 'META-INF/bar' + } + } + """ + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class' + ]) + + when: + assertShadowJarIsCachedAndRelocatable() + + then: + output.exists() + contains(output, [ + 'server/Server.class' + ]) + } + + /** + * Ensure that we get a cache miss when AppendingTransformer transforms are added and caching works when AppendingTransformer transforms are present + */ + @Unroll + def 'shadowJar is cached correctly when using AppendingTransformer'() { + given: + file('src/main/resources/foo/bar.properties') << "foo=bar" + file('src/main/java/server/Server.java') << """ + package server; + + public class Server {} + """.stripIndent() + + when: + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class' + ]) + + when: + // Add a transform + changeConfigurationTo """ + shadowJar { + transform(${AppendingTransformer.name}) { + resource = 'foo/bar.properties' + } + } + """ + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class', + 'foo/bar.properties' + ]) + + when: + assertShadowJarIsCachedAndRelocatable() + + then: + output.exists() + contains(output, [ + 'server/Server.class', + 'foo/bar.properties' + ]) + + when: + // Change the transform configuration + assert file('src/main/resources/foo/bar.properties').delete() + file('src/main/resources/foo/baz.properties') << "foo=baz" + changeConfigurationTo """ + shadowJar { + transform(${AppendingTransformer.name}) { + resource = 'foo/baz.properties' + } + } + """ + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class', + 'foo/baz.properties' + ]) + + when: + assertShadowJarIsCachedAndRelocatable() + + then: + output.exists() + contains(output, [ + 'server/Server.class', + 'foo/baz.properties' + ]) + } + + /** + * Ensure that we get a cache miss when XmlAppendingTransformer transforms are added and caching works when XmlAppendingTransformer transforms are present + */ + @Unroll + def 'shadowJar is cached correctly when using XmlAppendingTransformer'() { + given: + file('src/main/resources/foo/bar.xml') << "bar" + file('src/main/java/server/Server.java') << """ + package server; + + public class Server {} + """.stripIndent() + + when: + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class' + ]) + + when: + // Add a transform + changeConfigurationTo """ + shadowJar { + transform(${XmlAppendingTransformer.name}) { + resource = 'foo/bar.xml' + } + } + """ + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class', + 'foo/bar.xml' + ]) + + when: + assertShadowJarIsCachedAndRelocatable() + + then: + output.exists() + contains(output, [ + 'server/Server.class', + 'foo/bar.xml' + ]) + + when: + // Change the transform configuration + assert file('src/main/resources/foo/bar.xml').delete() + file('src/main/resources/foo/baz.xml') << "baz" + changeConfigurationTo """ + shadowJar { + transform(${AppendingTransformer.name}) { + resource = 'foo/baz.xml' + } + } + """ + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class', + 'foo/baz.xml' + ]) + + when: + assertShadowJarIsCachedAndRelocatable() + + then: + output.exists() + contains(output, [ + 'server/Server.class', + 'foo/baz.xml' + ]) + } + + /** + * Ensure that we get a cache miss when GroovyExtensionModuleTransformer transforms are added and caching works when GroovyExtensionModuleTransformer transforms are present + */ + @Unroll + def 'shadowJar is cached correctly when using GroovyExtensionModuleTransformer'() { + given: + file('src/main/java/server/Server.java') << """ + package server; + + public class Server {} + """.stripIndent() + + when: + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class' + ]) + + when: + // Add a transform + changeConfigurationTo """ + shadowJar { + transform(${GroovyExtensionModuleTransformer.name}) + } + """ + assertShadowJarExecutes() + + then: + output.exists() + contains(output, [ + 'server/Server.class' + ]) + + when: + assertShadowJarIsCachedAndRelocatable() + + then: + output.exists() + contains(output, [ + 'server/Server.class' + ]) + } +}