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 api/shadow.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions docs/changes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
76 changes: 75 additions & 1 deletion docs/configuration/minimizing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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"))
}
}
}
Expand All @@ -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:

Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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"))
}
}
"""
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = objectFactory.listProperty(DEFAULT_ARGS)

@get:Input val obfuscationEnabled: Property<Boolean> = objectFactory.property(false)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() },
Comment thread
Goooler marked this conversation as resolved.
inputJar = inputJar,
r8Args = r8Args,
extractedRulesFile = extractedRulesFile,
)
.joinToString(System.lineSeparator())
)

val arguments = buildList {
Expand All @@ -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)
Expand Down Expand Up @@ -125,11 +134,13 @@ internal class R8Minimizer(
}

private fun createRules(
baseDirectory: File,
inputJar: File,
r8Args: List<String>,
extractedRulesFile: File,
): List<String> {
return buildList {
add(baseDirectory.toBaseDirectoryRule())
if (shouldDisableOptimization(r8Args)) {
add(DefaultR8Spec.DONT_OPTIMIZE_RULE)
}
Expand Down Expand Up @@ -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<String> {
return when {
isDirectory ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Comment thread
Goooler marked this conversation as resolved.

/**
* Enable R8 name obfuscation while keeping Shadow's default no-optimization behavior.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down