For beginners to Gradle implementing plugins can look like a daunting task that includes many considerations and deep knowledge: organizing and structuring plugin logic, testing and debugging plugin code as well as publishing the plugin artifact to a repository for consumption.

In this section, you will learn how to properly design Gradle plugins based on established practices and apply them to your own projects. This section assumes you have:

  • Basic understanding of software engineering practices

  • Knowledge of Gradle fundamentals like project organization, task creation and configuration as well as the Gradle build lifecycle

Architecture

Reusable logic should be written as binary plugin

The Gradle User Manual differentiates two types of plugins: script plugins and binary plugins. Script plugins are basically just plain old Gradle build scripts with a different name. While script plugins have their place for organizing build logic in a Gradle project, it’s hard to keep them well-maintained, they are hard to test and you can’t define new reusable types in them.

Binary plugins should be used whenever logic needs to be reused or shared across independent projects. They allow for properly structuring code into classes and packages, are cachable, can follow a versioning scheme to enable smooth upgrade procedures and are easily testable.

Consider the impact on performance

As a developer of Gradle plugins you have full freedom in defining and organizing code. Any logic imaginable can be implemented. When designing Gradle plugins always be aware of the impact on the end user. Seemingly simple logic can have a considerable impact on the execution performance of a build. That’s especially the case when code of a plugin is executed during the configuration phase of the build lifecycle e.g. resolving dependencies by iterating over them, making HTTP calls or writing to files. The section on optimizing Gradle build performance will give you additional code examples, pitfalls and recommendations.

As you write plugin code ask yourself whether the code shouldn’t rather be run during the execution phase. If you suspect issues with your plugin code, try creating a build scan to identify bottlenecks. The Gradle profiler can help with automating build scan generation and gathering more low-level information.

Convention over configuration

Convention over configuration is a software engineering paradigm that allows a tool or framework to make an attempt at decreasing the number of decisions the user has to make without losing its flexibility. What does that mean for Gradle plugins? Gradle plugins can provide users with sensible defaults and standards (conventions) in a certain context. Let’s take the Java plugin as an example.

  • It defines the directory src/main/java as the default source directory for compilation.

  • The output directory for compiled source code and other artifacts (like the JAR file) is build.

As long as the user of the plugin does not prefer to use other conventions, no additional configuration is needed in the consuming build script. It simply works out-of-the-box. However, if the user prefers other standards, then the default conventions can be reconfigured. You get the best of both worlds.

In practice you will find that most users are comfortable with the default conventions until there’s a good reason to change them e.g. if you have to work with a legacy project. When writing your own plugins, make sure that you pick sensible defaults. You can find out if you did pick sensible conventions for your plugin if you see that the majority of plugin consumers don’t have to reconfigure them.

Let’s have a look at an example for conventions introduced by a plugin. The plugin retrieves information from a server by making HTTP calls. The default URL used by the plugin is configured to point to a server within an organization developing the plugin: https://www.myorg.com/server. A good way to make the default URL configurable is to introduce an extension. An extension exposes a custom DSL for capturing user input that influences the runtime behavior. The following example shows such a custom DSL for the discussed example:

Example 1. build.gradle
build.gradle.kts
plugins {
   id("org.myorg.server")
}

server {
    url = "http://localhost:8080/server"
}
build.gradle
plugins {
   id 'org.myorg.server'
}

server {
    url = 'http://localhost:8080/server'
}

As you can see, the user only declares the "what" - the server the plugin should reach out to. The actual inner workings - the "how" - is completely hidden from the end user.

Capabilities vs. conventions

The functionality brought in by a plugin can be extremely powerful but also very opinionated. That’s especially the case if a plugin predefines tasks and conventions that a project inherits automatically when applying it. Sometimes the reality that you - as plugin developer - choose for your users might simply look different than expected. For that very reason you need to make a plugin as flexible and configurable as possible.

One way to provide these quality criteria is to separate capabilities from conventions. In practice that means separating general-purpose functionality from pre-configured, opinionated functionality. Let’s have a look at an example to explain this seemingly abstract concept. There are two Gradle core plugins that demonstrate the concept perfectly: the Java Base plugin and the Java plugin.

  • The Java Base plugin just provided un-opinionated functionality and general purpose concepts. For example it formalized the concept of a SourceSet and introduces dependency management configurations. However, it doesn’t actually create tasks you’d use as a Java developer on a regular basis nor does it create instances of source set.

  • The Java plugin applies the Java Base plugin internally and inherits all its functionality. On top, it creates source set instances like main and test, creates tasks well-known to Java developers like classes, jar or javadoc. It also establishes a lifecycle between those tasks that make sense for the domain.

The bottom line is that we separated capabilities from conventions. If a user decides that they doesn’t like the tasks created or doesn’t want to reconfigure a lot of the conventions because that’s not how the project structure looks like, then they can just fall back to applying the Java Base plugin and take matters into their own hands.

You should consider using the same technique when designing your own plugins. You can develop both plugins within the same project and ship their compiled classes and identifiers with the same binary artifact. The following code example shows how to apply a plugin from another one, so-called plugin composition:

MyBasePlugin.java
import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class MyBasePlugin implements Plugin<Project> {
    public void apply(Project project) {
        // define capabilities
    }
}
MyPlugin.java
import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class MyPlugin implements Plugin<Project> {
    public void apply(Project project) {
        project.getPlugins().apply(MyBasePlugin.class);

        // define conventions
    }
}

For inspiration, here are two open-source plugins that apply the concept:

Technologies

Prefer using a statically-typed language to implement a plugin

Gradle doesn’t take a stance on the programming language you should choose for implementing a plugin. It’s a developer’s choice as long as the plugin binary can be executed on the JVM.

It is recommended to use a statically-typed language like Java or Kotlin for implementing plugins to decrease the likelihood of binary incompatibilities. Should you decide on using Groovy for your plugin implementation then it is a good choice to use the annotation @groovy.transform.CompileStatic.

The recommendation to use a statically-typed language is independent from the language choice for writing tests for your plugin code. The use of dynamic Groovy and (its very capable testing and mocking framework) Spock is a very viable and common option.

Restricting the plugin implementation to Gradle’s public API

To be able to build a Gradle plugin you’ll need to tell your project to use a compile-time dependency on the Gradle API. Your build script would usually contain the following declaration:

build.gradle.kts
dependencies {
    implementation(gradleApi())
}
build.gradle
dependencies {
    implementation gradleApi()
}

It’s important to understand that this dependency includes the full Gradle runtime. For historical reasons, public and internal Gradle API have not been separated yet.

To ensure the best backward and forward compatibility with other Gradle versions you should only use the public API. In most cases it will support the use case you are trying to support with your plugin. Keep in mind that internal APIs are subject to change and can easily break your plugin from one Gradle version to another. Please open an issue on GitHub if you are looking for a public API that is currently internal-only.

How do you know if a class is part of the public API? If you can find the class referenced in the DSL guide or the Javadocs then you can safely assume that it is public. In the future, we are planning to clearly separate public from internal API which will allow end users to declare the relevant dependency in the build script.

Minimizing the use of external libraries

As application developers we have become quite accustomed to the use of external libraries to avoid having to write fundamental functionality. You likely do not want to go without your beloved Guava or HttpClient library anymore. Keep in mind that some of the libraries might pull in a huge graph of transitive dependencies when declared through Gradle’s dependency management system. The dependency report does not render dependencies declared for the classpath configuration of the build script, effectively the classpath of the declared plugins and their transitive dependencies. However, you can call the help task buildEnvironment to render the full dependency graph. To demonstrate the functionality let’s assume the following build script:

build.gradle.kts
plugins {
    id("org.asciidoctor.jvm.convert") version "3.2.0"
}
build.gradle
plugins {
    id 'org.asciidoctor.jvm.convert' version '3.2.0'
}

The output of the task clearly indicates the classpath of the classpath configuration:

$ gradle buildEnvironment

> Task :buildEnvironment

------------------------------------------------------------
Root project 'external-libraries'
------------------------------------------------------------

classpath
\--- org.asciidoctor.jvm.convert:org.asciidoctor.jvm.convert.gradle.plugin:3.2.0
     \--- org.asciidoctor:asciidoctor-gradle-jvm:3.2.0
          +--- org.ysb33r.gradle:grolifant:0.16.1
          |    \--- org.tukaani:xz:1.6
          \--- org.asciidoctor:asciidoctor-gradle-base:3.2.0
               \--- org.ysb33r.gradle:grolifant:0.16.1 (*)

(*) - Indicates repeated occurrences of a transitive dependency subtree. Gradle expands transitive dependency subtrees only once per project; repeat occurrences only display the root of the subtree, followed by this annotation.

A web-based, searchable dependency report is available by adding the --scan option.

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

It’s important to understand that a Gradle plugin does not run in its own, isolated classloader. In turn those dependencies might conflict with other versions of the same library being resolved from other plugins and might lead to unexpected runtime behavior. When writing Gradle plugins consider if you really need a specific library or if you could just implement a simple method yourself.

For logic that is executed as part of task execution, use the Worker API that allows you to isolate libraries.