diff --git a/docs/configuration/relocation/README.md b/docs/configuration/relocation/README.md index 0f64e9b6a..8d4063611 100644 --- a/docs/configuration/relocation/README.md +++ b/docs/configuration/relocation/README.md @@ -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 diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt index 66fb16535..0e11c26f4 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt @@ -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 { @@ -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)) } @@ -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 transform( dependenciesBlock: String = "", transformerBlock: String = "", diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/CachingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/CachingTest.kt index 3f049e542..08a5bfc61 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/CachingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/CachingTest.kt @@ -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( """ 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 f79f732ae..772c8184b 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 @@ -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 = "", diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index 2665459a1..ddaa6fc1d 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -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() = diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformerTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformerTest.kt index 5d99cfd1e..4874bd783 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformerTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformerTest.kt @@ -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 @@ -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() + ) + } + } + @Test // #70, #71 fun transformProjectResources() { val servicesBarEntry = "META-INF/services/foo.Bar"