diff --git a/api/shadow.api b/api/shadow.api index 1e7e202dd..a176485a2 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -29,6 +29,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugi public static final field CONFIGURATION_NAME Ljava/lang/String; public static final field Companion Lcom/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin$Companion; public static final field EXTENSION_NAME Ljava/lang/String; + public static final field R8_CONFIGURATION_NAME Ljava/lang/String; public static final field SHADOW Ljava/lang/String; public fun ()V public synthetic fun apply (Ljava/lang/Object;)V @@ -40,6 +41,9 @@ public final class com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin$C public final synthetic fun getShadow (Lorg/gradle/api/artifacts/ConfigurationContainer;)Lorg/gradle/api/NamedDomainObjectProvider; } +public abstract interface annotation class com/github/jengelman/gradle/plugins/shadow/ShadowDslMarker : java/lang/annotation/Annotation { +} + public abstract interface class com/github/jengelman/gradle/plugins/shadow/ShadowExtension { public abstract fun getAddShadowJarToAssembleLifecycle ()Lorg/gradle/api/provider/Property; public abstract fun getAddShadowVariantIntoJavaComponent ()Lorg/gradle/api/provider/Property; @@ -202,6 +206,27 @@ public abstract interface class com/github/jengelman/gradle/plugins/shadow/tasks public abstract fun inheritFrom ([Ljava/lang/Object;Lorg/gradle/api/Action;)V } +public abstract interface class com/github/jengelman/gradle/plugins/shadow/tasks/MinimizeSpec : com/github/jengelman/gradle/plugins/shadow/tasks/DependencyFilter { + public abstract fun getTool ()Lorg/gradle/api/provider/Property; + public abstract fun r8 (Lorg/gradle/api/Action;)V +} + +public final class com/github/jengelman/gradle/plugins/shadow/tasks/MinimizeTool : java/lang/Enum { + public static final field DEPENDENCY_ANALYZER Lcom/github/jengelman/gradle/plugins/shadow/tasks/MinimizeTool; + public static final field R8 Lcom/github/jengelman/gradle/plugins/shadow/tasks/MinimizeTool; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/github/jengelman/gradle/plugins/shadow/tasks/MinimizeTool; + public static fun values ()[Lcom/github/jengelman/gradle/plugins/shadow/tasks/MinimizeTool; +} + +public abstract interface class com/github/jengelman/gradle/plugins/shadow/tasks/R8Spec { + public abstract fun enableObfuscation ()V + public abstract fun enableOptimization ()V + public abstract fun getArgs ()Lorg/gradle/api/provider/ListProperty; + public abstract fun getKeepRuleFiles ()Lorg/gradle/api/file/ConfigurableFileCollection; + public abstract fun getKeepRules ()Lorg/gradle/api/provider/ListProperty; +} + public class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction : org/gradle/api/internal/file/copy/CopyAction { public static final field CONSTANT_TIME_FOR_ZIP_ENTRIES J public static final field Companion Lcom/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction$Companion; @@ -232,13 +257,17 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar public fun getEnableAutoRelocation ()Lorg/gradle/api/provider/Property; public fun getEnableKotlinModuleRemapping ()Lorg/gradle/api/provider/Property; public fun getExcludes ()Ljava/util/Set; + protected abstract fun getExecOperations ()Lorg/gradle/process/ExecOperations; public fun getFailOnDuplicateEntries ()Lorg/gradle/api/provider/Property; public fun getIncludedDependencies ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getIncludes ()Ljava/util/Set; + public fun getJavaLauncher ()Lorg/gradle/api/provider/Property; public fun getMainClass ()Lorg/gradle/api/provider/Property; public fun getManifest ()Lcom/github/jengelman/gradle/plugins/shadow/tasks/InheritManifest; public synthetic fun getManifest ()Lorg/gradle/api/java/archives/Manifest; public fun getMinimizeJar ()Lorg/gradle/api/provider/Property; + public fun getMinimizeSpec ()Lcom/github/jengelman/gradle/plugins/shadow/tasks/MinimizeSpec; + public fun getR8Classpath ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getRelocationPrefix ()Lorg/gradle/api/provider/Property; public fun getRelocators ()Lorg/gradle/api/provider/SetProperty; public fun getSourceSetsClassesDirs ()Lorg/gradle/api/file/ConfigurableFileCollection; diff --git a/docs/changes/README.md b/docs/changes/README.md index 59754291a..3d0666f25 100644 --- a/docs/changes/README.md +++ b/docs/changes/README.md @@ -7,6 +7,7 @@ - Check `DuplicatesStrategy` for merging transformers. ([#2026](https://github.com/GradleUp/shadow/pull/2026)) This will log warnings when an incompatible `DuplicatesStrategy` (e.g., `EXCLUDE`) is applied in Gradle configuration for built-in `ResourceTransformer`s. +- Add R8 as an opt-in `minimize { r8 { ... } }` tool for shrinking the final shadowed JAR. ([#2077](https://github.com/GradleUp/shadow/pull/2077)) ### Changed diff --git a/docs/configuration/minimizing/README.md b/docs/configuration/minimizing/README.md index 91ab66b5d..179ce4992 100644 --- a/docs/configuration/minimizing/README.md +++ b/docs/configuration/minimizing/README.md @@ -72,6 +72,209 @@ 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 +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. + +The default R8 configuration only shrinks unused code. It disables name minification and optimization. + +=== "Kotlin" + + ```kotlin + repositories { + google() + } + + tasks.shadowJar { + minimize { + r8 { + // Optional extra configuration + keepRules.add("-keep class com.example.ReflectiveApi { *; }") + keepRuleFiles.from(layout.projectDirectory.file("r8-rules.pro")) + } + } + } + ``` + +=== "Groovy" + + ```groovy + repositories { + google() + } + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + minimize { + r8 { + // Optional extra configuration + keepRules.add('-keep class com.example.ReflectiveApi { *; }') + keepRuleFiles.from(layout.projectDirectory.file('r8-rules.pro')) + } + } + } + ``` + +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: + +=== "Kotlin" + + ```kotlin + dependencies { + shadowR8("com.android.tools:r8:9.1.31") + } + ``` + +=== "Groovy" + + ```groovy + dependencies { + shadowR8 'com.android.tools:r8:9.1.31' + } + ``` + +Advanced R8 command line arguments can be added with `args`. Replacing the default `args` value removes Shadow's +default command line arguments, so prefer the helper functions for common obfuscation and optimization toggles. These +helpers are independent and can be used together. + +For example, to downgrade R8 warnings to info: + +=== "Kotlin" + + ```kotlin + repositories { + google() + } + + tasks.shadowJar { + minimize { + r8 { + args.addAll(listOf("--map-diagnostics", "warning", "info")) + } + } + } + ``` + +=== "Groovy" + + ```groovy + repositories { + google() + } + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + minimize { + r8 { + args.addAll(['--map-diagnostics', 'warning', 'info']) + } + } + } + ``` + +To enable name obfuscation: + +=== "Kotlin" + + ```kotlin + repositories { + google() + } + + tasks.shadowJar { + minimize { + r8 { + enableObfuscation() + } + } + } + ``` + +=== "Groovy" + + ```groovy + repositories { + google() + } + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + minimize { + r8 { + enableObfuscation() + } + } + } + ``` + +To enable optimization: + +=== "Kotlin" + + ```kotlin + repositories { + google() + } + + tasks.shadowJar { + minimize { + r8 { + enableOptimization() + } + } + } + ``` + +=== "Groovy" + + ```groovy + repositories { + google() + } + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + minimize { + r8 { + enableOptimization() + } + } + } + ``` + +To enable both: + +=== "Kotlin" + + ```kotlin + repositories { + google() + } + + tasks.shadowJar { + minimize { + r8 { + enableObfuscation() + enableOptimization() + } + } + } + ``` + +=== "Groovy" + + ```groovy + repositories { + google() + } + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + minimize { + r8 { + enableObfuscation() + enableOptimization() + } + } + } + ``` [ShadowJar.dependencies]: ../../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/dependencies.html 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 06818ef0f..ae5c2616c 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 @@ -7,6 +7,8 @@ import assertk.assertions.isEqualTo import com.github.jengelman.gradle.plugins.shadow.internal.MinimizeDependencyFilter import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey import com.github.jengelman.gradle.plugins.shadow.testkit.JarPath +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.getMainAttr import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransformer @@ -340,6 +342,40 @@ class CachingTest : BasePluginTest() { } } + @Test + fun r8KeepRuleFileChanged() { + val previousTaskPath = taskPath + taskPath = serverShadowJarPath + try { + writeR8Repository() + writeR8ClientAndServerModules() + val keepRules = path("server/r8-rules.pro") + keepRules.writeText("") + + assertExecutionSuccess() + assertThat(outputServerShadowedJar).useAll { + containsAtLeast("server/Server.class", "client/Used.class", *manifestEntries) + containsNone("client/Reflective.class") + } + + keepRules.writeText("-keep class client.Reflective { *; }") + + assertExecutionSuccess() + assertThat(outputServerShadowedJar).useAll { + containsAtLeast( + "server/Server.class", + "client/Used.class", + "client/Reflective.class", + *manifestEntries, + ) + containsNone("client/Unused.class") + } + assertExecutionsFromCacheAndUpToDate() + } finally { + taskPath = previousTaskPath + } + } + @Test fun relocatorChanged() { projectScript.appendText( @@ -513,4 +549,87 @@ class CachingTest : BasePluginTest() { val result = runWithSuccess(taskPath) assertThat(result).taskOutcomeEquals(taskPath, expectedOutcome) } + + private fun writeR8Repository() { + settingsScript.writeText( + settingsScript.readText().replace("mavenCentral()", "mavenCentral()\n google()") + ) + } + + private fun writeR8ClientAndServerModules() { + settingsScript.appendText( + """ + include 'client', 'server' + """ + .trimIndent() + ) + projectScript.writeText("") + + path("client/src/main/java/client/Used.java") + .writeText( + """ + package client; + public class Used { + public static String name() { + return "used"; + } + } + """ + .trimIndent() + ) + path("client/src/main/java/client/Unused.java") + .writeText( + """ + package client; + public class Unused {} + """ + .trimIndent() + ) + path("client/src/main/java/client/Reflective.java") + .writeText( + """ + package client; + public class Reflective {} + """ + .trimIndent() + ) + path("client/build.gradle") + .writeText( + """ + ${getDefaultProjectBuildScript("java")} + """ + .trimIndent() + lineSeparator + ) + + path("server/src/main/java/server/Server.java") + .writeText( + """ + package server; + import client.Used; + public class Server { + public String name() { + return Used.name(); + } + } + """ + .trimIndent() + ) + path("server/build.gradle") + .writeText( + """ + ${getDefaultProjectBuildScript("java")} + dependencies { + implementation project(':client') + } + $shadowJarTask { + minimize { + r8 { + keepRuleFiles.from(file("r8-rules.pro")) + } + } + } + """ + .trimIndent() + lineSeparator + ) + } } 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 aa50d6e82..5ef1f623c 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 @@ -1,14 +1,22 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.hasSize +import assertk.assertions.isEqualTo import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.SHADOW_JAR_TASK_NAME import com.github.jengelman.gradle.plugins.shadow.testkit.JarPath 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.util.Issue +import java.net.URLClassLoader +import java.util.ServiceLoader import kotlin.io.path.appendText +import kotlin.io.path.readText import kotlin.io.path.writeText +import org.gradle.api.JavaVersion import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource @@ -296,6 +304,199 @@ class MinimizeTest : BasePluginTest() { } } + @Test + fun minimizeWithR8ShrinksUnusedDependencyClasses() { + writeR8Repository() + writeR8ClientAndServerModules( + serverShadowBlock = + """ + minimize { + r8 {} + } + """ + .trimIndent() + ) + + runWithSuccess(serverShadowJarPath) + + assertThat(outputServerShadowedJar).useAll { + containsOnly( + "server/", + "server/Server.class", + "client/", + "client/Used.class", + *manifestEntries, + ) + } + } + + @Test + fun minimizeWithR8KeepsServiceProviders() { + writeR8Repository() + writeR8ServiceModules() + + runWithSuccess(serverShadowJarPath) + + assertThat(outputServerShadowedJar).useAll { + containsOnly( + "server/", + "server/Server.class", + "service/", + "service/Greeter.class", + "service/DefaultGreeter.class", + "META-INF/services/", + "META-INF/services/service.Greeter", + *manifestEntries, + ) + getContent("META-INF/services/service.Greeter").isEqualTo("service.DefaultGreeter\n") + } + val shadowJarUrl = outputServerShadowedJar.use { it.path.toUri().toURL() } + URLClassLoader(arrayOf(shadowJarUrl), null).use { loader -> + val serviceClass = loader.loadClass("service.Greeter") + assertThat(ServiceLoader.load(serviceClass, loader).toList()).hasSize(1) + } + } + + @Test + fun minimizeWithR8HonorsCustomKeepRules() { + writeR8Repository() + writeR8ClientAndServerModules( + serverShadowBlock = + """ + minimize { + r8 { + keepRules.add("-keep class client.Reflective { *; }") + } + } + """ + .trimIndent() + ) + + runWithSuccess(serverShadowJarPath) + + assertThat(outputServerShadowedJar).useAll { + containsOnly( + "server/", + "server/Server.class", + "client/", + "client/Used.class", + "client/Reflective.class", + *manifestEntries, + ) + } + } + + @Test + fun minimizeWithR8CanEnableObfuscation() { + writeR8Repository() + writeR8ClientAndServerModules( + serverShadowBlock = + """ + minimize { + r8 { + enableObfuscation() + } + } + """ + .trimIndent() + ) + + runWithSuccess(serverShadowJarPath) + + assertThat(outputServerShadowedJar).useAll { + containsOnly( + "server/", + "server/Server.class", + "a/", + "a/a.class", + *manifestEntries, + ) + } + } + + @Test + fun minimizeWithR8CanEnableOptimization() { + writeR8Repository() + writeR8ClientAndServerModules( + serverShadowBlock = + """ + minimize { + r8 { + enableOptimization() + } + } + """ + .trimIndent() + ) + + runWithSuccess(serverShadowJarPath) + + assertThat(outputServerShadowedJar).useAll { + containsOnly( + "server/", + "server/Server.class", + *manifestEntries, + ) + } + } + + @Test + fun minimizeWithR8HonorsDependencyExcludes() { + writeR8Repository() + writeR8ClientAndServerModules( + serverShadowBlock = + """ + minimize { + exclude(project(':client')) + r8 {} + } + """ + .trimIndent() + ) + + runWithSuccess(serverShadowJarPath) + + assertThat(outputServerShadowedJar).useAll { + containsOnly( + "server/", + "server/Server.class", + "client/", + "client/Used.class", + "client/Unused.class", + "client/Reflective.class", + *manifestEntries, + ) + } + } + + @Test + fun minimizeWithR8UsesJavaToolchain() { + writeR8Repository() + writeR8ClientAndServerModules( + serverProjectBlock = + """ + java { + toolchain.languageVersion = JavaLanguageVersion.of(${JavaVersion.current().majorVersion}) + } + """ + .trimIndent(), + serverShadowBlock = + """ + doFirst { + logger.lifecycle("R8 launcher JDK " + javaLauncher.get().metadata.languageVersion.asInt()) + } + minimize { + r8 {} + } + """ + .trimIndent(), + ) + + val result = runWithSuccess(serverShadowJarPath) + + assertThat(result.output).contains("R8 launcher JDK ${JavaVersion.current().majorVersion}") + } + private fun writeApiLibAndImplModules() { settingsScript.appendText( """ @@ -385,4 +586,153 @@ class MinimizeTest : BasePluginTest() { .trimIndent() + lineSeparator ) } + + private fun writeR8Repository() { + settingsScript.writeText( + settingsScript.readText().replace("mavenCentral()", "mavenCentral()\n google()") + ) + } + + private fun writeR8ClientAndServerModules( + serverShadowBlock: String, + serverProjectBlock: String = "", + ) { + settingsScript.appendText( + """ + include 'client', 'server' + """ + .trimIndent() + ) + projectScript.writeText("") + + path("client/src/main/java/client/Used.java") + .writeText( + """ + package client; + public class Used { + public static String name() { + return "used"; + } + } + """ + .trimIndent() + ) + path("client/src/main/java/client/Unused.java") + .writeText( + """ + package client; + public class Unused {} + """ + .trimIndent() + ) + path("client/src/main/java/client/Reflective.java") + .writeText( + """ + package client; + public class Reflective {} + """ + .trimIndent() + ) + path("client/build.gradle") + .writeText( + """ + ${getDefaultProjectBuildScript("java")} + """ + .trimIndent() + lineSeparator + ) + + path("server/src/main/java/server/Server.java") + .writeText( + """ + package server; + import client.Used; + public class Server { + public String name() { + return Used.name(); + } + } + """ + .trimIndent() + ) + path("server/build.gradle") + .writeText( + """ + ${getDefaultProjectBuildScript("java")} + $serverProjectBlock + dependencies { + implementation project(':client') + } + $shadowJarTask { + $serverShadowBlock + } + """ + .trimIndent() + lineSeparator + ) + } + + private fun writeR8ServiceModules() { + settingsScript.appendText( + """ + include 'service', 'server' + """ + .trimIndent() + ) + projectScript.writeText("") + + path("service/src/main/java/service/Greeter.java") + .writeText( + """ + package service; + public interface Greeter { + String greet(); + } + """ + .trimIndent() + ) + path("service/src/main/java/service/DefaultGreeter.java") + .writeText( + """ + package service; + public class DefaultGreeter implements Greeter { + public String greet() { + return "hello"; + } + } + """ + .trimIndent() + ) + path("service/src/main/resources/META-INF/services/service.Greeter") + .writeText("service.DefaultGreeter") + path("service/build.gradle") + .writeText( + """ + ${getDefaultProjectBuildScript("java")} + """ + .trimIndent() + lineSeparator + ) + + path("server/src/main/java/server/Server.java") + .writeText( + """ + package server; + public class Server {} + """ + .trimIndent() + ) + path("server/build.gradle") + .writeText( + """ + ${getDefaultProjectBuildScript("java")} + dependencies { + implementation project(':service') + } + $shadowJarTask { + minimize { + 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 6c4f6fe8e..085752556 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 @@ -24,6 +24,15 @@ public abstract class ShadowBasePlugin : Plugin { } @Suppress("EagerGradleConfiguration") // this should be created eagerly. configurations.create(CONFIGURATION_NAME) + @Suppress("EagerGradleConfiguration") // this should be created eagerly. + configurations.create(R8_CONFIGURATION_NAME) { + it.description = "R8 executable used by ShadowJar R8 minimization." + it.isCanBeConsumed = false + it.isCanBeResolved = true + it.defaultDependencies { dependencies -> + dependencies.add(project.dependencies.create(DEFAULT_R8_DEPENDENCY)) + } + } } public companion object { @@ -44,6 +53,8 @@ public abstract class ShadowBasePlugin : Plugin { public const val SHADOW: String = "shadow" public const val EXTENSION_NAME: String = SHADOW public const val CONFIGURATION_NAME: String = SHADOW + public const val R8_CONFIGURATION_NAME: String = "shadowR8" + internal const val DEFAULT_R8_DEPENDENCY: String = "com.android.tools:r8:9.1.31" @get:JvmSynthetic public inline val ConfigurationContainer.shadow: NamedDomainObjectProvider diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowDslMarker.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowDslMarker.kt new file mode 100644 index 000000000..126b3d06b --- /dev/null +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowDslMarker.kt @@ -0,0 +1,4 @@ +package com.github.jengelman.gradle.plugins.shadow + +/** Restricts nested Shadow DSL blocks from accidentally calling outer receiver APIs. */ +@DslMarker public annotation class ShadowDslMarker 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 new file mode 100644 index 000000000..d714d210d --- /dev/null +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultMinimizeSpec.kt @@ -0,0 +1,38 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +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 +import javax.inject.Inject +import org.gradle.api.Action +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) + } + + 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/DefaultR8Spec.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultR8Spec.kt new file mode 100644 index 000000000..6f3dc3db7 --- /dev/null +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultR8Spec.kt @@ -0,0 +1,38 @@ +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.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 { + private val defaultArgs: ListProperty = objectFactory.listProperty(DEFAULT_ARGS) + + @get:Input val obfuscationEnabled: Property = objectFactory.property(false) + + @get:Input val optimizationEnabled: Property = objectFactory.property(false) + + override val args: ListProperty = objectFactory.listProperty(defaultArgs) + + override val keepRules: ListProperty = objectFactory.listProperty() + + override val keepRuleFiles: ConfigurableFileCollection = objectFactory.fileCollection() + + override fun enableObfuscation() { + defaultArgs.set(emptyList()) + obfuscationEnabled.set(true) + } + + override fun enableOptimization() { + optimizationEnabled.set(true) + } + + internal companion object { + const val NO_MINIFICATION_ARG = "--no-minification" + const val DONT_OPTIMIZE_RULE = "-dontoptimize" + val DEFAULT_ARGS = listOf(NO_MINIFICATION_ARG) + } +} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/GradleCompat.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/GradleCompat.kt index 347863dd6..b6e6ea22a 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/GradleCompat.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/GradleCompat.kt @@ -13,6 +13,7 @@ import org.gradle.api.plugins.JavaPlugin import org.gradle.api.plugins.JavaPlugin.API_CONFIGURATION_NAME import org.gradle.api.plugins.JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.provider.ListProperty import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property import org.gradle.api.provider.Provider @@ -107,6 +108,19 @@ internal inline fun ObjectFactory.property( } } +@Suppress("UNCHECKED_CAST") +internal inline fun ObjectFactory.listProperty( + defaultValue: Any? = null +): ListProperty = + listProperty(V::class.java).apply { + defaultValue ?: return@apply + if (defaultValue is Provider<*>) { + convention(defaultValue as Provider>) + } else { + convention(defaultValue as Iterable) + } + } + @Suppress("UNCHECKED_CAST") internal inline fun ObjectFactory.setProperty( defaultValue: Any? = null diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt index 75fdd5d46..e22a33bff 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/MinimizeDependencyFilter.kt @@ -4,7 +4,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.DependencyFilter import org.gradle.api.Project import org.gradle.api.artifacts.ResolvedDependency -internal class MinimizeDependencyFilter(project: Project) : +internal open class MinimizeDependencyFilter(project: Project) : DependencyFilter.AbstractDependencyFilter(project) { override fun resolve( dependencies: Set, 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 new file mode 100644 index 000000000..84d7f20bc --- /dev/null +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/R8Minimizer.kt @@ -0,0 +1,345 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator +import com.github.jengelman.gradle.plugins.shadow.relocation.relocateClass +import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption.REPLACE_EXISTING +import java.util.jar.JarFile +import org.apache.tools.zip.UnixStat +import org.apache.tools.zip.Zip64Mode +import org.apache.tools.zip.ZipOutputStream +import org.gradle.api.GradleException +import org.gradle.api.file.FileCollection +import org.gradle.api.logging.Logger +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.bundling.ZipEntryCompression +import org.gradle.jvm.toolchain.JavaLauncher +import org.gradle.process.ExecOperations + +/** + * Runs R8 as a final-archive shrinker. + * + * Shadow first writes the complete jar, including relocations, resource transformers, merged + * service files, and duplicate handling. R8 then processes that exact artifact. + * + * R8 does not know about Shadow's reproducible archive settings, so its output is normalized before + * 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, and service descriptors keep providers for + * downstream `ServiceLoader` users. 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. + */ +internal class R8Minimizer( + private val execOperations: ExecOperations, + private val logger: Logger, + private val r8Classpath: FileCollection, + 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, + private val zip64: Boolean, + private val entryCompression: ZipEntryCompression, + private val metadataCharset: String?, +) { + 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." + ) + } + + val r8Dir = temporaryDir.resolve("r8").also { it.mkdirs() } + val rulesFile = r8Dir.resolve("rules.pro") + val r8Output = r8Dir.resolve("output.jar") + val normalizedOutput = r8Dir.resolve("normalized-output.jar") + val launcher = javaLauncher.orNull + val javaHome = + launcher?.metadata?.installationPath?.asFile?.absolutePath ?: System.getProperty("java.home") + if (javaHome.isNullOrBlank()) { + throw GradleException("R8 minimization requires the java.home system property.") + } + + val r8Args = r8Spec.args.get() + rulesFile.writeText(createRules(inputJar, r8Args).joinToString(System.lineSeparator())) + + val arguments = buildList { + add("--classfile") + add("--output") + add(r8Output.absolutePath) + add("--pg-conf") + add(rulesFile.absolutePath) + add("--lib") + add(javaHome) + addAll(r8Args) + add(inputJar.absolutePath) + } + + logger.info("Running R8 to minimize {}.", inputJar) + execOperations.javaexec { + it.classpath = r8Classpath + it.mainClass.set(R8_MAIN_CLASS) + if (launcher != null) { + it.executable = launcher.executablePath.asFile.absolutePath + } + it.args(arguments) + } + + normalizeJar(r8Output, normalizedOutput) + Files.move(normalizedOutput.toPath(), inputJar.toPath(), REPLACE_EXISTING) + } + + private fun createRules(inputJar: File, r8Args: List): List { + val rules = linkedSetOf() + if (shouldDisableOptimization(r8Args)) { + rules += DefaultR8Spec.DONT_OPTIMIZE_RULE + } + rules += sourceKeepRules(inputJar) + rules += keptDependencyRules(inputJar) + rules += serviceKeepRules(inputJar) + r8Spec.keepRuleFiles.files + .sortedBy { it.absolutePath } + .forEach { file -> + if (file.isFile) { + rules += file.readText().lineSequence().toList() + } + } + rules += r8Spec.keepRules.get() + return rules.toList() + } + + private fun shouldDisableOptimization(r8Args: List): Boolean { + return !r8Spec.optimizationEnabled.get() && + (r8Spec.obfuscationEnabled.get() || DefaultR8Spec.NO_MINIFICATION_ARG in r8Args) + } + + // Project classes are the public surface of the shadowed jar, even when nothing in the input jar + // refers to every class directly. + private fun sourceKeepRules(inputJar: File): List { + val jarClasses = jarClassEntries(inputJar) + return sourceSetsClassesDirs + .asSequence() + .filter(File::isDirectory) + .flatMap { dir -> + dir + .walkTopDown() + .filter { it.isFile && it.name.endsWith(".class") } + .mapNotNull { file -> + file.toClassName(relativeTo = dir) + } + } + .map { relocators.relocateClass(it) } + .filter { it.isJavaTypeName() } + .filter { className -> "${className.replace('.', '/')}.class" in jarClasses } + .distinct() + .sorted() + .map { "-keep,includedescriptorclasses class $it { *; }" } + .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 serviceKeepRules(inputJar: File): List { + val rules = linkedSetOf() + JarFile(inputJar).use { jarFile -> + jarFile + .entries() + .asSequence() + .filter { !it.isDirectory && it.name.startsWith(SERVICES_PATH) } + .sortedBy { it.name } + .forEach { entry -> + val serviceClass = entry.name.removePrefix(SERVICES_PATH).replace('/', '.') + if (serviceClass.isJavaTypeName()) { + rules += "-keep class $serviceClass { *; }" + } + jarFile.getInputStream(entry).bufferedReader().useLines { lines -> + lines + .map { it.substringBefore('#').trim() } + .filter { it.isNotEmpty() && it.isJavaTypeName() } + .forEach { provider -> rules += "-keep class $provider { *; }" } + } + } + } + return rules.toList() + } + + private fun jarClassEntries(inputJar: File): Set { + return JarFile(inputJar).use { jarFile -> + jarFile + .entries() + .asSequence() + .filter { !it.isDirectory && it.name.endsWith(".class") } + .map { it.name } + .toSet() + } + } + + private fun File.toClassName(relativeTo: File): String? { + if (name == "module-info.class" || name == "package-info.class") return null + return relativeTo + .toPath() + .relativize(toPath()) + .toString() + .replace(File.separatorChar, '/') + .removeSuffix(".class") + .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) { + val entries = + JarFile(inputJar).use { jarFile -> + jarFile + .entries() + .asSequence() + .filter { !it.isDirectory } + .map { entry -> + R8JarEntry( + name = entry.name, + time = entry.time, + bytes = jarFile.getInputStream(entry).use { it.readBytes() }, + ) + } + .toList() + } + val orderedEntries = if (reproducibleFileOrder) entries.sortedBy { it.name } else entries + val entryCompressionMethod = + when (entryCompression) { + ZipEntryCompression.DEFLATED -> ZipOutputStream.DEFLATED + ZipEntryCompression.STORED -> ZipOutputStream.STORED + } + val zipOutputStream = + if (entryCompressionMethod == ZipOutputStream.STORED) { + ZipOutputStream(outputJar) + } else { + ZipOutputStream(outputJar.outputStream().buffered()) + } + zipOutputStream.use { zos -> + if (metadataCharset != null) { + zos.setEncoding(metadataCharset) + } + zos.setUseZip64(if (zip64) Zip64Mode.AsNeeded else Zip64Mode.Never) + zos.setMethod(entryCompressionMethod) + val added = mutableSetOf() + + fun addParentDirs(name: String) { + val parent = name.substringBeforeLast('/', "") + if (parent.isEmpty()) return + addParentDirs(parent) + val entryName = "$parent/" + if (added.add(entryName)) { + zos.putNextEntry( + zipEntry(entryName, preserveFileTimestamps) { + unixMode = UnixStat.DIR_FLAG or DEFAULT_DIR_MODE + } + ) + zos.closeEntry() + } + } + + orderedEntries.forEach { entry -> + addParentDirs(entry.name) + if (added.add(entry.name)) { + zos.putNextEntry( + zipEntry(entry.name, preserveFileTimestamps, entry.time) { + unixMode = UnixStat.FILE_FLAG or DEFAULT_FILE_MODE + } + ) + zos.write(entry.bytes) + zos.closeEntry() + } + } + } + } + + private fun String.isJavaTypeName(): Boolean = javaTypeNameRegex.matches(this) + + // Not a data class because of the bytearray + private class R8JarEntry(val name: String, val time: Long, val bytes: ByteArray) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as R8JarEntry + + if (time != other.time) return false + if (name != other.name) return false + if (!bytes.contentEquals(other.bytes)) return false + + return true + } + + override fun hashCode(): Int { + var result = time.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + bytes.contentHashCode() + return result + } + + override fun toString(): String { + return "R8JarEntry(name='$name', time=$time, bytes=${bytes.toHexString()})" + } + } + + private companion object { + const val R8_MAIN_CLASS = "com.android.tools.r8.R8" + const val SERVICES_PATH = "META-INF/services/" + const val DEFAULT_DIR_MODE = 493 // 0755 + const val DEFAULT_FILE_MODE = 420 // 0644 + // Keep only ordinary dot-separated Java type names in generated rules. This filters out blank + // service lines, comments, malformed providers, and JVM-only names R8 would reject. + val javaTypeNameRegex = Regex("[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*") + } +} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/DependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/DependencyFilter.kt index 93291853a..e14a5ea74 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/DependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/DependencyFilter.kt @@ -10,6 +10,7 @@ import org.gradle.api.artifacts.ResolvedDependency import org.gradle.api.file.FileCollection import org.gradle.api.provider.Provider import org.gradle.api.specs.Spec +import org.gradle.api.tasks.Internal // DependencyFilter is used as Gradle Input in ShadowJar, so it must be Serializable. public interface DependencyFilter : Serializable { @@ -36,8 +37,12 @@ public interface DependencyFilter : Serializable { public abstract class AbstractDependencyFilter( @Transient private val project: Project, - @Transient protected val includeSpecs: MutableList> = mutableListOf(), - @Transient protected val excludeSpecs: MutableList> = mutableListOf(), + @get:Internal + @Transient + protected val includeSpecs: MutableList> = mutableListOf(), + @get:Internal + @Transient + protected val excludeSpecs: MutableList> = mutableListOf(), ) : DependencyFilter { protected abstract fun resolve( 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 new file mode 100644 index 000000000..40eb94641 --- /dev/null +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/MinimizeSpec.kt @@ -0,0 +1,20 @@ +package com.github.jengelman.gradle.plugins.shadow.tasks + +import com.github.jengelman.gradle.plugins.shadow.ShadowDslMarker +import org.gradle.api.Action +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input + +/** Configures how [ShadowJar.minimize] removes unused code from the shadowed JAR. */ +@ShadowDslMarker +public interface MinimizeSpec : DependencyFilter { + /** + * The tool used to minimize the shadowed JAR. + * + * Defaults to [MinimizeTool.DEPENDENCY_ANALYZER]. + */ + @get:Input public val tool: Property + + /** Use R8 to minimize the shadowed JAR and configure its options. */ + 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 new file mode 100644 index 000000000..609ce0852 --- /dev/null +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/MinimizeTool.kt @@ -0,0 +1,10 @@ +package com.github.jengelman.gradle.plugins.shadow.tasks + +/** A tool that can minimize a shadowed JAR. */ +public enum class MinimizeTool { + /** Shadow's default, simple dependency analyzer. */ + DEPENDENCY_ANALYZER, + + /** R8 classfile shrinking. */ + R8, +} 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 new file mode 100644 index 000000000..3a694129a --- /dev/null +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/R8Spec.kt @@ -0,0 +1,44 @@ +package com.github.jengelman.gradle.plugins.shadow.tasks + +import com.github.jengelman.gradle.plugins.shadow.ShadowDslMarker +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity + +/** Minimal R8 configuration for [ShadowJar.minimize]. */ +@ShadowDslMarker +public interface R8Spec { + /** + * Additional R8 command line arguments. + * + * Defaults to `--no-minification`, so R8 shrinks without renaming classes. + */ + @get:Input public val args: ListProperty + + /** Additional ProGuard/R8 keep rules. */ + @get:Input public val keepRules: ListProperty + + /** Files containing additional ProGuard/R8 keep rules. */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + public val keepRuleFiles: ConfigurableFileCollection + + /** + * Enable R8 name obfuscation while keeping Shadow's default no-optimization behavior. + * + * This removes Shadow's default `--no-minification` argument. Optimization remains disabled + * unless [enableOptimization] is also called. + */ + public fun enableObfuscation() + + /** + * Enable R8 optimization while keeping Shadow's default no-obfuscation behavior. + * + * This removes Shadow's generated `-dontoptimize` rule. Name obfuscation remains disabled unless + * [enableObfuscation] is also called. + */ + public fun enableOptimization() +} 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 956d679e4..9d66712f8 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 @@ -1,14 +1,18 @@ package com.github.jengelman.gradle.plugins.shadow.tasks import com.github.jengelman.gradle.plugins.shadow.ShadowBasePlugin +import com.github.jengelman.gradle.plugins.shadow.ShadowBasePlugin.Companion.R8_CONFIGURATION_NAME import com.github.jengelman.gradle.plugins.shadow.ShadowBasePlugin.Companion.shadow 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.MinimizeDependencyFilter +import com.github.jengelman.gradle.plugins.shadow.internal.DefaultMinimizeSpec +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 import com.github.jengelman.gradle.plugins.shadow.internal.fileCollection import com.github.jengelman.gradle.plugins.shadow.internal.getApiJars +import com.github.jengelman.gradle.plugins.shadow.internal.javaPluginExtension +import com.github.jengelman.gradle.plugins.shadow.internal.javaToolchainService import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey import com.github.jengelman.gradle.plugins.shadow.internal.multiReleaseAttributeKey import com.github.jengelman.gradle.plugins.shadow.internal.property @@ -61,11 +65,14 @@ import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.bundling.Jar import org.gradle.api.tasks.bundling.ZipEntryCompression import org.gradle.api.tasks.options.Option +import org.gradle.jvm.toolchain.JavaLauncher import org.gradle.language.base.plugins.LifecycleBasePlugin +import org.gradle.process.ExecOperations @CacheableTask public abstract class ShadowJar : Jar() { - private val dependencyFilterForMinimize = MinimizeDependencyFilter(project) + private val defaultMinimizeSpec = objectFactory.newInstance(DefaultMinimizeSpec::class.java) + private val shadowDependencies = project.provider { // Find shadow configuration here instead of get, as the ShadowJar tasks could be registered // without Shadow plugin applied. @@ -98,16 +105,29 @@ public abstract class ShadowJar : Jar() { ) public open val minimizeJar: Property = objectFactory.property(false) + /** Options for [minimize]. */ + @get:Nested public open val minimizeSpec: MinimizeSpec = defaultMinimizeSpec + @get:Classpath public open val toMinimize: ConfigurableFileCollection = objectFactory.fileCollection { - minimizeJar.map { - if (it) (dependencyFilterForMinimize.resolve(configurations.get()) - apiJars) else emptySet() + minimizeJar.zip(minimizeSpec.tool) { enabled, tool -> + if (!enabled) return@zip emptySet() + when (tool) { + MinimizeTool.DEPENDENCY_ANALYZER, + MinimizeTool.R8 -> minimizeSpec.resolve(configurations.get()) - apiJars + } } } @get:Classpath public open val apiJars: ConfigurableFileCollection = objectFactory.fileCollection { - minimizeJar.map { if (it) project.getApiJars() else emptySet() } + minimizeJar.zip(minimizeSpec.tool) { enabled, tool -> + if (!enabled) return@zip project.provider { emptyList() } + when (tool) { + MinimizeTool.DEPENDENCY_ANALYZER, + MinimizeTool.R8 -> project.getApiJars() + } + } } @get:InputFiles @@ -124,6 +144,22 @@ public abstract class ShadowJar : Jar() { } } + @get:Classpath + public open val r8Classpath: ConfigurableFileCollection = objectFactory.fileCollection { + minimizeJar.zip(minimizeSpec.tool) { enabled, tool -> + if (enabled && tool == MinimizeTool.R8) { + project.configurations.findByName(R8_CONFIGURATION_NAME) ?: project.files() + } else { + emptySet() + } + } + } + + /** Java launcher used when running R8. */ + @get:Nested + @get:Optional + public open val javaLauncher: Property = objectFactory.property() + /** [ResourceTransformer]s to be applied in the shadow steps. */ @get:Nested public open val transformers: SetProperty = objectFactory.setProperty() @@ -282,13 +318,15 @@ public abstract class ShadowJar : Jar() { */ override fun getDuplicatesStrategy(): DuplicatesStrategy = super.getDuplicatesStrategy() + @get:Inject protected abstract val execOperations: ExecOperations + @get:Inject protected abstract val archiveOperations: ArchiveOperations - /** Enable [minimizeJar] and execute the [action] with the [DependencyFilter] for minimize. */ + /** Enable [minimizeJar] 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(dependencyFilterForMinimize) + action.execute(minimizeSpec) } /** Extra dependency operations to be applied in the shadow steps. */ @@ -476,6 +514,7 @@ public abstract class ShadowJar : Jar() { } injectManifestAttributes() super.copy() + minimizeWithR8() } @Suppress("InternalGradleApiUsage") // For creating ShadowCopyAction. @@ -505,7 +544,7 @@ public abstract class ShadowJar : Jar() { } } val unusedClasses = - if (minimizeJar.get()) { + if (minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { val unusedTracker = UnusedTracker( sourceSetsClassesDirs = sourceSetsClassesDirs.files, @@ -628,6 +667,28 @@ 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 + R8Minimizer( + execOperations = execOperations, + logger = logger, + r8Classpath = r8Classpath, + r8Spec = defaultMinimizeSpec.r8Spec, + javaLauncher = javaLauncher, + sourceSetsClassesDirs = sourceSetsClassesDirs.files, + keptDependencyFiles = keptDependencyFiles, + relocators = relocators.get() + packageRelocators, + preserveFileTimestamps = isPreserveFileTimestamps, + reproducibleFileOrder = isReproducibleFileOrder, + zip64 = isZip64, + entryCompression = entryCompression, + metadataCharset = metadataCharset, + ) + .minimize(archiveFile.get().asFile, temporaryDir) + } + public companion object { public const val SHADOW_JAR_TASK_NAME: String = "shadowJar" @@ -659,6 +720,12 @@ public abstract class ShadowJar : Jar() { jarTask.get().manifest, ) + project.plugins.withId("org.gradle.java") { + task.javaLauncher.convention( + project.javaToolchainService.launcherFor(project.javaPluginExtension.toolchain) + ) + } + action.execute(task) } .also { task -> 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 d5bf5e8f4..a9da924d2 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 @@ -152,6 +152,14 @@ class ShadowPropertiesTest { assertThat(failOnDuplicateEntries.get()).isFalse() assertThat(minimizeJar.get()).isFalse() assertThat(mainClass.orNull).isNull() + assertThat(javaLauncher.get().metadata.jvmVersion) + .isEqualTo( + javaToolchainService + .launcherFor(javaPluginExtension.toolchain) + .get() + .metadata + .jvmVersion + ) assertThat(relocationPrefix.get()).isEqualTo(ShadowBasePlugin.SHADOW) assertThat(configurations.get()).containsOnly(runtimeConfiguration)