Skip to content

Commit fd72306

Browse files
Gooolerbric3
andauthored
Document how to make a shadowed JAR the default output of a project (#1941)
Co-authored-by: Brice Dutheil <brice.dutheil@gmail.com>
1 parent 6a52076 commit fd72306

2 files changed

Lines changed: 253 additions & 0 deletions

File tree

docs/multi-project/README.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,128 @@ configuration of the shadowed project.
2828
}
2929
```
3030

31+
## Making the Shadowed JAR the Default Artifact
3132

33+
When a project needs to expose the shadowed JAR as its default output — so that consumers can depend on it without
34+
specifying the `shadow` configuration explicitly — you can reconfigure the consumable configurations `apiElements` and
35+
`runtimeElements` to publish the shadowed JAR instead of the regular JAR.
36+
37+
As a reminder, configurations like `api` and `implementation` are where dependencies are declared (_declarable_), while
38+
`apiElements` and `runtimeElements` are what Gradle consumes when projects depend on each other.
39+
40+
By tuning these consumable configurations, a simple declaration like `implementation(project(":api"))` will resolve to
41+
the shadowed JAR by default, preventing accidental consumption of the unshadowed artifact.
42+
43+
**In the shadowed project (`:api`):**
44+
45+
=== "Kotlin"
46+
47+
```kotlin
48+
plugins {
49+
`java-library`
50+
id("com.gradleup.shadow")
51+
}
52+
53+
configurations {
54+
named("apiElements") {
55+
outgoing.artifacts.clear()
56+
outgoing.variants.clear()
57+
outgoing.artifact(tasks.shadowJar)
58+
}
59+
named("runtimeElements") {
60+
outgoing.artifacts.clear()
61+
outgoing.variants.clear()
62+
outgoing.artifact(tasks.shadowJar)
63+
}
64+
}
65+
```
66+
67+
=== "Groovy"
68+
69+
```groovy
70+
plugins {
71+
id 'java-library'
72+
id 'com.gradleup.shadow'
73+
}
74+
75+
configurations {
76+
apiElements {
77+
outgoing.artifacts.clear()
78+
outgoing.variants.clear()
79+
outgoing.artifact(tasks.named('shadowJar'))
80+
}
81+
runtimeElements {
82+
outgoing.artifacts.clear()
83+
outgoing.variants.clear()
84+
outgoing.artifact(tasks.named('shadowJar'))
85+
}
86+
}
87+
```
88+
89+
!!! important
90+
91+
Clearing `outgoing.variants` ensures Gradle doesn't select the unshadowed `classes` variant by default during compilation.
92+
93+
**Consuming projects can then depend on `:api` without specifying the `shadow` configuration:**
94+
95+
=== "Kotlin"
96+
97+
```kotlin
98+
dependencies {
99+
implementation(project(":api"))
100+
}
101+
```
102+
103+
=== "Groovy"
104+
105+
```groovy
106+
dependencies {
107+
implementation project(':api')
108+
}
109+
```
110+
111+
### Excluding Transitive Dependencies
112+
113+
If you want to exclude transitive dependencies that were bundled into the shadow JAR, you can add `exclude` rules to the
114+
configurations as well:
115+
116+
=== "Kotlin"
117+
118+
```kotlin
119+
configurations {
120+
named("apiElements") {
121+
outgoing.artifacts.clear()
122+
outgoing.variants.clear()
123+
outgoing.artifact(tasks.shadowJar)
124+
exclude(group = "com.example", module = "bundled-library")
125+
}
126+
named("runtimeElements") {
127+
outgoing.artifacts.clear()
128+
outgoing.variants.clear()
129+
outgoing.artifact(tasks.shadowJar)
130+
exclude(group = "com.example", module = "bundled-library")
131+
}
132+
}
133+
```
134+
135+
=== "Groovy"
136+
137+
```groovy
138+
configurations {
139+
apiElements {
140+
outgoing.artifacts.clear()
141+
outgoing.variants.clear()
142+
outgoing.artifact(tasks.named('shadowJar'))
143+
exclude group: 'com.example', module: 'bundled-library'
144+
}
145+
runtimeElements {
146+
outgoing.artifacts.clear()
147+
outgoing.variants.clear()
148+
outgoing.artifact(tasks.named('shadowJar'))
149+
exclude group: 'com.example', module: 'bundled-library'
150+
}
151+
}
152+
```
32153

33154
[Jar]: https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Jar.html
34155
[ShadowJar]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/index.html

src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,138 @@ class JavaPluginsTest : BasePluginTest() {
192192
}
193193
}
194194

195+
@Issue("https://github.com/GradleUp/shadow/issues/1893")
196+
@Test
197+
fun consumeShadowedProjectViaApiElementsAndRuntimeElements() {
198+
settingsScript.appendText(
199+
"""
200+
include 'client', 'server'
201+
"""
202+
.trimIndent()
203+
)
204+
projectScript.writeText("")
205+
206+
path("client/src/main/java/client/Client.java")
207+
.writeText(
208+
"""
209+
package client;
210+
public class Client {}
211+
"""
212+
.trimIndent()
213+
)
214+
path("client/build.gradle")
215+
.writeText(
216+
"""
217+
${getDefaultProjectBuildScript("java-library")}
218+
dependencies {
219+
api 'junit:junit:3.8.2'
220+
}
221+
$shadowJarTask {
222+
relocate 'junit.framework', 'client.junit.framework'
223+
}
224+
configurations {
225+
apiElements {
226+
outgoing.artifacts.clear()
227+
outgoing.variants.clear()
228+
outgoing.artifact($shadowJarTask)
229+
}
230+
runtimeElements {
231+
outgoing.artifacts.clear()
232+
outgoing.variants.clear()
233+
outgoing.artifact($shadowJarTask)
234+
}
235+
}
236+
"""
237+
.trimIndent() + lineSeparator
238+
)
239+
240+
path("server/src/main/java/server/Server.java")
241+
.writeText(
242+
"""
243+
package server;
244+
import client.Client;
245+
import client.junit.framework.Test;
246+
public class Server {}
247+
"""
248+
.trimIndent()
249+
)
250+
path("server/build.gradle")
251+
.writeText(
252+
"""
253+
${getDefaultProjectBuildScript("java", applyShadowPlugin = false)}
254+
dependencies {
255+
// No `configuration: "shadow"` needed!
256+
implementation project(':client')
257+
}
258+
"""
259+
.trimIndent() + lineSeparator
260+
)
261+
262+
// Running server:jar to ensure it compiles against the shadowed client
263+
runWithSuccess(":server:jar")
264+
265+
// The fact that server compiled successfully against `client.junit.framework.Test`
266+
// means it consumed the shadowed artifact during compilation.
267+
assertThat(jarPath("server/build/libs/server-1.0.jar")).useAll {
268+
containsAtLeast("server/Server.class")
269+
}
270+
}
271+
272+
@Issue("https://github.com/GradleUp/shadow/issues/1893")
273+
@Test
274+
fun excludeRulesPreventBundledDepsOnConsumerClasspath() {
275+
settingsScript.appendText("include 'foo', 'consumer'$lineSeparator")
276+
projectScript.writeText("")
277+
278+
path("foo/build.gradle")
279+
.writeText(
280+
"""
281+
${getDefaultProjectBuildScript("java-library")}
282+
dependencies {
283+
implementation 'my:a:1.0'
284+
}
285+
configurations {
286+
named('apiElements') {
287+
outgoing.artifacts.clear()
288+
outgoing.variants.clear()
289+
outgoing.artifact(tasks.named('shadowJar'))
290+
exclude(group: 'my', module: 'a')
291+
}
292+
named('runtimeElements') {
293+
outgoing.artifacts.clear()
294+
outgoing.variants.clear()
295+
outgoing.artifact(tasks.named('shadowJar'))
296+
exclude(group: 'my', module: 'a')
297+
}
298+
}
299+
"""
300+
.trimIndent() + lineSeparator
301+
)
302+
303+
path("consumer/build.gradle")
304+
.writeText(
305+
"""
306+
${getDefaultProjectBuildScript("java", applyShadowPlugin = false)}
307+
dependencies {
308+
implementation project(':foo')
309+
}
310+
tasks.register('printClasspathFiles') {
311+
def cp = configurations.runtimeClasspath
312+
doLast {
313+
cp.files.each { logger.lifecycle(it.name) }
314+
}
315+
}
316+
"""
317+
.trimIndent() + lineSeparator
318+
)
319+
320+
val result = runWithSuccess(":consumer:printClasspathFiles")
321+
assertThat(result.output).all {
322+
contains("foo-1.0-all.jar")
323+
doesNotContain("a-1.0.jar")
324+
}
325+
}
326+
195327
@Issue("https://github.com/GradleUp/shadow/issues/1606")
196328
@Test
197329
fun shadowExposedCustomSourceSetOutput() {

0 commit comments

Comments
 (0)