Introduction to writing Gradle plugins

Introduction to Gradle plugins

Last Updated on September 7, 2021

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'
}

Info: plugins which are bundled with Gradle, such as the java plugin don’t require a version

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.

Save your fingers: if you don’t want to type out the code but do want to try this example, grab the code now from this GitHub repository.

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.

Use of abstract: the FileDiffExtension class itself and its methods are abstract. Gradle fills in the blanks for us, automatically creating concrete methods with default RegularyFileProperty values returned. The same approach is also used in the following task class.

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

Debugging plugin tests: to debug plugin code executed during a test run, you need to add withDebug(true) to the GradleRunner.create() builder. This means that if you start your test in debug mode, in an IDE such as IntelliJ IDEA for example, you’ll be able to stop at a breakpoint which you set within your plugin code.

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.

Plugin descriptor: the java-gradle-plugin generates a plugin descriptor file in the META-INF directory of the built jar file. This file defines the way that consumers of the plugin will reference it. e.g. given our build script configuration, the plugin can be included in a project by the id com.tomgregory.file-diff.

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.xml

We need to update settings.xml 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')
}

Info: here we’re comparing a Gradle built jar file to one built with Maven, to make sure they’re similar.

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.

If you liked this article, be sure to check out other articles here on my blog on the subject of build automation.

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
If you prefer to learn in video format, check out this accompanying video to this post on the Tom Gregory Tech YouTube channel.

Get going with Gradle course
Gradle icon

Want to learn more about Gradle?
Check out the full selection of Gradle tutorials.

Introduction to writing Gradle plugins

10 thoughts on “Introduction to writing Gradle plugins

  1. I created my own custom plugin, which just writes a sentence into an output file, including an input parameter value. I used your tutorial as a reference, but I found a few issues.

    1. My plugin was not being found in the test, because I was creating the build.gradle file indicating the plugin as `apply plugin: ‘com.cristianlm.test-plugin’`. After I changed it to `plugins { id: ‘com.cristianlm.test-plugin’} it started finding it.
    2. The GradleRunner builder was throwing NullPointerException, because of the way I declared the input variable in the task class. At the end, it worked when I initialized it in the task class constructor like so `testProperty = getProject().getObjects().property(String.class);`.
    3. I couldn’t get the output file content via `result.output`, that only gives me gradle standard output. In order to get the content of the file I did `String output = new File(testProjectDir.root.toString() + “/build/test-plugin-output.txt”).text`. I don’t know if it’s the best way, but at least the test passes now 🙂

    My plugin: https://github.com/zeitgeist2018/gradle-plugin/blob/master/src/test/groovy/com/cristianlm/gradle_plugin/TestPluginTest.groovy#L40

    1. Hi Cristian. Thanks for the comment. Nice work on creating a plugin, but I can’t access the URL you’ve provided (getting a 404). Is your repo public? If you make it public I can take a look to see what the differences are in your repo. I’ve double checked the file-diff-plugin in GitHub and it’s in working order.

    1. Hi. I took a look and agree with the existing comment that input = file(A.txt) doesn’t look correct. There should be quotes. Assuming this doesn’t work, can you please rerun with the `–stacktrace` option so we can be sure which line is failing?

  2. HI, Tom really amazing article!! Quick question, how do you debug this groovy code on intellij ? Is necessary special configuration for this?

    1. Hi Alexandre. Thanks for the nice comment 🙂

      Regarding debugging the plugins, it depends on what exactly you’re trying to do:

      • if you want to debug a plugin from one of its tests (e.g. FileDiffPluginFunctionalTest described in the article), then you need to add withDebug(true) to the GradleRunner.create() statement. Then you can run the test in debug mode in IntelliJ.
      • if you want to debug a plugin which is running in a build of a project, you can run the build with -Dorg.gradle.debug=true then attach a remote debugger from IntelliJ in your plugin project. Gradle have a very helpful article on this topic.

      Hope it helps!

  3. Could you pls explain in some detail utility of Property Interface used in the above sample along with object factory and what does objects.property(file) does?
    Thanks

    1. Hi D B. Thanks for the comment. I’ve updated the article to reflect more recent Gradle changes (version 7.2).

      The extension and task classes are now abstract, with abstract methods for the file properties. Gradle automatically provides implementations for these. It means we don’t need to use objects any more.

      Regarding the Property interface itself, there are a few reasons it’s “best practice” in plugins. A big one is that you can refer to the property before its value has been set. This is useful when linking an output of one task to an input of another. I have an article on why you’d want to do this Gradle task inputs and outputs.

      Hope it helps.

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to top

Get the newsletter

Found this article helpful? Subscribe for monthly updates.

✅ All of my latest articles for the month
✅ Access to video tutorials
✅ Exclusive tips not found on my website