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
76 changes: 76 additions & 0 deletions docs/configuration/relocation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,84 @@ relocating), you can try out the trick like:
This is useful in some cases like [#759](https://github.com/GradleUp/shadow/issues/759) mentioned. See
[Configuring Shadowed Dependencies](../dependencies/README.md) for more information about `configurations`.

## Relocating with R8

As an alternative to Shadow's built-in `relocate` configuration (which uses ASM to rename package prefixes during
JAR merging), you can use [R8][r8-minimizing] to handle package relocation (also referred to as *repackaging*).

R8 performs whole-program analysis during its minimization pass to safely relocate classes while respecting Java
access visibility constraints (such as package-private and `protected` members). For more details on R8 rules, see
the [Global options for additional optimization][android-r8-global-options] and ProGuard manual for
[-repackageclasses][repackageclasses], [-allowaccessmodification][allowaccessmodification]
and [-keeppackagenames][keeppackagenames].

### Configuring R8 Repackaging

To use R8 for package relocation, enable R8 under `minimize` and provide ProGuard repackaging directives via
`proguardRules` or an external rule file:

=== "Kotlin"

```kotlin
repositories {
google()
}

tasks.shadowJar {
minimize {
r8 {
proguardRules.addAll(
// Repackage all relocatable classes into a single destination package
"-repackageclasses 'shadow.repackaged'",
// Optional: widen access to public to allow R8 to relocate more classes
"-allowaccessmodification",
// Optional: preserve specific package names if needed
"-keeppackagenames com.example.keep.**",
)
}
}
}
```

=== "Groovy"

```groovy
repositories {
google()
}

tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
minimize {
r8 {
proguardRules.addAll(
// Repackage all relocatable classes into a single destination package
"-repackageclasses 'shadow.repackaged'",
// Optional: widen access to public to allow R8 to relocate more classes
"-allowaccessmodification",
// Optional: preserve specific package names if needed
"-keeppackagenames com.example.keep.**",
)
}
}
}
```

### Comparison: Shadow `relocate` vs. R8 Repackaging

| Feature | Shadow `relocate` (`SimpleRelocator`) | R8 Repackaging (`-repackageclasses`) |
|:----------------------------|:--------------------------------------------------|:----------------------------------------------------|
| **Execution Stage** | During JAR merging (ASM bytecode transformation) | Post-merge whole-program optimization |
| **Relocation Scope** | Explicit per-prefix or per-class pattern matching | Whole-program automatic relocation |
| **Visibility Handling** | Direct string/type renaming (no visibility check) | Analyzes package-private & protected constraints |
| **Shrinking / Obfuscation** | Relocation only | Combined with shrinking (optional name obfuscation) |



[#1622]: https://github.com/GradleUp/shadow/issues/1622
[kotlin-metadata]: https://kotlinlang.org/docs/metadata-jvm.html
[kotlin-reflection]: https://kotlinlang.org/docs/reflection.html
[r8-minimizing]: ../minimizing/README.md#minimizing-with-r8
[android-r8-global-options]: https://developer.android.com/topic/performance/app-optimization/global-options#global-options
[repackageclasses]: https://www.guardsquare.com/manual/configuration/usage#repackageclasses
[allowaccessmodification]: https://www.guardsquare.com/manual/configuration/usage#allowaccessmodification
[keeppackagenames]: https://www.guardsquare.com/manual/configuration/usage#keeppackagenames
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.io.TempDir
import org.vafer.jdeb.shaded.objectweb.asm.ClassWriter
import org.vafer.jdeb.shaded.objectweb.asm.Opcodes

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
abstract class BasePluginTest {
Expand Down Expand Up @@ -189,6 +191,12 @@ abstract class BasePluginTest {
.trimMargin()
}

fun writeR8Repository() {
settingsScript.writeText(
settingsScript.readText().replace("mavenCentral()", "mavenCentral()\n google()")
)
}

fun jarPath(relative: String, parent: Path = projectRoot): JarPath {
return JarPath(parent.resolve(relative))
}
Expand Down Expand Up @@ -431,6 +439,15 @@ abstract class BasePluginTest {
}
}

fun createEmptyClassBytes(internalName: String): ByteArray {
return ClassWriter(0)
.apply {
visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, internalName, null, "java/lang/Object", null)
visitEnd()
}
.toByteArray()
}

inline fun <reified T : ResourceTransformer> transform(
dependenciesBlock: String = "",
transformerBlock: String = "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -570,12 +570,6 @@ class CachingTest : BasePluginTest() {
assertThat(result).taskOutcomeEquals(taskPath, expectedOutcome)
}

private fun writeR8Repository() {
settingsScript.writeText(
settingsScript.readText().replace("mavenCentral()", "mavenCentral()\n google()")
)
}

private fun writeR8ClientAndServerModules() {
settingsScript.appendText(
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -749,12 +749,6 @@ class MinimizeTest : BasePluginTest() {
)
}

private fun writeR8Repository() {
settingsScript.writeText(
settingsScript.readText().replace("mavenCentral()", "mavenCentral()\n google()")
)
}

private fun writeR8ClientAndServerModules(
serverShadowBlock: String,
serverProjectBlock: String = "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,58 @@ class RelocationTest : BasePluginTest() {
}
}

@Test
fun relocateWithR8() {
writeClass(packageName = "my", withImports = false) {
"""
|package my;
|import foo.Foo;
|public class Main {
| public static void main(String[] args) {
| System.out.println(Foo.class);
| }
|}
"""
.trimMargin()
}
val fooJar =
buildJar("foo.jar") {
insert("foo/Foo.class", createEmptyClassBytes("foo/Foo"))
}

writeR8Repository()
projectScript.appendText(
"""
|dependencies {
| ${implementationFiles(fooJar)}
|}
|$shadowJarTask {
| minimize {
| r8 {
| proguardRules.addAll(
| "-repackageclasses 'relocated'",
| )
| }
| }
|}
"""
.trimMargin()
)

runWithSuccess(shadowJarPath)

assertThat(outputShadowedJar).useAll {
containsOnly(
"my/",
"my/Main.class",
"relocated/",
"relocated/foo/",
"relocated/foo/Foo.class",
*manifestEntries,
)
}
}

private companion object {
@JvmStatic
fun preserveLastModifiedProvider() =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.containsMatch
import assertk.assertions.isEqualTo
import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly
import com.github.jengelman.gradle.plugins.shadow.testkit.getContent
import kotlin.io.path.appendText
import kotlin.io.path.writeText
Expand Down Expand Up @@ -125,6 +126,70 @@ class ServiceFileTransformerTest : BaseTransformerTest() {
}
}

@Test
fun serviceResourceTransformerWithR8Relocation() {
val one = buildJarOne {
insert("com/example/Driver.class", createEmptyClassBytes("com/example/Driver"))
insert("foo/FooDriver.class", createEmptyClassBytes("foo/FooDriver"))
insert(
"META-INF/services/com.example.Driver",
"foo.FooDriver",
)
}
val two = buildJarTwo {
insert("bar/BarDriver.class", createEmptyClassBytes("bar/BarDriver"))
insert(
"META-INF/services/com.example.Driver",
"bar.BarDriver",
)
}

writeR8Repository()
projectScript.appendText(
"""
|dependencies {
| ${implementationFiles(one, two)}
|}
|$shadowJarTask {
| mergeServiceFiles()
| minimize {
| r8 {
| proguardRules.addAll(
| "-repackageclasses 'relocated'",
| )
| }
| }
|}
"""
.trimMargin()
)

runWithSuccess(shadowJarPath)

assertThat(outputShadowedJar).useAll {
containsOnly(
"bar/",
"bar/BarDriver.class",
"com/",
"com/example/",
"com/example/Driver.class",
"foo/",
"foo/FooDriver.class",
"META-INF/services/",
"META-INF/services/com.example.Driver",
*manifestEntries,
)
getContent("META-INF/services/com.example.Driver")
.isEqualTo(
"""
|foo.FooDriver
|bar.BarDriver
|"""
.trimMargin()
)
}
Comment on lines +169 to +190

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been fixed in #2174.

}

@Test // #70, #71
fun transformProjectResources() {
val servicesBarEntry = "META-INF/services/foo.Bar"
Expand Down