Most Gradle builds use at least a few plugins to add extra functionality, so why not make use of this powerful mechanism by writing your own Gradle plugins? In this article, you’ll discover how to encapsulate your own specific Gradle build logic into a plugin, in a simple and reusable way.

UPDATED in September 2021 to use Gradle 7.2, Spock 3, and JUnit 5.

Gradle plugins overview

So what exactly is a Gradle plugin? It’s an add-on for your build, which you can apply using the Gradle plugin DSL (domain specific language), like this:

plugins {
    id 'org.springframework.boot' version '2.5.4'
}

When you apply a plugin it’s going to do one of two things for you (or both):

1) Expose one or more tasks to allow you to run some specific action

An example of this type of task is the Maven publish plugin, which exposes tasks such as publishToMavenLocal to push an artifact to your local Maven repository

Often these tasks will be automatically configured into the Gradle task graph through a dependsOn relationship. This means you won’t need to run them explicitly yourself.

2) Perform some behaviour automatically when the plugin is applied

This behaviour may include configuring certain elements of the build. For example, the Java plugin configures the main and test source sets.

Configuration

Additionally, many plugins allow you to configure them in specific ways.

For example, with the Java plugin you can configure tests to be included or excluded by tag, like this:

test {
    useJUnitPlatform {
        includeTags 'fast'
        excludeTags 'slow'
    }
}

We’re going to look at how to create similar types of behaviour in the following example plugin. You’ll then be able to apply the same concepts to your own project’s specific requirements.

A plugin example

To understand how to write plugins in Gradle, we’ll build up an example plugin to do a diff (or comparison) between the file size of two files. The functionality will include:

  • ability to configure in the build script the two files to be diffed e.g.
fileDiff {
    file1 = file('testFile1.txt')
    file2 = file('testFile2.txt')
}
  • exposing a task to do the diff itself e.g. ./gradlew fileDiff
  • writing the diff result to the console and also to a target file e.g. testFile1.txt was the largest file at 43776237 bytes.
  • making use of Gradle’s incremental builds by only rerunning the task when one of the two input files changes
  • an automated integration test for the plugin, as we always test our code right? 😉

Plugin design

So what classes do we need in a plugin, and how do they relate to our specific requirements for this example?

  • plugin class - this defines what happens when the plugin gets applied. This may be to create some tasks that can be executed in the build, or configure the build in some way. In the case of the file-diff plugin, we’ll add a task to do the diff
  • extension class - this is a data object, representing the configuration that can be set for the plugin in build.gradle. For the file-diff plugin it will contain the two input files whose size will be compared.
  • task class - this class defines an action to be performed. An instance of it is normally created and bound to a task name made available to the user to run in a Gradle command. For our plugin, we’ll add a task to do the diff, and it will be available to the user by running ./gradlew fileDiff.

Publishing Gradle plugins

When you write a plugin in Gradle, if you want to share it between multiple projects then the best option is to create the plugin in a separate repository. This way, you can publish it to a private or public Maven repository, and then apply it in whatever project you need:

Of course you can also publish to Maven local (on your personal development machine), which we’ll be using for this example.

1) A new project

We’re going to start off by creating a new project for the plugin, imaginatively called file-diff-plugin. Let’s initialise the project with Git and Gradle:

mkdir file-diff-plugin
cd file-diff-plugin
git init
gradle init

You’ll also want to add .gradle/, .idea/, and build/ to a .gitignore file, so they’re not committed.

2) Initial build script

Our build.gradle file will initially contain the following:

plugins {
    id 'groovy'
    id 'java-gradle-plugin'
}

group = 'com.tomgregory'
version = '0.0.1-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'org.spockframework:spock-core:2.0-groovy-3.0'
}

gradlePlugin {
    plugins {
        fileDiff {
            id = 'com.tomgregory.file-diff'
            implementationClass = 'com.tomgregory.plugins.filediff.FileDiffPlugin'
        }
    }
}

test {
    useJUnitPlatform()
}

This simple build includes:

  • Groovy plugin - the language we’ll be using to write our plugin (you can also use Java)
  • java-gradle-plugin - sets up various configurations you need to do plugin development (read docs)
  • Spock testing framework - a Groovy testing framework, making it very easy to write give-when-then style tests
  • Gradle plugin configuration - sets an id of com.tomgregory.file-diff which we can use to reference the plugin in tests and in other projects once it’s published
  • Gradle test configuration - the version of Spock we configured uses the latest JUnit 5, which we enable in Gradle with useJUnitPlatform()

3) Extension class

The extension class represents the configuration for the plugin. Create the directory structure src/main/groovy/com/tomgregory/plugins/filediff where we’ll be putting all our classes.

Let’s create FileDiffExtension.groovy which will contain the following code:

package com.tomgregory.plugins.filediff

import org.gradle.api.file.RegularFileProperty

abstract class FileDiffExtension {
    abstract RegularFileProperty getFile1()
    abstract RegularFileProperty getFile2()
}

We have two properties representing the input files. Notice these are of the RegularFileProperty type which extends Property. In Gradle Property objects are a way to refer to a value before it’s actually set. This is useful with plugins and tasks since the property value may be referenced before its actual value has been configured in the build script.

4) Task class

Let’s create the task class, FileDiffTask.groovy, which will perform the diff itself:

package com.tomgregory.plugins.filediff

import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction

abstract class FileDiffTask extends DefaultTask {
    @InputFile
    abstract RegularFileProperty getFile1()
    @InputFile
    abstract RegularFileProperty getFile2()
    @OutputFile
    abstract RegularFileProperty getResultFile()

    FileDiffTask() {
        resultFile.convention(project.layout.buildDirectory.file('diff-result.txt'))
    }

    @TaskAction
    def diff() {
        String diffResult
        if (size(file1) == size(file2)) {
            diffResult = "Files have the same size at ${file1.get().asFile.size()} bytes."
        } else {
            File largestFile = size(file1) > size(file2) ? file1.get().asFile: file2.get().asFile
            diffResult = "${largestFile.toString()} was the largest file at ${largestFile.size()} bytes."
        }

        resultFile.get().asFile.write diffResult

        println "File written to $resultFile"
        println diffResult
    }

    private static long size(RegularFileProperty regularFileProperty) {
        return regularFileProperty.get().asFile.size()
    }
}

Note here we have:

  • two input file properties defined. The plugin will be responsible for mapping the properties from the extension to those on the task.
  • an @InputFile annotation on the two input file properties, which means that Gradle will watch for changes to these files, and only re-run the task if they do change
  • an output file property defined, which the plugin will write the diff result to.
  • an @OutputFile annotation on the result file property. Gradle watches this file, and only re-runs the task if it changes.
  • a method annotated with @TaskAction, signifying that this method is the one to run when the task is executed. It uses the two file inputs to work out which is the larger, and generated some text output. The result is then written to the resultFile and standard output.

5) Plugin class

The plugin class, FileDiffPlugin.groovy, brings together the previous two classes:

package com.tomgregory.plugins.filediff

import org.gradle.api.Plugin
import org.gradle.api.Project

class FileDiffPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        project.extensions.create('fileDiff', FileDiffExtension)

        project.tasks.register('fileDiff', FileDiffTask) {
            file1 = project.fileDiff.file1
            file2 = project.fileDiff.file2
        }
    }
}

Notice here that we’re:

  • overriding the apply method. This method gets called when the plugin gets applied in the build.gradle file.
  • creating an extension called filediff. This means that in the build.gradle in which this plugin is applied, we’ll be able to define the configuration in a filediff configuration block.
  • registering a task of type FileDiffTask. Inside the task, we’re simply mapping the properties from the FileDiffExtension to the FileDiffTask.

6) Adding a plugin integration test

Gradle provides a nice mechanism for writing an integration test for our Gradle plugin. It allows us to:

  • dynamically generate a build.gradle file which references our plugin
  • run a specific task against that build
  • verify that the task ran successfully

Create a file in src/test/groovy/com/tomgregory/plugins/filediff called FileDiffPluginFunctionalTest.groovy with the following contents:

package com.tomgregory.plugins.filediff

import org.gradle.testkit.runner.GradleRunner
import spock.lang.Specification
import spock.lang.TempDir

import static org.gradle.testkit.runner.TaskOutcome.SUCCESS

class FileDiffPluginFunctionalTest extends Specification {
    @TempDir
    File testProjectDir
    File buildFile

    def setup() {
        buildFile = new File(testProjectDir, 'build.gradle')
        buildFile << """
            plugins {
                id 'com.tomgregory.file-diff'
            }
        """
    }

    def "can  diff 2 files of same length"() {
        given:
        File testFile1 = new File(testProjectDir, 'testFile1.txt')
        testFile1.createNewFile()
        File testFile2 = new File(testProjectDir, 'testFile2.txt')
        testFile2.createNewFile()

        buildFile << """
            fileDiff {
                file1 = file('${testFile1.getName()}')
                file2 = file('${testFile2.getName()}')
            }
        """

        when:
        def result = GradleRunner.create()
                .withProjectDir(testProjectDir)
                .withArguments('fileDiff')
                .withPluginClasspath()
                .build()

        then:
        result.output.contains("Files have the same size")
        result.task(":fileDiff").outcome == SUCCESS
    }
}

And note here:

  • we’re using @TempDir to create a temporary folder for our build
  • in the setup (equivalent to JUnit’s @Before method) we’re creating a build.gradle file that applies our plugin
  • in the test’s given section we’re creating two empty temporary files. We also append to the build.gradle the configuration defining the two files.
  • in the test’s when section we’re using GradleRunner in a library called gradle-test-kit to run the fileDiff task
  • finally, in the then section of the test we’re asserting that the build output contains the expected text and that the task completed successfully

We can now run the test by executing./gradlew test and you’ll see that it passes! ✅

Test summary in build/reports/tests/test/index.html

7) Deploy to Maven local repository

To deploy to our ~/.m2/repository local Maven repository we’ll need to add an additional plugin to the build.gradle:

plugins {
    ....
    id 'maven-publish'
}

This will help get our jar file in the right Maven format, and provide the publishToMavenLocal task.

Now just run ./gradlew publishToMavenLocal and you can see the jar has been deployed to the expected location, along with all the associated metadata:

If you’re looking closely, you may also notice an additional artifact gets added to our Maven local repository:

The file-diff artifact is simply a reference to the file-diff-plugin artifact. This means we can reference the plugin in the Gradle plugin DSL as simply com.tomgregory.file-diff, as we’ll see in the following section.

Using a published Gradle plugin in another project

Let’s use our brand new plugin in another project to compare two files!

If you saw the previous article about performing a Maven to Gradle migration, you’ll remember we had a requirement to compare the size of two generated jar files. We can now make use of our plugin here to make things even easier.

Clone this GitHub repository or use your own project if you like.

Update settings.gradle

We need to update settings.gradle to tell Gradle to look in the Maven local repository for plugins. Do this by adding this block of code to the very beginning of the file:

pluginManagement {
    repositories {
        mavenLocal()
        gradlePluginPortal()
    }
}

Now in build.gradle apply the plugin with:

plugins {
    ...
    id 'com.tomgregory.file-diff' version '0.0.1-SNAPSHOT'
}

And configure it with this code block:

fileDiff {
    file1 = file('build/libs/maven-to-gradle-0.0.1-SNAPSHOT.jar')
    file2 = file('target/maven-to-gradle-0.0.1-SNAPSHOT.jar')
}

Now run the task with ./gradlew fileDiff and you’ll see a diff-result.txt file gets created in the build directory:

C:\workspace\maven-to-gradle\target\maven-to-gradle-0.0.1-SNAPSHOT.jar was the largest file at 43776237 bytes.

Final thoughts

Writing simple plugins can be a surprisingly straightforward process. It’s not only be a great way to add new custom functionality to your build, but also to clean up existing build files into simple reusable components.

Resources

GITHUB Clone this GitHub repository for access to all the code in this article

DOCS See these great Gradle docs for more info on plugins

Video

Check out the accompanying video from the Tom Gregory Tech YouTube channel.