Skip to content
Merged
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
121 changes: 121 additions & 0 deletions docs/multi-project/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,128 @@ configuration of the shadowed project.
}
```

## Making the Shadowed JAR the Default Artifact

When a project needs to expose the shadowed JAR as its default output — so that consumers can depend on it without
specifying the `shadow` configuration explicitly — you can reconfigure the consumable configurations `apiElements` and
`runtimeElements` to publish the shadowed JAR instead of the regular JAR.

As a reminder, configurations like `api` and `implementation` are where dependencies are declared (_declarable_), while
`apiElements` and `runtimeElements` are what Gradle consumes when projects depend on each other.

By tuning these consumable configurations, a simple declaration like `implementation(project(":api"))` will resolve to
the shadowed JAR by default, preventing accidental consumption of the unshadowed artifact.

**In the shadowed project (`:api`):**

=== "Kotlin"

```kotlin
plugins {
`java-library`
id("com.gradleup.shadow")
}

configurations {
named("apiElements") {
outgoing.artifacts.clear()
outgoing.variants.clear()
outgoing.artifact(tasks.shadowJar)
}
named("runtimeElements") {
outgoing.artifacts.clear()
outgoing.variants.clear()
outgoing.artifact(tasks.shadowJar)
}
}
Comment thread
Goooler marked this conversation as resolved.
```

=== "Groovy"

```groovy
plugins {
id 'java-library'
id 'com.gradleup.shadow'
}

configurations {
apiElements {
outgoing.artifacts.clear()
outgoing.variants.clear()
outgoing.artifact(tasks.named('shadowJar'))
}
runtimeElements {
outgoing.artifacts.clear()
outgoing.variants.clear()
outgoing.artifact(tasks.named('shadowJar'))
}
}
```

!!! important

Clearing `outgoing.variants` ensures Gradle doesn't select the unshadowed `classes` variant by default during compilation.

**Consuming projects can then depend on `:api` without specifying the `shadow` configuration:**

=== "Kotlin"

```kotlin
dependencies {
implementation(project(":api"))
}
```

=== "Groovy"

```groovy
dependencies {
implementation project(':api')
}
```

### Excluding Transitive Dependencies

If you want to exclude transitive dependencies that were bundled into the shadow JAR, you can add `exclude` rules to the
configurations as well:

=== "Kotlin"

```kotlin
configurations {
named("apiElements") {
outgoing.artifacts.clear()
outgoing.variants.clear()
outgoing.artifact(tasks.shadowJar)
exclude(group = "com.example", module = "bundled-library")
}
named("runtimeElements") {
outgoing.artifacts.clear()
outgoing.variants.clear()
outgoing.artifact(tasks.shadowJar)
exclude(group = "com.example", module = "bundled-library")
}
}
```

=== "Groovy"

```groovy
configurations {
apiElements {
outgoing.artifacts.clear()
outgoing.variants.clear()
outgoing.artifact(tasks.named('shadowJar'))
exclude group: 'com.example', module: 'bundled-library'
}
runtimeElements {
outgoing.artifacts.clear()
outgoing.variants.clear()
outgoing.artifact(tasks.named('shadowJar'))
exclude group: 'com.example', module: 'bundled-library'
}
}
```

[Jar]: https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Jar.html
[ShadowJar]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,138 @@ class JavaPluginsTest : BasePluginTest() {
}
}

@Issue("https://github.com/GradleUp/shadow/issues/1893")
@Test
fun consumeShadowedProjectViaApiElementsAndRuntimeElements() {
settingsScript.appendText(
"""
Comment thread
Goooler marked this conversation as resolved.
include 'client', 'server'
"""
.trimIndent()
)
projectScript.writeText("")

path("client/src/main/java/client/Client.java")
.writeText(
"""
package client;
public class Client {}
"""
.trimIndent()
)
path("client/build.gradle")
.writeText(
"""
${getDefaultProjectBuildScript("java-library")}
Comment on lines +216 to +217
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nitpick: alignment seems off.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is formatted by ktfmt. We can do nothing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh, maybe it's ktfmt opinionated style rules that don't follow the "standard" kotlin. I know it's possible to choose the official kotlin ones, as these are not the default.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Since we require an indent size of two, we have to accept the Google style. To be honest, I prefer ktlint.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah I just saw, the project is using google

shadow/build.gradle.kts

Lines 54 to 55 in 6a52076

kotlin { ktfmt(libs.ktfmt.get().version).googleStyle() }
kotlinGradle { ktfmt(libs.ktfmt.get().version).googleStyle() }

FYI, the standard kotlin style is settable via .kotlinlangStyle()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

kotlinlangStyle enforces indent size for 4. And it's not much different from the Google style for actual styles.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Interesting! Thanks for the precision.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This should be addressed by diffplug/spotless#2872.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cool thanks

dependencies {
api 'junit:junit:3.8.2'
}
$shadowJarTask {
relocate 'junit.framework', 'client.junit.framework'
}
configurations {
apiElements {
outgoing.artifacts.clear()
outgoing.variants.clear()
outgoing.artifact($shadowJarTask)
}
runtimeElements {
outgoing.artifacts.clear()
outgoing.variants.clear()
outgoing.artifact($shadowJarTask)
}
}
"""
.trimIndent() + lineSeparator
)

path("server/src/main/java/server/Server.java")
.writeText(
"""
package server;
import client.Client;
import client.junit.framework.Test;
public class Server {}
"""
.trimIndent()
)
path("server/build.gradle")
.writeText(
"""
${getDefaultProjectBuildScript("java", applyShadowPlugin = false)}
dependencies {
// No `configuration: "shadow"` needed!
implementation project(':client')
}
"""
.trimIndent() + lineSeparator
)

// Running server:jar to ensure it compiles against the shadowed client
runWithSuccess(":server:jar")

// The fact that server compiled successfully against `client.junit.framework.Test`
// means it consumed the shadowed artifact during compilation.
assertThat(jarPath("server/build/libs/server-1.0.jar")).useAll {
containsAtLeast("server/Server.class")
}
}

@Issue("https://github.com/GradleUp/shadow/issues/1893")
@Test
fun excludeRulesPreventBundledDepsOnConsumerClasspath() {
settingsScript.appendText("include 'foo', 'consumer'$lineSeparator")
projectScript.writeText("")

path("foo/build.gradle")
.writeText(
"""
${getDefaultProjectBuildScript("java-library")}
dependencies {
implementation 'my:a:1.0'
}
configurations {
named('apiElements') {
outgoing.artifacts.clear()
outgoing.variants.clear()
outgoing.artifact(tasks.named('shadowJar'))
exclude(group: 'my', module: 'a')
}
named('runtimeElements') {
outgoing.artifacts.clear()
outgoing.variants.clear()
outgoing.artifact(tasks.named('shadowJar'))
exclude(group: 'my', module: 'a')
}
}
"""
.trimIndent() + lineSeparator
)

path("consumer/build.gradle")
.writeText(
"""
${getDefaultProjectBuildScript("java", applyShadowPlugin = false)}
dependencies {
implementation project(':foo')
}
tasks.register('printClasspathFiles') {
def cp = configurations.runtimeClasspath
doLast {
cp.files.each { logger.lifecycle(it.name) }
}
}
"""
.trimIndent() + lineSeparator
)

val result = runWithSuccess(":consumer:printClasspathFiles")
assertThat(result.output).all {
contains("foo-1.0-all.jar")
doesNotContain("a-1.0.jar")
}
}

@Issue("https://github.com/GradleUp/shadow/issues/1606")
@Test
fun shadowExposedCustomSourceSetOutput() {
Expand Down