diff --git a/api/shadow.api b/api/shadow.api index 749648679..085eba2b5 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -286,6 +286,9 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar public final fun minimize ()V public fun minimize (Lorg/gradle/api/Action;)V public static synthetic fun minimize$default (Lcom/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar;Lorg/gradle/api/Action;ILjava/lang/Object;)V + public final fun r8 ()V + public fun r8 (Lorg/gradle/api/Action;)V + public static synthetic fun r8$default (Lcom/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar;Lorg/gradle/api/Action;ILjava/lang/Object;)V public final fun relocate (Lcom/github/jengelman/gradle/plugins/shadow/relocation/Relocator;)V public fun relocate (Lcom/github/jengelman/gradle/plugins/shadow/relocation/Relocator;Lorg/gradle/api/Action;)V public final fun relocate (Ljava/lang/Class;)V diff --git a/docs/changes/README.md b/docs/changes/README.md index 1a004aeb8..e2137c8ff 100644 --- a/docs/changes/README.md +++ b/docs/changes/README.md @@ -10,7 +10,9 @@ ### Deprecated - Deprecate `keepRules` and `keepRuleFiles` in `R8Spec`. ([#2120](https://github.com/GradleUp/shadow/pull/2120)) -- Deprecate `ShadowJar.minimizeJar`; call `ShadowJar.minimize()` explicitly instead. ([#2124](https://github.com/GradleUp/shadow/pull/2124)) +- Deprecate `minimize { r8 { ... } }` in favor of the standalone `r8 { ... }` block. ([#2123](https://github.com/GradleUp/shadow/pull/2123)) + The previous DSL remains available as a compatibility layer until Shadow 10. +- Deprecate `ShadowJar.minimizeJar`; call `ShadowJar.minimize()` explicitly instead. ([#2124](https://github.com/GradleUp/shadow/pull/2124)) The property will be made non-public in Shadow 10. ## [9.6.1](https://github.com/GradleUp/shadow/releases/tag/9.6.1) - 2026-07-22 diff --git a/docs/configuration/minimizing/README.md b/docs/configuration/minimizing/README.md index 57cb861b9..1a2b0c3b4 100644 --- a/docs/configuration/minimizing/README.md +++ b/docs/configuration/minimizing/README.md @@ -72,15 +72,19 @@ Similar to [`ShadowJar.dependencies`][ShadowJar.dependencies], projects can also > When excluding a `project`, all dependencies of the excluded `project` are automatically excluded from > minimization as well. -## Minimizing with R8 +## Post-processing with R8 Shadow can also run [R8](https://r8.googlesource.com/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. +whole-program shrinking independently of the default dependency analyzer. R8 runs after Shadow has merged, +transformed, relocated, and optionally minimized the JAR, so service descriptors in `META-INF/services` are used to +keep service providers. The default R8 configuration only shrinks unused code. It disables name minification and optimization. Shadow also extracts R8 rules published in dependency JARs, for example under `META-INF/proguard`. +The `minimize` and `r8` blocks are independent and can be enabled separately or together. Dependency exclusions in +`minimize` only configure Shadow's dependency analyzer; use ProGuard rules to keep classes during R8 post-processing. + === "Kotlin" ```kotlin @@ -89,12 +93,10 @@ Shadow also extracts R8 rules published in dependency JARs, for example under `M } tasks.shadowJar { - minimize { - r8 { - // Optional extra configuration - proguardRules.add("-keep class com.example.ReflectiveApi { *; }") - proguardRuleFiles.from(layout.projectDirectory.file("r8-rules.pro")) - } + r8 { + // Optional extra configuration + proguardRules.add("-keep class com.example.ReflectiveApi { *; }") + proguardRuleFiles.from(layout.projectDirectory.file("r8-rules.pro")) } } ``` @@ -107,12 +109,10 @@ Shadow also extracts R8 rules published in dependency JARs, for example under `M } tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { - minimize { - r8 { - // Optional extra configuration - proguardRules.add('-keep class com.example.ReflectiveApi { *; }') - proguardRuleFiles.from(layout.projectDirectory.file('r8-rules.pro')) - } + r8 { + // Optional extra configuration + proguardRules.add('-keep class com.example.ReflectiveApi { *; }') + proguardRuleFiles.from(layout.projectDirectory.file('r8-rules.pro')) } } ``` @@ -150,10 +150,8 @@ For example, to downgrade R8 warnings to info: } tasks.shadowJar { - minimize { - r8 { - args.addAll(listOf("--map-diagnostics", "warning", "info")) - } + r8 { + args.addAll(listOf("--map-diagnostics", "warning", "info")) } } ``` @@ -166,10 +164,8 @@ For example, to downgrade R8 warnings to info: } tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { - minimize { - r8 { - args.addAll(['--map-diagnostics', 'warning', 'info']) - } + r8 { + args.addAll(['--map-diagnostics', 'warning', 'info']) } } ``` @@ -184,10 +180,8 @@ To enable name obfuscation: } tasks.shadowJar { - minimize { - r8 { - enableObfuscation() - } + r8 { + enableObfuscation() } } ``` @@ -200,10 +194,8 @@ To enable name obfuscation: } tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { - minimize { - r8 { - enableObfuscation() - } + r8 { + enableObfuscation() } } ``` @@ -218,10 +210,8 @@ To enable optimization: } tasks.shadowJar { - minimize { - r8 { - enableOptimization() - } + r8 { + enableOptimization() } } ``` @@ -234,10 +224,8 @@ To enable optimization: } tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { - minimize { - r8 { - enableOptimization() - } + r8 { + enableOptimization() } } ``` @@ -252,11 +240,9 @@ To enable both: } tasks.shadowJar { - minimize { - r8 { - enableObfuscation() - enableOptimization() - } + r8 { + enableObfuscation() + enableOptimization() } } ``` @@ -269,11 +255,9 @@ To enable both: } tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { - minimize { - r8 { - enableObfuscation() - enableOptimization() - } + r8 { + enableObfuscation() + enableOptimization() } } ``` diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 6d370c541..1d57b852c 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -152,6 +152,8 @@ Here are the options that can be passed to the `shadowJar`: --no-add-multi-release-attribute Disables option --add-multi-release-attribute. --enable-auto-relocation Enables auto relocation of packages in the dependencies. --no-enable-auto-relocation Disables option --enable-auto-relocation. +--enable-r8 Runs R8 over the final shadowed JAR. +--no-enable-r8 Disables option --enable-r8. --enable-kotlin-module-remapping Enables remapping of Kotlin module metadata files. --no-enable-kotlin-module-remapping Disables option --enable-kotlin-module-remapping. --fail-on-duplicate-entries Fails build if the ZIP entries in the shadowed JAR are duplicate. 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 4a94f2402..5db06f5bb 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 @@ -668,10 +668,8 @@ class CachingTest : BasePluginTest() { implementation project(':client') } $shadowJarTask { - minimize { - r8 { - proguardRuleFiles.from(file("r8-rules.pro")) - } + r8 { + proguardRuleFiles.from(file("r8-rules.pro")) } } """ diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt index e1b2f7003..d2d069d61 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt @@ -123,6 +123,8 @@ class JavaPluginsTest : BasePluginTest() { "--no-add-multi-release-attribute Disables option --add-multi-release-attribute.", "--enable-auto-relocation Enables auto relocation of packages in the dependencies.", "--no-enable-auto-relocation Disables option --enable-auto-relocation.", + "--enable-r8 Runs R8 over the final shadowed JAR.", + "--no-enable-r8 Disables option --enable-r8.", "--enable-kotlin-module-remapping Enables remapping of Kotlin module metadata files.", "--no-enable-kotlin-module-remapping Disables option --enable-kotlin-module-remapping.", "--fail-on-duplicate-entries Fails build if the ZIP entries in the shadowed JAR are duplicate.", @@ -813,9 +815,7 @@ class JavaPluginsTest : BasePluginTest() { """ ${getDefaultProjectBuildScript(applyShadowPlugin = false)} def $customShadowJar = tasks.register('$customShadowJar', ${ShadowJar::class.java.name}) { - minimize { - r8 {} - } + r8 {} } """ .trimIndent() @@ -825,7 +825,7 @@ class JavaPluginsTest : BasePluginTest() { assertThat(result.output) .contains( - "R8 minimization requires a non-empty R8 classpath. Apply the Shadow plugin or configure the shadowR8 configuration." + "R8 post-processing requires a non-empty R8 classpath. Apply the Shadow plugin or configure the shadowR8 configuration." ) } 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 fee83e74f..6321a1ba2 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 @@ -305,13 +305,39 @@ class MinimizeTest : BasePluginTest() { } @Test - fun minimizeWithR8ShrinksUnusedDependencyClasses() { + fun r8ShrinksUnusedDependencyClasses() { + writeR8Repository() + writeR8ClientAndServerModules( + serverShadowBlock = + """ + r8 {} + """ + .trimIndent() + ) + + runWithSuccess(serverShadowJarPath) + + assertThat(outputServerShadowedJar).useAll { + containsOnly( + "server/", + "server/Server.class", + "client/", + "client/Used.class", + *manifestEntries, + ) + } + } + + @Test + fun legacyMinimizeR8DslDelegatesToStandaloneR8() { writeR8Repository() writeR8ClientAndServerModules( serverShadowBlock = """ minimize { - r8 {} + r8 { + proguardRules.add("-keep class client.Reflective { *; }") + } } """ .trimIndent() @@ -325,13 +351,49 @@ class MinimizeTest : BasePluginTest() { "server/Server.class", "client/", "client/Used.class", + "client/Reflective.class", *manifestEntries, ) } } + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun enableR8ByCliOption(enable: Boolean) { + writeR8Repository() + writeR8ClientAndServerModules(serverShadowBlock = "") + + if (enable) { + runWithSuccess(serverShadowJarPath, "--enable-r8") + } else { + runWithSuccess(serverShadowJarPath, "--no-enable-r8") + } + + assertThat(outputServerShadowedJar).useAll { + if (enable) { + containsOnly( + "server/", + "server/Server.class", + "client/", + "client/Used.class", + *manifestEntries, + ) + } else { + containsOnly( + "server/", + "server/Server.class", + "client/", + "client/Used.class", + "client/Unused.class", + "client/Reflective.class", + *manifestEntries, + ) + } + } + } + @Test - fun minimizeWithR8KeepsServiceProviders() { + fun r8KeepsServiceProviders() { writeR8Repository() writeR8ServiceModules() @@ -358,15 +420,13 @@ class MinimizeTest : BasePluginTest() { } @Test - fun minimizeWithR8HonorsCustomProguardRules() { + fun r8HonorsCustomProguardRules() { writeR8Repository() writeR8ClientAndServerModules( serverShadowBlock = """ - minimize { - r8 { - proguardRules.add("-keep class client.Reflective { *; }") - } + r8 { + proguardRules.add("-keep class client.Reflective { *; }") } """ .trimIndent() @@ -387,14 +447,12 @@ class MinimizeTest : BasePluginTest() { } @Test - fun minimizeWithR8UsesClasspathRules() { + fun r8UsesClasspathRules() { writeR8Repository() writeR8ClientAndServerModules( serverShadowBlock = """ - minimize { - r8 {} - } + r8 {} """ .trimIndent() ) @@ -418,14 +476,12 @@ class MinimizeTest : BasePluginTest() { } @Test - fun minimizeWithR8PreservesRepeatedLinesInClasspathRules() { + fun r8PreservesRepeatedLinesInClasspathRules() { writeR8Repository() writeR8ClientAndServerModules( serverShadowBlock = """ - minimize { - r8 {} - } + r8 {} """ .trimIndent() ) @@ -460,15 +516,13 @@ class MinimizeTest : BasePluginTest() { } @Test - fun minimizeWithR8CanEnableObfuscation() { + fun r8CanEnableObfuscation() { writeR8Repository() writeR8ClientAndServerModules( serverShadowBlock = """ - minimize { - r8 { - enableObfuscation() - } + r8 { + enableObfuscation() } """ .trimIndent() @@ -488,15 +542,13 @@ class MinimizeTest : BasePluginTest() { } @Test - fun minimizeWithR8CanEnableOptimization() { + fun r8CanEnableOptimization() { writeR8Repository() writeR8ClientAndServerModules( serverShadowBlock = """ - minimize { - r8 { - enableOptimization() - } + r8 { + enableOptimization() } """ .trimIndent() @@ -514,14 +566,16 @@ class MinimizeTest : BasePluginTest() { } @Test - fun minimizeWithR8HonorsDependencyExcludes() { + fun minimizeAndR8CanBeConfiguredIndependently() { writeR8Repository() writeR8ClientAndServerModules( serverShadowBlock = """ minimize { exclude(project(':client')) - r8 {} + } + r8 { + proguardRules.add("-keep class client.** { *; }") } """ .trimIndent() @@ -543,7 +597,7 @@ class MinimizeTest : BasePluginTest() { } @Test - fun minimizeWithR8UsesJavaToolchain() { + fun r8UsesJavaToolchain() { writeR8Repository() writeR8ClientAndServerModules( serverProjectBlock = @@ -558,9 +612,7 @@ class MinimizeTest : BasePluginTest() { doFirst { logger.lifecycle("R8 launcher JDK " + javaLauncher.get().metadata.languageVersion.asInt()) } - minimize { - r8 {} - } + r8 {} """ .trimIndent(), ) @@ -800,9 +852,7 @@ class MinimizeTest : BasePluginTest() { implementation project(':service') } $shadowJarTask { - minimize { - r8 {} - } + r8 {} } """ .trimIndent() + lineSeparator diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin.kt index ea43d72e1..dc10bbfc2 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin.kt @@ -26,7 +26,7 @@ public abstract class ShadowBasePlugin : Plugin { it.description = "Specify runtime dependencies that are not merged into the final JAR." } configurations.register(R8_CONFIGURATION_NAME) { - it.description = "R8 executable used by ShadowJar R8 minimization." + it.description = "R8 executable used by ShadowJar post-processing." it.isCanBeConsumed = false // Defer the dependency resolving. it.isCanBeResolved = false diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultMinimizeSpec.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultMinimizeSpec.kt index d714d210d..4686407ea 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultMinimizeSpec.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultMinimizeSpec.kt @@ -1,5 +1,8 @@ +@file:Suppress("OVERRIDE_DEPRECATION", "DEPRECATION") + package com.github.jengelman.gradle.plugins.shadow.internal +import com.github.jengelman.gradle.plugins.shadow.tasks.DependencyFilter import com.github.jengelman.gradle.plugins.shadow.tasks.MinimizeSpec import com.github.jengelman.gradle.plugins.shadow.tasks.MinimizeTool import com.github.jengelman.gradle.plugins.shadow.tasks.R8Spec @@ -9,28 +12,17 @@ import org.gradle.api.Project import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Property import org.gradle.api.tasks.Internal -import org.gradle.api.tasks.Nested -import org.gradle.api.tasks.Optional internal open class DefaultMinimizeSpec @Inject constructor( project: Project, objectFactory: ObjectFactory, -) : MinimizeDependencyFilter(project), MinimizeSpec { - @get:Internal - internal val r8Spec: DefaultR8Spec by lazy { - objectFactory.newInstance(DefaultR8Spec::class.java) - } - + @get:Internal internal val r8Spec: DefaultR8Spec, +) : MinimizeSpec, DependencyFilter by MinimizeDependencyFilter(project) { override val tool: Property = objectFactory.property(MinimizeTool.DEPENDENCY_ANALYZER) - @get:Nested - @get:Optional - val r8SpecForInputs: R8Spec? - get() = if (tool.orNull == MinimizeTool.R8) r8Spec else null - override fun r8(action: Action) { tool.set(MinimizeTool.R8) action.execute(r8Spec) 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 488989ec5..7771e8e8d 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 @@ -27,9 +27,8 @@ import org.gradle.process.ExecOperations * replacing the original jar. * * Generated rules are based on the final jar contents. Source-set classes are kept as roots, - * dependencies excluded from minimization are kept, service descriptors keep providers for - * downstream `ServiceLoader` users, and R8 consumer rules are extracted from the jar. User rule - * files and inline rules are appended last. + * service descriptors keep providers for downstream `ServiceLoader` users, and R8 consumer rules + * are extracted from the jar. User rule files and inline rules are appended last. * * The default R8 configuration is shrink-only. Shadow passes `--no-minification` to disable name * obfuscation and generates `-dontoptimize` unless optimization is enabled explicitly. @@ -41,7 +40,6 @@ internal class R8Minimizer( private val r8Spec: DefaultR8Spec, private val javaLauncher: Provider, private val sourceSetsClassesDirs: Iterable, - private val keptDependencyFiles: Iterable, private val relocators: Iterable, private val preserveFileTimestamps: Boolean, private val reproducibleFileOrder: Boolean, @@ -52,7 +50,7 @@ internal class R8Minimizer( fun minimize(inputJar: File, temporaryDir: File) { if (r8Classpath.isEmpty) { throw GradleException( - "R8 minimization requires a non-empty R8 classpath. Apply the Shadow plugin or configure the shadowR8 configuration." + "R8 post-processing requires a non-empty R8 classpath. Apply the Shadow plugin or configure the shadowR8 configuration." ) } @@ -65,7 +63,7 @@ internal class R8Minimizer( val javaHome = launcher?.metadata?.installationPath?.asFile?.absolutePath ?: System.getProperty("java.home") if (javaHome.isNullOrBlank()) { - throw GradleException("R8 minimization requires the java.home system property.") + throw GradleException("R8 post-processing requires the java.home system property.") } extractClasspathRules(inputJar, extractedRulesFile, launcher) @@ -137,7 +135,6 @@ internal class R8Minimizer( add(DefaultR8Spec.DONT_OPTIMIZE_RULE) } addAll(sourceProguardRules(inputJar)) - addAll(keptDependencyRules(inputJar)) addAll(serviceProguardRules(inputJar)) addAll(extractedRulesFile.readLines()) r8Spec.proguardRuleFiles.files @@ -180,22 +177,6 @@ internal class R8Minimizer( .toList() } - // Keep dependencies users explicitly excluded from minimization, matching the existing - // minimize { exclude(...) } contract for the default analyzer. - private fun keptDependencyRules(inputJar: File): List { - val jarClasses = jarClassEntries(inputJar) - return keptDependencyFiles - .asSequence() - .flatMap { it.classNames() } - .map { relocators.relocateClass(it) } - .filter { it.isJavaTypeName() } - .filter { className -> "${className.replace('.', '/')}.class" in jarClasses } - .distinct() - .sorted() - .map { "-keep class $it { *; }" } - .toList() - } - // Service descriptors are usage edges for downstream ServiceLoader calls, so keep the service // interface and every listed provider even if R8 sees no direct references. private fun serviceProguardRules(inputJar: File): List { @@ -244,35 +225,6 @@ internal class R8Minimizer( .replace('/', '.') } - private fun File.classNames(): Sequence { - return when { - isDirectory -> - walkTopDown() - .filter { it.isFile && it.name.endsWith(".class") } - .mapNotNull { - it.toClassName(relativeTo = this) - } - isFile -> - JarFile(this) - .use { jarFile -> - jarFile - .entries() - .asSequence() - .filter { !it.isDirectory && it.name.endsWith(".class") } - .mapNotNull { it.name.toClassName() } - .toList() - } - .asSequence() - else -> emptySequence() - } - } - - private fun String.toClassName(): String? { - val name = substringAfterLast('/') - if (name == "module-info.class" || name == "package-info.class") return null - return removeSuffix(".class").replace('/', '.') - } - // R8 writes a fresh jar, so rewrite it through Shadow's archive settings to preserve // reproducible ordering, timestamps, compression, zip64, and metadata charset behavior. private fun normalizeJar(inputJar: File, outputJar: File) { diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/MinimizeSpec.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/MinimizeSpec.kt index 5ed372e95..de4de95e0 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/MinimizeSpec.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/MinimizeSpec.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package com.github.jengelman.gradle.plugins.shadow.tasks import org.gradle.api.Action @@ -5,14 +7,24 @@ import org.gradle.api.provider.Property import org.gradle.api.tasks.Input /** Configures how [ShadowJar.minimize] removes unused code from the shadowed JAR. */ +@Deprecated(message = "This compatibility layer will be removed in Shadow 10.") public interface MinimizeSpec : DependencyFilter { /** * The tool used to minimize the shadowed JAR. * * Defaults to [MinimizeTool.DEPENDENCY_ANALYZER]. */ - @get:Input public val tool: Property + @Deprecated( + message = + "Configure R8 with `ShadowJar.r8` instead. This compatibility property will be removed in Shadow 10." + ) + @get:Input + public val tool: Property - /** Use R8 to minimize the shadowed JAR and configure its options. */ + /** Use R8 to post-process the shadowed JAR and configure its options. */ + @Deprecated( + message = + "Use the standalone `ShadowJar.r8` block instead. This compatibility DSL will be removed in Shadow 10." + ) public fun r8(action: Action) } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/MinimizeTool.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/MinimizeTool.kt index 609ce0852..dab633a1d 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/MinimizeTool.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/MinimizeTool.kt @@ -1,6 +1,7 @@ package com.github.jengelman.gradle.plugins.shadow.tasks /** A tool that can minimize a shadowed JAR. */ +@Deprecated(message = "This compatibility layer will be removed in Shadow 10.") public enum class MinimizeTool { /** Shadow's default, simple dependency analyzer. */ DEPENDENCY_ANALYZER, 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 2f254a2a8..8259b77b3 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 @@ -8,7 +8,7 @@ import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity -/** Minimal R8 configuration for [ShadowJar.minimize]. */ +/** Configures R8 post-processing enabled by [ShadowJar.r8]. */ @ShadowDsl public interface R8Spec { /** diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index d5f7a03dd..f673c4c89 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -7,6 +7,7 @@ import com.github.jengelman.gradle.plugins.shadow.ShadowDsl import com.github.jengelman.gradle.plugins.shadow.internal.DefaultDependencyFilter import com.github.jengelman.gradle.plugins.shadow.internal.DefaultInheritManifest import com.github.jengelman.gradle.plugins.shadow.internal.DefaultMinimizeSpec +import com.github.jengelman.gradle.plugins.shadow.internal.DefaultR8Spec import com.github.jengelman.gradle.plugins.shadow.internal.R8Minimizer import com.github.jengelman.gradle.plugins.shadow.internal.UnusedTracker import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey @@ -76,7 +77,10 @@ import org.gradle.process.ExecOperations @ShadowDsl @CacheableTask public abstract class ShadowJar : Jar() { - private val defaultMinimizeSpec = objectFactory.newInstance(DefaultMinimizeSpec::class.java) + private val defaultR8Spec = objectFactory.newInstance(DefaultR8Spec::class.java) + + private val defaultMinimizeSpec = + objectFactory.newInstance(DefaultMinimizeSpec::class.java, defaultR8Spec) private val shadowDependencies = project.provider { // Find shadow configuration here instead of get, as the ShadowJar tasks could be registered @@ -114,26 +118,60 @@ public abstract class ShadowJar : Jar() { ) public open val minimizeJar: Property = objectFactory.property(false) + /** + * Runs R8 over the final shadowed JAR. + * + * Defaults to `false`. + */ + @get:Input + @get:Option( + option = "enable-r8", + description = "Runs R8 over the final shadowed JAR.", + ) + internal val enableR8: Property = objectFactory.property(false) + /** Options for [minimize]. */ - @get:Nested public open val minimizeSpec: MinimizeSpec = defaultMinimizeSpec + @Deprecated( + message = + "Configure R8 with `ShadowJar.r8` instead. This compatibility property will be removed in Shadow 10." + ) + @get:Nested + public open val minimizeSpec: @Suppress("DEPRECATION") MinimizeSpec = defaultMinimizeSpec + + private val minimizeWithDependencyAnalyzer = + _minimizeJar.zip(defaultMinimizeSpec.tool) { enabled, tool -> + @Suppress("DEPRECATION") + enabled && tool == MinimizeTool.DEPENDENCY_ANALYZER + } + + private val legacyR8Enabled = + _minimizeJar.zip(defaultMinimizeSpec.tool) { enabled, tool -> + @Suppress("DEPRECATION") + enabled && tool == MinimizeTool.R8 + } + + private val shouldRunR8 = + enableR8.zip(legacyR8Enabled) { explicitlyEnabled, legacyEnabled -> + explicitlyEnabled || legacyEnabled + } @get:Classpath public open val toMinimize: ConfigurableFileCollection = objectFactory.fileCollection { - _minimizeJar.map { + minimizeWithDependencyAnalyzer.map { if (it) (defaultMinimizeSpec.resolve(configurations.get()) - apiJars) else emptySet() } } @get:Classpath public open val apiJars: ConfigurableFileCollection = objectFactory.fileCollection { - _minimizeJar.map { if (it) project.getApiJars() else emptySet() } + minimizeWithDependencyAnalyzer.map { if (it) project.getApiJars() else emptySet() } } @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) public open val sourceSetsClassesDirs: ConfigurableFileCollection = objectFactory.fileCollection { - _minimizeJar.map { - if (it) { + minimizeWithDependencyAnalyzer.zip(shouldRunR8) { minimizeEnabled, r8IsEnabled -> + if (minimizeEnabled || r8IsEnabled) { project.sourceSets.map { sourceSet -> sourceSet.output.classesDirs.filter(File::isDirectory) } @@ -143,10 +181,16 @@ public abstract class ShadowJar : Jar() { } } + @Suppress("unused") // Trigger task executions after R8Spec inputs changed. + @get:Nested + @get:Optional + internal val r8SpecForInputs: R8Spec? + get() = if (shouldRunR8.get()) defaultR8Spec else null + @get:Classpath public open val r8Classpath: ConfigurableFileCollection = objectFactory.fileCollection { - _minimizeJar.zip(minimizeSpec.tool) { enabled, tool -> - if (enabled && tool == MinimizeTool.R8) { + shouldRunR8.map { enabled -> + if (enabled) { // Use findByName so custom ShadowJar tasks can be configured even when shadowR8 isn't // registered. project.configurations.findByName(R8_CONFIGURATION_NAME)?.apply { @@ -337,9 +381,16 @@ public abstract class ShadowJar : Jar() { /** Enable minimization and execute the [action] with the [MinimizeSpec] for minimize. */ @JvmOverloads - public open fun minimize(action: Action = Action {}) { + public open fun minimize(action: Action = Action {}) { _minimizeJar.set(true) - action.execute(minimizeSpec) + action.execute(defaultMinimizeSpec) + } + + /** Enable R8 post-processing and execute the [action] with the [R8Spec]. */ + @JvmOverloads + public open fun r8(action: Action = Action {}) { + enableR8.set(true) + action.execute(defaultR8Spec) } /** Extra dependency operations to be applied in the shadow steps. */ @@ -535,7 +586,7 @@ public abstract class ShadowJar : Jar() { } } val unusedClasses = - if (_minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { + if (minimizeWithDependencyAnalyzer.get()) { val unusedTracker = UnusedTracker( sourceSetsClassesDirs = sourceSetsClassesDirs.files, @@ -709,17 +760,14 @@ public abstract class ShadowJar : Jar() { } private fun minimizeWithR8() { - val useR8 = _minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.R8 - if (!useR8) return - val keptDependencyFiles = includedDependencies.files - toMinimize.files + if (!shouldRunR8.get()) return R8Minimizer( execOperations = execOperations, logger = logger, r8Classpath = r8Classpath, - r8Spec = defaultMinimizeSpec.r8Spec, + r8Spec = defaultR8Spec, javaLauncher = javaLauncher, sourceSetsClassesDirs = sourceSetsClassesDirs.files, - keptDependencyFiles = keptDependencyFiles, relocators = relocators.get() + packageRelocators, preserveFileTimestamps = isPreserveFileTimestamps, reproducibleFileOrder = isReproducibleFileOrder, diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt index 91c70e5ab..ba0bcc384 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt @@ -150,6 +150,7 @@ class ShadowPropertiesTest { assertThat(@Suppress("DEPRECATION") enableKotlinModuleRemapping.get()).isTrue() assertThat(failOnDuplicateEntries.get()).isFalse() assertThat(@Suppress("DEPRECATION") minimizeJar.get()).isFalse() + assertThat(enableR8.get()).isFalse() assertThat(mainClass.orNull).isNull() assertThat(javaLauncher.get().metadata.jvmVersion) .isEqualTo( 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/R8SpecTest.kt similarity index 66% rename from src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeSpecsTest.kt rename to src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8SpecTest.kt index 454739be1..45d356638 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/R8SpecTest.kt @@ -3,36 +3,14 @@ package com.github.jengelman.gradle.plugins.shadow.internal import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEmpty -import assertk.assertions.isEqualTo import assertk.assertions.isFalse -import assertk.assertions.isNull -import assertk.assertions.isSameInstanceAs import assertk.assertions.isTrue -import com.github.jengelman.gradle.plugins.shadow.tasks.MinimizeTool import org.gradle.testfixtures.ProjectBuilder import org.junit.jupiter.api.Test -class MinimizeSpecsTest { +class R8SpecTest { private val project = ProjectBuilder.builder().build() - @Test - fun defaultMinimizeSpecUsesDependencyAnalyzer() = - with(project.objects.newInstance(DefaultMinimizeSpec::class.java, project)) { - assertThat(tool.get()).isEqualTo(MinimizeTool.DEPENDENCY_ANALYZER) - assertThat(r8SpecForInputs).isNull() - } - - @Test - fun r8ConfiguresToolAndExposesSameSpecAsInput() = - with(project.objects.newInstance(DefaultMinimizeSpec::class.java, project)) { - lateinit var configured: Any - r8 { configured = it } - - assertThat(tool.get()).isEqualTo(MinimizeTool.R8) - assertThat(r8SpecForInputs).isSameInstanceAs(configured) - assertThat(r8Spec).isSameInstanceAs(configured) - } - @Test fun defaultR8SpecIsShrinkOnly() = with(project.objects.newInstance(DefaultR8Spec::class.java)) {