Skip to content
Closed
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
1 change: 1 addition & 0 deletions api/shadow.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions docs/changes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
29 changes: 24 additions & 5 deletions docs/publishing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public abstract class ShadowBasePlugin : Plugin<Project> {
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Boolean>

/**
* 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) }
}
Comment on lines +63 to +71
}
)
}

val compileClasspathConfig =
configurations.named(COMPILE_CLASSPATH_CONFIGURATION_NAME) { compileClasspath ->
compileClasspath.extendsFromCompat(shadowConfig)
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down