From 451e92f72473734dee35fc3d67569c13430d2bfe Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Sat, 15 Nov 2025 12:42:17 +0100 Subject: [PATCH 01/30] Add transformer to deduplicate identical files based content Adds a new `DeduplicatingResourceTransformer` that works different than `PreserveFirstFoundResourceTransformer`. `PreserveFirstFoundResourceTransformer` is to preserve the first resource that matches the configured paths and ignore all other ones. `DeduplicatingResourceTransformer` preserves resources by path _and_ identical content and fails for all not explicitly allowed (excluded) resources with different content. It works intentionally against all resources. The new one is intended to guard a couple of unexpected situations: * A (transitive) dependency brings a non-relocated version of a dependency that is also included elsewhere but with a different version. This could normally lead to unexpected exceptions during runtime. * Unintended inclusion or removal or legally important license information, see also `MergeLicenseResourceTransformer` (#1858). * Unintended removal or (false) inclusion of shaded dependency information via `META-INF/x/y/pom.xml`/`.properties` files, which can be important for dependency/license analyzation tools. Adding the functionality of `DeduplicatingResourceTransformer` to `PreserveFirstFoundResourceTransformer` became a bit too difficult without breaking the existing behavior of the latter. --- api/shadow.api | 9 ++ docs/changes/README.md | 1 + .../DeduplicatingResourceTransformerTest.kt | 86 +++++++++++ .../DeduplicatingResourceTransformer.kt | 137 ++++++++++++++++++ .../PreserveFirstFoundResourceTransformer.kt | 6 + .../DeduplicatingResourceTransformerTest.kt | 123 ++++++++++++++++ .../gradle/plugins/shadow/testkit/JarPath.kt | 29 ++++ 7 files changed, 391 insertions(+) create mode 100644 src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt create mode 100644 src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt create mode 100644 src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt diff --git a/api/shadow.api b/api/shadow.api index 218f8cb7e..0d90a6c24 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -329,6 +329,15 @@ public class com/github/jengelman/gradle/plugins/shadow/transformers/ComponentsX public final class com/github/jengelman/gradle/plugins/shadow/transformers/ComponentsXmlResourceTransformer$Companion { } +public class com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer : com/github/jengelman/gradle/plugins/shadow/transformers/PatternFilterableResourceTransformer { + public fun (Lorg/gradle/api/model/ObjectFactory;)V + public fun (Lorg/gradle/api/model/ObjectFactory;Lorg/gradle/api/tasks/util/PatternSet;)V + public fun canTransformResource (Lorg/gradle/api/file/FileTreeElement;)Z + public final fun getObjectFactory ()Lorg/gradle/api/model/ObjectFactory; + public fun hasTransformedResource ()Z + public fun modifyOutputStream (Lorg/apache/tools/zip/ZipOutputStream;Z)V +} + public class com/github/jengelman/gradle/plugins/shadow/transformers/DontIncludeResourceTransformer : com/github/jengelman/gradle/plugins/shadow/transformers/ResourceTransformer { public fun (Lorg/gradle/api/model/ObjectFactory;)V public fun canTransformResource (Lorg/gradle/api/file/FileTreeElement;)Z diff --git a/docs/changes/README.md b/docs/changes/README.md index 65c431963..098586a10 100644 --- a/docs/changes/README.md +++ b/docs/changes/README.md @@ -21,6 +21,7 @@ enableKotlinModuleRemapping = false } ``` +- Add `DeduplicatingResourceTransformer` to deduplicate on path _and_ content. ([#1859](https://github.com/GradleUp/shadow/pull/1859)) ### Changed diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt new file mode 100644 index 000000000..069b968d6 --- /dev/null +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt @@ -0,0 +1,86 @@ +package com.github.jengelman.gradle.plugins.shadow.transformers + +import assertk.assertThat +import assertk.assertions.any +import assertk.assertions.containsExactlyInAnyOrder +import assertk.assertions.containsSubList +import assertk.assertions.endsWith +import assertk.assertions.isEqualTo +import assertk.assertions.isSameInstanceAs +import com.github.jengelman.gradle.plugins.shadow.testkit.containsExactlyInAnyOrder +import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly +import com.github.jengelman.gradle.plugins.shadow.testkit.getContent +import com.github.jengelman.gradle.plugins.shadow.testkit.getContents +import kotlin.booleanArrayOf +import kotlin.io.path.appendText +import org.gradle.testkit.runner.TaskOutcome +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class DeduplicatingResourceTransformerTest : BaseTransformerTest() { + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun conflictExclusion(excludeAll: Boolean) { + val one = buildJarOne { + insert("multiple-contents", "content") + insert("single-source", "content") + insert("same-content-twice", "content") + insert("differing-content-2", "content") + } + val two = buildJarTwo { + insert("multiple-contents", "content-is-different") + insert("same-content-twice", "content") + insert("differing-content-2", "content-is-different") + } + + projectScript.appendText( + transform( + dependenciesBlock = implementationFiles(one, two), + transformerBlock = """ + exclude("multiple-contents") + ${if (excludeAll) "exclude(\"differing-content-2\")" else ""} + """.trimIndent(), + ), + ) + + if (excludeAll) { + runWithSuccess(shadowJarPath) + assertThat(outputShadowedJar).useAll { + containsExactlyInAnyOrder( + // twice: + "multiple-contents", + "multiple-contents", + "single-source", + "same-content-twice", + // twice: + "differing-content-2", + "differing-content-2", + "META-INF/", + "META-INF/MANIFEST.MF", + ) + getContents("multiple-contents").containsExactlyInAnyOrder("content", "content-is-different") + getContent("single-source").isEqualTo("content") + getContent("same-content-twice").isEqualTo("content") + getContents("differing-content-2").containsExactlyInAnyOrder("content", "content-is-different") + } + } else { + val buildResult = runWithFailure(shadowJarPath) + assertThat(buildResult.task(":shadowJar")!!.outcome).isSameInstanceAs(TaskOutcome.FAILED) + val outputLines = buildResult.output.lines() + assertThat(outputLines).containsSubList( + listOf( + // Keep this list approach for Unix/Windows test compatibility. + "Execution failed for task ':shadowJar'.", + "> Found 1 path duplicate(s) with different content in the shadow JAR:", + " * differing-content-2", + ), + ) + assertThat(outputLines).any { + it.endsWith("differing-content-2 (Hash: -1337566116240053116)") + } + assertThat(outputLines).any { + it.endsWith("differing-content-2 (Hash: -6159701213549668473)") + } + } + } +} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt new file mode 100644 index 000000000..e44518fdf --- /dev/null +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt @@ -0,0 +1,137 @@ +package com.github.jengelman.gradle.plugins.shadow.transformers + +import java.io.File +import java.nio.ByteBuffer +import java.security.MessageDigest +import javax.inject.Inject +import org.apache.tools.zip.ZipOutputStream +import org.gradle.api.GradleException +import org.gradle.api.file.FileTreeElement +import org.gradle.api.model.ObjectFactory +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.util.PatternSet + +/** + * Transformer to include files with identical content only once in the shadow JAR. + * + * Multiple files with the same path but different content lead to an error. + * + * Some scenarios for duplicate resources in a shadow jar: + * * Duplicate `.class` files + * Having duplicate `.class` files with different is a situation indicating that the resulting jar is + * built with _incompatible_ classes, likely leading to issues during runtime. + * This situation can happen when one dependency is (also) included in an uber jar. + * * Duplicate `META-INF///pom.properties`/`xml` files. + * Some dependencies contain shaded variants of other dependencies. + * Tools that inspect jar files to extract the included dependencies, for example, for license auditing + * use cases or tools that collect information of all included dependencies, may rely on these files. + * Hence, it is desirable to retain the duplicate resource `pom.properties`/`xml` resources. + * + * `DeduplicatingResourceTransformer` checks all entries in the resulting jar. + * It is generally not recommended to use any of the [include] configuration functions. + * + * There are reasons to retain duplicate resources with different contents in the resulting jar. + * This can be achieved with the [exclude] configuration functions. + * + * To exclude a path or pattern from being deduplicated, for example, legit + * `META-INF///pom.properties`/`xml`, configure the transformer with an exclusion + * like the following: + * ```kotlin + * tasks.named("shadowJar").configure { + * // Keep pom.* files from different Guava versions in the jar. + * exclude("META-INF/maven/com.google.guava/guava/pom.*") + * // Duplicates with different content for all other resource paths will raise an error. + * } + * ``` + * + * *Tip*: the [com.github.jengelman.gradle.plugins.shadow.tasks.FindResourceInClasspath] convenience task + * can be used to find resources in a Gradle classpath/configuration. + * + * *Warning* Do **not** combine [PreserveFirstFoundResourceTransformer] with this transformer. + */ +@CacheableTransformer +public open class DeduplicatingResourceTransformer( + final override val objectFactory: ObjectFactory, + patternSet: PatternSet, +) : PatternFilterableResourceTransformer(patternSet) { + @get:Internal + internal val sources: MutableMap = mutableMapOf() + + @Inject + public constructor(objectFactory: ObjectFactory) : this(objectFactory, PatternSet()) + + internal data class PathInfos(val failOnDuplicateContent: Boolean) { + val filesPerHash: MutableMap> = mutableMapOf() + + fun uniqueContentCount() = filesPerHash.size + + fun addFile(hash: Long, file: File): Boolean { + var filesForHash: MutableList? = filesPerHash[hash] + val new = filesForHash == null + if (new) { + filesForHash = mutableListOf() + filesPerHash[hash] = filesForHash + } + filesForHash.add(file) + return new + } + } + + override fun canTransformResource(element: FileTreeElement): Boolean { + val file = element.file + val hash = hashForFile(file) + + val pathInfos = sources.computeIfAbsent(element.path) { PathInfos(patternSpec.isSatisfiedBy(element)) } + val retainInOutput = pathInfos.addFile(hash, file) + + return !retainInOutput + } + + override fun hasTransformedResource(): Boolean = true + + internal fun duplicateContentViolations(): Map = sources.filter { (_, pathInfos) -> pathInfos.failOnDuplicateContent && pathInfos.uniqueContentCount() > 1 } + + override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) { + val duplicatePaths = duplicateContentViolations() + + if (!duplicatePaths.isEmpty()) { + val message = + "Found ${duplicatePaths.size} path duplicate(s) with different content in the shadow JAR:" + + duplicatePaths + .map { (path, infos) -> + " * $path${infos.filesPerHash.map { (hash, files) -> + files.joinToString { file -> " * ${file.path} (Hash: $hash)" } + }.joinToString("\n", "\n", "")}" + } + .joinToString("\n", "\n", "") + throw GradleException(message) + } + } + + // Gradle's configuration uses Java serialization, which cannot serialize `MessageDigest` instances. + // Using a rather dirty mechanism to memoize the MD instance for task/transformer execution. + @Transient + private var digest: MessageDigest? = null + + internal fun hashForFile(file: File): Long { + if (digest == null) { + digest = MessageDigest.getInstance("SHA-256") + } + val d = digest!! + try { + file.inputStream().use { + val buffer = ByteArray(8192) + while (true) { + val rd = it.read(buffer) + if (rd == -1) { + break + } + d.update(buffer, 0, rd) + } + } + return ByteBuffer.wrap(d.digest()).getLong(0) + } catch (e: Exception) { + throw RuntimeException("Failed to read data or calculate hash for $file", e) + } + } +} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/PreserveFirstFoundResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/PreserveFirstFoundResourceTransformer.kt index e0aaaa825..4f86739b3 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/PreserveFirstFoundResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/PreserveFirstFoundResourceTransformer.kt @@ -19,6 +19,12 @@ import org.gradle.api.tasks.util.PatternSet * want to ensure that only the first found resource is included in the final JAR. If there are multiple resources with * the same path in a project and its dependencies, the first one found should be the project's. * + * This transformer deduplicates included resources based on the path name. + * See [DeduplicatingResourceTransformer] for a transformer that deduplicates based on the paths and contents of + * the resources. + * + * *Warning* Do **not** combine [DeduplicatingResourceTransformer] with this transformer. + * * @see [DuplicatesStrategy] * @see [ShadowJar.getDuplicatesStrategy] */ diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt new file mode 100644 index 000000000..c4cd661b6 --- /dev/null +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt @@ -0,0 +1,123 @@ +package com.github.jengelman.gradle.plugins.shadow.transformers + +import assertk.assertThat +import assertk.assertions.containsExactlyInAnyOrder +import assertk.assertions.containsOnly +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isNotEqualTo +import assertk.assertions.isTrue +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class DeduplicatingResourceTransformerTest : BaseTransformerTest() { + + @TempDir + lateinit var tempDir: Path + + lateinit var file1: File + lateinit var file2: File + lateinit var file3: File + + var hash1: Long = 0L + var hash2: Long = 0L + var hash3: Long = 0L + + @BeforeEach + fun setupFiles() { + val file1 = tempDir.resolve("file1") + val file2 = tempDir.resolve("file2") + val file3 = tempDir.resolve("file3") + + val content1 = "content1".toByteArray() + val content2 = "content2".toByteArray() + + Files.write(file1, content1) + Files.write(file2, content1) + Files.write(file3, content2) + + this.file1 = file1.toFile() + this.file2 = file2.toFile() + this.file3 = file3.toFile() + + this.hash1 = transformer.hashForFile(this.file1) + this.hash2 = transformer.hashForFile(this.file2) + this.hash3 = transformer.hashForFile(this.file3) + } + + @Test + fun hashing() { + assertThat(hash1).isEqualTo(hash2) + assertThat(hash1).isNotEqualTo(hash3) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun duplicateContent(exclusionCheck: Boolean) { + with(transformer) { + if (!exclusionCheck) { + exclude("multiple-contents") + } + + // new path, new file content --> retain resource + assertThat(canTransformResource("multiple-contents", file1)).isFalse() + // same path, same file content --> skip resource + assertThat(canTransformResource("multiple-contents", file2)).isTrue() + // same path, different file content --> retain resource (even if it's a duplicate) + assertThat(canTransformResource("multiple-contents", file3)).isFalse() + + assertThat(canTransformResource("single-source", file1)).isFalse() + + assertThat(canTransformResource("same-content-twice", file1)).isFalse() + assertThat(canTransformResource("same-content-twice", file2)).isTrue() + + assertThat(canTransformResource("differing-content-2", file1)).isFalse() + assertThat(canTransformResource("differing-content-2", file3)).isFalse() + + assertThat(sources.keys).containsExactlyInAnyOrder( + "multiple-contents", + "single-source", + "same-content-twice", + "differing-content-2", + ) + + val pathInfosMultipleContents = sources["multiple-contents"]!! + assertThat(pathInfosMultipleContents.failOnDuplicateContent).isEqualTo(exclusionCheck) + assertThat(pathInfosMultipleContents.uniqueContentCount()).isEqualTo(2) + assertThat(pathInfosMultipleContents.filesPerHash).containsOnly( + hash1 to listOf(file1, file2), + hash3 to listOf(file3), + ) + + val pathInfosSingleSource = sources["single-source"]!! + assertThat(pathInfosSingleSource.failOnDuplicateContent).isTrue() + assertThat(pathInfosSingleSource.uniqueContentCount()).isEqualTo(1) + assertThat(pathInfosSingleSource.filesPerHash).containsOnly(hash1 to listOf(file1)) + + val pathInfosSameContentTwice = sources["same-content-twice"]!! + assertThat(pathInfosSameContentTwice.failOnDuplicateContent).isTrue() + assertThat(pathInfosSameContentTwice.uniqueContentCount()).isEqualTo(1) + assertThat(pathInfosSameContentTwice.filesPerHash).containsOnly(hash1 to listOf(file1, file2)) + + val pathInfosDifferingContent2 = sources["differing-content-2"]!! + assertThat(pathInfosDifferingContent2.failOnDuplicateContent).isTrue() + assertThat(pathInfosDifferingContent2.uniqueContentCount()).isEqualTo(2) + assertThat(pathInfosDifferingContent2.filesPerHash).containsOnly(hash1 to listOf(file1), hash3 to listOf(file3)) + + if (exclusionCheck) { + assertThat(duplicateContentViolations()).containsOnly( + "multiple-contents" to pathInfosMultipleContents, + "differing-content-2" to pathInfosDifferingContent2, + ) + } else { + assertThat(duplicateContentViolations()).containsOnly("differing-content-2" to pathInfosDifferingContent2) + } + } + } +} diff --git a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt index 41b41878f..eda69a8a1 100644 --- a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt +++ b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt @@ -2,11 +2,14 @@ package com.github.jengelman.gradle.plugins.shadow.testkit import assertk.Assert import assertk.assertions.containsAtLeast +import assertk.assertions.containsExactlyInAnyOrder import assertk.assertions.containsNone import assertk.assertions.containsOnly import java.io.InputStream +import java.nio.file.Files import java.nio.file.Path import java.util.jar.JarFile +import java.util.jar.JarInputStream import java.util.zip.ZipFile /** @@ -44,6 +47,26 @@ fun ZipFile.getStream(entryName: String): InputStream { fun Assert.getContent(entryName: String) = transform { it.getContent(entryName) } +/** + * Scans the jar file for all entries that match the specified [entryName], + * [getContent] or [getStream] return only one of the matching entries; + * which one these functions return is undefined. + */ +fun Assert.getContents(entryName: String) = transform { + Files.newInputStream(it.path).use { + val jarInput = JarInputStream(it) + val contents = mutableListOf() + while (true) { + val entry = jarInput.nextEntry ?: break + if (entry.name == entryName) { + contents.add(jarInput.readAllBytes().toString(Charsets.UTF_8)) + } + jarInput.closeEntry() + } + contents + } +} + fun Assert.getMainAttr(name: String) = transform { it.getMainAttr(name) } /** @@ -64,6 +87,12 @@ fun Assert.containsNone(vararg entries: String) = toEntries().containsN */ fun Assert.containsOnly(vararg entries: String) = toEntries().containsOnly(*entries) +/** + * Ensures the JAR contains only the specified entries. + * Used alone, without [containsAtLeast] or [containsNone]. + */ +fun Assert.containsExactlyInAnyOrder(vararg entries: String) = toEntries().containsExactlyInAnyOrder(*entries) + private fun Assert.toEntries() = transform { actual -> actual.entries().toList().map { it.name } } From a1f55dea9ef1230a2a20665a5af25934a97dd59e Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Tue, 18 Nov 2025 08:35:14 +0100 Subject: [PATCH 02/30] review --- .../DeduplicatingResourceTransformerTest.kt | 86 ----------- .../shadow/transformers/TransformersTest.kt | 138 ++++++++++++++++++ .../DeduplicatingResourceTransformerTest.kt | 12 +- .../gradle/plugins/shadow/testkit/JarPath.kt | 6 +- 4 files changed, 147 insertions(+), 95 deletions(-) delete mode 100644 src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt deleted file mode 100644 index 069b968d6..000000000 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.github.jengelman.gradle.plugins.shadow.transformers - -import assertk.assertThat -import assertk.assertions.any -import assertk.assertions.containsExactlyInAnyOrder -import assertk.assertions.containsSubList -import assertk.assertions.endsWith -import assertk.assertions.isEqualTo -import assertk.assertions.isSameInstanceAs -import com.github.jengelman.gradle.plugins.shadow.testkit.containsExactlyInAnyOrder -import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly -import com.github.jengelman.gradle.plugins.shadow.testkit.getContent -import com.github.jengelman.gradle.plugins.shadow.testkit.getContents -import kotlin.booleanArrayOf -import kotlin.io.path.appendText -import org.gradle.testkit.runner.TaskOutcome -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.ValueSource - -class DeduplicatingResourceTransformerTest : BaseTransformerTest() { - @ParameterizedTest - @ValueSource(booleans = [false, true]) - fun conflictExclusion(excludeAll: Boolean) { - val one = buildJarOne { - insert("multiple-contents", "content") - insert("single-source", "content") - insert("same-content-twice", "content") - insert("differing-content-2", "content") - } - val two = buildJarTwo { - insert("multiple-contents", "content-is-different") - insert("same-content-twice", "content") - insert("differing-content-2", "content-is-different") - } - - projectScript.appendText( - transform( - dependenciesBlock = implementationFiles(one, two), - transformerBlock = """ - exclude("multiple-contents") - ${if (excludeAll) "exclude(\"differing-content-2\")" else ""} - """.trimIndent(), - ), - ) - - if (excludeAll) { - runWithSuccess(shadowJarPath) - assertThat(outputShadowedJar).useAll { - containsExactlyInAnyOrder( - // twice: - "multiple-contents", - "multiple-contents", - "single-source", - "same-content-twice", - // twice: - "differing-content-2", - "differing-content-2", - "META-INF/", - "META-INF/MANIFEST.MF", - ) - getContents("multiple-contents").containsExactlyInAnyOrder("content", "content-is-different") - getContent("single-source").isEqualTo("content") - getContent("same-content-twice").isEqualTo("content") - getContents("differing-content-2").containsExactlyInAnyOrder("content", "content-is-different") - } - } else { - val buildResult = runWithFailure(shadowJarPath) - assertThat(buildResult.task(":shadowJar")!!.outcome).isSameInstanceAs(TaskOutcome.FAILED) - val outputLines = buildResult.output.lines() - assertThat(outputLines).containsSubList( - listOf( - // Keep this list approach for Unix/Windows test compatibility. - "Execution failed for task ':shadowJar'.", - "> Found 1 path duplicate(s) with different content in the shadow JAR:", - " * differing-content-2", - ), - ) - assertThat(outputLines).any { - it.endsWith("differing-content-2 (Hash: -1337566116240053116)") - } - assertThat(outputLines).any { - it.endsWith("differing-content-2 (Hash: -6159701213549668473)") - } - } - } -} diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt index 9da1f572b..a0ff2bced 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt @@ -2,29 +2,167 @@ package com.github.jengelman.gradle.plugins.shadow.transformers import assertk.all import assertk.assertThat +import assertk.assertions.any +import assertk.assertions.contains +import assertk.assertions.containsExactlyInAnyOrder +import assertk.assertions.containsSubList +import assertk.assertions.endsWith import assertk.assertions.isEqualTo import assertk.assertions.isNotEqualTo import assertk.assertions.isNotNull +import assertk.assertions.isSameInstanceAs import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey import com.github.jengelman.gradle.plugins.shadow.testkit.containsAtLeast +import com.github.jengelman.gradle.plugins.shadow.testkit.containsExactlyInAnyOrder import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.getContent +import com.github.jengelman.gradle.plugins.shadow.testkit.getContents import com.github.jengelman.gradle.plugins.shadow.testkit.getStream import com.github.jengelman.gradle.plugins.shadow.testkit.invariantEolString import com.github.jengelman.gradle.plugins.shadow.testkit.requireResourceAsPath import com.github.jengelman.gradle.plugins.shadow.util.Issue import java.util.jar.Attributes as JarAttribute +import kotlin.booleanArrayOf import kotlin.io.path.appendText import kotlin.io.path.invariantSeparatorsPathString import kotlin.io.path.readText import kotlin.io.path.writeText import kotlin.reflect.KClass import org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor.PLUGIN_CACHE_FILE +import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource +import org.junit.jupiter.params.provider.ValueSource class TransformersTest : BaseTransformerTest() { + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun deduplicatingResourceTransformer(excludeAll: Boolean) { + val one = buildJarOne { + insert("multiple-contents", "content") + insert("single-source", "content") + insert("same-content-twice", "content") + insert("differing-content-2", "content") + } + val two = buildJarTwo { + insert("multiple-contents", "content-is-different") + insert("same-content-twice", "content") + insert("differing-content-2", "content-is-different") + } + + projectScript.appendText( + transform( + dependenciesBlock = implementationFiles(one, two), + transformerBlock = """ + exclude("multiple-contents") + ${if (excludeAll) "exclude(\"differing-content-2\")" else ""} + """.trimIndent(), + ), + ) + + if (excludeAll) { + runWithSuccess(shadowJarPath) + assertThat(outputShadowedJar).useAll { + containsExactlyInAnyOrder( + // twice: + "multiple-contents", + "multiple-contents", + "single-source", + "same-content-twice", + // twice: + "differing-content-2", + "differing-content-2", + "META-INF/", + "META-INF/MANIFEST.MF", + ) + getContents("multiple-contents").containsExactlyInAnyOrder("content", "content-is-different") + getContent("single-source").isEqualTo("content") + getContent("same-content-twice").isEqualTo("content") + getContents("differing-content-2").containsExactlyInAnyOrder("content", "content-is-different") + } + } else { + val buildResult = runWithFailure(shadowJarPath) + assertThat(buildResult.task(":shadowJar")!!.outcome).isSameInstanceAs(TaskOutcome.FAILED) + val outputLines = buildResult.output.lines() + assertThat(outputLines).containsSubList( + listOf( + // Keep this list approach for Unix/Windows test compatibility. + "Execution failed for task ':shadowJar'.", + "> Found 1 path duplicate(s) with different content in the shadow JAR:", + " * differing-content-2", + ), + ) + assertThat(outputLines).any { + it.endsWith("differing-content-2 (Hash: -1337566116240053116)") + } + assertThat(outputLines).any { + it.endsWith("differing-content-2 (Hash: -6159701213549668473)") + } + } + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun deduplicatingResourceTransformerEXPERIMENT(excludeAll: Boolean) { + val one = buildJarOne { + insert("multiple-contents", "content") + insert("single-source", "content") + insert("same-content-twice", "content") + insert("differing-content-2", "content") + } + val two = buildJarTwo { + insert("multiple-contents", "content-is-different") + insert("same-content-twice", "content") + insert("differing-content-2", "content-is-different") + } + + projectScript.appendText( + transform( + dependenciesBlock = implementationFiles(one, two), + transformerBlock = """ + exclude("multiple-contents") + ${if (excludeAll) "exclude(\"differing-content-2\")" else ""} + """.trimIndent(), + ), + ) + + if (excludeAll) { + runWithSuccess(shadowJarPath) + assertThat(outputShadowedJar).useAll { + containsExactlyInAnyOrder( + // twice: + "multiple-contents", + "multiple-contents", + "single-source", + "same-content-twice", + // twice: + "differing-content-2", + "differing-content-2", + "META-INF/", + "META-INF/MANIFEST.MF", + ) + getContents("multiple-contents").containsExactlyInAnyOrder("content", "content-is-different") + getContent("single-source").isEqualTo("content") + getContent("same-content-twice").isEqualTo("content") + getContents("differing-content-2").containsExactlyInAnyOrder("content", "content-is-different") + } + } else { + val buildResult = runWithFailure(shadowJarPath) + assertThat(buildResult.task(":shadowJar")!!.outcome).isSameInstanceAs(TaskOutcome.FAILED) + assertThat(buildResult.output).contains( + """ + Execution failed for task ':shadowJar'. + > Found 1 path duplicate(s) with different content in the shadow JAR: + * differing-content-2 + """.trimIndent().replace("\n", System.lineSeparator()), + "differing-content-2 (Hash: -1337566116240053116)", + "differing-content-2 (Hash: -6159701213549668473)", + ) + } + } + @Test fun manifestRetained() { writeClass() diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt index c4cd661b6..2beee5f05 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt @@ -8,8 +8,8 @@ import assertk.assertions.isFalse import assertk.assertions.isNotEqualTo import assertk.assertions.isTrue import java.io.File -import java.nio.file.Files import java.nio.file.Path +import kotlin.io.path.writeText import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir @@ -35,12 +35,12 @@ class DeduplicatingResourceTransformerTest : BaseTransformerTest.getContent(entryName: String) = transform { it.getContent(en * [getContent] or [getStream] return only one of the matching entries; * which one these functions return is undefined. */ -fun Assert.getContents(entryName: String) = transform { - Files.newInputStream(it.path).use { - val jarInput = JarInputStream(it) +fun Assert.getContents(entryName: String) = transform { actual -> + JarInputStream(actual.inputStream()).use { jarInput -> val contents = mutableListOf() while (true) { val entry = jarInput.nextEntry ?: break From eac0124a0e95028d69954dff255df08a6d37eb34 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Tue, 18 Nov 2025 09:00:04 +0100 Subject: [PATCH 03/30] fix JarPath.getCOntents --- .../github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt index be2dd416f..e1d2fe605 100644 --- a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt +++ b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt @@ -54,7 +54,7 @@ fun Assert.getContent(entryName: String) = transform { it.getContent(en * which one these functions return is undefined. */ fun Assert.getContents(entryName: String) = transform { actual -> - JarInputStream(actual.inputStream()).use { jarInput -> + JarInputStream(actual.path.inputStream()).use { jarInput -> val contents = mutableListOf() while (true) { val entry = jarInput.nextEntry ?: break From 596c529f8fc7aa1e66e732bf02ec62d21d79d0e2 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Tue, 18 Nov 2025 09:17:50 +0100 Subject: [PATCH 04/30] cleanup --- .../shadow/transformers/TransformersTest.kt | 78 +------------------ 1 file changed, 4 insertions(+), 74 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt index a0ff2bced..ca354fab3 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt @@ -2,11 +2,8 @@ package com.github.jengelman.gradle.plugins.shadow.transformers import assertk.all import assertk.assertThat -import assertk.assertions.any import assertk.assertions.contains import assertk.assertions.containsExactlyInAnyOrder -import assertk.assertions.containsSubList -import assertk.assertions.endsWith import assertk.assertions.isEqualTo import assertk.assertions.isNotEqualTo import assertk.assertions.isNotNull @@ -62,72 +59,6 @@ class TransformersTest : BaseTransformerTest() { ), ) - if (excludeAll) { - runWithSuccess(shadowJarPath) - assertThat(outputShadowedJar).useAll { - containsExactlyInAnyOrder( - // twice: - "multiple-contents", - "multiple-contents", - "single-source", - "same-content-twice", - // twice: - "differing-content-2", - "differing-content-2", - "META-INF/", - "META-INF/MANIFEST.MF", - ) - getContents("multiple-contents").containsExactlyInAnyOrder("content", "content-is-different") - getContent("single-source").isEqualTo("content") - getContent("same-content-twice").isEqualTo("content") - getContents("differing-content-2").containsExactlyInAnyOrder("content", "content-is-different") - } - } else { - val buildResult = runWithFailure(shadowJarPath) - assertThat(buildResult.task(":shadowJar")!!.outcome).isSameInstanceAs(TaskOutcome.FAILED) - val outputLines = buildResult.output.lines() - assertThat(outputLines).containsSubList( - listOf( - // Keep this list approach for Unix/Windows test compatibility. - "Execution failed for task ':shadowJar'.", - "> Found 1 path duplicate(s) with different content in the shadow JAR:", - " * differing-content-2", - ), - ) - assertThat(outputLines).any { - it.endsWith("differing-content-2 (Hash: -1337566116240053116)") - } - assertThat(outputLines).any { - it.endsWith("differing-content-2 (Hash: -6159701213549668473)") - } - } - } - - @ParameterizedTest - @ValueSource(booleans = [false, true]) - fun deduplicatingResourceTransformerEXPERIMENT(excludeAll: Boolean) { - val one = buildJarOne { - insert("multiple-contents", "content") - insert("single-source", "content") - insert("same-content-twice", "content") - insert("differing-content-2", "content") - } - val two = buildJarTwo { - insert("multiple-contents", "content-is-different") - insert("same-content-twice", "content") - insert("differing-content-2", "content-is-different") - } - - projectScript.appendText( - transform( - dependenciesBlock = implementationFiles(one, two), - transformerBlock = """ - exclude("multiple-contents") - ${if (excludeAll) "exclude(\"differing-content-2\")" else ""} - """.trimIndent(), - ), - ) - if (excludeAll) { runWithSuccess(shadowJarPath) assertThat(outputShadowedJar).useAll { @@ -152,11 +83,10 @@ class TransformersTest : BaseTransformerTest() { val buildResult = runWithFailure(shadowJarPath) assertThat(buildResult.task(":shadowJar")!!.outcome).isSameInstanceAs(TaskOutcome.FAILED) assertThat(buildResult.output).contains( - """ - Execution failed for task ':shadowJar'. - > Found 1 path duplicate(s) with different content in the shadow JAR: - * differing-content-2 - """.trimIndent().replace("\n", System.lineSeparator()), + // Keep this list approach for Unix/Windows test compatibility. + "Execution failed for task ':shadowJar'.", + "> Found 1 path duplicate(s) with different content in the shadow JAR:", + " * differing-content-2", "differing-content-2 (Hash: -1337566116240053116)", "differing-content-2 (Hash: -6159701213549668473)", ) From 10fbe9afc340f31be96b6a055917ac63f540ef67 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Wed, 19 Nov 2025 12:23:38 +0100 Subject: [PATCH 05/30] simpler hashing + use hex-string --- .../DeduplicatingResourceTransformer.kt | 23 ++++++++++--------- .../DeduplicatingResourceTransformerTest.kt | 6 ++--- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt index e44518fdf..dd7c49bf1 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt @@ -1,15 +1,16 @@ package com.github.jengelman.gradle.plugins.shadow.transformers import java.io.File -import java.nio.ByteBuffer import java.security.MessageDigest import javax.inject.Inject +import org.apache.commons.io.input.MessageDigestInputStream import org.apache.tools.zip.ZipOutputStream import org.gradle.api.GradleException import org.gradle.api.file.FileTreeElement import org.gradle.api.model.ObjectFactory import org.gradle.api.tasks.Internal import org.gradle.api.tasks.util.PatternSet +import org.gradle.internal.impldep.org.apache.commons.codec.binary.Hex /** * Transformer to include files with identical content only once in the shadow JAR. @@ -61,11 +62,11 @@ public open class DeduplicatingResourceTransformer( public constructor(objectFactory: ObjectFactory) : this(objectFactory, PatternSet()) internal data class PathInfos(val failOnDuplicateContent: Boolean) { - val filesPerHash: MutableMap> = mutableMapOf() + val filesPerHash: MutableMap> = mutableMapOf() fun uniqueContentCount() = filesPerHash.size - fun addFile(hash: Long, file: File): Boolean { + fun addFile(hash: String, file: File): Boolean { var filesForHash: MutableList? = filesPerHash[hash] val new = filesForHash == null if (new) { @@ -113,23 +114,23 @@ public open class DeduplicatingResourceTransformer( @Transient private var digest: MessageDigest? = null - internal fun hashForFile(file: File): Long { + internal fun hashForFile(file: File): String { if (digest == null) { digest = MessageDigest.getInstance("SHA-256") } val d = digest!! try { + // We could replace this block with `o.a.c.codec.digest.DigestUtils.digest(MessageDigest, File)`, + // but adding a whole new dependency seemed a bit overkill. + // Using org.apache.commons.io.input.MessageDigestInputStream doesn't give simpler code either. file.inputStream().use { val buffer = ByteArray(8192) - while (true) { - val rd = it.read(buffer) - if (rd == -1) { - break - } - d.update(buffer, 0, rd) + var readBytes: Int + while (it.read(buffer).also { r -> readBytes = r } != -1) { + d.update(buffer, 0, readBytes) } } - return ByteBuffer.wrap(d.digest()).getLong(0) + return Hex.encodeHexString(d.digest(), true) } catch (e: Exception) { throw RuntimeException("Failed to read data or calculate hash for $file", e) } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt index 2beee5f05..9c34e321b 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt @@ -25,9 +25,9 @@ class DeduplicatingResourceTransformerTest : BaseTransformerTest Date: Wed, 19 Nov 2025 12:33:36 +0100 Subject: [PATCH 06/30] use the right hex thingy --- .../shadow/transformers/DeduplicatingResourceTransformer.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt index dd7c49bf1..217f56757 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt @@ -2,15 +2,14 @@ package com.github.jengelman.gradle.plugins.shadow.transformers import java.io.File import java.security.MessageDigest +import java.util.HexFormat import javax.inject.Inject -import org.apache.commons.io.input.MessageDigestInputStream import org.apache.tools.zip.ZipOutputStream import org.gradle.api.GradleException import org.gradle.api.file.FileTreeElement import org.gradle.api.model.ObjectFactory import org.gradle.api.tasks.Internal import org.gradle.api.tasks.util.PatternSet -import org.gradle.internal.impldep.org.apache.commons.codec.binary.Hex /** * Transformer to include files with identical content only once in the shadow JAR. @@ -130,7 +129,7 @@ public open class DeduplicatingResourceTransformer( d.update(buffer, 0, readBytes) } } - return Hex.encodeHexString(d.digest(), true) + return HexFormat.of().formatHex(d.digest()) } catch (e: Exception) { throw RuntimeException("Failed to read data or calculate hash for $file", e) } From 55a6dfb1881d08d03625b96cf8f3125fef7f156f Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Wed, 19 Nov 2025 13:38:44 +0100 Subject: [PATCH 07/30] fix test + make clean it's SHA256 --- .../gradle/plugins/shadow/transformers/TransformersTest.kt | 4 ++-- .../shadow/transformers/DeduplicatingResourceTransformer.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt index ca354fab3..e2aa79591 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt @@ -87,8 +87,8 @@ class TransformersTest : BaseTransformerTest() { "Execution failed for task ':shadowJar'.", "> Found 1 path duplicate(s) with different content in the shadow JAR:", " * differing-content-2", - "differing-content-2 (Hash: -1337566116240053116)", - "differing-content-2 (Hash: -6159701213549668473)", + "differing-content-2 (SHA256: ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73)", + "differing-content-2 (SHA256: aa845861bbd4578700e10487d85b25ead8723ee98fbf143df7b7e0bf1cb3385d)", ) } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt index 217f56757..c0eebafc9 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt @@ -100,7 +100,7 @@ public open class DeduplicatingResourceTransformer( duplicatePaths .map { (path, infos) -> " * $path${infos.filesPerHash.map { (hash, files) -> - files.joinToString { file -> " * ${file.path} (Hash: $hash)" } + files.joinToString { file -> " * ${file.path} (SHA256: $hash)" } }.joinToString("\n", "\n", "")}" } .joinToString("\n", "\n", "") From 07d426953dd1dd29c03718e29f46fb2b69975983 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Wed, 19 Nov 2025 14:46:04 +0100 Subject: [PATCH 08/30] Update src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../shadow/transformers/DeduplicatingResourceTransformer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt index c0eebafc9..88d9bf01c 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt @@ -18,7 +18,7 @@ import org.gradle.api.tasks.util.PatternSet * * Some scenarios for duplicate resources in a shadow jar: * * Duplicate `.class` files - * Having duplicate `.class` files with different is a situation indicating that the resulting jar is + * Having duplicate `.class` files with different content is a situation indicating that the resulting jar is * built with _incompatible_ classes, likely leading to issues during runtime. * This situation can happen when one dependency is (also) included in an uber jar. * * Duplicate `META-INF///pom.properties`/`xml` files. From eef142a5a7661899e14730edcc3aea26bdc4b0f1 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Wed, 19 Nov 2025 14:46:39 +0100 Subject: [PATCH 09/30] Update src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../gradle/plugins/shadow/transformers/TransformersTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt index e2aa79591..730f8937e 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt @@ -19,7 +19,6 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.invariantEolString import com.github.jengelman.gradle.plugins.shadow.testkit.requireResourceAsPath import com.github.jengelman.gradle.plugins.shadow.util.Issue import java.util.jar.Attributes as JarAttribute -import kotlin.booleanArrayOf import kotlin.io.path.appendText import kotlin.io.path.invariantSeparatorsPathString import kotlin.io.path.readText From c9b0bcb3c9bb6cf86392251b43c1ac3ffce0be65 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Wed, 19 Nov 2025 14:46:47 +0100 Subject: [PATCH 10/30] Update src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt index e1d2fe605..96b01f392 100644 --- a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt +++ b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt @@ -6,7 +6,6 @@ import assertk.assertions.containsExactlyInAnyOrder import assertk.assertions.containsNone import assertk.assertions.containsOnly import java.io.InputStream -import java.nio.file.Files import java.nio.file.Path import java.util.jar.JarFile import java.util.jar.JarInputStream From 99ddedaef54a00323bcb6f99e1b63dcb59a34c82 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Wed, 19 Nov 2025 14:47:35 +0100 Subject: [PATCH 11/30] Update src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt index 96b01f392..ee78db400 100644 --- a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt +++ b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt @@ -87,7 +87,7 @@ fun Assert.containsNone(vararg entries: String) = toEntries().containsN fun Assert.containsOnly(vararg entries: String) = toEntries().containsOnly(*entries) /** - * Ensures the JAR contains only the specified entries. + * Ensures the JAR contains exactly the specified entries, including duplicates, in any order. * Used alone, without [containsAtLeast] or [containsNone]. */ fun Assert.containsExactlyInAnyOrder(vararg entries: String) = toEntries().containsExactlyInAnyOrder(*entries) From fc59d6902df70a1cba8991fae0dbb45105ad1a91 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Wed, 19 Nov 2025 14:50:16 +0100 Subject: [PATCH 12/30] add .reset() --- .../shadow/transformers/DeduplicatingResourceTransformer.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt index 88d9bf01c..48f5b2d68 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt @@ -119,6 +119,7 @@ public open class DeduplicatingResourceTransformer( } val d = digest!! try { + d.reset() // We could replace this block with `o.a.c.codec.digest.DigestUtils.digest(MessageDigest, File)`, // but adding a whole new dependency seemed a bit overkill. // Using org.apache.commons.io.input.MessageDigestInputStream doesn't give simpler code either. From 71bbe8049ee630e4cf48db201b43f4d7f0a5b665 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Thu, 20 Nov 2025 09:38:53 +0100 Subject: [PATCH 13/30] rmi comment --- .../shadow/transformers/DeduplicatingResourceTransformer.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt index 48f5b2d68..fa7858c67 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt @@ -120,9 +120,6 @@ public open class DeduplicatingResourceTransformer( val d = digest!! try { d.reset() - // We could replace this block with `o.a.c.codec.digest.DigestUtils.digest(MessageDigest, File)`, - // but adding a whole new dependency seemed a bit overkill. - // Using org.apache.commons.io.input.MessageDigestInputStream doesn't give simpler code either. file.inputStream().use { val buffer = ByteArray(8192) var readBytes: Int From ef2b6b43726e34b7e1bc173c1bddcc91ecee43e8 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Thu, 20 Nov 2025 15:29:17 +0100 Subject: [PATCH 14/30] use DigestUtils --- build.gradle.kts | 1 + gradle/libs.versions.toml | 1 + .../transformers/DeduplicatingResourceTransformer.kt | 10 ++-------- .../DeduplicatingResourceTransformerTest.kt | 8 -------- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a558b798c..151dc231e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -119,6 +119,7 @@ dependencies { compileOnly(libs.kotlin.gradlePlugin) compileOnly(libs.kotlin.reflect) api(libs.apache.ant) // Types from Ant are exposed in the public API. + implementation(libs.apache.commonsCodec) implementation(libs.apache.commonsIo) implementation(libs.apache.log4j) implementation(libs.asm) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 66d678c43..f5e69498a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ pluginPublish = "2.0.0" [libraries] apache-ant = "org.apache.ant:ant:1.10.15" +apache-commonsCodec = "commons-codec:commons-codec:1.20.0" apache-commonsIo = "commons-io:commons-io:2.21.0" apache-log4j = "org.apache.logging.log4j:log4j-core:2.25.2" apache-maven-modelBuilder = "org.apache.maven:maven-model:3.9.11" diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt index fa7858c67..982d4e1c8 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt @@ -4,6 +4,7 @@ import java.io.File import java.security.MessageDigest import java.util.HexFormat import javax.inject.Inject +import org.apache.commons.codec.digest.DigestUtils import org.apache.tools.zip.ZipOutputStream import org.gradle.api.GradleException import org.gradle.api.file.FileTreeElement @@ -120,14 +121,7 @@ public open class DeduplicatingResourceTransformer( val d = digest!! try { d.reset() - file.inputStream().use { - val buffer = ByteArray(8192) - var readBytes: Int - while (it.read(buffer).also { r -> readBytes = r } != -1) { - d.update(buffer, 0, readBytes) - } - } - return HexFormat.of().formatHex(d.digest()) + return HexFormat.of().formatHex(DigestUtils.digest(d, file)) } catch (e: Exception) { throw RuntimeException("Failed to read data or calculate hash for $file", e) } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt index 9c34e321b..c64b46bed 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt @@ -5,13 +5,11 @@ import assertk.assertions.containsExactlyInAnyOrder import assertk.assertions.containsOnly import assertk.assertions.isEqualTo import assertk.assertions.isFalse -import assertk.assertions.isNotEqualTo import assertk.assertions.isTrue import java.io.File import java.nio.file.Path import kotlin.io.path.writeText import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource @@ -51,12 +49,6 @@ class DeduplicatingResourceTransformerTest : BaseTransformerTest Date: Thu, 20 Nov 2025 15:32:31 +0100 Subject: [PATCH 15/30] review --- .../shadow/transformers/TransformersTest.kt | 2 +- .../DeduplicatingResourceTransformer.kt | 37 ++++++++++--------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt index 730f8937e..c651617e7 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt @@ -80,7 +80,7 @@ class TransformersTest : BaseTransformerTest() { } } else { val buildResult = runWithFailure(shadowJarPath) - assertThat(buildResult.task(":shadowJar")!!.outcome).isSameInstanceAs(TaskOutcome.FAILED) + assertThat(buildResult).taskOutcomeEquals(":shadowJar", TaskOutcome.FAILED) assertThat(buildResult.output).contains( // Keep this list approach for Unix/Windows test compatibility. "Execution failed for task ':shadowJar'.", diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt index 982d4e1c8..82356a119 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt @@ -18,10 +18,12 @@ import org.gradle.api.tasks.util.PatternSet * Multiple files with the same path but different content lead to an error. * * Some scenarios for duplicate resources in a shadow jar: + * * * Duplicate `.class` files * Having duplicate `.class` files with different content is a situation indicating that the resulting jar is * built with _incompatible_ classes, likely leading to issues during runtime. * This situation can happen when one dependency is (also) included in an uber jar. + * * * Duplicate `META-INF///pom.properties`/`xml` files. * Some dependencies contain shaded variants of other dependencies. * Tools that inspect jar files to extract the included dependencies, for example, for license auditing @@ -37,6 +39,7 @@ import org.gradle.api.tasks.util.PatternSet * To exclude a path or pattern from being deduplicated, for example, legit * `META-INF///pom.properties`/`xml`, configure the transformer with an exclusion * like the following: + * * ```kotlin * tasks.named("shadowJar").configure { * // Keep pom.* files from different Guava versions in the jar. @@ -61,23 +64,6 @@ public open class DeduplicatingResourceTransformer( @Inject public constructor(objectFactory: ObjectFactory) : this(objectFactory, PatternSet()) - internal data class PathInfos(val failOnDuplicateContent: Boolean) { - val filesPerHash: MutableMap> = mutableMapOf() - - fun uniqueContentCount() = filesPerHash.size - - fun addFile(hash: String, file: File): Boolean { - var filesForHash: MutableList? = filesPerHash[hash] - val new = filesForHash == null - if (new) { - filesForHash = mutableListOf() - filesPerHash[hash] = filesForHash - } - filesForHash.add(file) - return new - } - } - override fun canTransformResource(element: FileTreeElement): Boolean { val file = element.file val hash = hashForFile(file) @@ -126,4 +112,21 @@ public open class DeduplicatingResourceTransformer( throw RuntimeException("Failed to read data or calculate hash for $file", e) } } + + internal data class PathInfos(val failOnDuplicateContent: Boolean) { + val filesPerHash: MutableMap> = mutableMapOf() + + fun uniqueContentCount() = filesPerHash.size + + fun addFile(hash: String, file: File): Boolean { + var filesForHash: MutableList? = filesPerHash[hash] + val new = filesForHash == null + if (new) { + filesForHash = mutableListOf() + filesPerHash[hash] = filesForHash + } + filesForHash.add(file) + return new + } + } } From 53c80ff07600cf8882159f124f74fbc8413ef5db Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 21 Nov 2025 10:11:21 +0800 Subject: [PATCH 16/30] Reduce `MessageDigest` --- .../DeduplicatingResourceTransformer.kt | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt index 82356a119..57cd3b146 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt @@ -1,8 +1,6 @@ package com.github.jengelman.gradle.plugins.shadow.transformers import java.io.File -import java.security.MessageDigest -import java.util.HexFormat import javax.inject.Inject import org.apache.commons.codec.digest.DigestUtils import org.apache.tools.zip.ZipOutputStream @@ -19,12 +17,12 @@ import org.gradle.api.tasks.util.PatternSet * * Some scenarios for duplicate resources in a shadow jar: * - * * Duplicate `.class` files + * - Duplicate `.class` files * Having duplicate `.class` files with different content is a situation indicating that the resulting jar is * built with _incompatible_ classes, likely leading to issues during runtime. * This situation can happen when one dependency is (also) included in an uber jar. * - * * Duplicate `META-INF///pom.properties`/`xml` files. + * - Duplicate `META-INF///pom.properties`/`xml` files. * Some dependencies contain shaded variants of other dependencies. * Tools that inspect jar files to extract the included dependencies, for example, for license auditing * use cases or tools that collect information of all included dependencies, may rely on these files. @@ -41,7 +39,7 @@ import org.gradle.api.tasks.util.PatternSet * like the following: * * ```kotlin - * tasks.named("shadowJar").configure { + * tasks.shadowJar { * // Keep pom.* files from different Guava versions in the jar. * exclude("META-INF/maven/com.google.guava/guava/pom.*") * // Duplicates with different content for all other resource paths will raise an error. @@ -76,8 +74,6 @@ public open class DeduplicatingResourceTransformer( override fun hasTransformedResource(): Boolean = true - internal fun duplicateContentViolations(): Map = sources.filter { (_, pathInfos) -> pathInfos.failOnDuplicateContent && pathInfos.uniqueContentCount() > 1 } - override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) { val duplicatePaths = duplicateContentViolations() @@ -95,19 +91,13 @@ public open class DeduplicatingResourceTransformer( } } - // Gradle's configuration uses Java serialization, which cannot serialize `MessageDigest` instances. - // Using a rather dirty mechanism to memoize the MD instance for task/transformer execution. - @Transient - private var digest: MessageDigest? = null + internal fun duplicateContentViolations(): Map = sources.filter { (_, pathInfos) -> + pathInfos.failOnDuplicateContent && pathInfos.uniqueContentCount() > 1 + } internal fun hashForFile(file: File): String { - if (digest == null) { - digest = MessageDigest.getInstance("SHA-256") - } - val d = digest!! try { - d.reset() - return HexFormat.of().formatHex(DigestUtils.digest(d, file)) + return DigestUtils.sha256Hex(file.inputStream()) } catch (e: Exception) { throw RuntimeException("Failed to read data or calculate hash for $file", e) } From 5afe7cfd0afdb79aaa9310314e49ac5063a24fa4 Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 21 Nov 2025 10:15:56 +0800 Subject: [PATCH 17/30] Update `DeduplicatingResourceTransformerTest` --- .../DeduplicatingResourceTransformerTest.kt | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt index c64b46bed..c1c688b16 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt @@ -19,34 +19,32 @@ class DeduplicatingResourceTransformerTest : BaseTransformerTest Date: Fri, 21 Nov 2025 10:19:07 +0800 Subject: [PATCH 18/30] Update `deduplicatingResourceTransformer` --- .../gradle/plugins/shadow/transformers/TransformersTest.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt index c651617e7..bfb3a0bcc 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt @@ -7,7 +7,6 @@ import assertk.assertions.containsExactlyInAnyOrder import assertk.assertions.isEqualTo import assertk.assertions.isNotEqualTo import assertk.assertions.isNotNull -import assertk.assertions.isSameInstanceAs import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey import com.github.jengelman.gradle.plugins.shadow.testkit.containsAtLeast import com.github.jengelman.gradle.plugins.shadow.testkit.containsExactlyInAnyOrder @@ -25,7 +24,7 @@ import kotlin.io.path.readText import kotlin.io.path.writeText import kotlin.reflect.KClass import org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor.PLUGIN_CACHE_FILE -import org.gradle.testkit.runner.TaskOutcome +import org.gradle.testkit.runner.TaskOutcome.FAILED import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource @@ -80,7 +79,7 @@ class TransformersTest : BaseTransformerTest() { } } else { val buildResult = runWithFailure(shadowJarPath) - assertThat(buildResult).taskOutcomeEquals(":shadowJar", TaskOutcome.FAILED) + assertThat(buildResult).taskOutcomeEquals(shadowJarPath, FAILED) assertThat(buildResult.output).contains( // Keep this list approach for Unix/Windows test compatibility. "Execution failed for task ':shadowJar'.", From cafc7a63940417762181c506abc56fd49b2f80d1 Mon Sep 17 00:00:00 2001 From: Zongle Wang Date: Fri, 21 Nov 2025 10:27:30 +0800 Subject: [PATCH 19/30] Update src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../gradle/plugins/shadow/testkit/JarPath.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt index ee78db400..fa194d1d7 100644 --- a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt +++ b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt @@ -90,7 +90,17 @@ fun Assert.containsOnly(vararg entries: String) = toEntries().containsO * Ensures the JAR contains exactly the specified entries, including duplicates, in any order. * Used alone, without [containsAtLeast] or [containsNone]. */ -fun Assert.containsExactlyInAnyOrder(vararg entries: String) = toEntries().containsExactlyInAnyOrder(*entries) +fun Assert.containsExactlyInAnyOrder(vararg entries: String) = transform { actual -> + JarInputStream(actual.path.inputStream()).use { jarInput -> + val allEntries = mutableListOf() + while (true) { + val entry = jarInput.nextEntry ?: break + allEntries.add(entry.name) + jarInput.closeEntry() + } + allEntries + } +}.containsExactlyInAnyOrder(*entries) private fun Assert.toEntries() = transform { actual -> actual.entries().toList().map { it.name } From d913899726f9267e52f53c1e030607f1f3ece539 Mon Sep 17 00:00:00 2001 From: Zongle Wang Date: Fri, 21 Nov 2025 10:27:39 +0800 Subject: [PATCH 20/30] Update src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../shadow/transformers/DeduplicatingResourceTransformer.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt index 57cd3b146..ed4741c32 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt @@ -82,9 +82,9 @@ public open class DeduplicatingResourceTransformer( "Found ${duplicatePaths.size} path duplicate(s) with different content in the shadow JAR:" + duplicatePaths .map { (path, infos) -> - " * $path${infos.filesPerHash.map { (hash, files) -> - files.joinToString { file -> " * ${file.path} (SHA256: $hash)" } - }.joinToString("\n", "\n", "")}" + " * $path\n${infos.filesPerHash.flatMap { (hash, files) -> + files.map { file -> " * ${file.path} (SHA256: $hash)" } + }.joinToString("\n")}" } .joinToString("\n", "\n", "") throw GradleException(message) From e672648552d61f0fc90de475c79227a3da6e48d2 Mon Sep 17 00:00:00 2001 From: Zongle Wang Date: Fri, 21 Nov 2025 10:28:55 +0800 Subject: [PATCH 21/30] Update src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../transformers/DeduplicatingResourceTransformer.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt index ed4741c32..47a1198ff 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt @@ -40,9 +40,11 @@ import org.gradle.api.tasks.util.PatternSet * * ```kotlin * tasks.shadowJar { - * // Keep pom.* files from different Guava versions in the jar. - * exclude("META-INF/maven/com.google.guava/guava/pom.*") - * // Duplicates with different content for all other resource paths will raise an error. + * transform(DeduplicatingResourceTransformer::class.java) { + * // Keep pom.* files from different Guava versions in the jar. + * exclude("META-INF/maven/com.google.guava/guava/pom.*") + * // Duplicates with different content for all other resource paths will raise an error. + * } * } * ``` * From 8c1b1c67524196fd95151e1380539ff0dbcded11 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Fri, 21 Nov 2025 08:37:34 +0100 Subject: [PATCH 22/30] flatten --- .../DeduplicatingResourceTransformerTest.kt | 112 +++++++++--------- 1 file changed, 55 insertions(+), 57 deletions(-) diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt index c1c688b16..0849dbf68 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt @@ -49,65 +49,63 @@ class DeduplicatingResourceTransformerTest : BaseTransformerTest retain resource - assertThat(canTransformResource("multiple-contents", file1)).isFalse() - // same path, same file content --> skip resource - assertThat(canTransformResource("multiple-contents", file2)).isTrue() - // same path, different file content --> retain resource (even if it's a duplicate) - assertThat(canTransformResource("multiple-contents", file3)).isFalse() - - assertThat(canTransformResource("single-source", file1)).isFalse() - - assertThat(canTransformResource("same-content-twice", file1)).isFalse() - assertThat(canTransformResource("same-content-twice", file2)).isTrue() - - assertThat(canTransformResource("differing-content-2", file1)).isFalse() - assertThat(canTransformResource("differing-content-2", file3)).isFalse() - - assertThat(sources.keys).containsExactlyInAnyOrder( - "multiple-contents", - "single-source", - "same-content-twice", - "differing-content-2", - ) + fun duplicateContent(exclusionCheck: Boolean) = with(transformer) { + if (!exclusionCheck) { + exclude("multiple-contents") + } - val pathInfosMultipleContents = sources.getValue("multiple-contents") - assertThat(pathInfosMultipleContents.failOnDuplicateContent).isEqualTo(exclusionCheck) - assertThat(pathInfosMultipleContents.uniqueContentCount()).isEqualTo(2) - assertThat(pathInfosMultipleContents.filesPerHash).containsOnly( - hash1 to listOf(file1, file2), - hash3 to listOf(file3), + // new path, new file content --> retain resource + assertThat(canTransformResource("multiple-contents", file1)).isFalse() + // same path, same file content --> skip resource + assertThat(canTransformResource("multiple-contents", file2)).isTrue() + // same path, different file content --> retain resource (even if it's a duplicate) + assertThat(canTransformResource("multiple-contents", file3)).isFalse() + + assertThat(canTransformResource("single-source", file1)).isFalse() + + assertThat(canTransformResource("same-content-twice", file1)).isFalse() + assertThat(canTransformResource("same-content-twice", file2)).isTrue() + + assertThat(canTransformResource("differing-content-2", file1)).isFalse() + assertThat(canTransformResource("differing-content-2", file3)).isFalse() + + assertThat(sources.keys).containsExactlyInAnyOrder( + "multiple-contents", + "single-source", + "same-content-twice", + "differing-content-2", + ) + + val pathInfosMultipleContents = sources.getValue("multiple-contents") + assertThat(pathInfosMultipleContents.failOnDuplicateContent).isEqualTo(exclusionCheck) + assertThat(pathInfosMultipleContents.uniqueContentCount()).isEqualTo(2) + assertThat(pathInfosMultipleContents.filesPerHash).containsOnly( + hash1 to listOf(file1, file2), + hash3 to listOf(file3), + ) + + val pathInfosSingleSource = sources.getValue("single-source") + assertThat(pathInfosSingleSource.failOnDuplicateContent).isTrue() + assertThat(pathInfosSingleSource.uniqueContentCount()).isEqualTo(1) + assertThat(pathInfosSingleSource.filesPerHash).containsOnly(hash1 to listOf(file1)) + + val pathInfosSameContentTwice = sources.getValue("same-content-twice") + assertThat(pathInfosSameContentTwice.failOnDuplicateContent).isTrue() + assertThat(pathInfosSameContentTwice.uniqueContentCount()).isEqualTo(1) + assertThat(pathInfosSameContentTwice.filesPerHash).containsOnly(hash1 to listOf(file1, file2)) + + val pathInfosDifferingContent2 = sources.getValue("differing-content-2") + assertThat(pathInfosDifferingContent2.failOnDuplicateContent).isTrue() + assertThat(pathInfosDifferingContent2.uniqueContentCount()).isEqualTo(2) + assertThat(pathInfosDifferingContent2.filesPerHash).containsOnly(hash1 to listOf(file1), hash3 to listOf(file3)) + + if (exclusionCheck) { + assertThat(duplicateContentViolations()).containsOnly( + "multiple-contents" to pathInfosMultipleContents, + "differing-content-2" to pathInfosDifferingContent2, ) - - val pathInfosSingleSource = sources.getValue("single-source") - assertThat(pathInfosSingleSource.failOnDuplicateContent).isTrue() - assertThat(pathInfosSingleSource.uniqueContentCount()).isEqualTo(1) - assertThat(pathInfosSingleSource.filesPerHash).containsOnly(hash1 to listOf(file1)) - - val pathInfosSameContentTwice = sources.getValue("same-content-twice") - assertThat(pathInfosSameContentTwice.failOnDuplicateContent).isTrue() - assertThat(pathInfosSameContentTwice.uniqueContentCount()).isEqualTo(1) - assertThat(pathInfosSameContentTwice.filesPerHash).containsOnly(hash1 to listOf(file1, file2)) - - val pathInfosDifferingContent2 = sources.getValue("differing-content-2") - assertThat(pathInfosDifferingContent2.failOnDuplicateContent).isTrue() - assertThat(pathInfosDifferingContent2.uniqueContentCount()).isEqualTo(2) - assertThat(pathInfosDifferingContent2.filesPerHash).containsOnly(hash1 to listOf(file1), hash3 to listOf(file3)) - - if (exclusionCheck) { - assertThat(duplicateContentViolations()).containsOnly( - "multiple-contents" to pathInfosMultipleContents, - "differing-content-2" to pathInfosDifferingContent2, - ) - } else { - assertThat(duplicateContentViolations()).containsOnly("differing-content-2" to pathInfosDifferingContent2) - } + } else { + assertThat(duplicateContentViolations()).containsOnly("differing-content-2" to pathInfosDifferingContent2) } } } From 23ee4efc6a3d9c386294f9c1498038835e580b5b Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Fri, 21 Nov 2025 08:44:42 +0100 Subject: [PATCH 23/30] use ZIS to get META-INF/MANIFEST.MF and keep functional parity with containsAtLeast/Only/None --- .../github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt index fa194d1d7..4061bd439 100644 --- a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt +++ b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt @@ -10,6 +10,7 @@ import java.nio.file.Path import java.util.jar.JarFile import java.util.jar.JarInputStream import java.util.zip.ZipFile +import java.util.zip.ZipInputStream import kotlin.io.path.inputStream /** @@ -91,7 +92,7 @@ fun Assert.containsOnly(vararg entries: String) = toEntries().containsO * Used alone, without [containsAtLeast] or [containsNone]. */ fun Assert.containsExactlyInAnyOrder(vararg entries: String) = transform { actual -> - JarInputStream(actual.path.inputStream()).use { jarInput -> + ZipInputStream(actual.path.inputStream()).use { jarInput -> val allEntries = mutableListOf() while (true) { val entry = jarInput.nextEntry ?: break From d4fd09948eb6863852b5bd63b1261e18960ba01a Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Fri, 21 Nov 2025 08:47:11 +0100 Subject: [PATCH 24/30] close stream --- .../shadow/transformers/DeduplicatingResourceTransformer.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt index 47a1198ff..d648b4746 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt @@ -99,7 +99,9 @@ public open class DeduplicatingResourceTransformer( internal fun hashForFile(file: File): String { try { - return DigestUtils.sha256Hex(file.inputStream()) + return file.inputStream().use { + DigestUtils.sha256Hex(it) + } } catch (e: Exception) { throw RuntimeException("Failed to read data or calculate hash for $file", e) } From aceaf8996d0e78180dcdeeed487d945e49dd7cb3 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Fri, 21 Nov 2025 09:21:34 +0100 Subject: [PATCH 25/30] review --- .../transformers/DeduplicatingResourceTransformer.kt | 11 +++-------- .../DeduplicatingResourceTransformerTest.kt | 3 --- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt index d648b4746..500ca4e46 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt @@ -79,7 +79,7 @@ public open class DeduplicatingResourceTransformer( override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) { val duplicatePaths = duplicateContentViolations() - if (!duplicatePaths.isEmpty()) { + if (duplicatePaths.isNotEmpty()) { val message = "Found ${duplicatePaths.size} path duplicate(s) with different content in the shadow JAR:" + duplicatePaths @@ -113,13 +113,8 @@ public open class DeduplicatingResourceTransformer( fun uniqueContentCount() = filesPerHash.size fun addFile(hash: String, file: File): Boolean { - var filesForHash: MutableList? = filesPerHash[hash] - val new = filesForHash == null - if (new) { - filesForHash = mutableListOf() - filesPerHash[hash] = filesForHash - } - filesForHash.add(file) + val new = hash !in filesPerHash + filesPerHash.getOrPut(hash) { mutableListOf() }.add(file) return new } } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt index 0849dbf68..1ded38a6e 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt @@ -8,7 +8,6 @@ import assertk.assertions.isFalse import assertk.assertions.isTrue import java.io.File import java.nio.file.Path -import kotlin.io.path.writeText import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.params.ParameterizedTest @@ -24,7 +23,6 @@ class DeduplicatingResourceTransformerTest : BaseTransformerTest Date: Fri, 5 Dec 2025 14:17:27 +0800 Subject: [PATCH 26/30] Cleanups --- .../shadow/transformers/TransformersTest.kt | 6 +-- .../DeduplicatingResourceTransformer.kt | 54 ++++++++++--------- .../DeduplicatingResourceTransformerTest.kt | 5 +- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt index bfb3a0bcc..b36d8ecb1 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt @@ -51,8 +51,8 @@ class TransformersTest : BaseTransformerTest() { transform( dependenciesBlock = implementationFiles(one, two), transformerBlock = """ - exclude("multiple-contents") - ${if (excludeAll) "exclude(\"differing-content-2\")" else ""} + exclude('multiple-contents') + ${if (excludeAll) "exclude('differing-content-2')" else ""} """.trimIndent(), ), ) @@ -83,7 +83,7 @@ class TransformersTest : BaseTransformerTest() { assertThat(buildResult.output).contains( // Keep this list approach for Unix/Windows test compatibility. "Execution failed for task ':shadowJar'.", - "> Found 1 path duplicate(s) with different content in the shadow JAR:", + "> Found 1 path duplicate(s) with different content in the shadowed JAR:", " * differing-content-2", "differing-content-2 (SHA256: ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73)", "differing-content-2 (SHA256: aa845861bbd4578700e10487d85b25ead8723ee98fbf143df7b7e0bf1cb3385d)", diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt index 500ca4e46..ebe0180ea 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt @@ -1,5 +1,6 @@ package com.github.jengelman.gradle.plugins.shadow.transformers +import com.github.jengelman.gradle.plugins.shadow.tasks.FindResourceInClasspath import java.io.File import javax.inject.Inject import org.apache.commons.codec.digest.DigestUtils @@ -11,7 +12,7 @@ import org.gradle.api.tasks.Internal import org.gradle.api.tasks.util.PatternSet /** - * Transformer to include files with identical content only once in the shadow JAR. + * Transformer to include files with identical content only once in the shadowed JAR. * * Multiple files with the same path but different content lead to an error. * @@ -28,7 +29,7 @@ import org.gradle.api.tasks.util.PatternSet * use cases or tools that collect information of all included dependencies, may rely on these files. * Hence, it is desirable to retain the duplicate resource `pom.properties`/`xml` resources. * - * `DeduplicatingResourceTransformer` checks all entries in the resulting jar. + * [DeduplicatingResourceTransformer] checks all entries in the resulting jar. * It is generally not recommended to use any of the [include] configuration functions. * * There are reasons to retain duplicate resources with different contents in the resulting jar. @@ -48,8 +49,8 @@ import org.gradle.api.tasks.util.PatternSet * } * ``` * - * *Tip*: the [com.github.jengelman.gradle.plugins.shadow.tasks.FindResourceInClasspath] convenience task - * can be used to find resources in a Gradle classpath/configuration. + * *Tip*: the [FindResourceInClasspath] convenience task can be used to find resources in a Gradle + * classpath/configuration. * * *Warning* Do **not** combine [PreserveFirstFoundResourceTransformer] with this transformer. */ @@ -66,9 +67,11 @@ public open class DeduplicatingResourceTransformer( override fun canTransformResource(element: FileTreeElement): Boolean { val file = element.file - val hash = hashForFile(file) + val hash = file.sha256Hex() - val pathInfos = sources.computeIfAbsent(element.path) { PathInfos(patternSpec.isSatisfiedBy(element)) } + val pathInfos = sources.computeIfAbsent(element.path) { + PathInfos(patternSpec.isSatisfiedBy(element)) + } val retainInOutput = pathInfos.addFile(hash, file) return !retainInOutput @@ -80,15 +83,16 @@ public open class DeduplicatingResourceTransformer( val duplicatePaths = duplicateContentViolations() if (duplicatePaths.isNotEmpty()) { - val message = - "Found ${duplicatePaths.size} path duplicate(s) with different content in the shadow JAR:" + - duplicatePaths - .map { (path, infos) -> - " * $path\n${infos.filesPerHash.flatMap { (hash, files) -> + val message = "Found ${duplicatePaths.size} path duplicate(s) with different content in the shadowed JAR:" + + duplicatePaths + .map { (path, infos) -> + " * $path\n${ + infos.filesPerHash.flatMap { (hash, files) -> files.map { file -> " * ${file.path} (SHA256: $hash)" } - }.joinToString("\n")}" - } - .joinToString("\n", "\n", "") + }.joinToString("\n") + }" + } + .joinToString("\n", "\n", "") throw GradleException(message) } } @@ -97,16 +101,6 @@ public open class DeduplicatingResourceTransformer( pathInfos.failOnDuplicateContent && pathInfos.uniqueContentCount() > 1 } - internal fun hashForFile(file: File): String { - try { - return file.inputStream().use { - DigestUtils.sha256Hex(it) - } - } catch (e: Exception) { - throw RuntimeException("Failed to read data or calculate hash for $file", e) - } - } - internal data class PathInfos(val failOnDuplicateContent: Boolean) { val filesPerHash: MutableMap> = mutableMapOf() @@ -118,4 +112,16 @@ public open class DeduplicatingResourceTransformer( return new } } + + internal companion object { + fun File.sha256Hex(): String { + try { + return inputStream().use { + DigestUtils.sha256Hex(it) + } + } catch (e: Exception) { + throw RuntimeException("Failed to read data or calculate hash for $this", e) + } + } + } } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt index 1ded38a6e..eba93f87b 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformerTest.kt @@ -6,6 +6,7 @@ import assertk.assertions.containsOnly import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue +import com.github.jengelman.gradle.plugins.shadow.transformers.DeduplicatingResourceTransformer.Companion.sha256Hex import java.io.File import java.nio.file.Path import org.junit.jupiter.api.BeforeEach @@ -40,8 +41,8 @@ class DeduplicatingResourceTransformerTest : BaseTransformerTest Date: Fri, 5 Dec 2025 14:30:23 +0800 Subject: [PATCH 27/30] Simplify the message --- .../DeduplicatingResourceTransformer.kt | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt index ebe0180ea..c469d7fc9 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt @@ -83,16 +83,17 @@ public open class DeduplicatingResourceTransformer( val duplicatePaths = duplicateContentViolations() if (duplicatePaths.isNotEmpty()) { - val message = "Found ${duplicatePaths.size} path duplicate(s) with different content in the shadowed JAR:" + - duplicatePaths - .map { (path, infos) -> - " * $path\n${ - infos.filesPerHash.flatMap { (hash, files) -> - files.map { file -> " * ${file.path} (SHA256: $hash)" } - }.joinToString("\n") - }" + val message = buildString { + append("Found ${duplicatePaths.size} path duplicate(s) with different content in the shadowed JAR:\n") + duplicatePaths.forEach { (path, infos) -> + append(" * $path\n") + infos.filesPerHash.forEach { (hash, files) -> + files.forEach { file -> + append(" * ${file.path} (SHA256: $hash)\n") + } } - .joinToString("\n", "\n", "") + } + } throw GradleException(message) } } From 28d38be3e156dec963da0449a5f21b8467989c1c Mon Sep 17 00:00:00 2001 From: Zongle Wang Date: Fri, 5 Dec 2025 14:48:02 +0800 Subject: [PATCH 28/30] Update src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../jengelman/gradle/plugins/shadow/testkit/JarPath.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt index 4061bd439..41426f009 100644 --- a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt +++ b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt @@ -49,9 +49,9 @@ fun ZipFile.getStream(entryName: String): InputStream { fun Assert.getContent(entryName: String) = transform { it.getContent(entryName) } /** - * Scans the jar file for all entries that match the specified [entryName], - * [getContent] or [getStream] return only one of the matching entries; - * which one these functions return is undefined. + * Scans the jar file for all entries that match the specified [entryName]. + * Unlike [getContent] or [getStream], which return only one of the matching entries + * (which one is undefined), this function returns all matching entries. */ fun Assert.getContents(entryName: String) = transform { actual -> JarInputStream(actual.path.inputStream()).use { jarInput -> From 471737ef895186da23d7e6250e0843622e033559 Mon Sep 17 00:00:00 2001 From: Zongle Wang Date: Fri, 5 Dec 2025 14:48:09 +0800 Subject: [PATCH 29/30] Update src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/PreserveFirstFoundResourceTransformer.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../transformers/PreserveFirstFoundResourceTransformer.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/PreserveFirstFoundResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/PreserveFirstFoundResourceTransformer.kt index 4f86739b3..a8b6527f8 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/PreserveFirstFoundResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/PreserveFirstFoundResourceTransformer.kt @@ -23,7 +23,8 @@ import org.gradle.api.tasks.util.PatternSet * See [DeduplicatingResourceTransformer] for a transformer that deduplicates based on the paths and contents of * the resources. * - * *Warning* Do **not** combine [DeduplicatingResourceTransformer] with this transformer. + * *Warning* Do **not** combine [DeduplicatingResourceTransformer] with this transformer, + * as they handle duplicates differently and combining them would lead to redundant or unexpected behavior. * * @see [DuplicatesStrategy] * @see [ShadowJar.getDuplicatesStrategy] From cc653e8783734de7bc1b9bc889f5e8f1d9df8497 Mon Sep 17 00:00:00 2001 From: Zongle Wang Date: Fri, 5 Dec 2025 14:48:18 +0800 Subject: [PATCH 30/30] Update src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../shadow/transformers/DeduplicatingResourceTransformer.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt index c469d7fc9..3c5498a84 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/DeduplicatingResourceTransformer.kt @@ -52,7 +52,8 @@ import org.gradle.api.tasks.util.PatternSet * *Tip*: the [FindResourceInClasspath] convenience task can be used to find resources in a Gradle * classpath/configuration. * - * *Warning* Do **not** combine [PreserveFirstFoundResourceTransformer] with this transformer. + * *Warning* Do **not** combine [PreserveFirstFoundResourceTransformer] with this transformer, + * as they handle duplicates differently and combining them would lead to redundant or unexpected behavior. */ @CacheableTransformer public open class DeduplicatingResourceTransformer(