Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions api/shadow.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
48 changes: 48 additions & 0 deletions docs/configuration/minimizing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ constructor(

@get:Input val optimizationEnabled: Property<Boolean> = objectFactory.property(false)

override val useDefaultRules: Property<Boolean> = objectFactory.property(true)

override val args: ListProperty<String> = objectFactory.listProperty(defaultArgs)

override val proguardRules: ListProperty<String> = objectFactory.listProperty()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,28 +107,31 @@ private fun createRules(
keptDependencyFiles: Iterable<File>,
relocators: Iterable<Relocator>,
): List<String> {
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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Boolean>

/**
* Additional R8 command line arguments.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down