Gr8 makes it easy to shadow, shrink, and minimize your jars.
Gradle has a very powerful plugin system. Unfortunately, Gradle handling of classpath/Classloaders for plugins has some serious limitations. For an example:
- Gradle will always force its bundled version of the Kotlin stdlib in the classpath. This makes it impossible to use Kotlin 1.5 APIs with Gradle 7.1 for an example because Gradle 7.1 uses Kotlin 1.4.
buildSrc
dependencies leak in the classpath. This causes very weird bugs during execution because a conflicting dependency might be forced in the classpath. This happens espectially with popular libraries such asokio
orantlr
.
By shadowing and relocating the plugin dependencies, it is possible to ship a plugin and all its dependencies without having to worry about what Gradle is going to put on the classpath.
As a nice bonus, it makes plugins standalone so consumers of your plugin don't need to declare additional repositories. The gr8
plugin for an example, uses R8
from the Google repo although it makes it available directly from the preconfigured Gradle plugin portal.
To make a shadowed Gradle plugin:
plugins {
id("org.jetbrains.kotlin.jvm").version("$kotlinVersion")
id("java-gradle-plugin")
id("com.gradleup.gr8").version("$gr8Version")
}
// Configuration dependencies that will be shadowed
val shadeConfiguration = configurations.create("shade")
dependencies {
// Using a redistributed version of Gradle instead of `gradleApi` provides more flexibility
// See https://github.com/gradle/gradle/issues/1835
compileOnly("dev.gradleplugins:gradle-api:7.1.1")
// Also set kotlin.stdlib.default.dependency=false in gradle.properties to avoid the
// plugin to add it to the "api" configuration
add("shade", "org.jetbrains.kotlin:kotlin-stdlib")
add("shade", "com.squareup.okhttp3:okhttp:4.9.0")
}
gr8 {
val shadowedJar = create("gr8") {
proguardFile("rules.pro")
configuration("shade")
}
// Replace the regular jar with the shadowed one in the publication
replaceOutgoingJar(shadowedJar)
// Removes the gradleApi dependency that java-gradle-plugin automatically adds
// Optional, but recommended when using a compileOnly dependency
// on dev.gradleplugins:gradle-api
removeGradleApiFromApi()
}
// Make the shadowed dependencies available during compilation/tests
configurations.named("compileOnly").configure {
extendsFrom(shadeConfiguration)
}
configurations.named("testImplementation").configure {
extendsFrom(shadeConfiguration)
}
Then customize your proguard rules. The below is the bare minimum. If you're using reflection, you might need more rules
# The Gradle API jar isn't added to the classpath, ignore the missing symbols
-ignorewarnings
# Allow to make some classes public so that we can repackage them without breaking package-private members
-allowaccessmodification
# Keep kotlin metadata so that the Kotlin compiler knows about top level functions and other things
-keep class kotlin.Metadata { *; }
# Keep FunctionX because they are used in the public API of Gradle/AGP/KGP
-keep class kotlin.jvm.functions.** { *; }
# Keep Unit for kts compatibility, functions in a Gradle extension returning a relocated Unit won't work
-keep class kotlin.Unit
# We need to keep type arguments (Signature) for Gradle to be able to instantiate abstract models like `Property`
-keepattributes Signature,Exceptions,*Annotation*,InnerClasses,PermittedSubclasses,EnclosingMethod,Deprecated,SourceFile,LineNumberTable
# Keep your public API so that it's callable from scripts
-keep class com.example.** { *; }
-repackageclasses com.example.relocated
Could I use the Shadow plugin instead?
The Gradle Shadow Plugin has been helping plugin authors for years and is a very stable solution. Unfortunately, it doesn't allow very granular configuration and might relocate constant strings that shouldn't be. In practice, any plugin that tries to read the "kotlin"
extension is subject to having its behaviour changed:
project.extensions.getByName("kotlin")
}
will be transformed to:
project.extensions.getByName("com.relocated.kotlin")
For plugins that generate source code and contain a lot of package names, this might be even more unpredictable and require weird workarounds.
By using R8
and proguard rules, Gr8
makes relocation more predictable and configurable.
Can I override the system classes used by R8
, like target JDK 11 with my plugin while building on Java 17?
If you set your Java toolchain then R8 will also use the same toolchain to discover system classes:
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(11))
}
}
If for some reason you want to override this explicitly:
gr8 {
val shadowedJar = create("gr8") {
proguardFile("rules.pro")
configuration("shade")
systemClassesToolchain {
languageVersion.set(JavaLanguageVersion.of(11))
}
}
}
Could I use the Gradle Worker API instead?
Yes, the Gradle Worker API ensures proper plugin isolation. It only works for task actions and requires some setup so shadowing/relocating is a more universal solution.
Are there any drawbacks?
Yes. Because every plugin now relocates its own version of kotlin-stdlib
, okio
and other dependendancies, it means more work for the Classloaders and more Metaspace being used. There's a risk that builds will use more memory although it hasn't been a big issue so far.
What does this bring compared to using R8 directly in a JavaExec
task?
Using R8 directly from a JavaExec
works as well. GR8 adds a few extra things like the ability to filter out some files in the dependencies. This is useful for an example to remove the dependencies rules that are otherwise automatically imported by R8.