From 194e2806bda4f32374c19f2cd37aebe32846ed2b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 09:38:56 +0000 Subject: [PATCH 1/7] Add ProGuardTransformer to merge META-INF/proguard/*.pro files Agent-Logs-Url: https://github.com/GradleUp/shadow/sessions/f625b90e-a9a1-43c9-894c-d5d337df2da6 Co-authored-by: Goooler <10363352+Goooler@users.noreply.github.com> --- .../transformers/ProGuardTransformerTest.kt | 42 +++++++ .../gradle/plugins/shadow/tasks/ShadowJar.kt | 16 +++ .../transformers/ProGuardTransformer.kt | 53 +++++++++ .../transformers/ProGuardTransformerTest.kt | 107 ++++++++++++++++++ 4 files changed, 218 insertions(+) create mode 100644 src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt create mode 100644 src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformer.kt create mode 100644 src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt new file mode 100644 index 000000000..bcdf455ab --- /dev/null +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt @@ -0,0 +1,42 @@ +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.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class ProGuardTransformerTest : BaseTransformerTest() { + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun mergeProGuardFiles(shortSyntax: Boolean) { + val proGuardEntry = "META-INF/proguard/app.pro" + val content1 = "-keep class com.foo.Bar { *; }" + val content2 = "-keep class com.foo.Baz { *; }" + val one = buildJarOne { insert(proGuardEntry, content1) } + val two = buildJarTwo { insert(proGuardEntry, content2) } + val config = + if (shortSyntax) { + """ + dependencies { + ${implementationFiles(one, two)} + } + $shadowJarTask { + mergeProGuardFiles() + } + """ + .trimIndent() + } else { + transform( + dependenciesBlock = implementationFiles(one, two), + ) + } + projectScript.appendText(config) + + runWithSuccess(shadowJarPath) + + val content = outputShadowedJar.use { it.getContent(proGuardEntry) } + assertThat(content).isEqualTo("$content1\n$content2") + } +} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 3bb828c73..4fc953149 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -20,6 +20,7 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.CacheableTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.GroovyExtensionModuleTransformer +import com.github.jengelman.gradle.plugins.shadow.transformers.ProGuardTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransformer.Companion.create import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer @@ -338,6 +339,21 @@ public abstract class ShadowJar : Jar() { transform(GroovyExtensionModuleTransformer::class.java, action = {}) } + /** + * Merge ProGuard rules files (`META-INF/proguard/**`) with [action]. + * + * *Warning*: In most cases, this should be used with the correct [getDuplicatesStrategy] to + * ensure duplicate ProGuard files are handled properly. See more details in the + * [Handling Duplicates Strategy](https://gradleup.com/shadow/configuration/merging/#handling-duplicates-strategy) + * section. + * + * @see [getDuplicatesStrategy] + */ + @JvmOverloads + public open fun mergeProGuardFiles(action: Action = Action {}) { + transform(ProGuardTransformer::class.java, action) + } + /** * Append contents to a resource in the jar. * diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformer.kt new file mode 100644 index 000000000..0d876d212 --- /dev/null +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformer.kt @@ -0,0 +1,53 @@ +package com.github.jengelman.gradle.plugins.shadow.transformers + +import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry +import org.apache.tools.zip.ZipOutputStream +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.util.PatternSet + +/** + * Resources transformer that merges entries in `META-INF/proguard` resources into a single + * resource. For example, if there are several `META-INF/proguard/app.pro` resources spread across + * many JARs the individual entries will all be concatenated into a single + * `META-INF/proguard/app.pro` resource packaged into the resultant JAR produced by the shading + * process. + */ +@CacheableTransformer +public open class ProGuardTransformer +@JvmOverloads +constructor( + patternSet: PatternSet = PatternSet().include(PROGUARD_PATTERN), +) : PatternFilterableResourceTransformer(patternSet = patternSet) { + @get:Internal internal val proGuardEntries = mutableMapOf>() + + @get:Internal // No need to mark this as an input as `getIncludes` is already marked as `@Input`. + public open var path: String = PROGUARD_PATH + set(value) { + field = value + // Reset the includes to match the new path. + setIncludes(listOf("$value/**")) + } + + override fun transform(context: TransformerContext) { + val lines = proGuardEntries.getOrPut(context.path) { mutableListOf() } + context.inputStream + .bufferedReader() + .use { it.readLines() } + .forEach { line -> lines.add(line) } + } + + override fun hasTransformedResource(): Boolean = proGuardEntries.isNotEmpty() + + override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) { + proGuardEntries.forEach { (path, lines) -> + os.putNextEntry(zipEntry(path, preserveFileTimestamps)) + os.write(lines.joinToString("\n").toByteArray()) + os.closeEntry() + } + } + + private companion object { + private const val PROGUARD_PATH = "META-INF/proguard" + private const val PROGUARD_PATTERN = "$PROGUARD_PATH/**" + } +} diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt new file mode 100644 index 000000000..4796e89bd --- /dev/null +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt @@ -0,0 +1,107 @@ +package com.github.jengelman.gradle.plugins.shadow.transformers + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isTrue +import com.github.jengelman.gradle.plugins.shadow.testkit.JarPath +import com.github.jengelman.gradle.plugins.shadow.testkit.getContent +import com.github.jengelman.gradle.plugins.shadow.util.zipOutputStream +import java.nio.file.Path +import kotlin.io.path.createTempFile +import kotlin.io.path.deleteExisting +import kotlin.io.path.outputStream +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +class ProGuardTransformerTest : BaseTransformerTest() { + private lateinit var tempJar: Path + + @BeforeEach + override fun beforeEach() { + super.beforeEach() + tempJar = createTempFile("shade.", ".jar") + } + + @AfterEach + fun afterEach() { + tempJar.deleteExisting() + } + + @ParameterizedTest + @MethodSource("resourceProvider") + fun canTransformResource(path: String, expected: Boolean) { + assertThat(transformer.canTransformResource(path)).isEqualTo(expected) + } + + @ParameterizedTest + @MethodSource("proGuardFileProvider") + fun transformProGuardFile(path: String, input1: String, input2: String, output: String) { + if (transformer.canTransformResource(path)) { + transformer.transform(textContext(path, input1)) + transformer.transform(textContext(path, input2)) + } + + assertThat(transformer.hasTransformedResource()).isTrue() + val entry = transformer.proGuardEntries.getValue(path).joinToString("\n") + assertThat(entry).isEqualTo(output) + } + + @Test + fun mergesMultipleFiles() { + val path = "META-INF/proguard/app.pro" + val content1 = "-keep class com.foo.Bar { *; }" + val content2 = "-keep class com.foo.Baz { *; }" + + transformer.transform(textContext(path, content1)) + transformer.transform(textContext(path, content2)) + + tempJar.outputStream().zipOutputStream().use { zos -> + transformer.modifyOutputStream(zos, false) + } + + val transformedContent = JarPath(tempJar).use { it.getContent(path) } + assertThat(transformedContent).isEqualTo("-keep class com.foo.Bar { *; }\n-keep class com.foo.Baz { *; }") + } + + @Test + fun canTransformAlternatePath() { + transformer.path = "META-INF/custom" + assertThat(transformer.canTransformResource("META-INF/custom/rules.pro")).isTrue() + assertThat(transformer.canTransformResource("META-INF/proguard/rules.pro")).isFalse() + } + + private companion object { + @JvmStatic + fun resourceProvider() = + listOf( + // path, expected + Arguments.of("META-INF/proguard/app.pro", true), + Arguments.of("META-INF/proguard/rules.pro", true), + Arguments.of("META-INF/services/com.acme.Foo", false), + Arguments.of("foo/bar.properties", false), + ) + + @JvmStatic + fun proGuardFileProvider() = + listOf( + // path, input1, input2, output + Arguments.of( + "META-INF/proguard/app.pro", + "-keep class com.foo.Bar", + "-keep class com.foo.Baz", + "-keep class com.foo.Bar\n-keep class com.foo.Baz", + ), + Arguments.of( + "META-INF/proguard/rules.pro", + "-keep class com.foo.**", + "-dontwarn com.foo.**", + "-keep class com.foo.**\n-dontwarn com.foo.**", + ), + ) + } +} From 36a92f2905fd33225367fbfd005b40b1b09a954d Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 27 Mar 2026 17:41:37 +0800 Subject: [PATCH 2/7] Revert changes in ShadowJar.kt --- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 4fc953149..3bb828c73 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -20,7 +20,6 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.CacheableTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.GroovyExtensionModuleTransformer -import com.github.jengelman.gradle.plugins.shadow.transformers.ProGuardTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransformer.Companion.create import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer @@ -339,21 +338,6 @@ public abstract class ShadowJar : Jar() { transform(GroovyExtensionModuleTransformer::class.java, action = {}) } - /** - * Merge ProGuard rules files (`META-INF/proguard/**`) with [action]. - * - * *Warning*: In most cases, this should be used with the correct [getDuplicatesStrategy] to - * ensure duplicate ProGuard files are handled properly. See more details in the - * [Handling Duplicates Strategy](https://gradleup.com/shadow/configuration/merging/#handling-duplicates-strategy) - * section. - * - * @see [getDuplicatesStrategy] - */ - @JvmOverloads - public open fun mergeProGuardFiles(action: Action = Action {}) { - transform(ProGuardTransformer::class.java, action) - } - /** * Append contents to a resource in the jar. * From 68d053a6fcc596bef1eb5ff4986669b20c075e66 Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 27 Mar 2026 17:42:21 +0800 Subject: [PATCH 3/7] Update changelog --- docs/changes/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changes/README.md b/docs/changes/README.md index 8c9b00ed1..0bdf934c0 100644 --- a/docs/changes/README.md +++ b/docs/changes/README.md @@ -3,6 +3,9 @@ ## [Unreleased](https://github.com/GradleUp/shadow/compare/9.4.1...HEAD) - 2026-xx-xx +### Added + +- Add `ProGuardTransformer` to merge `META-INF/proguard/*.pro` files. ([#1997](https://github.com/GradleUp/shadow/pull/1997)) ## [9.4.1](https://github.com/GradleUp/shadow/releases/tag/9.4.1) - 2026-03-27 From 53d6b55a245eb8a25ce35c6e0d7176b94ea43c1b Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 27 Mar 2026 17:43:29 +0800 Subject: [PATCH 4/7] Reformat and dump --- api/shadow.api | 11 +++++++++++ .../shadow/transformers/ProGuardTransformerTest.kt | 4 +--- .../shadow/transformers/ProGuardTransformer.kt | 10 +++------- .../shadow/transformers/ProGuardTransformerTest.kt | 3 ++- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/api/shadow.api b/api/shadow.api index 46f49e233..f50c08d38 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -460,6 +460,17 @@ public class com/github/jengelman/gradle/plugins/shadow/transformers/PreserveFir public fun getResources ()Lorg/gradle/api/provider/SetProperty; } +public class com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformer : com/github/jengelman/gradle/plugins/shadow/transformers/PatternFilterableResourceTransformer { + 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 getPath ()Ljava/lang/String; + public fun hasTransformedResource ()Z + public fun modifyOutputStream (Lorg/apache/tools/zip/ZipOutputStream;Z)V + public fun setPath (Ljava/lang/String;)V + public fun transform (Lcom/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext;)V +} + public class com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer : 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/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt index bcdf455ab..4f7660897 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt @@ -28,9 +28,7 @@ class ProGuardTransformerTest : BaseTransformerTest() { """ .trimIndent() } else { - transform( - dependenciesBlock = implementationFiles(one, two), - ) + transform(dependenciesBlock = implementationFiles(one, two)) } projectScript.appendText(config) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformer.kt index 0d876d212..52eb17f3c 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformer.kt @@ -15,9 +15,8 @@ import org.gradle.api.tasks.util.PatternSet @CacheableTransformer public open class ProGuardTransformer @JvmOverloads -constructor( - patternSet: PatternSet = PatternSet().include(PROGUARD_PATTERN), -) : PatternFilterableResourceTransformer(patternSet = patternSet) { +constructor(patternSet: PatternSet = PatternSet().include(PROGUARD_PATTERN)) : + PatternFilterableResourceTransformer(patternSet = patternSet) { @get:Internal internal val proGuardEntries = mutableMapOf>() @get:Internal // No need to mark this as an input as `getIncludes` is already marked as `@Input`. @@ -30,10 +29,7 @@ constructor( override fun transform(context: TransformerContext) { val lines = proGuardEntries.getOrPut(context.path) { mutableListOf() } - context.inputStream - .bufferedReader() - .use { it.readLines() } - .forEach { line -> lines.add(line) } + context.inputStream.bufferedReader().use { it.readLines() }.forEach { line -> lines.add(line) } } override fun hasTransformedResource(): Boolean = proGuardEntries.isNotEmpty() diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt index 4796e89bd..b9e31d9a0 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt @@ -65,7 +65,8 @@ class ProGuardTransformerTest : BaseTransformerTest() { } val transformedContent = JarPath(tempJar).use { it.getContent(path) } - assertThat(transformedContent).isEqualTo("-keep class com.foo.Bar { *; }\n-keep class com.foo.Baz { *; }") + assertThat(transformedContent) + .isEqualTo("-keep class com.foo.Bar { *; }\n-keep class com.foo.Baz { *; }") } @Test From 1e7adf4fbc644ee888b7437864baba4d91b7f8bf Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 27 Mar 2026 17:49:34 +0800 Subject: [PATCH 5/7] Tweaks --- api/shadow.api | 2 -- .../shadow/transformers/ProGuardTransformer.kt | 13 ++----------- .../shadow/transformers/ProGuardTransformerTest.kt | 5 ++--- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/api/shadow.api b/api/shadow.api index f50c08d38..5ae620f11 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -464,10 +464,8 @@ public class com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTra 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 getPath ()Ljava/lang/String; public fun hasTransformedResource ()Z public fun modifyOutputStream (Lorg/apache/tools/zip/ZipOutputStream;Z)V - public fun setPath (Ljava/lang/String;)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/ProGuardTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformer.kt index 52eb17f3c..01ea253a2 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformer.kt @@ -19,14 +19,6 @@ constructor(patternSet: PatternSet = PatternSet().include(PROGUARD_PATTERN)) : PatternFilterableResourceTransformer(patternSet = patternSet) { @get:Internal internal val proGuardEntries = mutableMapOf>() - @get:Internal // No need to mark this as an input as `getIncludes` is already marked as `@Input`. - public open var path: String = PROGUARD_PATH - set(value) { - field = value - // Reset the includes to match the new path. - setIncludes(listOf("$value/**")) - } - override fun transform(context: TransformerContext) { val lines = proGuardEntries.getOrPut(context.path) { mutableListOf() } context.inputStream.bufferedReader().use { it.readLines() }.forEach { line -> lines.add(line) } @@ -37,13 +29,12 @@ constructor(patternSet: PatternSet = PatternSet().include(PROGUARD_PATTERN)) : override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) { proGuardEntries.forEach { (path, lines) -> os.putNextEntry(zipEntry(path, preserveFileTimestamps)) - os.write(lines.joinToString("\n").toByteArray()) + os.write(lines.joinToString(System.lineSeparator()).toByteArray()) os.closeEntry() } } private companion object { - private const val PROGUARD_PATH = "META-INF/proguard" - private const val PROGUARD_PATTERN = "$PROGUARD_PATH/**" + private const val PROGUARD_PATTERN = "META-INF/proguard/**" } } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt index b9e31d9a0..7baf1f08d 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt @@ -2,7 +2,6 @@ package com.github.jengelman.gradle.plugins.shadow.transformers import assertk.assertThat import assertk.assertions.isEqualTo -import assertk.assertions.isFalse import assertk.assertions.isTrue import com.github.jengelman.gradle.plugins.shadow.testkit.JarPath import com.github.jengelman.gradle.plugins.shadow.testkit.getContent @@ -71,9 +70,9 @@ class ProGuardTransformerTest : BaseTransformerTest() { @Test fun canTransformAlternatePath() { - transformer.path = "META-INF/custom" + transformer.include("META-INF/custom") + assertThat(transformer.canTransformResource("META-INF/proguard/rules.pro")).isTrue() assertThat(transformer.canTransformResource("META-INF/custom/rules.pro")).isTrue() - assertThat(transformer.canTransformResource("META-INF/proguard/rules.pro")).isFalse() } private companion object { From c6dd36b4c96793b183e9599f9f58697be9f0dc81 Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 27 Mar 2026 18:05:11 +0800 Subject: [PATCH 6/7] Fix tests --- .../transformers/ProGuardTransformerTest.kt | 40 ------------------- .../shadow/transformers/TransformersTest.kt | 16 ++++++++ .../transformers/ProGuardTransformerTest.kt | 2 +- 3 files changed, 17 insertions(+), 41 deletions(-) delete mode 100644 src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt deleted file mode 100644 index 4f7660897..000000000 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt +++ /dev/null @@ -1,40 +0,0 @@ -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.params.ParameterizedTest -import org.junit.jupiter.params.provider.ValueSource - -class ProGuardTransformerTest : BaseTransformerTest() { - @ParameterizedTest - @ValueSource(booleans = [false, true]) - fun mergeProGuardFiles(shortSyntax: Boolean) { - val proGuardEntry = "META-INF/proguard/app.pro" - val content1 = "-keep class com.foo.Bar { *; }" - val content2 = "-keep class com.foo.Baz { *; }" - val one = buildJarOne { insert(proGuardEntry, content1) } - val two = buildJarTwo { insert(proGuardEntry, content2) } - val config = - if (shortSyntax) { - """ - dependencies { - ${implementationFiles(one, two)} - } - $shadowJarTask { - mergeProGuardFiles() - } - """ - .trimIndent() - } else { - transform(dependenciesBlock = implementationFiles(one, two)) - } - projectScript.appendText(config) - - runWithSuccess(shadowJarPath) - - val content = outputShadowedJar.use { it.getContent(proGuardEntry) } - assertThat(content).isEqualTo("$content1\n$content2") - } -} 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 e85c711e5..d428dfbf5 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 @@ -338,6 +338,22 @@ class TransformersTest : BaseTransformerTest() { } } + @Test + fun mergeProGuardFiles() { + val proGuardEntry = "META-INF/proguard/app.pro" + val content1 = "-keep class com.foo.Bar { *; }" + val content2 = "-keep class com.foo.Baz { *; }" + val one = buildJarOne { insert(proGuardEntry, content1) } + val two = buildJarTwo { insert(proGuardEntry, content2) } + val config = transform(dependenciesBlock = implementationFiles(one, two)) + projectScript.appendText(config) + + runWithSuccess(shadowJarPath) + + val content = outputShadowedJar.use { it.getContent(proGuardEntry) } + assertThat(content).isEqualTo("$content1\n$content2") + } + @ParameterizedTest @MethodSource("transformerConfigProvider") fun otherTransformers(pair: Pair>) { diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt index 7baf1f08d..8f2e43b6e 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt @@ -70,7 +70,7 @@ class ProGuardTransformerTest : BaseTransformerTest() { @Test fun canTransformAlternatePath() { - transformer.include("META-INF/custom") + transformer.include("META-INF/custom/**") assertThat(transformer.canTransformResource("META-INF/proguard/rules.pro")).isTrue() assertThat(transformer.canTransformResource("META-INF/custom/rules.pro")).isTrue() } From 12d91975fa61b412dfaa6008ce07da5e6c13addb Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 27 Mar 2026 18:24:28 +0800 Subject: [PATCH 7/7] Support relocating rules --- .../transformers/ProGuardTransformerTest.kt | 52 +++++++++++++++++++ .../shadow/relocation/SimpleRelocator.kt | 45 +++++++++------- .../transformers/ProGuardTransformer.kt | 11 +++- .../shadow/relocation/SimpleRelocatorTest.kt | 6 +++ .../transformers/ProGuardTransformerTest.kt | 37 +++++++++++++ 5 files changed, 131 insertions(+), 20 deletions(-) create mode 100644 src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt new file mode 100644 index 000000000..a87649b73 --- /dev/null +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt @@ -0,0 +1,52 @@ +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 ProGuardTransformerTest : BaseTransformerTest() { + @Test + fun mergeProGuardFiles() { + val proGuardEntry = "META-INF/proguard/app.pro" + val content1 = "-keep class com.foo.Bar { *; }" + val content2 = "-keep class com.foo.Baz { *; }" + val one = buildJarOne { insert(proGuardEntry, content1) } + val two = buildJarTwo { insert(proGuardEntry, content2) } + val config = transform(dependenciesBlock = implementationFiles(one, two)) + projectScript.appendText(config) + + runWithSuccess(shadowJarPath) + + val content = outputShadowedJar.use { it.getContent(proGuardEntry) } + assertThat(content).isEqualTo("$content1\n$content2") + } + + @Test + fun relocateProGuardFiles() { + val proGuardEntry = "META-INF/proguard/app.pro" + val content = "-keep class org.foo.Service { *; }\n-keep class org.foo.exclude.OtherService" + val one = buildJarOne { insert(proGuardEntry, content) } + val config = + """ + dependencies { + ${implementationFiles(one)} + } + $shadowJarTask { + relocate('org.foo', 'borg.foo') { + exclude 'org.foo.exclude.*' + } + transform(com.github.jengelman.gradle.plugins.shadow.transformers.ProGuardTransformer) + } + """ + .trimIndent() + projectScript.appendText(config) + + runWithSuccess(shadowJarPath) + + val transformedContent = outputShadowedJar.use { it.getContent(proGuardEntry) } + assertThat(transformedContent) + .isEqualTo("-keep class borg.foo.Service { *; }\n-keep class org.foo.exclude.OtherService") + } +} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt index 9d624a918..20a752fec 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt @@ -68,31 +68,40 @@ constructor( this.excludes.addAll(excludes) } - if (!rawString) { - // Create exclude pattern sets for sources. - for (exclude in this.excludes) { - // Excludes should be subpackages of the global pattern. - if (exclude.startsWith(this.pattern)) { - sourcePackageExcludes.add( - exclude.substring(this.pattern.length).replaceFirst("[.][*]$".toRegex(), "") - ) - } - // Excludes should be subpackages of the global pattern. - if (exclude.startsWith(pathPattern)) { - sourcePathExcludes.add( - exclude.substring(pathPattern.length).replaceFirst("/[*]$".toRegex(), "") - ) - } + updateSourceExcludes() + } + + private fun updateSourceExcludes() { + if (rawString) return + sourcePackageExcludes.clear() + sourcePathExcludes.clear() + // Create exclude pattern sets for sources. + for (exclude in this.excludes) { + // Excludes should be subpackages of the global pattern. + if (exclude.startsWith(this.pattern)) { + sourcePackageExcludes.add( + exclude.substring(this.pattern.length).replaceFirst("[.][*]$".toRegex(), "") + ) + } + // Excludes should be subpackages of the global pattern. + if (exclude.startsWith(pathPattern)) { + sourcePathExcludes.add( + exclude.substring(pathPattern.length).replaceFirst("/[*]$".toRegex(), "") + ) } } } public open fun include(pattern: String) { includes.addAll(normalizePatterns(listOf(pattern))) + includes.add(pattern) + updateSourceExcludes() } public open fun exclude(pattern: String) { excludes.addAll(normalizePatterns(listOf(pattern))) + excludes.add(pattern) + updateSourceExcludes() } override fun canRelocatePath(path: String): Boolean { @@ -206,7 +215,7 @@ constructor( */ val RX_ENDS_WITH_JAVA_KEYWORD: Pattern = Pattern.compile( - "\\b(import|package|public|protected|private|static|final|synchronized|abstract|volatile|extends|implements|throws) $" + + "\\b(import|package|class|interface|enum|public|protected|private|static|final|synchronized|abstract|volatile|extends|implements|throws|-keep) $" + "|" + "\\{@link( \\*)* $" + "|" + @@ -255,9 +264,7 @@ constructor( // Make sure that search pattern starts at word boundary and that we look for literal ".", not // regex jokers. val snippets = - sourceContent - .split(("\\b" + patternFrom.replace(".", "[.]") + "\\b").toRegex()) - .filter(CharSequence::isNotEmpty) + sourceContent.split(("\\b" + patternFrom.replace(".", "[.]") + "\\b").toRegex()) snippets.forEachIndexed { i, snippet -> val isFirstSnippet = i == 0 val previousSnippet = if (isFirstSnippet) "" else snippets[i - 1] diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformer.kt index 01ea253a2..1a80101ef 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformer.kt @@ -21,7 +21,16 @@ constructor(patternSet: PatternSet = PatternSet().include(PROGUARD_PATTERN)) : override fun transform(context: TransformerContext) { val lines = proGuardEntries.getOrPut(context.path) { mutableListOf() } - context.inputStream.bufferedReader().use { it.readLines() }.forEach { line -> lines.add(line) } + context.inputStream + .bufferedReader() + .use { it.readLines() } + .forEach { line -> + var relocatedLine = line + context.relocators.forEach { relocator -> + relocatedLine = relocator.applyToSourceContent(relocatedLine) + } + lines.add(relocatedLine) + } } override fun hasTransformedResource(): Boolean = proGuardEntries.isNotEmpty() diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt index 6141b1d7e..0d98281ae 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt @@ -344,6 +344,12 @@ class SimpleRelocatorTest { assertThat(relocator.canRelocatePath("org/foo/Class.class")).isFalse() } + @Test + fun relocateSourceAtStart() { + val relocator = SimpleRelocator("org.foo", "borg.foo") + assertThat(relocator.applyToSourceContent("org.foo.Bar")).isEqualTo("borg.foo.Bar") + } + @Test fun relocateSourceWithExcludesRaw() { val relocator = diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt index 8f2e43b6e..a7214f49b 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformerTest.kt @@ -3,6 +3,7 @@ package com.github.jengelman.gradle.plugins.shadow.transformers import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isTrue +import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator import com.github.jengelman.gradle.plugins.shadow.testkit.JarPath import com.github.jengelman.gradle.plugins.shadow.testkit.getContent import com.github.jengelman.gradle.plugins.shadow.util.zipOutputStream @@ -75,6 +76,42 @@ class ProGuardTransformerTest : BaseTransformerTest() { assertThat(transformer.canTransformResource("META-INF/custom/rules.pro")).isTrue() } + @Test + fun relocatedClasses() { + val relocator = SimpleRelocator("org.foo", "borg.foo", excludes = listOf("org.foo.exclude.*")) + val content = "-keep class org.foo.Service { *; }\n-keep class org.foo.exclude.OtherService" + val path = "META-INF/proguard/app.pro" + + transformer.transform(textContext(path, content, relocator)) + + tempJar.outputStream().zipOutputStream().use { zos -> + transformer.modifyOutputStream(zos, false) + } + + val transformedContent = JarPath(tempJar).use { it.getContent(path) } + assertThat(transformedContent) + .isEqualTo("-keep class borg.foo.Service { *; }\n-keep class org.foo.exclude.OtherService") + } + + @Test + fun mergeRelocatedFiles() { + val relocator = SimpleRelocator("org.foo", "borg.foo", excludes = listOf("org.foo.exclude.*")) + val content1 = "-keep class org.foo.Service { *; }" + val content2 = "-keep class org.foo.exclude.OtherService" + val path = "META-INF/proguard/app.pro" + + transformer.transform(textContext(path, content1, relocator)) + transformer.transform(textContext(path, content2, relocator)) + + tempJar.outputStream().zipOutputStream().use { zos -> + transformer.modifyOutputStream(zos, false) + } + + val transformedContent = JarPath(tempJar).use { it.getContent(path) } + assertThat(transformedContent) + .isEqualTo("-keep class borg.foo.Service { *; }\n-keep class org.foo.exclude.OtherService") + } + private companion object { @JvmStatic fun resourceProvider() =