From 0e1d56f751767ec41f3a720c119407a6bf71f1cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:07:17 +0000 Subject: [PATCH 1/8] Initial plan From 3d948d7a1288dad2550ea03ee24a4cf9dd1d4398 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:21:48 +0000 Subject: [PATCH 2/8] Add SpringBootTransformer for handling Spring Boot file patterns Co-authored-by: Goooler <10363352+Goooler@users.noreply.github.com> --- api/shadow.api | 18 ++ .../transformers/SpringBootTransformerTest.kt | 209 ++++++++++++++++++ .../transformers/SpringBootTransformer.kt | 118 ++++++++++ 3 files changed, 345 insertions(+) create mode 100644 src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformerTest.kt create mode 100644 src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt diff --git a/api/shadow.api b/api/shadow.api index 0d90a6c24..f8ff86034 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -521,6 +521,24 @@ public class com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFile public fun transform (Lcom/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext;)V } +public class com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer : com/github/jengelman/gradle/plugins/shadow/transformers/ResourceTransformer { + public static final field Companion Lcom/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer$Companion; + public static final field PATH_SPRING_AUTOCONFIGURE_METADATA Ljava/lang/String; + public static final field PATH_SPRING_FACTORIES Ljava/lang/String; + public static final field PATH_SPRING_HANDLERS Ljava/lang/String; + public static final field PATH_SPRING_SCHEMAS Ljava/lang/String; + public static final field PATH_SPRING_TOOLING Ljava/lang/String; + public fun (Lorg/gradle/api/model/ObjectFactory;)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 fun transform (Lcom/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext;)V +} + +public final class com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer$Companion { +} + public final class com/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext { public static final field Companion Lcom/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext$Companion; public fun (Ljava/lang/String;Ljava/io/InputStream;)V diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformerTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformerTest.kt new file mode 100644 index 000000000..2d809e47d --- /dev/null +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformerTest.kt @@ -0,0 +1,209 @@ +package com.github.jengelman.gradle.plugins.shadow.transformers + +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.github.jengelman.gradle.plugins.shadow.testkit.getContent +import kotlin.io.path.appendText +import org.junit.jupiter.api.Test + +class SpringBootTransformerTest : BaseTransformerTest() { + + @Test + fun mergeSpringFactories() { + val one = buildJarOne { + insert( + SpringBootTransformer.PATH_SPRING_FACTORIES, + "org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.FooAutoConfiguration", + ) + } + val two = buildJarTwo { + insert( + SpringBootTransformer.PATH_SPRING_FACTORIES, + "org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.BarAutoConfiguration", + ) + } + projectScript.appendText( + transform(dependenciesBlock = implementationFiles(one, two)) + ) + + runWithSuccess(shadowJarPath) + + val content = outputShadowedJar.use { it.getContent(SpringBootTransformer.PATH_SPRING_FACTORIES) } + assertThat(content).isEqualTo( + "org.springframework.boot.autoconfigure.EnableAutoConfiguration=" + + "com.example.FooAutoConfiguration,com.example.BarAutoConfiguration\n", + ) + } + + @Test + fun mergeSpringFactoriesWithMultipleKeys() { + val one = buildJarOne { + insert( + SpringBootTransformer.PATH_SPRING_FACTORIES, + "org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.FooAutoConfiguration\n" + + "org.springframework.context.ApplicationListener=com.example.FooListener", + ) + } + val two = buildJarTwo { + insert( + SpringBootTransformer.PATH_SPRING_FACTORIES, + "org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.BarAutoConfiguration\n" + + "org.springframework.context.ApplicationListener=com.example.BarListener", + ) + } + projectScript.appendText( + transform(dependenciesBlock = implementationFiles(one, two)) + ) + + runWithSuccess(shadowJarPath) + + val content = outputShadowedJar.use { it.getContent(SpringBootTransformer.PATH_SPRING_FACTORIES) } + assertThat(content).isEqualTo( + "org.springframework.boot.autoconfigure.EnableAutoConfiguration=" + + "com.example.FooAutoConfiguration,com.example.BarAutoConfiguration\n" + + "org.springframework.context.ApplicationListener=" + + "com.example.FooListener,com.example.BarListener\n", + ) + } + + @Test + fun mergeSpringImports() { + val one = buildJarOne { + insert( + "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports", + "com.example.FooAutoConfiguration", + ) + } + val two = buildJarTwo { + insert( + "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports", + "com.example.BarAutoConfiguration", + ) + } + projectScript.appendText( + transform(dependenciesBlock = implementationFiles(one, two)) + ) + + runWithSuccess(shadowJarPath) + + val content = outputShadowedJar.use { + it.getContent("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports") + } + assertThat(content).isEqualTo( + "com.example.FooAutoConfiguration\ncom.example.BarAutoConfiguration", + ) + } + + @Test + fun deduplicateSpringImports() { + val one = buildJarOne { + insert( + "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports", + "com.example.FooAutoConfiguration\ncom.example.BarAutoConfiguration", + ) + } + val two = buildJarTwo { + insert( + "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports", + "com.example.BarAutoConfiguration\ncom.example.BazAutoConfiguration", + ) + } + projectScript.appendText( + transform(dependenciesBlock = implementationFiles(one, two)) + ) + + runWithSuccess(shadowJarPath) + + val content = outputShadowedJar.use { + it.getContent("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports") + } + assertThat(content).isEqualTo( + "com.example.FooAutoConfiguration\ncom.example.BarAutoConfiguration\ncom.example.BazAutoConfiguration", + ) + } + + @Test + fun mergeSpringHandlers() { + val one = buildJarOne { + insert( + SpringBootTransformer.PATH_SPRING_HANDLERS, + "http\\://www.example.com/schema/foo=com.example.FooNamespaceHandler", + ) + } + val two = buildJarTwo { + insert( + SpringBootTransformer.PATH_SPRING_HANDLERS, + "http\\://www.example.com/schema/bar=com.example.BarNamespaceHandler", + ) + } + projectScript.appendText( + transform(dependenciesBlock = implementationFiles(one, two)) + ) + + runWithSuccess(shadowJarPath) + + val content = outputShadowedJar.use { it.getContent(SpringBootTransformer.PATH_SPRING_HANDLERS) } + assertThat(content).isEqualTo( + "http\\://www.example.com/schema/bar=com.example.BarNamespaceHandler\n" + + "http\\://www.example.com/schema/foo=com.example.FooNamespaceHandler\n", + ) + } + + @Test + fun relocateClassesInSpringFactories() { + val one = buildJarOne { + insert( + SpringBootTransformer.PATH_SPRING_FACTORIES, + "com.example.SomeInterface=com.example.SomeImplementation", + ) + } + projectScript.appendText( + """ + dependencies { + ${implementationFiles(one)} + } + $shadowJarTask { + transform(${SpringBootTransformer::class.java.name}) + relocate('com.example', 'shadow.example') + } + """ + .trimIndent() + ) + + runWithSuccess(shadowJarPath) + + val content = outputShadowedJar.use { it.getContent(SpringBootTransformer.PATH_SPRING_FACTORIES) } + assertThat(content).isEqualTo( + "shadow.example.SomeInterface=shadow.example.SomeImplementation\n", + ) + } + + @Test + fun relocateClassesInSpringImports() { + val one = buildJarOne { + insert( + "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports", + "com.example.FooAutoConfiguration", + ) + } + projectScript.appendText( + """ + dependencies { + ${implementationFiles(one)} + } + $shadowJarTask { + transform(${SpringBootTransformer::class.java.name}) + relocate('com.example', 'shadow.example') + } + """ + .trimIndent() + ) + + runWithSuccess(shadowJarPath) + + val content = outputShadowedJar.use { + it.getContent("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports") + } + assertThat(content).isEqualTo("shadow.example.FooAutoConfiguration") + } +} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt new file mode 100644 index 000000000..50115c84e --- /dev/null +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt @@ -0,0 +1,118 @@ +package com.github.jengelman.gradle.plugins.shadow.transformers + +import com.github.jengelman.gradle.plugins.shadow.internal.ReproducibleProperties +import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry +import com.github.jengelman.gradle.plugins.shadow.relocation.relocateClass +import java.util.Properties +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.tasks.Internal + +/** + * A resource transformer that handles Spring Boot configuration files to enable proper merging + * when creating a shadow JAR. + * + * The following Spring Boot resource files are handled: + * - `META-INF/spring.factories`: Properties file with comma-separated class name values, + * merged by appending values with a comma separator. + * - `META-INF/spring.handlers`: Properties file with class name values, merged by appending + * with a comma separator. + * - `META-INF/spring.schemas`: Properties file with schema URL-to-path mappings, merged by + * appending with a comma separator. + * - `META-INF/spring.tooling`: Properties file with tooling metadata, merged by appending + * with a comma separator. + * - `META-INF/spring-autoconfigure-metadata.properties`: Properties file with autoconfiguration + * metadata, merged by appending with a comma separator. + * - `META-INF/spring/*.imports`: Line-based files where each line is a fully qualified class + * name; lines are deduplicated and merged across JAR files. + * + * Class relocation is applied to both the keys and values of properties files, as well as to + * each line of `.imports` files. + * + * @see Issue #1489 + */ +@CacheableTransformer +public open class SpringBootTransformer +@Inject +constructor(final override val objectFactory: ObjectFactory) : ResourceTransformer { + @get:Internal + internal val propertiesEntries = mutableMapOf() + + @get:Internal + internal val importsEntries = mutableMapOf>() + + override fun canTransformResource(element: FileTreeElement): Boolean { + val path = element.path + return path in PROPERTIES_PATHS || isImportsFile(path) + } + + override fun transform(context: TransformerContext) { + val path = context.path + if (isImportsFile(path)) { + val entries = importsEntries.getOrPut(path) { linkedSetOf() } + context.inputStream + .bufferedReader() + .lineSequence() + .map { it.trim() } + .filter { it.isNotEmpty() && !it.startsWith("#") } + .map { context.relocators.relocateClass(it) } + .forEach { entries.add(it) } + } else { + val props = propertiesEntries.getOrPut(path) { ReproducibleProperties() } + val incoming = Properties().apply { load(context.inputStream.bufferedReader(Charsets.ISO_8859_1)) } + incoming.forEach { rawKey, rawValue -> + val key = context.relocators.relocateClass(rawKey as String) + val value = (rawValue as String).splitToSequence(",").joinToString(",") { part -> + context.relocators.relocateClass(part.trim()) + } + val existing = props.getProperty(key) + if (existing != null) { + props[key] = "$existing,$value" + } else { + props[key] = value + } + } + } + } + + override fun hasTransformedResource(): Boolean = + propertiesEntries.isNotEmpty() || importsEntries.isNotEmpty() + + override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) { + propertiesEntries.forEach { (path, props) -> + os.putNextEntry(zipEntry(path, preserveFileTimestamps)) + props.writeWithoutComments(Charsets.ISO_8859_1, os) + os.closeEntry() + } + importsEntries.forEach { (path, entries) -> + os.putNextEntry(zipEntry(path, preserveFileTimestamps)) + os.write(entries.joinToString("\n").toByteArray()) + os.closeEntry() + } + } + + public companion object { + public const val PATH_SPRING_FACTORIES: String = "META-INF/spring.factories" + public const val PATH_SPRING_HANDLERS: String = "META-INF/spring.handlers" + public const val PATH_SPRING_SCHEMAS: String = "META-INF/spring.schemas" + public const val PATH_SPRING_TOOLING: String = "META-INF/spring.tooling" + public const val PATH_SPRING_AUTOCONFIGURE_METADATA: String = + "META-INF/spring-autoconfigure-metadata.properties" + + private const val SPRING_IMPORTS_PREFIX = "META-INF/spring/" + + internal val PROPERTIES_PATHS = + setOf( + PATH_SPRING_FACTORIES, + PATH_SPRING_HANDLERS, + PATH_SPRING_SCHEMAS, + PATH_SPRING_TOOLING, + PATH_SPRING_AUTOCONFIGURE_METADATA, + ) + + internal fun isImportsFile(path: String): Boolean = + path.startsWith(SPRING_IMPORTS_PREFIX) && path.endsWith(".imports") + } +} From f32c353a29799b5800749271a974292236672c5b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:29:45 +0000 Subject: [PATCH 3/8] Address code review feedback and add changelog entry Co-authored-by: Goooler <10363352+Goooler@users.noreply.github.com> --- docs/changes/README.md | 4 +++ .../transformers/SpringBootTransformer.kt | 25 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/docs/changes/README.md b/docs/changes/README.md index d519966c6..ad3651505 100644 --- a/docs/changes/README.md +++ b/docs/changes/README.md @@ -3,6 +3,10 @@ ## [Unreleased](https://github.com/GradleUp/shadow/compare/9.3.3...HEAD) - 2026-xx-xx +### Added + +- Add `SpringBootTransformer` to handle Spring Boot configuration files merging. ([#1936](https://github.com/GradleUp/shadow/pull/1936)) + ## [9.3.2](https://github.com/GradleUp/shadow/releases/tag/9.3.2) - 2026-02-27 diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt index 50115c84e..3832dd6fb 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt @@ -2,7 +2,9 @@ package com.github.jengelman.gradle.plugins.shadow.transformers import com.github.jengelman.gradle.plugins.shadow.internal.ReproducibleProperties import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry +import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.relocateClass +import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath import java.util.Properties import javax.inject.Inject import org.apache.tools.zip.ZipOutputStream @@ -28,8 +30,9 @@ import org.gradle.api.tasks.Internal * - `META-INF/spring/*.imports`: Line-based files where each line is a fully qualified class * name; lines are deduplicated and merged across JAR files. * - * Class relocation is applied to both the keys and values of properties files, as well as to - * each line of `.imports` files. + * Class relocation is applied to both the keys and values of properties files (using path-based + * relocation for slash-notation values, and class-based relocation for dot-notation values), as + * well as to each line of `.imports` files. * * @see Issue #1489 */ @@ -61,11 +64,11 @@ constructor(final override val objectFactory: ObjectFactory) : ResourceTransform .forEach { entries.add(it) } } else { val props = propertiesEntries.getOrPut(path) { ReproducibleProperties() } - val incoming = Properties().apply { load(context.inputStream.bufferedReader(Charsets.ISO_8859_1)) } + val incoming = Properties().apply { load(context.inputStream.bufferedReader(PROPERTIES_CHARSET)) } incoming.forEach { rawKey, rawValue -> val key = context.relocators.relocateClass(rawKey as String) val value = (rawValue as String).splitToSequence(",").joinToString(",") { part -> - context.relocators.relocateClass(part.trim()) + context.relocators.relocateValue(part.trim()) } val existing = props.getProperty(key) if (existing != null) { @@ -83,7 +86,7 @@ constructor(final override val objectFactory: ObjectFactory) : ResourceTransform override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) { propertiesEntries.forEach { (path, props) -> os.putNextEntry(zipEntry(path, preserveFileTimestamps)) - props.writeWithoutComments(Charsets.ISO_8859_1, os) + props.writeWithoutComments(PROPERTIES_CHARSET, os) os.closeEntry() } importsEntries.forEach { (path, entries) -> @@ -103,6 +106,8 @@ constructor(final override val objectFactory: ObjectFactory) : ResourceTransform private const val SPRING_IMPORTS_PREFIX = "META-INF/spring/" + private val PROPERTIES_CHARSET = Charsets.ISO_8859_1 + internal val PROPERTIES_PATHS = setOf( PATH_SPRING_FACTORIES, @@ -114,5 +119,15 @@ constructor(final override val objectFactory: ObjectFactory) : ResourceTransform internal fun isImportsFile(path: String): Boolean = path.startsWith(SPRING_IMPORTS_PREFIX) && path.endsWith(".imports") + + /** + * Relocates a value that may be either a dot-notation class name (e.g., + * `com.example.MyClass`) or a slash-notation resource path (e.g., + * `com/example/schema.xsd`). Path-notation values are relocated using + * [Iterable.relocatePath], and class-notation values using [Iterable.relocateClass]. + */ + private fun Iterable.relocateValue(value: String): String { + return if (value.contains('/')) relocatePath(value) else relocateClass(value) + } } } From 63a02357f76ac1b43b60abd4d327d6f96cb4a295 Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 27 Feb 2026 20:44:18 +0800 Subject: [PATCH 4/8] Fix formats --- .../transformers/SpringBootTransformerTest.kt | 87 +++++++++++-------- .../transformers/SpringBootTransformer.kt | 42 ++++----- 2 files changed, 72 insertions(+), 57 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformerTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformerTest.kt index 2d809e47d..b8810ef8e 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformerTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformerTest.kt @@ -28,11 +28,13 @@ class SpringBootTransformerTest : BaseTransformerTest() { runWithSuccess(shadowJarPath) - val content = outputShadowedJar.use { it.getContent(SpringBootTransformer.PATH_SPRING_FACTORIES) } - assertThat(content).isEqualTo( - "org.springframework.boot.autoconfigure.EnableAutoConfiguration=" + - "com.example.FooAutoConfiguration,com.example.BarAutoConfiguration\n", - ) + val content = + outputShadowedJar.use { it.getContent(SpringBootTransformer.PATH_SPRING_FACTORIES) } + assertThat(content) + .isEqualTo( + "org.springframework.boot.autoconfigure.EnableAutoConfiguration=" + + "com.example.FooAutoConfiguration,com.example.BarAutoConfiguration\n" + ) } @Test @@ -57,13 +59,15 @@ class SpringBootTransformerTest : BaseTransformerTest() { runWithSuccess(shadowJarPath) - val content = outputShadowedJar.use { it.getContent(SpringBootTransformer.PATH_SPRING_FACTORIES) } - assertThat(content).isEqualTo( - "org.springframework.boot.autoconfigure.EnableAutoConfiguration=" + - "com.example.FooAutoConfiguration,com.example.BarAutoConfiguration\n" + - "org.springframework.context.ApplicationListener=" + - "com.example.FooListener,com.example.BarListener\n", - ) + val content = + outputShadowedJar.use { it.getContent(SpringBootTransformer.PATH_SPRING_FACTORIES) } + assertThat(content) + .isEqualTo( + "org.springframework.boot.autoconfigure.EnableAutoConfiguration=" + + "com.example.FooAutoConfiguration,com.example.BarAutoConfiguration\n" + + "org.springframework.context.ApplicationListener=" + + "com.example.FooListener,com.example.BarListener\n" + ) } @Test @@ -86,12 +90,14 @@ class SpringBootTransformerTest : BaseTransformerTest() { runWithSuccess(shadowJarPath) - val content = outputShadowedJar.use { - it.getContent("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports") - } - assertThat(content).isEqualTo( - "com.example.FooAutoConfiguration\ncom.example.BarAutoConfiguration", - ) + val content = + outputShadowedJar.use { + it.getContent( + "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports" + ) + } + assertThat(content) + .isEqualTo("com.example.FooAutoConfiguration\ncom.example.BarAutoConfiguration") } @Test @@ -114,12 +120,16 @@ class SpringBootTransformerTest : BaseTransformerTest() { runWithSuccess(shadowJarPath) - val content = outputShadowedJar.use { - it.getContent("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports") - } - assertThat(content).isEqualTo( - "com.example.FooAutoConfiguration\ncom.example.BarAutoConfiguration\ncom.example.BazAutoConfiguration", - ) + val content = + outputShadowedJar.use { + it.getContent( + "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports" + ) + } + assertThat(content) + .isEqualTo( + "com.example.FooAutoConfiguration\ncom.example.BarAutoConfiguration\ncom.example.BazAutoConfiguration" + ) } @Test @@ -142,11 +152,13 @@ class SpringBootTransformerTest : BaseTransformerTest() { runWithSuccess(shadowJarPath) - val content = outputShadowedJar.use { it.getContent(SpringBootTransformer.PATH_SPRING_HANDLERS) } - assertThat(content).isEqualTo( - "http\\://www.example.com/schema/bar=com.example.BarNamespaceHandler\n" + - "http\\://www.example.com/schema/foo=com.example.FooNamespaceHandler\n", - ) + val content = + outputShadowedJar.use { it.getContent(SpringBootTransformer.PATH_SPRING_HANDLERS) } + assertThat(content) + .isEqualTo( + "http\\://www.example.com/schema/bar=com.example.BarNamespaceHandler\n" + + "http\\://www.example.com/schema/foo=com.example.FooNamespaceHandler\n" + ) } @Test @@ -172,10 +184,10 @@ class SpringBootTransformerTest : BaseTransformerTest() { runWithSuccess(shadowJarPath) - val content = outputShadowedJar.use { it.getContent(SpringBootTransformer.PATH_SPRING_FACTORIES) } - assertThat(content).isEqualTo( - "shadow.example.SomeInterface=shadow.example.SomeImplementation\n", - ) + val content = + outputShadowedJar.use { it.getContent(SpringBootTransformer.PATH_SPRING_FACTORIES) } + assertThat(content) + .isEqualTo("shadow.example.SomeInterface=shadow.example.SomeImplementation\n") } @Test @@ -201,9 +213,12 @@ class SpringBootTransformerTest : BaseTransformerTest() { runWithSuccess(shadowJarPath) - val content = outputShadowedJar.use { - it.getContent("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports") - } + val content = + outputShadowedJar.use { + it.getContent( + "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports" + ) + } assertThat(content).isEqualTo("shadow.example.FooAutoConfiguration") } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt index 3832dd6fb..593ae5279 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt @@ -13,21 +13,21 @@ import org.gradle.api.model.ObjectFactory import org.gradle.api.tasks.Internal /** - * A resource transformer that handles Spring Boot configuration files to enable proper merging - * when creating a shadow JAR. + * A resource transformer that handles Spring Boot configuration files to enable proper merging when + * creating a shadow JAR. * * The following Spring Boot resource files are handled: - * - `META-INF/spring.factories`: Properties file with comma-separated class name values, - * merged by appending values with a comma separator. - * - `META-INF/spring.handlers`: Properties file with class name values, merged by appending - * with a comma separator. + * - `META-INF/spring.factories`: Properties file with comma-separated class name values, merged by + * appending values with a comma separator. + * - `META-INF/spring.handlers`: Properties file with class name values, merged by appending with a + * comma separator. * - `META-INF/spring.schemas`: Properties file with schema URL-to-path mappings, merged by * appending with a comma separator. - * - `META-INF/spring.tooling`: Properties file with tooling metadata, merged by appending - * with a comma separator. + * - `META-INF/spring.tooling`: Properties file with tooling metadata, merged by appending with a + * comma separator. * - `META-INF/spring-autoconfigure-metadata.properties`: Properties file with autoconfiguration * metadata, merged by appending with a comma separator. - * - `META-INF/spring/*.imports`: Line-based files where each line is a fully qualified class + * - `META-INF/spring/`*`.imports`: Line-based files where each line is a fully qualified class * name; lines are deduplicated and merged across JAR files. * * Class relocation is applied to both the keys and values of properties files (using path-based @@ -40,11 +40,9 @@ import org.gradle.api.tasks.Internal public open class SpringBootTransformer @Inject constructor(final override val objectFactory: ObjectFactory) : ResourceTransformer { - @get:Internal - internal val propertiesEntries = mutableMapOf() + @get:Internal internal val propertiesEntries = mutableMapOf() - @get:Internal - internal val importsEntries = mutableMapOf>() + @get:Internal internal val importsEntries = mutableMapOf>() override fun canTransformResource(element: FileTreeElement): Boolean { val path = element.path @@ -64,12 +62,14 @@ constructor(final override val objectFactory: ObjectFactory) : ResourceTransform .forEach { entries.add(it) } } else { val props = propertiesEntries.getOrPut(path) { ReproducibleProperties() } - val incoming = Properties().apply { load(context.inputStream.bufferedReader(PROPERTIES_CHARSET)) } + val incoming = + Properties().apply { load(context.inputStream.bufferedReader(PROPERTIES_CHARSET)) } incoming.forEach { rawKey, rawValue -> val key = context.relocators.relocateClass(rawKey as String) - val value = (rawValue as String).splitToSequence(",").joinToString(",") { part -> - context.relocators.relocateValue(part.trim()) - } + val value = + (rawValue as String).splitToSequence(",").joinToString(",") { part -> + context.relocators.relocateValue(part.trim()) + } val existing = props.getProperty(key) if (existing != null) { props[key] = "$existing,$value" @@ -121,10 +121,10 @@ constructor(final override val objectFactory: ObjectFactory) : ResourceTransform path.startsWith(SPRING_IMPORTS_PREFIX) && path.endsWith(".imports") /** - * Relocates a value that may be either a dot-notation class name (e.g., - * `com.example.MyClass`) or a slash-notation resource path (e.g., - * `com/example/schema.xsd`). Path-notation values are relocated using - * [Iterable.relocatePath], and class-notation values using [Iterable.relocateClass]. + * Relocates a value that may be either a dot-notation class name (e.g., `com.example.MyClass`) + * or a slash-notation resource path (e.g., `com/example/schema.xsd`). Path-notation values are + * relocated using [Iterable.relocatePath], and class-notation values using + * [Iterable.relocateClass]. */ private fun Iterable.relocateValue(value: String): String { return if (value.contains('/')) relocatePath(value) else relocateClass(value) From 259d7d0b909317b9790eb518f863773837b32595 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:52:07 +0000 Subject: [PATCH 5/8] Simplify SpringBootTransformer by extending PatternFilterableResourceTransformer Co-authored-by: Goooler <10363352+Goooler@users.noreply.github.com> --- api/shadow.api | 8 ++--- .../transformers/SpringBootTransformer.kt | 31 +++++++------------ 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/api/shadow.api b/api/shadow.api index f8ff86034..2933d4e6c 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -521,16 +521,16 @@ public class com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFile public fun transform (Lcom/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext;)V } -public class com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer : com/github/jengelman/gradle/plugins/shadow/transformers/ResourceTransformer { +public class com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer : com/github/jengelman/gradle/plugins/shadow/transformers/PatternFilterableResourceTransformer { public static final field Companion Lcom/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer$Companion; public static final field PATH_SPRING_AUTOCONFIGURE_METADATA Ljava/lang/String; public static final field PATH_SPRING_FACTORIES Ljava/lang/String; public static final field PATH_SPRING_HANDLERS Ljava/lang/String; public static final field PATH_SPRING_SCHEMAS Ljava/lang/String; public static final field PATH_SPRING_TOOLING Ljava/lang/String; - public fun (Lorg/gradle/api/model/ObjectFactory;)V - public fun canTransformResource (Lorg/gradle/api/file/FileTreeElement;)Z - public final fun getObjectFactory ()Lorg/gradle/api/model/ObjectFactory; + public fun ()V + public fun (Lorg/gradle/api/tasks/util/PatternSet;)V + public synthetic fun (Lorg/gradle/api/tasks/util/PatternSet;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun hasTransformedResource ()Z public fun modifyOutputStream (Lorg/apache/tools/zip/ZipOutputStream;Z)V public fun transform (Lcom/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext;)V diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt index 593ae5279..c3b69ef98 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt @@ -6,11 +6,9 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.relocateClass import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath import java.util.Properties -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.tasks.Internal +import org.gradle.api.tasks.util.PatternSet /** * A resource transformer that handles Spring Boot configuration files to enable proper merging when @@ -38,17 +36,21 @@ import org.gradle.api.tasks.Internal */ @CacheableTransformer public open class SpringBootTransformer -@Inject -constructor(final override val objectFactory: ObjectFactory) : ResourceTransformer { +@JvmOverloads +constructor( + patternSet: PatternSet = + PatternSet() + .include(PATH_SPRING_FACTORIES) + .include(PATH_SPRING_HANDLERS) + .include(PATH_SPRING_SCHEMAS) + .include(PATH_SPRING_TOOLING) + .include(PATH_SPRING_AUTOCONFIGURE_METADATA) + .include("$SPRING_IMPORTS_PREFIX*.imports"), +) : PatternFilterableResourceTransformer(patternSet = patternSet) { @get:Internal internal val propertiesEntries = mutableMapOf() @get:Internal internal val importsEntries = mutableMapOf>() - override fun canTransformResource(element: FileTreeElement): Boolean { - val path = element.path - return path in PROPERTIES_PATHS || isImportsFile(path) - } - override fun transform(context: TransformerContext) { val path = context.path if (isImportsFile(path)) { @@ -108,15 +110,6 @@ constructor(final override val objectFactory: ObjectFactory) : ResourceTransform private val PROPERTIES_CHARSET = Charsets.ISO_8859_1 - internal val PROPERTIES_PATHS = - setOf( - PATH_SPRING_FACTORIES, - PATH_SPRING_HANDLERS, - PATH_SPRING_SCHEMAS, - PATH_SPRING_TOOLING, - PATH_SPRING_AUTOCONFIGURE_METADATA, - ) - internal fun isImportsFile(path: String): Boolean = path.startsWith(SPRING_IMPORTS_PREFIX) && path.endsWith(".imports") From aca3dfb80d490f71146af510043b47bc444b465d Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 27 Feb 2026 20:59:03 +0800 Subject: [PATCH 6/8] Fix formats --- .../gradle/plugins/shadow/transformers/SpringBootTransformer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt index c3b69ef98..a74bf0530 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformer.kt @@ -45,7 +45,7 @@ constructor( .include(PATH_SPRING_SCHEMAS) .include(PATH_SPRING_TOOLING) .include(PATH_SPRING_AUTOCONFIGURE_METADATA) - .include("$SPRING_IMPORTS_PREFIX*.imports"), + .include("$SPRING_IMPORTS_PREFIX*.imports") ) : PatternFilterableResourceTransformer(patternSet = patternSet) { @get:Internal internal val propertiesEntries = mutableMapOf() From ba1e89ed3b0eb632f8dea73ad3e510f7e058fa78 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 1 Mar 2026 18:17:20 +0800 Subject: [PATCH 7/8] Imports --- .../transformers/SpringBootTransformerTest.kt | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformerTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformerTest.kt index b8810ef8e..8d192e9d8 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformerTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformerTest.kt @@ -3,6 +3,8 @@ package com.github.jengelman.gradle.plugins.shadow.transformers import assertk.assertThat import assertk.assertions.isEqualTo import com.github.jengelman.gradle.plugins.shadow.testkit.getContent +import com.github.jengelman.gradle.plugins.shadow.transformers.SpringBootTransformer.Companion.PATH_SPRING_FACTORIES +import com.github.jengelman.gradle.plugins.shadow.transformers.SpringBootTransformer.Companion.PATH_SPRING_HANDLERS import kotlin.io.path.appendText import org.junit.jupiter.api.Test @@ -12,13 +14,13 @@ class SpringBootTransformerTest : BaseTransformerTest() { fun mergeSpringFactories() { val one = buildJarOne { insert( - SpringBootTransformer.PATH_SPRING_FACTORIES, + PATH_SPRING_FACTORIES, "org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.FooAutoConfiguration", ) } val two = buildJarTwo { insert( - SpringBootTransformer.PATH_SPRING_FACTORIES, + PATH_SPRING_FACTORIES, "org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.BarAutoConfiguration", ) } @@ -28,8 +30,7 @@ class SpringBootTransformerTest : BaseTransformerTest() { runWithSuccess(shadowJarPath) - val content = - outputShadowedJar.use { it.getContent(SpringBootTransformer.PATH_SPRING_FACTORIES) } + val content = outputShadowedJar.use { it.getContent(PATH_SPRING_FACTORIES) } assertThat(content) .isEqualTo( "org.springframework.boot.autoconfigure.EnableAutoConfiguration=" + @@ -41,14 +42,14 @@ class SpringBootTransformerTest : BaseTransformerTest() { fun mergeSpringFactoriesWithMultipleKeys() { val one = buildJarOne { insert( - SpringBootTransformer.PATH_SPRING_FACTORIES, + PATH_SPRING_FACTORIES, "org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.FooAutoConfiguration\n" + "org.springframework.context.ApplicationListener=com.example.FooListener", ) } val two = buildJarTwo { insert( - SpringBootTransformer.PATH_SPRING_FACTORIES, + PATH_SPRING_FACTORIES, "org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.BarAutoConfiguration\n" + "org.springframework.context.ApplicationListener=com.example.BarListener", ) @@ -59,8 +60,7 @@ class SpringBootTransformerTest : BaseTransformerTest() { runWithSuccess(shadowJarPath) - val content = - outputShadowedJar.use { it.getContent(SpringBootTransformer.PATH_SPRING_FACTORIES) } + val content = outputShadowedJar.use { it.getContent(PATH_SPRING_FACTORIES) } assertThat(content) .isEqualTo( "org.springframework.boot.autoconfigure.EnableAutoConfiguration=" + @@ -136,13 +136,13 @@ class SpringBootTransformerTest : BaseTransformerTest() { fun mergeSpringHandlers() { val one = buildJarOne { insert( - SpringBootTransformer.PATH_SPRING_HANDLERS, + PATH_SPRING_HANDLERS, "http\\://www.example.com/schema/foo=com.example.FooNamespaceHandler", ) } val two = buildJarTwo { insert( - SpringBootTransformer.PATH_SPRING_HANDLERS, + PATH_SPRING_HANDLERS, "http\\://www.example.com/schema/bar=com.example.BarNamespaceHandler", ) } @@ -152,8 +152,7 @@ class SpringBootTransformerTest : BaseTransformerTest() { runWithSuccess(shadowJarPath) - val content = - outputShadowedJar.use { it.getContent(SpringBootTransformer.PATH_SPRING_HANDLERS) } + val content = outputShadowedJar.use { it.getContent(PATH_SPRING_HANDLERS) } assertThat(content) .isEqualTo( "http\\://www.example.com/schema/bar=com.example.BarNamespaceHandler\n" + @@ -164,10 +163,7 @@ class SpringBootTransformerTest : BaseTransformerTest() { @Test fun relocateClassesInSpringFactories() { val one = buildJarOne { - insert( - SpringBootTransformer.PATH_SPRING_FACTORIES, - "com.example.SomeInterface=com.example.SomeImplementation", - ) + insert(PATH_SPRING_FACTORIES, "com.example.SomeInterface=com.example.SomeImplementation") } projectScript.appendText( """ @@ -184,8 +180,7 @@ class SpringBootTransformerTest : BaseTransformerTest() { runWithSuccess(shadowJarPath) - val content = - outputShadowedJar.use { it.getContent(SpringBootTransformer.PATH_SPRING_FACTORIES) } + val content = outputShadowedJar.use { it.getContent(PATH_SPRING_FACTORIES) } assertThat(content) .isEqualTo("shadow.example.SomeInterface=shadow.example.SomeImplementation\n") } From 58ff0ec540870bc779b805e32af2f27d66bb7667 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 1 Mar 2026 18:24:39 +0800 Subject: [PATCH 8/8] Call invariantEolString --- .../transformers/SpringBootTransformerTest.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformerTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformerTest.kt index 8d192e9d8..8c1b63846 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformerTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/SpringBootTransformerTest.kt @@ -3,6 +3,7 @@ package com.github.jengelman.gradle.plugins.shadow.transformers import assertk.assertThat import assertk.assertions.isEqualTo import com.github.jengelman.gradle.plugins.shadow.testkit.getContent +import com.github.jengelman.gradle.plugins.shadow.testkit.invariantEolString import com.github.jengelman.gradle.plugins.shadow.transformers.SpringBootTransformer.Companion.PATH_SPRING_FACTORIES import com.github.jengelman.gradle.plugins.shadow.transformers.SpringBootTransformer.Companion.PATH_SPRING_HANDLERS import kotlin.io.path.appendText @@ -31,7 +32,7 @@ class SpringBootTransformerTest : BaseTransformerTest() { runWithSuccess(shadowJarPath) val content = outputShadowedJar.use { it.getContent(PATH_SPRING_FACTORIES) } - assertThat(content) + assertThat(content.invariantEolString) .isEqualTo( "org.springframework.boot.autoconfigure.EnableAutoConfiguration=" + "com.example.FooAutoConfiguration,com.example.BarAutoConfiguration\n" @@ -61,7 +62,7 @@ class SpringBootTransformerTest : BaseTransformerTest() { runWithSuccess(shadowJarPath) val content = outputShadowedJar.use { it.getContent(PATH_SPRING_FACTORIES) } - assertThat(content) + assertThat(content.invariantEolString) .isEqualTo( "org.springframework.boot.autoconfigure.EnableAutoConfiguration=" + "com.example.FooAutoConfiguration,com.example.BarAutoConfiguration\n" + @@ -96,7 +97,7 @@ class SpringBootTransformerTest : BaseTransformerTest() { "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports" ) } - assertThat(content) + assertThat(content.invariantEolString) .isEqualTo("com.example.FooAutoConfiguration\ncom.example.BarAutoConfiguration") } @@ -126,7 +127,7 @@ class SpringBootTransformerTest : BaseTransformerTest() { "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports" ) } - assertThat(content) + assertThat(content.invariantEolString) .isEqualTo( "com.example.FooAutoConfiguration\ncom.example.BarAutoConfiguration\ncom.example.BazAutoConfiguration" ) @@ -153,7 +154,7 @@ class SpringBootTransformerTest : BaseTransformerTest() { runWithSuccess(shadowJarPath) val content = outputShadowedJar.use { it.getContent(PATH_SPRING_HANDLERS) } - assertThat(content) + assertThat(content.invariantEolString) .isEqualTo( "http\\://www.example.com/schema/bar=com.example.BarNamespaceHandler\n" + "http\\://www.example.com/schema/foo=com.example.FooNamespaceHandler\n" @@ -181,7 +182,7 @@ class SpringBootTransformerTest : BaseTransformerTest() { runWithSuccess(shadowJarPath) val content = outputShadowedJar.use { it.getContent(PATH_SPRING_FACTORIES) } - assertThat(content) + assertThat(content.invariantEolString) .isEqualTo("shadow.example.SomeInterface=shadow.example.SomeImplementation\n") } @@ -214,6 +215,6 @@ class SpringBootTransformerTest : BaseTransformerTest() { "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports" ) } - assertThat(content).isEqualTo("shadow.example.FooAutoConfiguration") + assertThat(content.invariantEolString).isEqualTo("shadow.example.FooAutoConfiguration") } }