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..03be79475 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 configuration file with `R8Spec.configurationFile`. ([#2133](https://github.com/GradleUp/shadow/pull/2133)) + ### 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..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. @@ -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,78 @@ 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')) } } } ``` +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 + +- [`-printmapping`][-printmapping] +- [`-printseeds`][-printseeds] +- [`-printusage`][-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][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][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 published by Google Maven rather than Maven Central. Add `google()` to your repositories or override the dependency: @@ -278,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 fee83e74f..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 @@ -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 @@ -328,6 +329,19 @@ class MinimizeTest : BasePluginTest() { *manifestEntries, ) } + 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 $inputConfigPath + |-basedirectory '$outputConfigDir' + |-dontoptimize + |-keep,includedescriptorclasses class server.Server { *; } + |# End of content from $inputConfigPath + |""" + .trimMargin() + ) } @Test @@ -350,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) @@ -366,6 +380,7 @@ class MinimizeTest : BasePluginTest() { minimize { r8 { proguardRules.add("-keep class client.Reflective { *; }") + configurationFile.set(layout.buildDirectory.file("r8/config/final-configuration.txt")) } } """ @@ -384,6 +399,65 @@ class MinimizeTest : BasePluginTest() { *manifestEntries, ) } + 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 $inputConfigPath + |-basedirectory '$outputConfigDir' + |-dontoptimize + |-keep,includedescriptorclasses class server.Server { *; } + |-keep class client.Reflective { *; } + |# End of content from $inputConfigPath + |""" + .trimMargin() + ) + } + + @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) + + 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 $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 $inputConfigPath + |""" + .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") + assertThat(path("server/build/r8/reports/usage.txt").readText()) + .contains("client.Reflective", "client.Unused") } @Test @@ -415,6 +489,27 @@ class MinimizeTest : BasePluginTest() { *manifestEntries, ) } + 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 $inputConfigPath + |-basedirectory '$outputConfigDir' + |-dontoptimize + |-keep,includedescriptorclasses class server.Server { *; } + |# Rules extracted from: + |# $embeddedConfigPath + |-keep class client.Reflective { *; } + |# End of content from $inputConfigPath + |# The proguard configuration file for the following section is $embeddedConfigPath + |-keep class client.Reflective { *; } + |# End of content from $embeddedConfigPath + |""" + .trimMargin() + ) } @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..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 @@ -3,12 +3,19 @@ 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.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 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( + layout: ProjectLayout, + 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(layout.buildDirectory.file("shadowJar/r8/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..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 @@ -56,6 +56,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 @@ -69,7 +70,13 @@ internal class R8Minimizer( val r8Args = r8Spec.args.get() rulesFile.writeText( - createRules(inputJar, r8Args, extractedRulesFile).joinToString(System.lineSeparator()) + createRules( + baseDirectory = configurationFile.parentFile.apply { mkdirs() }, + inputJar = inputJar, + r8Args = r8Args, + extractedRulesFile = extractedRulesFile, + ) + .joinToString(System.lineSeparator()) ) val arguments = buildList { @@ -78,6 +85,8 @@ internal class R8Minimizer( add(r8Output.absolutePath) add("--pg-conf") add(rulesFile.absolutePath) + add("--pg-conf-output") + add(configurationFile.absolutePath) add("--lib") add(javaHome) addAll(r8Args) @@ -125,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) } @@ -241,6 +252,13 @@ internal class R8Minimizer( .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 { 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 2f254a2a8..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 @@ -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 collective ProGuard configuration output by R8. + * + * Defaults to `build/shadowJar/r8/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..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 @@ -41,6 +41,10 @@ class MinimizeSpecsTest { assertThat(optimizationEnabled.get()).isFalse() assertThat(proguardRules.get()).isEmpty() assertThat(proguardRuleFiles.files).isEmpty() + assertThat(configurationFile.get().asFile) + .isEqualTo( + project.layout.buildDirectory.file("shadowJar/r8/configuration.txt").get().asFile + ) } @Test