diff --git a/CHANGELOG.md b/CHANGELOG.md index 3656ff404..1896a0784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Improvements for `ManifestResourceTransformer`. ([#2200](https://github.com/GradleUp/shadow/pull/2200)) - Support removing manifest attributes using `NULL`. - Support manifest header relocation via configurable `attributesToRelocate` property. +- Allow disabling default ProGuard rules in R8 minimization with `R8Spec.useDefaultRules`. ([#2252](https://github.com/GradleUp/shadow/pull/2252)) ### Changed diff --git a/api/shadow.api b/api/shadow.api index 7da255cd1..72583c148 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -231,6 +231,7 @@ public abstract interface class com/github/jengelman/gradle/plugins/shadow/tasks public fun getKeepRules ()Lorg/gradle/api/provider/ListProperty; public abstract fun getProguardRuleFiles ()Lorg/gradle/api/file/ConfigurableFileCollection; public abstract fun getProguardRules ()Lorg/gradle/api/provider/ListProperty; + public abstract fun getUseDefaultRules ()Lorg/gradle/api/provider/Property; } public class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction : org/gradle/api/internal/file/copy/CopyAction { diff --git a/docs/configuration/minimizing/README.md b/docs/configuration/minimizing/README.md index 782ee90e2..1719514b0 100644 --- a/docs/configuration/minimizing/README.md +++ b/docs/configuration/minimizing/README.md @@ -374,6 +374,54 @@ To enable both: } ``` +### Customizing Rules Without Defaults + +By default, Shadow generates fallback keep rules for project classes, excluded dependencies, and service descriptors, +and generates `-dontoptimize` to disable optimization unless explicitly enabled. + +To take full control over Shadow-generated rules and maximize R8 optimizations (such as shrinking unused project classes +or methods and running optimizations), disable `useDefaultRules`: + +=== ":material-language-kotlin: build.gradle.kts" + + ```kotlin + repositories { + google() + } + + tasks.shadowJar { + minimize { + r8 { + useDefaultRules = false + proguardRules.add("-keep class com.example.Main { public static void main(java.lang.String[]); }") + } + } + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + repositories { + google() + } + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + minimize { + r8 { + useDefaultRules = false + proguardRules.add('-keep class com.example.Main { public static void main(java.lang.String[]); }') + } + } + } + ``` + +> [!NOTE] +> Setting `useDefaultRules = false` only disables Shadow's auto-generated rules. This does not disable consumer rules +> embedded in dependency JARs (e.g. under `META-INF/proguard`). Furthermore, name obfuscation remains disabled by +> default unless `enableObfuscation()` is called or `args` is customized. + + [-printmapping]: https://www.guardsquare.com/manual/configuration/usage#printmapping [-printseeds]: https://www.guardsquare.com/manual/configuration/usage#printseeds diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/R8MinimizationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/R8MinimizationTest.kt index 6a3bf274a..ac8826d89 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/R8MinimizationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/R8MinimizationTest.kt @@ -132,6 +132,47 @@ class R8MinimizationTest : BasePluginTest() { ) } + @Test + fun disableDefaultRules() { + writeR8AppAndLibModules( + appShadowBlock = + """ + |minimize { + | r8 { + | useDefaultRules = false + | proguardRules.add("-keep class lib.Reflective { *; }") + | configurationFile = layout.buildDirectory.file("r8/config/final-configuration.txt") + | } + |} + """ + .trimMargin() + ) + + runWithSuccess(appShadowJarPath) + + assertThat(outputAppShadowedJar).useAll { + containsExactly( + "lib/Reflective.class", + manifestEntry, + ) + classLoader { + loadClass("lib.Reflective") + } + } + val inputConfigPath = path("app/build/tmp/shadowJar/r8/rules.pro").toRealPath() + val outputConfigDir = path("app/build/r8/config").toRealPath() + assertThat(path("app/build/r8/config/final-configuration.txt").readText().invariantEolString) + .isEqualTo( + """ + |# The proguard configuration file for the following section is $inputConfigPath + |-basedirectory '$outputConfigDir' + |-keep class lib.Reflective { *; } + |# End of content from $inputConfigPath + |""" + .trimMargin() + ) + } + @Test fun canKeepDirectories() { writeR8AppAndLibModules( 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 7cdaf9d0d..bc112017b 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 @@ -22,6 +22,8 @@ constructor( @get:Input val optimizationEnabled: Property = objectFactory.property(false) + override val useDefaultRules: Property = objectFactory.property(true) + override val args: ListProperty = objectFactory.listProperty(defaultArgs) override val proguardRules: ListProperty = objectFactory.listProperty() 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 9e5f7e5d2..779605cd0 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 @@ -107,28 +107,31 @@ private fun createRules( keptDependencyFiles: Iterable, relocators: Iterable, ): List { - val (jarClasses, serviceRules) = inputJar.analyzeInputJar() return buildList { add("-basedirectory '${baseDirectory.escapedAbsPath}'") - val shouldDisableOptimization = - !r8Spec.optimizationEnabled.get() && - (r8Spec.obfuscationEnabled.get() || DefaultR8Spec.NO_MINIFICATION_ARG in r8Args) - if (shouldDisableOptimization) { - add(DefaultR8Spec.DONT_OPTIMIZE_RULE) + if (r8Spec.useDefaultRules.get()) { + val shouldDisableOptimization = + !r8Spec.optimizationEnabled.get() && + (r8Spec.obfuscationEnabled.get() || DefaultR8Spec.NO_MINIFICATION_ARG in r8Args) + if (shouldDisableOptimization) { + add(DefaultR8Spec.DONT_OPTIMIZE_RULE) + } + + val (jarClasses, serviceRules) = inputJar.analyzeInputJar() + addAll( + // Project classes are the public surface of the shadowed jar, even when nothing in the + // input jar refers to every class directly. + sourceSetsClassesDirs.toKeepRules(jarClasses, relocators, "-keep,includedescriptorclasses") + ) + addAll( + // Keep dependencies users explicitly excluded from minimization, matching the existing + // minimize { exclude(...) } contract for the default analyzer. + keptDependencyFiles.toKeepRules(jarClasses, relocators, "-keep") + ) + addAll(serviceRules) } - addAll( - // Project classes are the public surface of the shadowed jar, even when nothing in the input - // jar refers to every class directly. - sourceSetsClassesDirs.toKeepRules(jarClasses, relocators, "-keep,includedescriptorclasses") - ) - addAll( - // Keep dependencies users explicitly excluded from minimization, matching the existing - // minimize { exclude(...) } contract for the default analyzer. - keptDependencyFiles.toKeepRules(jarClasses, relocators, "-keep") - ) - addAll(serviceRules) r8Spec.proguardRuleFiles .filter { it.isFile } .sortedBy { it.absolutePath } 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 429bff47a..716a972a8 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 @@ -4,6 +4,7 @@ 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.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.OutputFile @@ -13,6 +14,21 @@ import org.gradle.api.tasks.PathSensitivity /** Minimal R8 configuration for [ShadowJar.minimize]. */ @ShadowDsl public interface R8Spec { + /** + * Whether to apply Shadow's default ProGuard rules for R8 minimization. + * + * When enabled (default), Shadow automatically generates keep rules for project classes, excluded + * dependencies, and service descriptors, and disables optimization unless explicitly enabled. + * + * When disabled, Shadow-generated default rules are omitted, giving full control over Shadow's + * rule generation and maximizing R8 optimization potential. Note that consumer rules embedded in + * dependency JARs may still be applied by R8, and name obfuscation remains disabled by default + * unless [enableObfuscation] is called or [args] is customized. + * + * Defaults to `true`. + */ + @get:Input public val useDefaultRules: Property + /** * Additional R8 command line arguments. * 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 0c3b5d006..0f2864902 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 @@ -36,6 +36,7 @@ class MinimizeSpecsTest { @Test fun defaultR8SpecIsShrinkOnly() = with(project.objects.newInstance(DefaultR8Spec::class.java)) { + assertThat(useDefaultRules.get()).isTrue() assertThat(args.get()).containsExactly(DefaultR8Spec.NO_MINIFICATION_ARG) assertThat(obfuscationEnabled.get()).isFalse() assertThat(optimizationEnabled.get()).isFalse()