From 2a756fc9121e79a030dbb59295b7589e2b6bbfae Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Fri, 14 Nov 2025 14:41:17 +0100 Subject: [PATCH 1/2] Don't let `ApacheLicenseResourceTransformer` remove `META-INF/LICENSE*` The `ApacheLicenseResourceTransformer` effectively removes all `META-INF/LICENSE*` files from the resulting jar. This change _changes_ the behavior of `ApacheLicenseResourceTransformer` to concatenate the included license files. Identical license texts are included once. Adapted the `ApacheLicenseResourceTransformerTest` for this change. Fixes #1842 --- docs/changes/README.md | 1 + .../ApacheLicenseResourceTransformer.kt | 51 ++++++++++++++++++- .../ApacheLicenseResourceTransformerTest.kt | 35 +++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/docs/changes/README.md b/docs/changes/README.md index e6ac914f2..03070e34f 100644 --- a/docs/changes/README.md +++ b/docs/changes/README.md @@ -8,6 +8,7 @@ - Change the group of `startShadowScripts` from `application` to `other`. ([#1797](https://github.com/GradleUp/shadow/pull/1797)) - Update ASM and jdependency to support Java 26. ([#1799](https://github.com/GradleUp/shadow/pull/1799)) - Bump min Gradle requirement to 9.0.0. ([#1801](https://github.com/GradleUp/shadow/pull/1801)) +- Fix `ApacheLicenseResourceTransformer` to not remove `META-INF/LICENSE*` files ([#1842](https://github.com/GradleUp/shadow/pull/1842)) ## [9.2.2](https://github.com/GradleUp/shadow/compare/9.2.2) - 2025-09-26 diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheLicenseResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheLicenseResourceTransformer.kt index 71f29c862..330bc51a4 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheLicenseResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheLicenseResourceTransformer.kt @@ -1,6 +1,14 @@ package com.github.jengelman.gradle.plugins.shadow.transformers +import com.github.jengelman.gradle.plugins.shadow.internal.property +import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry +import java.nio.charset.StandardCharsets +import javax.inject.Inject +import org.apache.tools.zip.ZipOutputStream import org.gradle.api.file.FileTreeElement +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input /** * Prevents duplicate copies of the license. @@ -10,7 +18,24 @@ import org.gradle.api.file.FileTreeElement * @author John Engelman */ @CacheableTransformer -public open class ApacheLicenseResourceTransformer : ResourceTransformer by ResourceTransformer.Companion { +public open class ApacheLicenseResourceTransformer @Inject constructor( + final override val objectFactory: ObjectFactory, +) : ResourceTransformer by ResourceTransformer.Companion { + + private val elements: MutableSet = LinkedHashSet() + + /** + * The file encoding of the `LICENSE` file. + */ + @get:Input + public open val charsetName: Property = objectFactory.property(Charsets.UTF_8.name()) + + /** + * The separator placed between two licenses. + */ + @get:Input + public open val separator: Property = objectFactory.property("\n\n${"-".repeat(120)}\n\n") + override fun canTransformResource(element: FileTreeElement): Boolean { val path = element.path return LICENSE_PATH.equals(path, ignoreCase = true) || @@ -18,6 +43,30 @@ public open class ApacheLicenseResourceTransformer : ResourceTransformer by Reso LICENSE_MD_PATH.regionMatches(0, path, 0, LICENSE_MD_PATH.length, ignoreCase = true) } + override fun transform(context: TransformerContext) { + val bytes = context.inputStream.readAllBytes() + val content = bytes.toString(StandardCharsets.UTF_8).trim('\n', '\r') + if (!content.isEmpty()) { + elements.add(content) + } + } + + override fun hasTransformedResource(): Boolean = true + + override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) { + os.putNextEntry(zipEntry(LICENSE_PATH, preserveFileTimestamps)) + var first = true + val separator = separator.get().toByteArray(StandardCharsets.UTF_8) + for (element in elements) { + if (!first) { + os.write(separator) + } + os.write(element.toByteArray(StandardCharsets.UTF_8)) + first = false + } + os.closeEntry() + } + private companion object { private const val LICENSE_PATH = "META-INF/LICENSE" private const val LICENSE_TXT_PATH = "META-INF/LICENSE.txt" diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheLicenseResourceTransformerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheLicenseResourceTransformerTest.kt index 556e9c6bc..f6928eb9d 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheLicenseResourceTransformerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheLicenseResourceTransformerTest.kt @@ -1,8 +1,13 @@ package com.github.jengelman.gradle.plugins.shadow.transformers import assertk.assertThat +import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue +import java.io.ByteArrayOutputStream +import java.nio.charset.Charset +import java.util.zip.ZipInputStream +import org.apache.tools.zip.ZipOutputStream import org.junit.jupiter.api.Test /** @@ -23,4 +28,34 @@ class ApacheLicenseResourceTransformerTest : BaseTransformerTest Date: Fri, 14 Nov 2025 15:28:41 +0100 Subject: [PATCH 2/2] Configurable source path patterns for Apache license/notice resource transformers The current patterns (`META-INF/LICENSE` + `META-INF/NOTICE` et al) work for many dependencies, but sadly not all. Some dependencies put those files in the jar's "root directory". This change allows configuring the set of matched paths, and also allows overriding the output path (just in case). --- docs/changes/README.md | 1 + .../ApacheLicenseResourceTransformer.kt | 67 ++++++++++++++----- .../ApacheNoticeResourceTransformer.kt | 41 +++++++++--- 3 files changed, 81 insertions(+), 28 deletions(-) diff --git a/docs/changes/README.md b/docs/changes/README.md index 03070e34f..43c08b8d3 100644 --- a/docs/changes/README.md +++ b/docs/changes/README.md @@ -9,6 +9,7 @@ - Update ASM and jdependency to support Java 26. ([#1799](https://github.com/GradleUp/shadow/pull/1799)) - Bump min Gradle requirement to 9.0.0. ([#1801](https://github.com/GradleUp/shadow/pull/1801)) - Fix `ApacheLicenseResourceTransformer` to not remove `META-INF/LICENSE*` files ([#1842](https://github.com/GradleUp/shadow/pull/1842)) +- Configurable match-patterns for `ApacheLicenseResourceTransformer` + `ApacheNoticeResourceTransformer` files ([#1842](https://github.com/GradleUp/shadow/pull/1845)) ## [9.2.2](https://github.com/GradleUp/shadow/compare/9.2.2) - 2025-09-26 diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheLicenseResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheLicenseResourceTransformer.kt index 330bc51a4..b5f2b469a 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheLicenseResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheLicenseResourceTransformer.kt @@ -8,6 +8,7 @@ import org.apache.tools.zip.ZipOutputStream import org.gradle.api.file.FileTreeElement import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.Input /** @@ -24,6 +25,38 @@ public open class ApacheLicenseResourceTransformer @Inject constructor( private val elements: MutableSet = LinkedHashSet() + /** + * Paths to consider as a license files, evaluated using `it.equals(path, ignoreCase = true)`. + * + * Defaults to `META-INF/LICENSE`. + */ + @get:Input + public open val paths: SetProperty = objectFactory.setProperty(String::class.java).value(setOf("META-INF/LICENSE")) + + /** + * Paths to consider as a license files, evaluated using `it.regionMatches(0, path, 0, it.length, ignoreCase = true)`. + * + * Defaults to `META-INF/LICENSE.txt` and `META-INF/LICENSE.md`. + */ + @get:Input + public open val regionMatchPaths: SetProperty = objectFactory.setProperty(String::class.java).value(setOf("META-INF/LICENSE.txt", "META-INF/LICENSE.md")) + + /** + * Path of the resulting output file. + * + * Defaults to `META-INF/LICENSE`. + */ + @get:Input + public open val outputPath: Property = objectFactory.property("META-INF/LICENSE") + + /** + * Whether to include an empty output, if no input file matches. + * + * Defaults to `false`. + */ + @get:Input + public open val writeEmpty: Property = objectFactory.property(false) + /** * The file encoding of the `LICENSE` file. */ @@ -38,9 +71,11 @@ public open class ApacheLicenseResourceTransformer @Inject constructor( override fun canTransformResource(element: FileTreeElement): Boolean { val path = element.path - return LICENSE_PATH.equals(path, ignoreCase = true) || - LICENSE_TXT_PATH.regionMatches(0, path, 0, LICENSE_TXT_PATH.length, ignoreCase = true) || - LICENSE_MD_PATH.regionMatches(0, path, 0, LICENSE_MD_PATH.length, ignoreCase = true) + return paths.get().any { + it.equals(path, ignoreCase = true) + } || regionMatchPaths.get().any { + it.regionMatches(0, path, 0, it.length, ignoreCase = true) + } } override fun transform(context: TransformerContext) { @@ -54,22 +89,18 @@ public open class ApacheLicenseResourceTransformer @Inject constructor( override fun hasTransformedResource(): Boolean = true override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) { - os.putNextEntry(zipEntry(LICENSE_PATH, preserveFileTimestamps)) - var first = true - val separator = separator.get().toByteArray(StandardCharsets.UTF_8) - for (element in elements) { - if (!first) { - os.write(separator) + if (!elements.isEmpty()) { + os.putNextEntry(zipEntry(outputPath.get(), preserveFileTimestamps)) + var first = true + val separator = separator.get().toByteArray(StandardCharsets.UTF_8) + for (element in elements) { + if (!first) { + os.write(separator) + } + os.write(element.toByteArray(StandardCharsets.UTF_8)) + first = false } - os.write(element.toByteArray(StandardCharsets.UTF_8)) - first = false + os.closeEntry() } - os.closeEntry() - } - - private companion object { - private const val LICENSE_PATH = "META-INF/LICENSE" - private const val LICENSE_TXT_PATH = "META-INF/LICENSE.txt" - private const val LICENSE_MD_PATH = "META-INF/LICENSE.md" } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformer.kt index 40109113a..eaf6eacb3 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformer.kt @@ -12,6 +12,7 @@ import org.apache.tools.zip.ZipOutputStream import org.gradle.api.file.FileTreeElement import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.Input /** @@ -34,6 +35,30 @@ public open class ApacheNoticeResourceTransformer @Inject constructor( */ private var fallbackCopyright: String? = null + /** + * Paths to consider as a notice files, evaluated using `it.equals(path, ignoreCase = true)`. + * + * Defaults to `META-INF/NOTICE`. + */ + @get:Input + public open val paths: SetProperty = objectFactory.setProperty(String::class.java).value(setOf("META-INF/NOTICE")) + + /** + * Paths to consider as a notice files, evaluated using `it.regionMatches(0, path, 0, it.length, ignoreCase = true)`. + * + * Defaults to `META-INF/NOTICE.txt` and `META-INF/NOTICE.md`. + */ + @get:Input + public open val regionMatchPaths: SetProperty = objectFactory.setProperty(String::class.java).value(setOf("META-INF/NOTICE.txt", "META-INF/NOTICE.md")) + + /** + * Path of the resulting output file. + * + * Defaults to `META-INF/NOTICE`. + */ + @get:Input + public open val outputPath: Property = objectFactory.property("META-INF/NOTICE") + @get:Input public open val projectName: Property = objectFactory.property("") @@ -77,9 +102,11 @@ public open class ApacheNoticeResourceTransformer @Inject constructor( override fun canTransformResource(element: FileTreeElement): Boolean { val path = element.path - return NOTICE_PATH.equals(path, ignoreCase = true) || - NOTICE_TXT_PATH.equals(path, ignoreCase = true) || - NOTICE_MD_PATH.equals(path, ignoreCase = true) + return paths.get().any { + it.equals(path, ignoreCase = true) + } || regionMatchPaths.get().any { + it.regionMatches(0, path, 0, it.length, ignoreCase = true) + } } override fun transform(context: TransformerContext) { @@ -184,16 +211,10 @@ public open class ApacheNoticeResourceTransformer @Inject constructor( } } - os.putNextEntry(zipEntry(NOTICE_PATH, preserveFileTimestamps)) + os.putNextEntry(zipEntry(outputPath.get(), preserveFileTimestamps)) os.write(sb.toString().trim().toByteArray(charset)) os.closeEntry() entries.clear() } - - private companion object { - private const val NOTICE_PATH = "META-INF/NOTICE" - private const val NOTICE_TXT_PATH = "META-INF/NOTICE.txt" - private const val NOTICE_MD_PATH = "META-INF/NOTICE.md" - } }