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
207 changes: 202 additions & 5 deletions docs/publishing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,41 +295,98 @@ You may want to publish the shadowed JAR instead of the original JAR. This can b
```

Because the default `archiveClassifier` of [`Jar`][Jar] is `""` (empty), setting the `archiveClassifier` of
[`ShadowJar`][ShadowJar] to `""` (empty) will make collisions between the outputs of these two tasks in some cases. If
you don't need the standard JAR, you can disable the `jar` task like:
[`ShadowJar`][ShadowJar] to `""` (empty) will make collisions between the outputs of standard tasks and `shadowJar`:

- The binary shadowed JAR is output to `<archiveBaseName>-<archiveVersion>.jar`, conflicting with the `jar` task.
- When `generateSourcesJar` is enabled (such as when `java.withSourcesJar()` is used), the companion shadowed sources
JAR is output to `<archiveBaseName>-<archiveVersion>-sources.jar`, conflicting with the standard `sourcesJar` task.

If you want to replace standard JARs with the shadowed ones, disable the standard `jar` and `sourcesJar` tasks:

=== ":material-language-kotlin: build.gradle.kts"

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

java {
withSourcesJar()
}

tasks.jar {
enabled = false
}

tasks.named<Jar>("sourcesJar") {
enabled = false
}
```

=== ":simple-apachegroovy: build.gradle"

```groovy
plugins {
id('java')
id('com.gradleup.shadow')
}

java {
withSourcesJar()
}

tasks.named('jar', Jar) {
enabled = false
}

tasks.named('sourcesJar', Jar) {
enabled = false
}
```

Or set a different `archiveClassifier` for the standard [`Jar`][Jar] like:
Or set different `archiveClassifier` values for the standard tasks:

=== ":material-language-kotlin: build.gradle.kts"

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

java {
withSourcesJar()
}

tasks.jar {
archiveClassifier = "ignored"
}

tasks.named<Jar>("sourcesJar") {
(this as org.gradle.jvm.tasks.Jar).archiveClassifier = "ignored-sources"
}
```

=== ":simple-apachegroovy: build.gradle"

```groovy
plugins {
id('java')
id('com.gradleup.shadow')
}

java {
withSourcesJar()
}

tasks.named('jar', Jar) {
archiveClassifier = 'ignored'
}

tasks.named('sourcesJar', Jar) {
archiveClassifier = 'ignored-sources'
}
```

## Publishing the Shadowed Gradle Plugins
Expand Down Expand Up @@ -588,16 +645,156 @@ When Gradle's standard `java.withSourcesJar()` is enabled, the Shadow plugin aut
The published Maven publication will include both `<artifactId>-<version>-all.jar` and
`<artifactId>-<version>-all-sources.jar`.

### Local File Names vs. Published Classifiers

The Shadow plugin distinguishes between the **local output file** on disk and the **published artifact classifier** in
Maven repositories and Gradle Module Metadata:

| Configuration | Local Output File (`archiveSourcesFile` in `build/libs`) | Published Classifier | Published File (Maven Repository) | Use Case |
|:----------------------------------------|:---------------------------------------------------------|:---------------------|:--------------------------------------------|:---------------------------------------------------|
| `archiveClassifier = "all"` *(default)* | `<name>-<version>-all-sources.jar` | `all-sources` | `<artifactId>-<version>-all-sources.jar` | **Coexistence** (coexists with standard `sources`) |
| `archiveClassifier = "shaded"` | `<name>-<version>-shaded-sources.jar` | `shaded-sources` | `<artifactId>-<version>-shaded-sources.jar` | **Coexistence** (custom classifier) |
| `archiveClassifier = ""` | `<name>-<version>-sources.jar` | `sources` | `<artifactId>-<version>-sources.jar` | **Replacement** (replaces standard `sources`) |

#### Coexistence Scenario

When publishing alongside standard Java artifacts (e.g. publishing `from(components["java"])` with
`shadow.addShadowVariantIntoJavaComponent = true`), the standard sources variant uses classifier `sources`. To prevent
coordinate collisions within the same publication, the shadowed sources variant dynamically derives its classifier as
`<archiveClassifier>-sources` (such as `all-sources` or `shaded-sources`).

#### Replacement Scenario

When configuring `shadowJar` to replace the standard JAR (`archiveClassifier = ""`), the companion shadowed sources JAR
automatically uses the standard `sources` classifier.

To publish shadowed artifacts as the primary publication:

1. **Publish from `components["shadow"]` (Recommended)**: Publish the `shadow` component directly in your Maven
publication, and disable standard archive tasks to prevent destination file collisions in `build/libs`:

=== ":material-language-kotlin: build.gradle.kts"

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

java {
withSourcesJar()
}

tasks.jar {
enabled = false
}

tasks.named<Jar>("sourcesJar") {
enabled = false
}

tasks.shadowJar {
archiveClassifier = ""
}

publishing {
publications {
create<MavenPublication>("shadow") {
from(components["shadow"])
}
}
}
```

=== ":simple-apachegroovy: build.gradle"

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

java {
withSourcesJar()
}

tasks.named('jar', Jar) {
enabled = false
}

tasks.named('sourcesJar', Jar) {
enabled = false
}

tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
archiveClassifier = ''
}

publishing {
publications {
shadow(MavenPublication) {
from components.shadow
}
}
}
```

2. **Publish from `components["java"]`**: If publishing `from(components["java"])`, disabling the `jar` or `sourcesJar`
tasks does not remove standard variants from the `java` software component. You must also explicitly skip the
standard publication variants:

=== ":material-language-kotlin: build.gradle.kts"

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

java {
withSourcesJar()
}

components.named<org.gradle.api.component.AdhocComponentWithVariants>("java") {
withVariantsFromConfiguration(configurations["runtimeElements"]) { skip() }
withVariantsFromConfiguration(configurations["sourcesElements"]) { skip() }
}
```

=== ":simple-apachegroovy: build.gradle"

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

java {
withSourcesJar()
}

components.named('java', org.gradle.api.component.AdhocComponentWithVariants) {
withVariantsFromConfiguration(configurations.runtimeElements) { skip() }
withVariantsFromConfiguration(configurations.sourcesElements) { skip() }
}
```

> [!NOTE]
> Generating the companion shadowed sources JAR is controlled by [`generateSourcesJar`][ShadowJar.generateSourcesJar].
> In Java projects, it defaults to `true` when `java.withSourcesJar()` is enabled, and `false` otherwise to avoid
> unnecessary build overhead for application builds. If `withSourcesJar()` is omitted, publishing from
> `components["shadow"]` will only publish the shadowed binary JAR, preserving backward compatibility for existing builds.
> `components["shadow"]` will only publish the shadowed binary JAR, preserving backward compatibility for existing
builds.
> You can also explicitly toggle generation via `generateSourcesJar = true` (or `--generate-sources-jar`).

### Customizing the Sources Archive File

The companion shadowed sources JAR output location is configured via [`ShadowJar.archiveSourcesFile`][ShadowJar.archiveSourcesFile],
The companion shadowed sources JAR output location is configured via
[`ShadowJar.archiveSourcesFile`][ShadowJar.archiveSourcesFile],
which defaults to the same destination and base name as `archiveFile` with `-sources.jar` suffix:

=== ":material-language-kotlin: build.gradle.kts"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,10 +408,16 @@ abstract class BasePluginTest {
}
}

fun createEmptyClassBytes(internalName: String): ByteArray {
fun createEmptyClassBytes(
internalName: String,
sourceFile: String? = "${internalName.substringAfterLast('/')}.java",
): ByteArray {
return ClassWriter(0)
.apply {
visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, internalName, null, "java/lang/Object", null)
if (sourceFile != null) {
visitSource(sourceFile, null)
}
visitEnd()
}
.toByteArray()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package com.github.jengelman.gradle.plugins.shadow

import assertk.assertThat
import com.github.jengelman.gradle.plugins.shadow.testkit.classLoader
import com.github.jengelman.gradle.plugins.shadow.testkit.containsAtLeast
import com.github.jengelman.gradle.plugins.shadow.testkit.containsNone
import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly
import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass
import kotlin.io.path.appendText
Expand Down Expand Up @@ -260,12 +258,10 @@ class FilteringTest : BasePluginTest() {
runWithSuccess(shadowJarPath)

assertThat(outputShadowedJar).useAll {
containsAtLeast("g/G.class")
containsNone("h/H.class", "h/UnusedH.class")
containsOnly(*entriesInAB, "g/", "g/G.class", *manifestEntries)
}
assertThat(outputShadowedSourcesJar).useAll {
containsAtLeast("g/G.java")
containsNone("h/H.java", "h/UnusedH.java")
containsOnly("g/", "g/G.java", *manifestEntries)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ class JavaPluginsTest : BasePluginTest() {
// 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")
containsOnly("server/", "server/Server.class", *manifestEntries)
}
}

Expand Down Expand Up @@ -1176,7 +1176,13 @@ class JavaPluginsTest : BasePluginTest() {
runWithSuccess(":app:$SHADOW_JAR_TASK_NAME")

assertThat(jarPath("app/build/libs/app-all.jar")).useAll {
containsAtLeast("com/company/Main.class", "com/company/Utils.class", manifestEntry)
containsOnly(
"com/",
"com/company/",
"com/company/Main.class",
"com/company/Utils.class",
*manifestEntries,
)
}
}

Expand Down Expand Up @@ -1316,6 +1322,69 @@ class JavaPluginsTest : BasePluginTest() {
)
}

@Test
fun sourcesJarPreservesResourceRelativePath() {
writeClass()
path("src/main/resources/config/sub/app.properties").writeText("key=value")

projectScript.appendText(
"""
|$shadowJarTask {
| generateSourcesJar = true
|}
"""
.trimMargin()
)

runWithSuccess(shadowJarPath)

assertThat(outputShadowedSourcesJar).useAll {
containsOnly(
"my/",
"config/",
"config/sub/",
"my/Main.java",
"config/sub/app.properties",
*manifestEntries,
)
}
}

@Test
fun sourcesJarHandlesOverlappingSourceDirectoryPrefixes() {
writeClass()
path("src/main/res/a.properties").writeText("a=1")
path("src/main/resources/b.properties").writeText("b=2")

projectScript.appendText(
"""
|sourceSets {
| main {
| resources {
| srcDir 'src/main/res'
| }
| }
|}
|$shadowJarTask {
| generateSourcesJar = true
|}
"""
.trimMargin()
)

runWithSuccess(shadowJarPath)

assertThat(outputShadowedSourcesJar).useAll {
containsOnly(
"my/",
"my/Main.java",
"a.properties",
"b.properties",
*manifestEntries,
)
}
}

private fun dependencies(configuration: String, vararg flags: String): String {
return runWithSuccess("dependencies", "--configuration", configuration, *flags).output
}
Expand Down
Loading