From 3ce232f5e46f23e554560a61559641022134d239 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 29 Jul 2026 21:30:46 +0800 Subject: [PATCH 01/17] Expose generated R8 configuration file --- api/shadow.api | 1 + docs/changes/README.md | 4 ++++ docs/configuration/minimizing/README.md | 5 +++++ .../gradle/plugins/shadow/MinimizeTest.kt | 5 +++++ .../plugins/shadow/internal/DefaultR8Spec.kt | 14 +++++++++++++- .../gradle/plugins/shadow/internal/R8Minimizer.kt | 7 ++++--- .../gradle/plugins/shadow/tasks/R8Spec.kt | 9 +++++++++ .../plugins/shadow/internal/MinimizeSpecsTest.kt | 2 ++ 8 files changed, 43 insertions(+), 4 deletions(-) diff --git a/api/shadow.api b/api/shadow.api index 749648679..bfc17e70d 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -226,6 +226,7 @@ public abstract interface class com/github/jengelman/gradle/plugins/shadow/tasks public abstract fun enableObfuscation ()V public abstract fun enableOptimization ()V public abstract fun getArgs ()Lorg/gradle/api/provider/ListProperty; + public abstract fun getConfigurationFile ()Lorg/gradle/api/file/RegularFileProperty; public fun getKeepRuleFiles ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getKeepRules ()Lorg/gradle/api/provider/ListProperty; public abstract fun getProguardRuleFiles ()Lorg/gradle/api/file/ConfigurableFileCollection; diff --git a/docs/changes/README.md b/docs/changes/README.md index a8eb00dde..8a0c873c0 100644 --- a/docs/changes/README.md +++ b/docs/changes/README.md @@ -3,6 +3,10 @@ ## [Unreleased](https://github.com/GradleUp/shadow/compare/9.6.1...HEAD) - 2026-xx-xx +### Added + +- Allow configuring the final R8 ProGuard configuration file with `R8Spec.configurationFile`. + ### Changed - Bump min Gradle requirement to 9.4.0. ([#2114](https://github.com/GradleUp/shadow/pull/2114)) diff --git a/docs/configuration/minimizing/README.md b/docs/configuration/minimizing/README.md index 57cb861b9..cabea91f0 100644 --- a/docs/configuration/minimizing/README.md +++ b/docs/configuration/minimizing/README.md @@ -94,6 +94,7 @@ Shadow also extracts R8 rules published in dependency JARs, for example under `M // Optional extra configuration proguardRules.add("-keep class com.example.ReflectiveApi { *; }") proguardRuleFiles.from(layout.projectDirectory.file("r8-rules.pro")) + configurationFile.set(layout.buildDirectory.file("r8/configuration.txt")) } } } @@ -112,11 +113,15 @@ Shadow also extracts R8 rules published in dependency JARs, for example under `M // Optional extra configuration proguardRules.add('-keep class com.example.ReflectiveApi { *; }') proguardRuleFiles.from(layout.projectDirectory.file('r8-rules.pro')) + configurationFile.set(layout.buildDirectory.file('r8/configuration.txt')) } } } ``` +Shadow writes the final generated ProGuard configuration to +`build/shadowJar/configuration.txt` by default. Set `configurationFile` to retain it elsewhere. + Shadow resolves R8 from the `shadowR8` configuration. The default dependency is `com.android.tools:r8`, which is published by Google Maven rather than Maven Central. Add `google()` to your repositories or override the dependency: diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index fee83e74f..a0b2a7885 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt @@ -328,6 +328,8 @@ class MinimizeTest : BasePluginTest() { *manifestEntries, ) } + assertThat(path("server/build/shadowJar/configuration.txt").readText()) + .contains("-keep,includedescriptorclasses class server.Server { *; }") } @Test @@ -366,6 +368,7 @@ class MinimizeTest : BasePluginTest() { minimize { r8 { proguardRules.add("-keep class client.Reflective { *; }") + configurationFile.set(layout.buildDirectory.file("r8/final-configuration.txt")) } } """ @@ -384,6 +387,8 @@ class MinimizeTest : BasePluginTest() { *manifestEntries, ) } + assertThat(path("server/build/r8/final-configuration.txt").readText()) + .contains("-keep class client.Reflective { *; }") } @Test diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultR8Spec.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultR8Spec.kt index a0200bb87..6290d0250 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultR8Spec.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultR8Spec.kt @@ -2,13 +2,20 @@ package com.github.jengelman.gradle.plugins.shadow.internal import com.github.jengelman.gradle.plugins.shadow.tasks.R8Spec import javax.inject.Inject +import org.gradle.api.Project import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.Input -internal open class DefaultR8Spec @Inject constructor(objectFactory: ObjectFactory) : R8Spec { +internal open class DefaultR8Spec +@Inject +constructor( + project: Project, + objectFactory: ObjectFactory, +) : R8Spec { private val defaultArgs: ListProperty = objectFactory.listProperty(DEFAULT_ARGS) @get:Input val obfuscationEnabled: Property = objectFactory.property(false) @@ -21,6 +28,11 @@ internal open class DefaultR8Spec @Inject constructor(objectFactory: ObjectFacto override val proguardRuleFiles: ConfigurableFileCollection = objectFactory.fileCollection() + override val configurationFile: RegularFileProperty = + objectFactory + .fileProperty() + .convention(project.layout.buildDirectory.file("shadowJar/configuration.txt")) + override fun enableObfuscation() { defaultArgs.set(emptyList()) obfuscationEnabled.set(true) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt index 117cd1b1b..5305e1b25 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt @@ -55,7 +55,7 @@ internal class R8Minimizer( val r8Dir = temporaryDir.resolve("r8").also { it.mkdirs() } val extractedRulesFile = r8Dir.resolve("classpath-rules.pro") - val rulesFile = r8Dir.resolve("rules.pro") + val configurationFile = r8Spec.configurationFile.get().asFile val r8Output = r8Dir.resolve("output.jar") val normalizedOutput = r8Dir.resolve("normalized-output.jar") val launcher = javaLauncher.orNull @@ -68,7 +68,8 @@ internal class R8Minimizer( extractClasspathRules(inputJar, extractedRulesFile, launcher) val r8Args = r8Spec.args.get() - rulesFile.writeText( + configurationFile.parentFile.mkdirs() + configurationFile.writeText( createRules(inputJar, r8Args, extractedRulesFile).joinToString(System.lineSeparator()) ) @@ -77,7 +78,7 @@ internal class R8Minimizer( add("--output") add(r8Output.absolutePath) add("--pg-conf") - add(rulesFile.absolutePath) + add(configurationFile.absolutePath) add("--lib") add(javaHome) addAll(r8Args) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/R8Spec.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/R8Spec.kt index 2f254a2a8..fef5ca8d8 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/R8Spec.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/R8Spec.kt @@ -2,9 +2,11 @@ package com.github.jengelman.gradle.plugins.shadow.tasks import com.github.jengelman.gradle.plugins.shadow.ShadowDsl import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.ListProperty import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity @@ -43,6 +45,13 @@ public interface R8Spec { @get:PathSensitive(PathSensitivity.RELATIVE) public val proguardRuleFiles: ConfigurableFileCollection + /** + * The final ProGuard configuration generated for R8. + * + * Defaults to `build/shadowJar/configuration.txt`. + */ + @get:OutputFile public val configurationFile: RegularFileProperty + /** * Enable R8 name obfuscation while keeping Shadow's default no-optimization behavior. * diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeSpecsTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeSpecsTest.kt index 454739be1..7cd0af760 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeSpecsTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeSpecsTest.kt @@ -41,6 +41,8 @@ class MinimizeSpecsTest { assertThat(optimizationEnabled.get()).isFalse() assertThat(proguardRules.get()).isEmpty() assertThat(proguardRuleFiles.files).isEmpty() + assertThat(configurationFile.get().asFile) + .isEqualTo(project.layout.buildDirectory.file("shadowJar/configuration.txt").get().asFile) } @Test From 308ab33b506af418676a9ae70fa92ec3386b7f78 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 29 Jul 2026 21:33:48 +0800 Subject: [PATCH 02/17] Assert complete R8 configuration output --- .../gradle/plugins/shadow/MinimizeTest.kt | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index a0b2a7885..563be6a30 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt @@ -329,7 +329,13 @@ class MinimizeTest : BasePluginTest() { ) } assertThat(path("server/build/shadowJar/configuration.txt").readText()) - .contains("-keep,includedescriptorclasses class server.Server { *; }") + .isEqualTo( + """ + -dontoptimize + -keep,includedescriptorclasses class server.Server { *; } + """ + .trimIndent() + ) } @Test @@ -388,7 +394,14 @@ class MinimizeTest : BasePluginTest() { ) } assertThat(path("server/build/r8/final-configuration.txt").readText()) - .contains("-keep class client.Reflective { *; }") + .isEqualTo( + """ + -dontoptimize + -keep,includedescriptorclasses class server.Server { *; } + -keep class client.Reflective { *; } + """ + .trimIndent() + ) } @Test From b5c899f24c79e5312eff8a999f447958a67c5c75 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 29 Jul 2026 21:44:29 +0800 Subject: [PATCH 03/17] Assert extracted R8 configuration rules --- .../jengelman/gradle/plugins/shadow/MinimizeTest.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index 563be6a30..7daf6d677 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt @@ -433,6 +433,17 @@ class MinimizeTest : BasePluginTest() { *manifestEntries, ) } + assertThat(path("server/build/shadowJar/configuration.txt").readText()) + .isEqualTo( + """ + -dontoptimize + -keep,includedescriptorclasses class server.Server { *; } + # Rules extracted from: + # ${outputServerShadowedJar.path.toRealPath()}:META-INF/proguard/client.pro + -keep class client.Reflective { *; } + """ + .trimIndent() + ) } @Test From 8b2e642de2fe8738e607595a4a464517be023b47 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 29 Jul 2026 21:53:49 +0800 Subject: [PATCH 04/17] Document R8 report outputs --- docs/configuration/minimizing/README.md | 61 +++++++++++++++++++ .../gradle/plugins/shadow/MinimizeTest.kt | 40 ++++++++++++ 2 files changed, 101 insertions(+) diff --git a/docs/configuration/minimizing/README.md b/docs/configuration/minimizing/README.md index cabea91f0..d51deef8f 100644 --- a/docs/configuration/minimizing/README.md +++ b/docs/configuration/minimizing/README.md @@ -122,6 +122,67 @@ Shadow also extracts R8 rules published in dependency JARs, for example under `M Shadow writes the final generated ProGuard configuration to `build/shadowJar/configuration.txt` by default. Set `configurationFile` to retain it elsewhere. +R8 also supports ProGuard reporting options such as +[`-printmapping`](https://www.guardsquare.com/manual/configuration/usage#printmapping), +[`-printseeds`](https://www.guardsquare.com/manual/configuration/usage#printseeds), and +[`-printusage`](https://www.guardsquare.com/manual/configuration/usage#printusage). Add them as +`proguardRules` when you want to retain name mappings, matched keep rules, or removed code: + +=== "Kotlin" + + ```kotlin + repositories { + google() + } + + tasks.shadowJar { + minimize { + r8 { + enableObfuscation() + configurationFile.set(layout.buildDirectory.file("r8/configuration.txt")) + proguardRules.addAll( + "-printmapping reports/mapping.txt", + "-printseeds reports/seeds.txt", + "-printusage reports/usage.txt", + ) + } + } + } + ``` + +=== "Groovy" + + ```groovy + repositories { + google() + } + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + minimize { + r8 { + enableObfuscation() + configurationFile.set(layout.buildDirectory.file('r8/configuration.txt')) + proguardRules.addAll( + '-printmapping reports/mapping.txt', + '-printseeds reports/seeds.txt', + '-printusage reports/usage.txt', + ) + } + } + } + ``` + +Relative report paths are resolved from the directory containing `configurationFile`. The example above writes the +reports under `build/r8/reports`. Use absolute paths if the reports must be written independently of the configuration +file location. This behavior follows +[R8's configuration parser](https://r8.googlesource.com/r8/+/refs/tags/9.1.31/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java). +`-printmapping` only contains renamed items, so call `enableObfuscation()` when you need a useful mapping. + +These reporting options belong in the build's R8 configuration, not in rules published inside a dependency JAR. +Android's +[library optimization guidance](https://developer.android.com/topic/performance/app-optimization/library-optimization#optimization-requirements) +lists them among the global options that library authors should not publish as consumer keep rules. + Shadow resolves R8 from the `shadowR8` configuration. The default dependency is `com.android.tools:r8`, which is published by Google Maven rather than Maven Central. Add `google()` to your repositories or override the dependency: diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index 7daf6d677..bd850d7d0 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt @@ -404,6 +404,46 @@ class MinimizeTest : BasePluginTest() { ) } + @Test + fun minimizeWithR8GeneratesReportsRelativeToConfigurationFile() { + writeR8Repository() + writeR8ClientAndServerModules( + serverShadowBlock = + """ + minimize { + r8 { + enableObfuscation() + configurationFile.set(layout.buildDirectory.file("r8/configuration.txt")) + proguardRules.addAll( + "-printmapping reports/mapping.txt", + "-printseeds reports/seeds.txt", + "-printusage reports/usage.txt", + ) + } + } + """ + .trimIndent() + ) + + runWithSuccess(serverShadowJarPath) + + assertThat(path("server/build/r8/configuration.txt").readText()) + .isEqualTo( + """ + -dontoptimize + -keep,includedescriptorclasses class server.Server { *; } + -printmapping reports/mapping.txt + -printseeds reports/seeds.txt + -printusage reports/usage.txt + """ + .trimIndent() + ) + assertThat(path("server/build/r8/reports/mapping.txt").readText()).contains("client.Used") + assertThat(path("server/build/r8/reports/seeds.txt").readText()).contains("server.Server") + assertThat(path("server/build/r8/reports/usage.txt").readText()) + .contains("client.Reflective", "client.Unused") + } + @Test fun minimizeWithR8UsesClasspathRules() { writeR8Repository() From 6e2aa2c926ae07f99a28a5cbc3a388be73cbb74f Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 29 Jul 2026 22:05:55 +0800 Subject: [PATCH 05/17] Let R8 output collective configuration --- docs/configuration/minimizing/README.md | 4 +-- .../gradle/plugins/shadow/MinimizeTest.kt | 34 ++++++++++++++++--- .../plugins/shadow/internal/R8Minimizer.kt | 15 ++++++-- .../gradle/plugins/shadow/tasks/R8Spec.kt | 2 +- 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/docs/configuration/minimizing/README.md b/docs/configuration/minimizing/README.md index d51deef8f..c5c171b73 100644 --- a/docs/configuration/minimizing/README.md +++ b/docs/configuration/minimizing/README.md @@ -119,8 +119,8 @@ Shadow also extracts R8 rules published in dependency JARs, for example under `M } ``` -Shadow writes the final generated ProGuard configuration to -`build/shadowJar/configuration.txt` by default. Set `configurationFile` to retain it elsewhere. +R8 writes the collective ProGuard configuration it used to `build/shadowJar/configuration.txt` by default. Shadow +passes this location to R8 with `--pg-conf-output`. Set `configurationFile` to retain it elsewhere. R8 also supports ProGuard reporting options such as [`-printmapping`](https://www.guardsquare.com/manual/configuration/usage#printmapping), diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index bd850d7d0..e69cce1ec 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt @@ -328,13 +328,18 @@ class MinimizeTest : BasePluginTest() { *manifestEntries, ) } + val inputConfiguration = path("server/build/tmp/shadowJar/r8/configuration.pro").toRealPath() + val configurationDirectory = path("server/build/shadowJar").toRealPath() assertThat(path("server/build/shadowJar/configuration.txt").readText()) .isEqualTo( """ + # The proguard configuration file for the following section is $inputConfiguration + -basedirectory '$configurationDirectory' -dontoptimize -keep,includedescriptorclasses class server.Server { *; } + # End of content from $inputConfiguration """ - .trimIndent() + .trimIndent() + lineSeparator ) } @@ -393,14 +398,19 @@ class MinimizeTest : BasePluginTest() { *manifestEntries, ) } + val inputConfiguration = path("server/build/tmp/shadowJar/r8/configuration.pro").toRealPath() + val configurationDirectory = path("server/build/r8").toRealPath() assertThat(path("server/build/r8/final-configuration.txt").readText()) .isEqualTo( """ + # The proguard configuration file for the following section is $inputConfiguration + -basedirectory '$configurationDirectory' -dontoptimize -keep,includedescriptorclasses class server.Server { *; } -keep class client.Reflective { *; } + # End of content from $inputConfiguration """ - .trimIndent() + .trimIndent() + lineSeparator ) } @@ -427,16 +437,21 @@ class MinimizeTest : BasePluginTest() { runWithSuccess(serverShadowJarPath) + val inputConfiguration = path("server/build/tmp/shadowJar/r8/configuration.pro").toRealPath() + val configurationDirectory = path("server/build/r8").toRealPath() assertThat(path("server/build/r8/configuration.txt").readText()) .isEqualTo( """ + # The proguard configuration file for the following section is $inputConfiguration + -basedirectory '$configurationDirectory' -dontoptimize -keep,includedescriptorclasses class server.Server { *; } -printmapping reports/mapping.txt -printseeds reports/seeds.txt -printusage reports/usage.txt + # End of content from $inputConfiguration """ - .trimIndent() + .trimIndent() + lineSeparator ) assertThat(path("server/build/r8/reports/mapping.txt").readText()).contains("client.Used") assertThat(path("server/build/r8/reports/seeds.txt").readText()).contains("server.Server") @@ -473,16 +488,25 @@ class MinimizeTest : BasePluginTest() { *manifestEntries, ) } + val inputConfiguration = path("server/build/tmp/shadowJar/r8/configuration.pro").toRealPath() + val configurationDirectory = path("server/build/shadowJar").toRealPath() + val embeddedRules = "${outputServerShadowedJar.path.toRealPath()}:META-INF/proguard/client.pro" assertThat(path("server/build/shadowJar/configuration.txt").readText()) .isEqualTo( """ + # The proguard configuration file for the following section is $inputConfiguration + -basedirectory '$configurationDirectory' -dontoptimize -keep,includedescriptorclasses class server.Server { *; } # Rules extracted from: - # ${outputServerShadowedJar.path.toRealPath()}:META-INF/proguard/client.pro + # $embeddedRules + -keep class client.Reflective { *; } + # End of content from $inputConfiguration + # The proguard configuration file for the following section is $embeddedRules -keep class client.Reflective { *; } + # End of content from $embeddedRules """ - .trimIndent() + .trimIndent() + lineSeparator ) } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt index 5305e1b25..6ad05ba4c 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt @@ -55,6 +55,7 @@ internal class R8Minimizer( val r8Dir = temporaryDir.resolve("r8").also { it.mkdirs() } val extractedRulesFile = r8Dir.resolve("classpath-rules.pro") + val inputConfigurationFile = r8Dir.resolve("configuration.pro") val configurationFile = r8Spec.configurationFile.get().asFile val r8Output = r8Dir.resolve("output.jar") val normalizedOutput = r8Dir.resolve("normalized-output.jar") @@ -69,8 +70,12 @@ internal class R8Minimizer( val r8Args = r8Spec.args.get() configurationFile.parentFile.mkdirs() - configurationFile.writeText( - createRules(inputJar, r8Args, extractedRulesFile).joinToString(System.lineSeparator()) + inputConfigurationFile.writeText( + buildList { + add("-basedirectory ${configurationFile.parentFile.asProguardPath()}") + addAll(createRules(inputJar, r8Args, extractedRulesFile)) + } + .joinToString(System.lineSeparator()) ) val arguments = buildList { @@ -78,6 +83,8 @@ internal class R8Minimizer( add("--output") add(r8Output.absolutePath) add("--pg-conf") + add(inputConfigurationFile.absolutePath) + add("--pg-conf-output") add(configurationFile.absolutePath) add("--lib") add(javaHome) @@ -242,6 +249,10 @@ internal class R8Minimizer( .replace('/', '.') } + private fun File.asProguardPath(): String { + return "'${absolutePath.replace("\\", "\\\\").replace("'", "\\'")}'" + } + private fun File.classNames(): Sequence { return when { isDirectory -> diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/R8Spec.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/R8Spec.kt index fef5ca8d8..80a7665a1 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/R8Spec.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/R8Spec.kt @@ -46,7 +46,7 @@ public interface R8Spec { public val proguardRuleFiles: ConfigurableFileCollection /** - * The final ProGuard configuration generated for R8. + * The collective ProGuard configuration output by R8. * * Defaults to `build/shadowJar/configuration.txt`. */ From 08926e689dd88220c41e13bc08af8193df974ca3 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 29 Jul 2026 22:09:18 +0800 Subject: [PATCH 06/17] Normalize R8 configuration test line endings --- .../gradle/plugins/shadow/MinimizeTest.kt | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index e69cce1ec..5895e883c 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt @@ -10,6 +10,7 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.containsAtLeast import com.github.jengelman.gradle.plugins.shadow.testkit.containsNone import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.getContent +import com.github.jengelman.gradle.plugins.shadow.testkit.invariantEolString import com.github.jengelman.gradle.plugins.shadow.util.Issue import java.net.URLClassLoader import java.util.ServiceLoader @@ -330,7 +331,7 @@ class MinimizeTest : BasePluginTest() { } val inputConfiguration = path("server/build/tmp/shadowJar/r8/configuration.pro").toRealPath() val configurationDirectory = path("server/build/shadowJar").toRealPath() - assertThat(path("server/build/shadowJar/configuration.txt").readText()) + assertThat(path("server/build/shadowJar/configuration.txt").readText().invariantEolString) .isEqualTo( """ # The proguard configuration file for the following section is $inputConfiguration @@ -339,7 +340,7 @@ class MinimizeTest : BasePluginTest() { -keep,includedescriptorclasses class server.Server { *; } # End of content from $inputConfiguration """ - .trimIndent() + lineSeparator + .trimIndent() + "\n" ) } @@ -400,7 +401,7 @@ class MinimizeTest : BasePluginTest() { } val inputConfiguration = path("server/build/tmp/shadowJar/r8/configuration.pro").toRealPath() val configurationDirectory = path("server/build/r8").toRealPath() - assertThat(path("server/build/r8/final-configuration.txt").readText()) + assertThat(path("server/build/r8/final-configuration.txt").readText().invariantEolString) .isEqualTo( """ # The proguard configuration file for the following section is $inputConfiguration @@ -410,7 +411,7 @@ class MinimizeTest : BasePluginTest() { -keep class client.Reflective { *; } # End of content from $inputConfiguration """ - .trimIndent() + lineSeparator + .trimIndent() + "\n" ) } @@ -439,7 +440,7 @@ class MinimizeTest : BasePluginTest() { val inputConfiguration = path("server/build/tmp/shadowJar/r8/configuration.pro").toRealPath() val configurationDirectory = path("server/build/r8").toRealPath() - assertThat(path("server/build/r8/configuration.txt").readText()) + assertThat(path("server/build/r8/configuration.txt").readText().invariantEolString) .isEqualTo( """ # The proguard configuration file for the following section is $inputConfiguration @@ -451,7 +452,7 @@ class MinimizeTest : BasePluginTest() { -printusage reports/usage.txt # End of content from $inputConfiguration """ - .trimIndent() + lineSeparator + .trimIndent() + "\n" ) assertThat(path("server/build/r8/reports/mapping.txt").readText()).contains("client.Used") assertThat(path("server/build/r8/reports/seeds.txt").readText()).contains("server.Server") @@ -491,7 +492,7 @@ class MinimizeTest : BasePluginTest() { val inputConfiguration = path("server/build/tmp/shadowJar/r8/configuration.pro").toRealPath() val configurationDirectory = path("server/build/shadowJar").toRealPath() val embeddedRules = "${outputServerShadowedJar.path.toRealPath()}:META-INF/proguard/client.pro" - assertThat(path("server/build/shadowJar/configuration.txt").readText()) + assertThat(path("server/build/shadowJar/configuration.txt").readText().invariantEolString) .isEqualTo( """ # The proguard configuration file for the following section is $inputConfiguration @@ -506,7 +507,7 @@ class MinimizeTest : BasePluginTest() { -keep class client.Reflective { *; } # End of content from $embeddedRules """ - .trimIndent() + lineSeparator + .trimIndent() + "\n" ) } From 24727fd73bde5ef28895f6be776d661d26628a70 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 29 Jul 2026 22:10:54 +0800 Subject: [PATCH 07/17] Use margin strings for R8 expectations --- .../gradle/plugins/shadow/MinimizeTest.kt | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index 5895e883c..7f2226b1f 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt @@ -334,13 +334,13 @@ class MinimizeTest : BasePluginTest() { assertThat(path("server/build/shadowJar/configuration.txt").readText().invariantEolString) .isEqualTo( """ - # The proguard configuration file for the following section is $inputConfiguration - -basedirectory '$configurationDirectory' - -dontoptimize - -keep,includedescriptorclasses class server.Server { *; } - # End of content from $inputConfiguration - """ - .trimIndent() + "\n" + |# The proguard configuration file for the following section is $inputConfiguration + |-basedirectory '$configurationDirectory' + |-dontoptimize + |-keep,includedescriptorclasses class server.Server { *; } + |# End of content from $inputConfiguration + |""" + .trimMargin() ) } @@ -404,14 +404,14 @@ class MinimizeTest : BasePluginTest() { assertThat(path("server/build/r8/final-configuration.txt").readText().invariantEolString) .isEqualTo( """ - # The proguard configuration file for the following section is $inputConfiguration - -basedirectory '$configurationDirectory' - -dontoptimize - -keep,includedescriptorclasses class server.Server { *; } - -keep class client.Reflective { *; } - # End of content from $inputConfiguration - """ - .trimIndent() + "\n" + |# The proguard configuration file for the following section is $inputConfiguration + |-basedirectory '$configurationDirectory' + |-dontoptimize + |-keep,includedescriptorclasses class server.Server { *; } + |-keep class client.Reflective { *; } + |# End of content from $inputConfiguration + |""" + .trimMargin() ) } @@ -443,16 +443,16 @@ class MinimizeTest : BasePluginTest() { assertThat(path("server/build/r8/configuration.txt").readText().invariantEolString) .isEqualTo( """ - # The proguard configuration file for the following section is $inputConfiguration - -basedirectory '$configurationDirectory' - -dontoptimize - -keep,includedescriptorclasses class server.Server { *; } - -printmapping reports/mapping.txt - -printseeds reports/seeds.txt - -printusage reports/usage.txt - # End of content from $inputConfiguration - """ - .trimIndent() + "\n" + |# The proguard configuration file for the following section is $inputConfiguration + |-basedirectory '$configurationDirectory' + |-dontoptimize + |-keep,includedescriptorclasses class server.Server { *; } + |-printmapping reports/mapping.txt + |-printseeds reports/seeds.txt + |-printusage reports/usage.txt + |# End of content from $inputConfiguration + |""" + .trimMargin() ) assertThat(path("server/build/r8/reports/mapping.txt").readText()).contains("client.Used") assertThat(path("server/build/r8/reports/seeds.txt").readText()).contains("server.Server") @@ -495,19 +495,19 @@ class MinimizeTest : BasePluginTest() { assertThat(path("server/build/shadowJar/configuration.txt").readText().invariantEolString) .isEqualTo( """ - # The proguard configuration file for the following section is $inputConfiguration - -basedirectory '$configurationDirectory' - -dontoptimize - -keep,includedescriptorclasses class server.Server { *; } - # Rules extracted from: - # $embeddedRules - -keep class client.Reflective { *; } - # End of content from $inputConfiguration - # The proguard configuration file for the following section is $embeddedRules - -keep class client.Reflective { *; } - # End of content from $embeddedRules - """ - .trimIndent() + "\n" + |# The proguard configuration file for the following section is $inputConfiguration + |-basedirectory '$configurationDirectory' + |-dontoptimize + |-keep,includedescriptorclasses class server.Server { *; } + |# Rules extracted from: + |# $embeddedRules + |-keep class client.Reflective { *; } + |# End of content from $inputConfiguration + |# The proguard configuration file for the following section is $embeddedRules + |-keep class client.Reflective { *; } + |# End of content from $embeddedRules + |""" + .trimMargin() ) } From 86117ef303ebc551360349e597b989da2e07ac72 Mon Sep 17 00:00:00 2001 From: Zongle Wang Date: Wed, 29 Jul 2026 22:12:17 +0800 Subject: [PATCH 08/17] Update README with new features and changes --- docs/changes/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes/README.md b/docs/changes/README.md index 8a0c873c0..5b3cce3f5 100644 --- a/docs/changes/README.md +++ b/docs/changes/README.md @@ -5,7 +5,7 @@ ### Added -- Allow configuring the final R8 ProGuard configuration file with `R8Spec.configurationFile`. +- Allow configuring the final R8 ProGuard configuration file with `R8Spec.configurationFile`. ([#2133](https://github.com/GradleUp/shadow/pull/2133)) ### Changed From 7862528722c16852b20451195d5b503714f985c5 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 29 Jul 2026 22:17:17 +0800 Subject: [PATCH 09/17] Cleanups --- .../gradle/plugins/shadow/internal/DefaultR8Spec.kt | 6 +++--- .../jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultR8Spec.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultR8Spec.kt index 6290d0250..5ad18f0b7 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultR8Spec.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultR8Spec.kt @@ -2,8 +2,8 @@ package com.github.jengelman.gradle.plugins.shadow.internal import com.github.jengelman.gradle.plugins.shadow.tasks.R8Spec import javax.inject.Inject -import org.gradle.api.Project import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.ProjectLayout import org.gradle.api.file.RegularFileProperty import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.ListProperty @@ -13,7 +13,7 @@ import org.gradle.api.tasks.Input internal open class DefaultR8Spec @Inject constructor( - project: Project, + layout: ProjectLayout, objectFactory: ObjectFactory, ) : R8Spec { private val defaultArgs: ListProperty = objectFactory.listProperty(DEFAULT_ARGS) @@ -31,7 +31,7 @@ constructor( override val configurationFile: RegularFileProperty = objectFactory .fileProperty() - .convention(project.layout.buildDirectory.file("shadowJar/configuration.txt")) + .convention(layout.buildDirectory.file("shadowJar/configuration.txt")) override fun enableObfuscation() { defaultArgs.set(emptyList()) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt index 6ad05ba4c..270266e5a 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt @@ -55,7 +55,7 @@ internal class R8Minimizer( val r8Dir = temporaryDir.resolve("r8").also { it.mkdirs() } val extractedRulesFile = r8Dir.resolve("classpath-rules.pro") - val inputConfigurationFile = r8Dir.resolve("configuration.pro") + val rulesFile = r8Dir.resolve("configuration.pro") val configurationFile = r8Spec.configurationFile.get().asFile val r8Output = r8Dir.resolve("output.jar") val normalizedOutput = r8Dir.resolve("normalized-output.jar") @@ -70,7 +70,7 @@ internal class R8Minimizer( val r8Args = r8Spec.args.get() configurationFile.parentFile.mkdirs() - inputConfigurationFile.writeText( + rulesFile.writeText( buildList { add("-basedirectory ${configurationFile.parentFile.asProguardPath()}") addAll(createRules(inputJar, r8Args, extractedRulesFile)) @@ -83,7 +83,7 @@ internal class R8Minimizer( add("--output") add(r8Output.absolutePath) add("--pg-conf") - add(inputConfigurationFile.absolutePath) + add(rulesFile.absolutePath) add("--pg-conf-output") add(configurationFile.absolutePath) add("--lib") From efdeb45c00ba1a198b92ffcdf18e98f58f2a50e0 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 29 Jul 2026 22:21:40 +0800 Subject: [PATCH 10/17] Nest default R8 configuration output --- docs/configuration/minimizing/README.md | 2 +- .../jengelman/gradle/plugins/shadow/MinimizeTest.kt | 8 ++++---- .../gradle/plugins/shadow/internal/DefaultR8Spec.kt | 2 +- .../jengelman/gradle/plugins/shadow/tasks/R8Spec.kt | 2 +- .../gradle/plugins/shadow/internal/MinimizeSpecsTest.kt | 4 +++- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/configuration/minimizing/README.md b/docs/configuration/minimizing/README.md index c5c171b73..6678fbe2d 100644 --- a/docs/configuration/minimizing/README.md +++ b/docs/configuration/minimizing/README.md @@ -119,7 +119,7 @@ Shadow also extracts R8 rules published in dependency JARs, for example under `M } ``` -R8 writes the collective ProGuard configuration it used to `build/shadowJar/configuration.txt` by default. Shadow +R8 writes the collective ProGuard configuration it used to `build/shadowJar/r8/configuration.txt` by default. Shadow passes this location to R8 with `--pg-conf-output`. Set `configurationFile` to retain it elsewhere. R8 also supports ProGuard reporting options such as diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index 7f2226b1f..ddc5c1d35 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt @@ -330,8 +330,8 @@ class MinimizeTest : BasePluginTest() { ) } val inputConfiguration = path("server/build/tmp/shadowJar/r8/configuration.pro").toRealPath() - val configurationDirectory = path("server/build/shadowJar").toRealPath() - assertThat(path("server/build/shadowJar/configuration.txt").readText().invariantEolString) + val configurationDirectory = path("server/build/shadowJar/r8").toRealPath() + assertThat(path("server/build/shadowJar/r8/configuration.txt").readText().invariantEolString) .isEqualTo( """ |# The proguard configuration file for the following section is $inputConfiguration @@ -490,9 +490,9 @@ class MinimizeTest : BasePluginTest() { ) } val inputConfiguration = path("server/build/tmp/shadowJar/r8/configuration.pro").toRealPath() - val configurationDirectory = path("server/build/shadowJar").toRealPath() + val configurationDirectory = path("server/build/shadowJar/r8").toRealPath() val embeddedRules = "${outputServerShadowedJar.path.toRealPath()}:META-INF/proguard/client.pro" - assertThat(path("server/build/shadowJar/configuration.txt").readText().invariantEolString) + assertThat(path("server/build/shadowJar/r8/configuration.txt").readText().invariantEolString) .isEqualTo( """ |# The proguard configuration file for the following section is $inputConfiguration diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultR8Spec.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultR8Spec.kt index 5ad18f0b7..d415f5a34 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultR8Spec.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultR8Spec.kt @@ -31,7 +31,7 @@ constructor( override val configurationFile: RegularFileProperty = objectFactory .fileProperty() - .convention(layout.buildDirectory.file("shadowJar/configuration.txt")) + .convention(layout.buildDirectory.file("shadowJar/r8/configuration.txt")) override fun enableObfuscation() { defaultArgs.set(emptyList()) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/R8Spec.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/R8Spec.kt index 80a7665a1..fce034bbc 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/R8Spec.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/R8Spec.kt @@ -48,7 +48,7 @@ public interface R8Spec { /** * The collective ProGuard configuration output by R8. * - * Defaults to `build/shadowJar/configuration.txt`. + * Defaults to `build/shadowJar/r8/configuration.txt`. */ @get:OutputFile public val configurationFile: RegularFileProperty diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeSpecsTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeSpecsTest.kt index 7cd0af760..0c3b5d006 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeSpecsTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeSpecsTest.kt @@ -42,7 +42,9 @@ class MinimizeSpecsTest { assertThat(proguardRules.get()).isEmpty() assertThat(proguardRuleFiles.files).isEmpty() assertThat(configurationFile.get().asFile) - .isEqualTo(project.layout.buildDirectory.file("shadowJar/configuration.txt").get().asFile) + .isEqualTo( + project.layout.buildDirectory.file("shadowJar/r8/configuration.txt").get().asFile + ) } @Test From 19182bd176ff961fb8cd664f3ed549e5de13129a Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 29 Jul 2026 22:31:12 +0800 Subject: [PATCH 11/17] Preserve Windows paths in R8 configuration --- .../jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt index 270266e5a..4c76dacf5 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt @@ -250,7 +250,7 @@ internal class R8Minimizer( } private fun File.asProguardPath(): String { - return "'${absolutePath.replace("\\", "\\\\").replace("'", "\\'")}'" + return "'${absolutePath.replace("'", "\\'")}'" } private fun File.classNames(): Sequence { From 466e18181b0bc752eb85e9be2126adb3482bbb09 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 30 Jul 2026 09:31:04 +0800 Subject: [PATCH 12/17] Close R8 classpath rules test jar --- .../github/jengelman/gradle/plugins/shadow/MinimizeTest.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index ddc5c1d35..9dc098234 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt @@ -491,7 +491,9 @@ class MinimizeTest : BasePluginTest() { } val inputConfiguration = path("server/build/tmp/shadowJar/r8/configuration.pro").toRealPath() val configurationDirectory = path("server/build/shadowJar/r8").toRealPath() - val embeddedRules = "${outputServerShadowedJar.path.toRealPath()}:META-INF/proguard/client.pro" + val embeddedRules = outputServerShadowedJar.use { + "${it.path.toRealPath()}:META-INF/proguard/client.pro" + } assertThat(path("server/build/shadowJar/r8/configuration.txt").readText().invariantEolString) .isEqualTo( """ From 01b54191fab9fcd629bb24fd4c27a651c42cacf9 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 30 Jul 2026 09:32:29 +0800 Subject: [PATCH 13/17] Cleanups --- docs/configuration/minimizing/README.md | 22 +++++++++++++------ .../gradle/plugins/shadow/MinimizeTest.kt | 7 +++--- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/configuration/minimizing/README.md b/docs/configuration/minimizing/README.md index 6678fbe2d..4813168cb 100644 --- a/docs/configuration/minimizing/README.md +++ b/docs/configuration/minimizing/README.md @@ -74,7 +74,7 @@ Similar to [`ShadowJar.dependencies`][ShadowJar.dependencies], projects can also ## Minimizing with R8 -Shadow can also run [R8](https://r8.googlesource.com/r8) over the final shadowed JAR. This is useful when you want +Shadow can also run [R8][R8] over the final shadowed JAR. This is useful when you want whole-program shrinking instead of the default dependency analyzer. R8 runs after Shadow has merged, transformed, and relocated the JAR, so service descriptors in `META-INF/services` are used to keep service providers. @@ -123,10 +123,12 @@ R8 writes the collective ProGuard configuration it used to `build/shadowJar/r8/c passes this location to R8 with `--pg-conf-output`. Set `configurationFile` to retain it elsewhere. R8 also supports ProGuard reporting options such as -[`-printmapping`](https://www.guardsquare.com/manual/configuration/usage#printmapping), -[`-printseeds`](https://www.guardsquare.com/manual/configuration/usage#printseeds), and -[`-printusage`](https://www.guardsquare.com/manual/configuration/usage#printusage). Add them as -`proguardRules` when you want to retain name mappings, matched keep rules, or removed code: + +- [`-printmapping`][-printmapping] +- [`-printseeds`][-printseeds] +- [`-printusage`][-printusage] + +Add them as `proguardRules` when you want to retain name mappings, matched keep rules, or removed code: === "Kotlin" @@ -175,12 +177,12 @@ R8 also supports ProGuard reporting options such as Relative report paths are resolved from the directory containing `configurationFile`. The example above writes the reports under `build/r8/reports`. Use absolute paths if the reports must be written independently of the configuration file location. This behavior follows -[R8's configuration parser](https://r8.googlesource.com/r8/+/refs/tags/9.1.31/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java). +[R8's configuration parser][ProguardConfigurationParser]. `-printmapping` only contains renamed items, so call `enableObfuscation()` when you need a useful mapping. These reporting options belong in the build's R8 configuration, not in rules published inside a dependency JAR. Android's -[library optimization guidance](https://developer.android.com/topic/performance/app-optimization/library-optimization#optimization-requirements) +[library optimization guidance][library-optimization-guidance] lists them among the global options that library authors should not publish as consumer keep rules. Shadow resolves R8 from the `shadowR8` configuration. The default dependency is `com.android.tools:r8`, which is @@ -344,4 +346,10 @@ To enable both: } ``` +[-printmapping]: https://www.guardsquare.com/manual/configuration/usage#printmapping +[-printseeds]: https://www.guardsquare.com/manual/configuration/usage#printseeds +[-printusage]: https://www.guardsquare.com/manual/configuration/usage#printusage +[library-optimization-guidance]: https://developer.android.com/topic/performance/app-optimization/library-optimization +[R8]: https://r8.googlesource.com/r8 +[ProguardConfigurationParser]: https://r8.googlesource.com/r8/+/refs/tags/9.1.31/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java [ShadowJar.dependencies]: ../../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/dependencies.html diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index 9dc098234..673be29ed 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt @@ -364,7 +364,7 @@ class MinimizeTest : BasePluginTest() { ) getContent("META-INF/services/service.Greeter").isEqualTo("service.DefaultGreeter\n") } - val shadowJarUrl = outputServerShadowedJar.use { it.path.toUri().toURL() } + val shadowJarUrl = outputServerShadowedJar.use { it.toUri().toURL() } URLClassLoader(arrayOf(shadowJarUrl), null).use { loader -> val serviceClass = loader.loadClass("service.Greeter") assertThat(ServiceLoader.load(serviceClass, loader).toList()).hasSize(1) @@ -491,9 +491,8 @@ class MinimizeTest : BasePluginTest() { } val inputConfiguration = path("server/build/tmp/shadowJar/r8/configuration.pro").toRealPath() val configurationDirectory = path("server/build/shadowJar/r8").toRealPath() - val embeddedRules = outputServerShadowedJar.use { - "${it.path.toRealPath()}:META-INF/proguard/client.pro" - } + val embeddedRules = + "${path("server/build/libs/server-1.0-all.jar").toRealPath()}:META-INF/proguard/client.pro" assertThat(path("server/build/shadowJar/r8/configuration.txt").readText().invariantEolString) .isEqualTo( """ From 606f431ac6668fe31e4dd72bbedaa88a7f089472 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 30 Jul 2026 10:03:56 +0800 Subject: [PATCH 14/17] Refine R8 configuration rule creation --- .../gradle/plugins/shadow/MinimizeTest.kt | 6 +++--- .../plugins/shadow/internal/R8Minimizer.kt | 20 ++++++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index 673be29ed..c76e6e636 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt @@ -380,7 +380,7 @@ class MinimizeTest : BasePluginTest() { minimize { r8 { proguardRules.add("-keep class client.Reflective { *; }") - configurationFile.set(layout.buildDirectory.file("r8/final-configuration.txt")) + configurationFile.set(layout.buildDirectory.file("r8/config/final-configuration.txt")) } } """ @@ -400,8 +400,8 @@ class MinimizeTest : BasePluginTest() { ) } val inputConfiguration = path("server/build/tmp/shadowJar/r8/configuration.pro").toRealPath() - val configurationDirectory = path("server/build/r8").toRealPath() - assertThat(path("server/build/r8/final-configuration.txt").readText().invariantEolString) + val configurationDirectory = path("server/build/r8/config").toRealPath() + assertThat(path("server/build/r8/config/final-configuration.txt").readText().invariantEolString) .isEqualTo( """ |# The proguard configuration file for the following section is $inputConfiguration diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt index 4c76dacf5..859893ca0 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt @@ -69,12 +69,13 @@ internal class R8Minimizer( extractClasspathRules(inputJar, extractedRulesFile, launcher) val r8Args = r8Spec.args.get() - configurationFile.parentFile.mkdirs() rulesFile.writeText( - buildList { - add("-basedirectory ${configurationFile.parentFile.asProguardPath()}") - addAll(createRules(inputJar, r8Args, extractedRulesFile)) - } + createRules( + baseDirectory = configurationFile.parentFile.apply { mkdirs() }, + inputJar = inputJar, + r8Args = r8Args, + extractedRulesFile = extractedRulesFile, + ) .joinToString(System.lineSeparator()) ) @@ -133,11 +134,13 @@ internal class R8Minimizer( } private fun createRules( + baseDirectory: File, inputJar: File, r8Args: List, extractedRulesFile: File, ): List { return buildList { + add(baseDirectory.toBaseDirectoryRule()) if (shouldDisableOptimization(r8Args)) { add(DefaultR8Spec.DONT_OPTIMIZE_RULE) } @@ -249,8 +252,11 @@ internal class R8Minimizer( .replace('/', '.') } - private fun File.asProguardPath(): String { - return "'${absolutePath.replace("'", "\\'")}'" + private fun File.toBaseDirectoryRule(): String { + // Preserve Windows separators: escaping backslashes changes the paths R8 writes to the + // collective configuration produced through --pg-conf-output. + val normalizedPath = absolutePath.replace("'", "\\'") + return "-basedirectory '$normalizedPath'" } private fun File.classNames(): Sequence { From 3af36ab22ecf37b02d352ed952f5a2f81ec5f90e Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 30 Jul 2026 10:19:22 +0800 Subject: [PATCH 15/17] Update changelog --- docs/changes/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes/README.md b/docs/changes/README.md index 5b3cce3f5..03be79475 100644 --- a/docs/changes/README.md +++ b/docs/changes/README.md @@ -5,7 +5,7 @@ ### Added -- Allow configuring the final R8 ProGuard configuration file with `R8Spec.configurationFile`. ([#2133](https://github.com/GradleUp/shadow/pull/2133)) +- Allow configuring the final R8 configuration file with `R8Spec.configurationFile`. ([#2133](https://github.com/GradleUp/shadow/pull/2133)) ### Changed From 5288344830fec8246e822fc54f079cab1f0fc8e6 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 30 Jul 2026 10:22:05 +0800 Subject: [PATCH 16/17] Restore rulesFile name --- .../jengelman/gradle/plugins/shadow/MinimizeTest.kt | 8 ++++---- .../gradle/plugins/shadow/internal/R8Minimizer.kt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index c76e6e636..c6f392be9 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt @@ -329,7 +329,7 @@ class MinimizeTest : BasePluginTest() { *manifestEntries, ) } - val inputConfiguration = path("server/build/tmp/shadowJar/r8/configuration.pro").toRealPath() + val inputConfiguration = path("server/build/tmp/shadowJar/r8/rules.pro").toRealPath() val configurationDirectory = path("server/build/shadowJar/r8").toRealPath() assertThat(path("server/build/shadowJar/r8/configuration.txt").readText().invariantEolString) .isEqualTo( @@ -399,7 +399,7 @@ class MinimizeTest : BasePluginTest() { *manifestEntries, ) } - val inputConfiguration = path("server/build/tmp/shadowJar/r8/configuration.pro").toRealPath() + val inputConfiguration = path("server/build/tmp/shadowJar/r8/rules.pro").toRealPath() val configurationDirectory = path("server/build/r8/config").toRealPath() assertThat(path("server/build/r8/config/final-configuration.txt").readText().invariantEolString) .isEqualTo( @@ -438,7 +438,7 @@ class MinimizeTest : BasePluginTest() { runWithSuccess(serverShadowJarPath) - val inputConfiguration = path("server/build/tmp/shadowJar/r8/configuration.pro").toRealPath() + val inputConfiguration = path("server/build/tmp/shadowJar/r8/rules.pro").toRealPath() val configurationDirectory = path("server/build/r8").toRealPath() assertThat(path("server/build/r8/configuration.txt").readText().invariantEolString) .isEqualTo( @@ -489,7 +489,7 @@ class MinimizeTest : BasePluginTest() { *manifestEntries, ) } - val inputConfiguration = path("server/build/tmp/shadowJar/r8/configuration.pro").toRealPath() + val inputConfiguration = path("server/build/tmp/shadowJar/r8/rules.pro").toRealPath() val configurationDirectory = path("server/build/shadowJar/r8").toRealPath() val embeddedRules = "${path("server/build/libs/server-1.0-all.jar").toRealPath()}:META-INF/proguard/client.pro" diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt index 859893ca0..e4035a2c9 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt @@ -55,7 +55,7 @@ internal class R8Minimizer( val r8Dir = temporaryDir.resolve("r8").also { it.mkdirs() } val extractedRulesFile = r8Dir.resolve("classpath-rules.pro") - val rulesFile = r8Dir.resolve("configuration.pro") + val rulesFile = r8Dir.resolve("rules.pro") val configurationFile = r8Spec.configurationFile.get().asFile val r8Output = r8Dir.resolve("output.jar") val normalizedOutput = r8Dir.resolve("normalized-output.jar") From d12a19c3786ee65f0ea4b04623359ac68257e320 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 30 Jul 2026 10:27:12 +0800 Subject: [PATCH 17/17] Clarify R8 configuration path names in tests --- .../gradle/plugins/shadow/MinimizeTest.kt | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index c6f392be9..b2c961b93 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt @@ -329,16 +329,16 @@ class MinimizeTest : BasePluginTest() { *manifestEntries, ) } - val inputConfiguration = path("server/build/tmp/shadowJar/r8/rules.pro").toRealPath() - val configurationDirectory = path("server/build/shadowJar/r8").toRealPath() + val inputConfigPath = path("server/build/tmp/shadowJar/r8/rules.pro").toRealPath() + val outputConfigDir = path("server/build/shadowJar/r8").toRealPath() assertThat(path("server/build/shadowJar/r8/configuration.txt").readText().invariantEolString) .isEqualTo( """ - |# The proguard configuration file for the following section is $inputConfiguration - |-basedirectory '$configurationDirectory' + |# The proguard configuration file for the following section is $inputConfigPath + |-basedirectory '$outputConfigDir' |-dontoptimize |-keep,includedescriptorclasses class server.Server { *; } - |# End of content from $inputConfiguration + |# End of content from $inputConfigPath |""" .trimMargin() ) @@ -399,17 +399,17 @@ class MinimizeTest : BasePluginTest() { *manifestEntries, ) } - val inputConfiguration = path("server/build/tmp/shadowJar/r8/rules.pro").toRealPath() - val configurationDirectory = path("server/build/r8/config").toRealPath() + val inputConfigPath = path("server/build/tmp/shadowJar/r8/rules.pro").toRealPath() + val outputConfigDir = path("server/build/r8/config").toRealPath() assertThat(path("server/build/r8/config/final-configuration.txt").readText().invariantEolString) .isEqualTo( """ - |# The proguard configuration file for the following section is $inputConfiguration - |-basedirectory '$configurationDirectory' + |# The proguard configuration file for the following section is $inputConfigPath + |-basedirectory '$outputConfigDir' |-dontoptimize |-keep,includedescriptorclasses class server.Server { *; } |-keep class client.Reflective { *; } - |# End of content from $inputConfiguration + |# End of content from $inputConfigPath |""" .trimMargin() ) @@ -438,19 +438,19 @@ class MinimizeTest : BasePluginTest() { runWithSuccess(serverShadowJarPath) - val inputConfiguration = path("server/build/tmp/shadowJar/r8/rules.pro").toRealPath() - val configurationDirectory = path("server/build/r8").toRealPath() + val inputConfigPath = path("server/build/tmp/shadowJar/r8/rules.pro").toRealPath() + val outputConfigDir = path("server/build/r8").toRealPath() assertThat(path("server/build/r8/configuration.txt").readText().invariantEolString) .isEqualTo( """ - |# The proguard configuration file for the following section is $inputConfiguration - |-basedirectory '$configurationDirectory' + |# The proguard configuration file for the following section is $inputConfigPath + |-basedirectory '$outputConfigDir' |-dontoptimize |-keep,includedescriptorclasses class server.Server { *; } |-printmapping reports/mapping.txt |-printseeds reports/seeds.txt |-printusage reports/usage.txt - |# End of content from $inputConfiguration + |# End of content from $inputConfigPath |""" .trimMargin() ) @@ -489,24 +489,24 @@ class MinimizeTest : BasePluginTest() { *manifestEntries, ) } - val inputConfiguration = path("server/build/tmp/shadowJar/r8/rules.pro").toRealPath() - val configurationDirectory = path("server/build/shadowJar/r8").toRealPath() - val embeddedRules = + val inputConfigPath = path("server/build/tmp/shadowJar/r8/rules.pro").toRealPath() + val outputConfigDir = path("server/build/shadowJar/r8").toRealPath() + val embeddedConfigPath = "${path("server/build/libs/server-1.0-all.jar").toRealPath()}:META-INF/proguard/client.pro" assertThat(path("server/build/shadowJar/r8/configuration.txt").readText().invariantEolString) .isEqualTo( """ - |# The proguard configuration file for the following section is $inputConfiguration - |-basedirectory '$configurationDirectory' + |# The proguard configuration file for the following section is $inputConfigPath + |-basedirectory '$outputConfigDir' |-dontoptimize |-keep,includedescriptorclasses class server.Server { *; } |# Rules extracted from: - |# $embeddedRules + |# $embeddedConfigPath |-keep class client.Reflective { *; } - |# End of content from $inputConfiguration - |# The proguard configuration file for the following section is $embeddedRules + |# End of content from $inputConfigPath + |# The proguard configuration file for the following section is $embeddedConfigPath |-keep class client.Reflective { *; } - |# End of content from $embeddedRules + |# End of content from $embeddedConfigPath |""" .trimMargin() )