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
3 changes: 2 additions & 1 deletion api/shadow.api
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ public final class com/github/jengelman/gradle/plugins/shadow/tasks/InheritManif

public class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction : org/gradle/api/internal/file/copy/CopyAction {
public static final field Companion Lcom/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction$Companion;
public fun <init> (Ljava/io/File;Lkotlin/jvm/functions/Function1;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;ZLjava/lang/String;)V
public fun <init> (Ljava/io/File;Lkotlin/jvm/functions/Function1;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;ZZLjava/lang/String;)V
public fun execute (Lorg/gradle/api/internal/file/copy/CopyActionProcessingStream;)Lorg/gradle/api/tasks/WorkResult;
}

Expand All @@ -205,6 +205,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar
public fun getDependencyFilter ()Lorg/gradle/api/provider/Property;
public fun getEnableAutoRelocation ()Lorg/gradle/api/provider/Property;
public fun getExcludes ()Ljava/util/Set;
public fun getFailOnDuplicateEntries ()Lorg/gradle/api/provider/Property;
public fun getIncludedDependencies ()Lorg/gradle/api/file/ConfigurableFileCollection;
public fun getIncludes ()Ljava/util/Set;
public fun getManifest ()Lcom/github/jengelman/gradle/plugins/shadow/tasks/InheritManifest;
Expand Down
24 changes: 16 additions & 8 deletions docs/changes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,26 @@
- Add `PreserveFirstFoundResourceTransformer`. ([#1548](https://github.com/GradleUp/shadow/pull/1548))
This is useful when you set `shadowJar.duplicatesStrategy = DuplicatesStrategy.INCLUDE` (the default behavior) and
want to ensure that only the first found resource is included in the final JAR.
- Fail build if the ZIP entries in the shadowed JAR are duplicate. ([#1552](https://github.com/GradleUp/shadow/pull/1552))
This feature is controlled by the `shadowJar.failOnDuplicateEntries` property, which is `false` by default.
Related to setting `duplicatesStrategy = DuplicatesStrategy.FAIL` but there are some differences:
- It only checks the entries in the shadowed jar, not the input files.
- It works with setting `duplicatesStrategy` to any value.
- It provides a more strict check before the JAR is created.

### Changed

- **BREAKING CHANGE:** Rename `ShadowJar`'s `enableRelocation` to `enableAutoRelocation`. ([#1541](https://github.com/GradleUp/shadow/pull/1541))
The Command Line options are also updated:
```
--enable-auto-relocation Enables auto relocation of packages in the dependencies.
--no-enable-auto-relocation Disables option --enable-auto-relocation.
--minimize-jar Minimizes the jar by removing unused classes.
--no-minimize-jar Disables option --minimize-jar.
--relocation-prefix Prefix used for auto relocation of packages in the dependencies.
--rerun Causes the task to be re-run even if up-to-date.
--enable-auto-relocation Enables auto relocation of packages in the dependencies.
--no-enable-auto-relocation Disables option --enable-auto-relocation.
--fail-on-duplicate-entries Fails build if the ZIP entries in the shadowed JAR are duplicate.
--no-fail-on-duplicate-entries Disables option --fail-on-duplicate-entries.
--minimize-jar Minimizes the jar by removing unused classes.
--no-minimize-jar Disables option --minimize-jar.
--relocation-prefix Prefix used for auto relocation of packages in the dependencies.
--rerun Causes the task to be re-run even if up-to-date.
```

## [9.0.0-rc2](https://github.com/GradleUp/shadow/releases/tag/9.0.0-rc2) - 2025-07-23
Expand Down Expand Up @@ -95,7 +103,7 @@
- **BREAKING CHANGE:** Mark `RelocatorRemapper` as `internal`. ([#1227](https://github.com/GradleUp/shadow/pull/1227))
- **BREAKING CHANGE:** Bump min Java requirement to 11. ([#1242](https://github.com/GradleUp/shadow/pull/1242))
- **BREAKING CHANGE:** Move tracking unused classes logic out of `ShadowCopyAction`. ([#1257](https://github.com/GradleUp/shadow/pull/1257))
- Reduce duplicated `SimpleRelocator` to improve performance. ([#1271](https://github.com/GradleUp/shadow/pull/1271))
- Reduce duplicate `SimpleRelocator` to improve performance. ([#1271](https://github.com/GradleUp/shadow/pull/1271))
- **BREAKING CHANGE:** Move `DependencyFilter` into `tasks` package. ([#1272](https://github.com/GradleUp/shadow/pull/1272))
- **BREAKING CHANGE:** Change the default `duplicatesStrategy` from `EXCLUDE` to `INCLUDE`. ([#1233](https://github.com/GradleUp/shadow/pull/1233))
- `ShadowJar` recognized `DuplicatesStrategy.EXCLUDE` as the default, but the other strategies didn't work properly.
Expand Down Expand Up @@ -282,7 +290,7 @@
### Changed

- **BREAKING CHANGE:** Move tracking unused classes logic out of `ShadowCopyAction`. ([#1257](https://github.com/GradleUp/shadow/pull/1257))
- Reduce duplicated `SimpleRelocator` to improve performance. ([#1271](https://github.com/GradleUp/shadow/pull/1271))
- Reduce duplicate `SimpleRelocator` to improve performance. ([#1271](https://github.com/GradleUp/shadow/pull/1271))
- **BREAKING CHANGE:** Move `DependencyFilter` into `tasks` package. ([#1272](https://github.com/GradleUp/shadow/pull/1272))
- **BREAKING CHANGE:** Change the default `duplicatesStrategy` from `EXCLUDE` to `INCLUDE`. ([#1233](https://github.com/GradleUp/shadow/pull/1233))
- `ShadowJar` recognized `DuplicatesStrategy.EXCLUDE` as the default, but the other strategies didn't work properly.
Expand Down
6 changes: 3 additions & 3 deletions docs/configuration/merging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ override it like:
Different strategies will lead to different results for `foo/bar` files in the JARs to be merged:

- `EXCLUDE`: The **first** `foo/bar` file will be included in the final JAR.
- `FAIL`: **Fail** the build with a `DuplicateFileCopyingException` if there are duplicated `foo/bar` files.
- `FAIL`: **Fail** the build with a `DuplicateFileCopyingException` if there are duplicate `foo/bar` files.
- `INCLUDE`: The **last** `foo/bar` file will be included in the final JAR (the default behavior).
- `INHERIT`: **Fail** the build with an exception like
`Entry .* is a duplicate but no duplicate handling strategy has been set`.
Expand Down Expand Up @@ -424,11 +424,11 @@ Different strategies will lead to different results for `foo/bar` files in the J
```

The [`ServiceFileTransformer`][ServiceFileTransformer] will not work as expected because the `duplicatesStrategy` will
exclude the duplicated service files beforehand. However, this behavior might be what you expected for duplicated
exclude the duplicate service files beforehand. However, this behavior might be what you expected for duplicate
`foo/bar` files, preventing them from being included.
Want `ResourceTransformer`s and `duplicatesStrategy` to work together? There is a way to achieve this, leave the
`duplicatesStrategy` as `INCLUDE` and declare a custom [`ResourceTransformer`][ResourceTransformer] to handle the
duplicated files.
duplicate files.
If you just want to keep the current behavior and preserve the first found resource, there is a simple built-in one to
handle this called [`PreserveFirstFoundResourceTransformer`][PreserveFirstFoundResourceTransformer].

Expand Down
14 changes: 8 additions & 6 deletions docs/getting-started/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,14 @@ build script. Passing property values on the command line is particularly helpfu
Here are the options that can be passed to the `shadowJar`:

```
--enable-auto-relocation Enables auto relocation of packages in the dependencies.
--no-enable-auto-relocation Disables option --enable-auto-relocation.
--minimize-jar Minimizes the jar by removing unused classes.
--no-minimize-jar Disables option --minimize-jar.
--relocation-prefix Prefix used for auto relocation of packages in the dependencies.
--rerun Causes the task to be re-run even if up-to-date.
--enable-auto-relocation Enables auto relocation of packages in the dependencies.
--no-enable-auto-relocation Disables option --enable-auto-relocation.
--fail-on-duplicate-entries Fails build if the ZIP entries in the shadowed JAR are duplicate.
--no-fail-on-duplicate-entries Disables option --fail-on-duplicate-entries.
--minimize-jar Minimizes the jar by removing unused classes.
--no-minimize-jar Disables option --minimize-jar.
--relocation-prefix Prefix used for auto relocation of packages in the dependencies.
--rerun Causes the task to be re-run even if up-to-date.
```

Also, you can view more information about the [`ShadowJar`][ShadowJar] task by running the following command:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ class JavaPluginTest : BasePluginTest() {
isNotEmpty()
containsOnly(project.runtimeConfiguration)
}
assertThat(failOnDuplicateEntries.get()).isFalse()
}

assertThat(shadowConfig.artifacts.files).contains(shadowTask.archiveFile.get().asFile)
Expand All @@ -107,6 +108,8 @@ class JavaPluginTest : BasePluginTest() {
assertThat(result.output).contains(
"--enable-auto-relocation Enables auto relocation of packages in the dependencies.",
"--no-enable-auto-relocation Disables option --enable-auto-relocation.",
"--fail-on-duplicate-entries Fails build if the ZIP entries in the shadowed JAR are duplicate.",
"--no-fail-on-duplicate-entries Disables option --fail-on-duplicate-entries",
"--minimize-jar Minimizes the jar by removing unused classes.",
"--no-minimize-jar Disables option --minimize-jar.",
"--relocation-prefix Prefix used for auto relocation of packages in the dependencies.",
Expand Down Expand Up @@ -828,6 +831,57 @@ class JavaPluginTest : BasePluginTest() {
}
}

@ParameterizedTest
@ValueSource(booleans = [false, true])
fun failBuildIfDuplicateEntries(enable: Boolean) {
path("src/main/resources/a.properties").writeText("invalid a")
projectScriptPath.appendText(
"""
dependencies {
${implementationFiles(artifactAJar)}
}
$shadowJar {
failOnDuplicateEntries = $enable
}
""".trimIndent(),
)

val result = if (enable) {
runWithFailure(shadowJarTask)
} else {
run(shadowJarTask, "--info")
}

assertThat(result.output).contains(
"Duplicate entries found in the shadowed JAR:",
"a.properties (2 times)",
)
}

@ParameterizedTest
@ValueSource(booleans = [false, true])
fun failBuildIfDuplicateEntriesByCliOption(enable: Boolean) {
path("src/main/resources/a.properties").writeText("invalid a")
projectScriptPath.appendText(
"""
dependencies {
${implementationFiles(artifactAJar)}
}
""".trimIndent(),
)

val result = if (enable) {
runWithFailure(shadowJarTask, "--fail-on-duplicate-entries")
} else {
run(shadowJarTask, "--info")
}

assertThat(result.output).contains(
"Duplicate entries found in the shadowed JAR:",
"a.properties (2 times)",
)
}

private fun dependencies(configuration: String, vararg flags: String): String {
return run("dependencies", "--configuration", configuration, *flags).output
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ class RelocationTest : BasePluginTest() {

@ParameterizedTest
@MethodSource("relocationCliOptionProvider")
fun enableAutoRelocationByCliOption(enableAutoRelocation: Boolean, relocationPrefix: String) {
fun enableAutoRelocationByCliOption(enable: Boolean, relocationPrefix: String) {
val mainClassEntry = writeClass()
projectScriptPath.appendText(
"""
Expand All @@ -557,14 +557,14 @@ class RelocationTest : BasePluginTest() {
.filterNot { it.startsWith("$relocationPrefix/META-INF/") }
.toTypedArray()

if (enableAutoRelocation) {
if (enable) {
run(shadowJarTask, "--enable-auto-relocation", "--relocation-prefix=$relocationPrefix")
} else {
run(shadowJarTask, "--relocation-prefix=$relocationPrefix")
}

assertThat(outputShadowJar).useAll {
if (enableAutoRelocation) {
if (enable) {
containsOnly(
"my/",
"$relocationPrefix/",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public open class ShadowCopyAction(
private val relocators: Set<Relocator>,
private val unusedClasses: Set<String>,
private val preserveFileTimestamps: Boolean,
private val failOnDuplicateEntries: Boolean,
private val encoding: String?,
) : CopyAction {
private val visitedDirs = mutableMapOf<String, FileCopyDetails>()
Expand All @@ -53,8 +54,8 @@ public open class ShadowCopyAction(
zipOutStream.use { zos ->
stream.process(StreamAction(zos))
processTransformers(zos)
// This must be called as the last step to ensure that directories are added after all files.
addDirs(zos)
addDirs(zos) // This must be called after adding all file entries to avoid duplicate directories being added.
checkDuplicateEntries(zos)
}
} catch (e: Exception) {
if (e is Zip64RequiredException || e.cause is Zip64RequiredException) {
Expand Down Expand Up @@ -89,8 +90,7 @@ public open class ShadowCopyAction(

private fun addDirs(zos: ZipOutputStream) {
@Suppress("UNCHECKED_CAST")
val entries = zos::class.java.getDeclaredField("entries").apply { isAccessible = true }
.get(zos).cast<List<ZipEntry>>().map { it.name }
val entries = zos.entries.map { it.name }
val added = entries.toMutableSet()
val currentTimeMillis = System.currentTimeMillis()

Expand Down Expand Up @@ -118,6 +118,22 @@ public open class ShadowCopyAction(
}
}

private fun checkDuplicateEntries(zos: ZipOutputStream) {
val entries = zos.entries.map { it.name }
val duplicates = entries.groupingBy { it }.eachCount().filter { it.value > 1 }
if (duplicates.isNotEmpty()) {
val dupEntries = duplicates.entries.joinToString(separator = "\n") {
"${it.key} (${it.value} times)"
}
val message = "Duplicate entries found in the shadowed JAR: \n$dupEntries"
if (failOnDuplicateEntries) {
throw GradleException(message)
} else {
logger.info(message)
}
}
}

private inner class StreamAction(
private val zipOutStr: ZipOutputStream,
) : CopyActionProcessingStreamAction {
Expand Down Expand Up @@ -237,5 +253,11 @@ public open class ShadowCopyAction(
* 1980-02-01 00:00:00 (318182400000).
*/
public val CONSTANT_TIME_FOR_ZIP_ENTRIES: Long = GregorianCalendar(1980, 1, 1, 0, 0, 0).timeInMillis

private val ZipOutputStream.entries: List<ZipEntry>
get() {
return this::class.java.getDeclaredField("entries").apply { isAccessible = true }
.get(this).cast<List<ZipEntry>>()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,20 @@ public abstract class ShadowJar :
@get:Option(option = "relocation-prefix", description = "Prefix used for auto relocation of packages in the dependencies.")
public open val relocationPrefix: Property<String> = objectFactory.property(ShadowBasePlugin.SHADOW)

/**
* Fails build if the ZIP entries in the shadowed JAR are duplicate.
*
* This is related to setting [duplicatesStrategy] to [DuplicatesStrategy.FAIL] but there are some differences:
* - It only checks the entries in the shadowed jar, not the input files.
* - It works with setting [duplicatesStrategy] to any value.
* - It provides a more strict check before the JAR is created.
*
* Defaults to `false`.
*/
@get:Input
@get:Option(option = "fail-on-duplicate-entries", description = "Fails build if the ZIP entries in the shadowed JAR are duplicate.")
public open val failOnDuplicateEntries: Property<Boolean> = objectFactory.property(false)

@Internal
override fun getManifest(): InheritManifest = super.getManifest() as InheritManifest

Expand Down Expand Up @@ -362,12 +376,13 @@ public abstract class ShadowJar :
emptySet()
}
return ShadowCopyAction(
archiveFile.get().asFile,
zosProvider,
transformers.get(),
relocators.get() + packageRelocators,
unusedClasses,
isPreserveFileTimestamps,
zipFile = archiveFile.get().asFile,
zosProvider = zosProvider,
transformers = transformers.get(),
relocators = relocators.get() + packageRelocators,
unusedClasses = unusedClasses,
preserveFileTimestamps = isPreserveFileTimestamps,
failOnDuplicateEntries = failOnDuplicateEntries.get(),
metadataCharset,
)
}
Expand Down