diff --git a/build.gradle b/build.gradle index 9ae24de90..57b238cd5 100644 --- a/build.gradle +++ b/build.gradle @@ -118,4 +118,4 @@ task relocateShadowJar(type: ConfigureShadowRelocation) { target = tasks.shadowJar } -tasks.shadowJar.dependsOn tasks.relocateShadowJar \ No newline at end of file +tasks.shadowJar.dependsOn tasks.relocateShadowJar diff --git a/src/docs/asciidoc/15-minimizing.adoc b/src/docs/asciidoc/15-minimizing.adoc index fd5b5dc1f..48c6365b8 100644 --- a/src/docs/asciidoc/15-minimizing.adoc +++ b/src/docs/asciidoc/15-minimizing.adoc @@ -23,3 +23,8 @@ shadowJar { } } ---- + +[NOTE] +==== +Dependencies scoped as `api` will automatically excluded from minimization and used as "entry points" on minimization. +==== 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 5c0b923eb..cde3ec5eb 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 @@ -1,6 +1,8 @@ package com.github.jengelman.gradle.plugins.shadow.internal import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.ProjectDependency import org.gradle.api.file.FileCollection import org.gradle.api.tasks.SourceSet import org.vafer.jdependency.Clazz @@ -13,9 +15,10 @@ class UnusedTracker { private final List projectUnits private final Clazzpath cp = new Clazzpath() - private UnusedTracker(List classDirs, FileCollection toMinimize) { + private UnusedTracker(List classDirs, FileCollection classJars, FileCollection toMinimize) { this.toMinimize = toMinimize projectUnits = classDirs.collect { cp.addClazzpathUnit(it) } + projectUnits.addAll(classJars.collect { cp.addClazzpathUnit(it) }) } Set findUnused() { @@ -33,12 +36,33 @@ class UnusedTracker { } } - static UnusedTracker forProject(Project project, FileCollection toMinimize) { + static UnusedTracker forProject(Project project, List configurations, DependencyFilter dependencyFilter) { + def apiJars = getApiJarsFromProject(project) + FileCollection toMinimize = dependencyFilter.resolve(configurations) - apiJars + final List classDirs = new ArrayList<>() for (SourceSet sourceSet in project.sourceSets) { - Iterable classesDirs = sourceSet.output.hasProperty('classesDirs') ? sourceSet.output.classesDirs : [sourceSet.output.classesDir] + Iterable classesDirs = sourceSet.output.classesDirs classDirs.addAll(classesDirs.findAll { it.isDirectory() }) } - return new UnusedTracker(classDirs, toMinimize) + return new UnusedTracker(classDirs, apiJars, toMinimize) + } + + private static FileCollection getApiJarsFromProject(Project project) { + def apiDependencies = project.configurations.asMap['api']?.dependencies ?: null + if (apiDependencies == null) return project.files() + + def runtimeConfiguration = project.configurations.asMap['runtimeClasspath'] ?: project.configurations.runtime + def apiJars = new LinkedList() + apiDependencies.each { dep -> + if (dep instanceof ProjectDependency) { + apiJars.addAll(getApiJarsFromProject(dep.dependencyProject)) + apiJars.add(runtimeConfiguration.find { it.name.endsWith("${dep.name}.jar") } as File) + } else { + apiJars.add(runtimeConfiguration.find { it.name.startsWith("${dep.name}-") } as File) + } + } + + return project.files(apiJars) } } 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 88f0b78bb..159d90f6e 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 @@ -79,8 +79,7 @@ public InheritManifest getManifest() { @Override protected CopyAction createCopyAction() { DocumentationRegistry documentationRegistry = getServices().get(DocumentationRegistry.class); - FileCollection toMinimize = dependencyFilterForMinimize.resolve(configurations); - final UnusedTracker unusedTracker = UnusedTracker.forProject(getProject(), toMinimize); + final UnusedTracker unusedTracker = UnusedTracker.forProject(getProject(), configurations, dependencyFilterForMinimize); return new ShadowCopyAction(getArchivePath(), getInternalCompressor(), documentationRegistry, this.getMetadataCharset(), transformers, relocators, getRootPatternSet(), shadowStats, versionUtil, isPreserveFileTimestamps(), minimizeJar, unusedTracker); 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 43e0788f1..ea18ebc83 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 @@ -318,6 +318,160 @@ class ShadowPluginSpec extends PluginSpecification { doesNotContain(serverOutput, ['client/Client.class']) } + /** + * 'api' used as api for 'impl', and depended on 'lib'. 'junit' is independent. + * The minimize shall remove 'junit', but not 'api'. + * Unused classes of 'api' and theirs dependencies also shouldn't be removed. + */ + def 'use minimize with dependencies with api scope'() { + given: + file('settings.gradle') << """ + include 'api', 'lib', 'impl' + """.stripIndent() + + file('lib/src/main/java/lib/LibEntity.java') << """ + package lib; + public interface LibEntity {} + """.stripIndent() + + file('lib/src/main/java/lib/UnusedLibEntity.java') << """ + package lib; + public class UnusedLibEntity implements LibEntity {} + """.stripIndent() + + file('lib/build.gradle') << """ + apply plugin: 'java' + repositories { maven { url "${repo.uri}" } } + """.stripIndent() + + file('api/src/main/java/api/Entity.java') << """ + package api; + public interface Entity {} + """.stripIndent() + + file('api/src/main/java/api/UnusedEntity.java') << """ + package api; + import lib.LibEntity; + public class UnusedEntity implements LibEntity {} + """.stripIndent() + + file('api/build.gradle') << """ + apply plugin: 'java' + repositories { maven { url "${repo.uri}" } } + dependencies { + compile 'junit:junit:3.8.2' + compile project(':lib') + } + """.stripIndent() + + file('impl/src/main/java/impl/SimpleEntity.java') << """ + package impl; + import api.Entity; + public class SimpleEntity implements Entity {} + """.stripIndent() + + file('impl/build.gradle') << """ + apply plugin: 'java-library' + apply plugin: 'com.github.johnrengelman.shadow' + + shadowJar { + minimize() + } + + repositories { maven { url "${repo.uri}" } } + dependencies { api project(':api') } + """.stripIndent() + + File serverOutput = file('impl/build/libs/impl-all.jar') + + when: + runner.withArguments(':impl:shadowJar', '--stacktrace').withDebug(true).build() + + then: + contains(serverOutput, [ + 'impl/SimpleEntity.class', + 'api/Entity.class', + 'api/UnusedEntity.class', + 'lib/LibEntity.class', + ]) + doesNotContain(serverOutput, ['junit/framework/Test.class', 'lib/UnusedLibEntity.class']) + } + + /** + * 'api' used as api for 'impl', and 'lib' used as api for 'api'. + * Unused classes of 'api' and 'lib' shouldn't be removed. + */ + def 'use minimize with transitive dependencies with api scope'() { + given: + file('settings.gradle') << """ + include 'api', 'lib', 'impl' + """.stripIndent() + + file('lib/src/main/java/lib/LibEntity.java') << """ + package lib; + public interface LibEntity {} + """.stripIndent() + + file('lib/src/main/java/lib/UnusedLibEntity.java') << """ + package lib; + public class UnusedLibEntity implements LibEntity {} + """.stripIndent() + + file('lib/build.gradle') << """ + apply plugin: 'java' + repositories { maven { url "${repo.uri}" } } + """.stripIndent() + + file('api/src/main/java/api/Entity.java') << """ + package api; + public interface Entity {} + """.stripIndent() + + file('api/src/main/java/api/UnusedEntity.java') << """ + package api; + import lib.LibEntity; + public class UnusedEntity implements LibEntity {} + """.stripIndent() + + file('api/build.gradle') << """ + apply plugin: 'java-library' + repositories { maven { url "${repo.uri}" } } + dependencies { api project(':lib') } + """.stripIndent() + + file('impl/src/main/java/impl/SimpleEntity.java') << """ + package impl; + import api.Entity; + public class SimpleEntity implements Entity {} + """.stripIndent() + + file('impl/build.gradle') << """ + apply plugin: 'java-library' + apply plugin: 'com.github.johnrengelman.shadow' + + shadowJar { + minimize() + } + + repositories { maven { url "${repo.uri}" } } + dependencies { api project(':api') } + """.stripIndent() + + File serverOutput = file('impl/build/libs/impl-all.jar') + + when: + runner.withArguments(':impl:shadowJar', '--stacktrace').withDebug(true).build() + + then: + contains(serverOutput, [ + 'impl/SimpleEntity.class', + 'api/Entity.class', + 'api/UnusedEntity.class', + 'lib/LibEntity.class', + 'lib/UnusedLibEntity.class' + ]) + } + def 'depend on project shadow jar'() { given: file('settings.gradle') << """