diff --git a/api/shadow.api b/api/shadow.api index 7171f6849..002065b73 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -48,6 +48,7 @@ public abstract interface annotation class com/github/jengelman/gradle/plugins/s } public abstract interface class com/github/jengelman/gradle/plugins/shadow/ShadowExtension { + public abstract fun getAddExcludedDependenciesToShadowConfiguration ()Lorg/gradle/api/provider/Property; public abstract fun getAddShadowJarToAssembleLifecycle ()Lorg/gradle/api/provider/Property; public abstract fun getAddShadowVariantIntoJavaComponent ()Lorg/gradle/api/provider/Property; public abstract fun getAddTargetJvmVersionAttribute ()Lorg/gradle/api/provider/Property; diff --git a/docs/changes/README.md b/docs/changes/README.md index 647063868..3a75b3220 100644 --- a/docs/changes/README.md +++ b/docs/changes/README.md @@ -6,6 +6,13 @@ ### Added - Extract R8 rules from dependency JARs when using `minimize { r8 { ... } }`. ([#2089](https://github.com/GradleUp/shadow/pull/2089)) +- Add an opt-in option to add excluded dependencies into `shadow` configuration. ([#2106](https://github.com/GradleUp/shadow/pull/2106)) + ```kotlin + shadow { + // This is disabled by default. + addExcludedDependenciesToShadowConfiguration = true + } + ``` ### Changed diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 28028c22b..c5960add3 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -157,11 +157,30 @@ When configuring publishing with the Shadow plugin, the dependencies in the `sha configuration, are translated to become `RUNTIME` scoped dependencies of the published artifact. -No other dependencies are automatically configured for inclusion in the POM file. -For example, excluded dependencies are **not** automatically added to the POM file or -if the configuration for merging are modified by specifying -`shadowJar.configurations = [configurations.myConfiguration]`, there is no automatic -configuration of the POM file. +By default, no other dependencies are automatically configured for inclusion in the POM file. +You can opt in to adding dependencies excluded by `shadowJar.dependencies` to the `shadow` +configuration. They are added as non-transitive dependencies so that their own dependencies, +which may already be merged into the shadowed JAR, are not exposed to consumers again. + +=== "Kotlin" + + ```kotlin + shadow { + addExcludedDependenciesToShadowConfiguration = true + } + ``` + +=== "Groovy" + + ```groovy + shadow { + addExcludedDependenciesToShadowConfiguration = true + } + ``` + +If the configuration for merging is modified by specifying +`shadowJar.configurations = [configurations.myConfiguration]`, the excluded dependencies from +that configuration are also added when this option is enabled. This automatic configuration occurs _only_ when using the above methods for configuring publishing. If this behavior is not desirable, then publishing **must** diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/ApplicationPluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/ApplicationPluginTest.kt index ff0c213e4..ee8731fcd 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/ApplicationPluginTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/ApplicationPluginTest.kt @@ -117,6 +117,41 @@ class ApplicationPluginTest : BasePluginTest() { .contains("Hello, World! (bar) from Main", "Refs: junit.framework.Test") } + @Test + fun installShadowIncludesExcludedDependencyWithoutItsTransitiveDependencies() { + prepare( + dependenciesBlock = "implementation 'my:d:1.0'", + projectBlock = + """ + shadow { + addExcludedDependenciesToShadowConfiguration = true + } + shadowJar { + dependencies { + exclude(dependency('my:d:1.0')) + } + } + """ + .trimIndent(), + ) + + runWithSuccess(installShadowDistPath) + + val installPath = path("build/install/") + assertThat(installPath.walkEntries()) + .containsOnly( + "myapp-shadow/bin/myapp", + "myapp-shadow/bin/myapp.bat", + "myapp-shadow/lib/d-1.0.jar", + "myapp-shadow/lib/myapp-1.0-all.jar", + ) + commonAssertions( + jarPath("myapp-shadow/lib/myapp-1.0-all.jar", installPath), + entriesContained = arrayOf(mainClass, "c.properties"), + classPathAttr = "d-1.0.jar", + ) + } + @Test fun installShadowDoesNotExecuteDependentShadowTask() { prepare() diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt index 8e0aa9af1..b4b69050e 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt @@ -25,6 +25,7 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.getMainAttr import com.github.jengelman.gradle.plugins.shadow.testkit.getStream import com.github.jengelman.gradle.plugins.shadow.testkit.testGradleVersion import com.github.jengelman.gradle.plugins.shadow.util.Issue +import com.github.jengelman.gradle.plugins.shadow.util.JarBuilder import com.github.jengelman.gradle.plugins.shadow.util.prependText import com.github.jengelman.gradle.plugins.shadow.util.runProcess import kotlin.io.path.appendText @@ -604,6 +605,121 @@ class JavaPluginsTest : BasePluginTest() { assertThat(value).isEqualTo("junit-3.8.2.jar") } + @Issue("https://github.com/GradleUp/shadow/issues/265") + @Test + fun addExcludedDependenciesIntoShadowConfiguration() { + projectScript.appendText( + """ + dependencies { + shadow 'my:a:1.0' + implementation 'my:b:1.0' + } + shadow { + addExcludedDependenciesToShadowConfiguration = true + } + $shadowJarTask { + dependencies { + exclude(dependency('my:b:1.0')) + } + } + """ + .trimIndent() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedJar).useAll { + containsOnly(*manifestEntries) + getMainAttr(classPathAttributeKey).isEqualTo("b-1.0.jar a-1.0.jar") + } + } + + @Test + fun addExcludedDependencyIntoShadowConfigurationWithoutItsTransitiveDependencies() { + projectScript.appendText( + """ + dependencies { + implementation 'my:d:1.0' + } + shadow { + addExcludedDependenciesToShadowConfiguration = true + } + $shadowJarTask { + dependencies { + exclude(dependency('my:d:1.0')) + } + } + """ + .trimIndent() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedJar).useAll { + containsOnly("c.properties", *manifestEntries) + getMainAttr(classPathAttributeKey).isEqualTo("d-1.0.jar") + } + } + + @Test + fun addExcludedProjectDependencyIntoShadowConfiguration() { + settingsScript.appendText("include 'client'$lineSeparator") + path("client/build.gradle").writeText(getDefaultProjectBuildScript()) + projectScript.appendText( + """ + dependencies { + implementation project(':client') + } + shadow { + addExcludedDependenciesToShadowConfiguration = true + } + $shadowJarTask { + dependencies { + exclude(project(':client')) + } + } + """ + .trimIndent() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedJar).useAll { + containsOnly(*manifestEntries) + getMainAttr(classPathAttributeKey).isEqualTo("client-1.0.jar") + } + } + + @Test + fun preserveArtifactSelectorWhenAddingExcludedDependencyIntoShadowConfiguration() { + JarBuilder(localRepo.root.resolve("my/a/1.0/a-1.0-tests.jar")) + .insert("a-tests.properties", "a-tests") + .write() + projectScript.appendText( + """ + dependencies { + implementation 'my:a:1.0:tests' + } + shadow { + addExcludedDependenciesToShadowConfiguration = true + } + $shadowJarTask { + dependencies { + exclude(dependency('my:a:1.0')) + } + } + """ + .trimIndent() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedJar).useAll { + containsOnly(*manifestEntries) + getMainAttr(classPathAttributeKey).isEqualTo("a-1.0-tests.jar") + } + } + @Issue("https://github.com/GradleUp/shadow/issues/203") @ParameterizedTest @EnumSource(ZipEntryCompression::class) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index a790c6a79..fc7cecb9e 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -57,6 +57,44 @@ class PublishingTest : BasePluginTest() { settingsScript.appendText("rootProject.name = 'maven'$lineSeparator") } + @Test + fun publishExcludedDependencyWithoutItsTransitiveDependencies() { + projectScript.appendText( + publishConfiguration( + projectBlock = + """ + shadow { + addExcludedDependenciesToShadowConfiguration = true + } + """ + .trimIndent(), + dependenciesBlock = "implementation 'my:d:1.0'", + shadowBlock = + """ + dependencies { + exclude(dependency('my:d:1.0')) + } + """ + .trimIndent(), + ) + lineSeparator + ) + + publish() + + val pomPath = repoPath("my/maven-all/1.0/maven-all-1.0.pom") + assertPomCommon(pomPath, arrayOf("my:d:1.0")) + assertThat( + pomReader.read(pomPath).dependencies.single().exclusions.map { exclusion -> + "${exclusion.groupId}:${exclusion.artifactId}" + } + ) + .containsOnly("*:*") + assertShadowVariantCommon( + gmmAdapter.fromJson(repoPath("my/maven-all/1.0/maven-all-1.0.module")), + coordinates = arrayOf("my:d:1.0"), + ) + } + @Test fun publishShadowJarWithCorrectTargetJvm() { projectScript.appendText( diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin.kt index ea43d72e1..f0e466400 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin.kt @@ -16,6 +16,7 @@ public abstract class ShadowBasePlugin : Plugin { override fun apply(project: Project): Unit = with(project) { with(extensions.create(EXTENSION_NAME, ShadowExtension::class.java)) { + addExcludedDependenciesToShadowConfiguration.convention(false) addShadowVariantIntoJavaComponent.convention(true) addTargetJvmVersionAttribute.convention(true) bundlingAttribute.convention(Bundling.SHADOWED) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowExtension.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowExtension.kt index 784dc4db7..48cbe86ea 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowExtension.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowExtension.kt @@ -5,6 +5,16 @@ import org.gradle.api.attributes.java.TargetJvmVersion import org.gradle.api.provider.Property public interface ShadowExtension { + /** + * If `true`, dependencies excluded from the shadowed JAR by its dependency filter are added to + * the `shadow` configuration as non-transitive dependencies. This makes them available to + * consumers of the published shadowed component without also pulling in dependencies that were + * merged into the JAR. + * + * Defaults to `false`. + */ + public val addExcludedDependenciesToShadowConfiguration: Property + /** * If `true`, publishes the [ShadowJavaPlugin.SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME] as an * optional variant of the `java` component. This affects how consumers resolve the published diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 01fecd521..71b62035f 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -16,6 +16,10 @@ import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.ConfigurationContainer import org.gradle.api.artifacts.ConsumableConfiguration +import org.gradle.api.artifacts.ExternalModuleDependency +import org.gradle.api.artifacts.ModuleDependency +import org.gradle.api.artifacts.ResolvedArtifact +import org.gradle.api.artifacts.component.ProjectComponentIdentifier import org.gradle.api.attributes.Bundling import org.gradle.api.attributes.Category import org.gradle.api.attributes.LibraryElements @@ -50,6 +54,25 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl protected open fun Project.configureConfigurations() { val shadowConfig = configurations.shadow + + shadowConfig.configure { configuration -> + configuration.dependencies.addAllLater( + shadow.addExcludedDependenciesToShadowConfiguration.flatMap { enabled -> + if (!enabled) return@flatMap provider { emptyList() } + + tasks.shadowJar.map { shadowJar -> + val includedFiles = shadowJar.includedDependencies.files + shadowJar.configurations + .get() + .flatMap { it.resolvedConfiguration.resolvedArtifacts } + .filterNot { it.file in includedFiles } + .distinctBy { it.id.componentIdentifier to it.file } + .map { artifact -> createShadowDependency(artifact) } + } + } + ) + } + val compileClasspathConfig = configurations.named(COMPILE_CLASSPATH_CONFIGURATION_NAME) { compileClasspath -> compileClasspath.extendsFromCompat(shadowConfig) @@ -114,6 +137,29 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl } } + private fun Project.createShadowDependency(artifact: ResolvedArtifact): ModuleDependency { + val componentIdentifier = artifact.id.componentIdentifier + val dependency = + if ( + componentIdentifier is ProjectComponentIdentifier && + componentIdentifier.build.buildPath == ":" + ) { + dependencies.project(mapOf("path" to componentIdentifier.projectPath)) as ModuleDependency + } else { + dependencies.create(artifact.moduleVersion.id.toString()) as ExternalModuleDependency + } + dependency.isTransitive = false + if (dependency is ExternalModuleDependency) { + dependency.artifact { dependencyArtifact -> + dependencyArtifact.name = artifact.name + dependencyArtifact.type = artifact.type + dependencyArtifact.extension = artifact.extension + dependencyArtifact.classifier = artifact.classifier + } + } + return dependency + } + protected open fun Project.configureComponents() { val shadowRuntimeElements = configurations.shadowRuntimeElements val shadowComponent = softwareComponentFactory.adhoc(COMPONENT_NAME) diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt index 7ce1be2f8..4627ac9a5 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt @@ -64,6 +64,7 @@ class ShadowPropertiesTest { assertThat(tasks.findByName(SHADOW_JAR_TASK_NAME)).isNull() with(extensions.getByType(ShadowExtension::class.java)) { + assertThat(addExcludedDependenciesToShadowConfiguration.get()).isFalse() assertThat(addShadowVariantIntoJavaComponent.get()).isTrue() assertThat(addTargetJvmVersionAttribute.get()).isTrue() assertThat(bundlingAttribute.get()).isEqualTo(Bundling.SHADOWED)