Skip to content

GradleUp/gr8

Repository files navigation

Gr8 Gradle Plugin Portal

Gr8 is Gradle + R8.

Gr8 makes it easy to shadow, shrink, and minimize your jars.

Motivation

Gradle has a very powerful plugin system. Unfortunately, Gradle handling of classpath/Classloaders for plugins has some serious limitations. For an example:

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.

Usage

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

FAQ

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.