July 14, 2017 Thierry Lacour   Techblog

How to write, test and publish a custom Gradle plugin

Kickstart your plugin development with a documented example

A practical guide to writing, testing and publishing custom Gradle plugins, complete with a demo repository for a running start!

Intro

Plugins are a great way of distributing Gradle functionality, whether they be public plugins offering tool integrations, like the Artifactory plugin, or corporate plugins sharing common tasks internally, like those we help our customers develop.

Unfortunately, I found the documentation and examples on writing a custom Gradle plugin to be somewhat scattered. I decided to write a very basic Gradle plugin as a bootstrap, intended to give me, and now you, a running start when developing a custom Gradle plugin. It contains Hello, world! tasks, tests and the means of publishing. I wrote this blog in tandem, as a form of documentation. Enjoy!

The plugin repository

You can find the Gradle plugin bootstrap on GitHub at Praqma/gradle-plugin-bootstrap. It’s a fully functional Gradle plugin. Feel free to use it as a starting point, just clone it and edit it to suit your needs.

The bits and pieces

I’ll briefly go over the individual pieces that makes up a Gradle plugin. You’ll find everything mentioned in the repository, but I’ll cover them in greater detail here than I could in the comments.

The entry point

Example found in:
src/main/groovy/com/praqma/demo/DemoPlugin.groovy

This is the heart of the plugin. You’ll find the apply method here, as it implements org.gradle.api.Plugin. Gradle calls this method when it applies your plugin to a project. This is where you can add your tasks, extensions, and so forth.

In the example plugin, you’ll find I moved such things to a separate module. This is only to prevent the main plugin class from exploding in size. Think of it as a recommendation when you’re expecting to add many different tasks to your plugin.

Registering the entry point

Example found in:
build.gradle

Before Gradle can apply your plugin, you’ll have to tell it where it can find it. Do this by applying and configuring the java-gradle-plugin Gradle plugin in the build.gradle file. Apply the plugin through the plugins closure at the top of your build.gradle file:

plugins {
    id 'java-gradle-plugin'
}

Configure your plugin in the gradlePlugin closure, provided by the java-gradle-plugin. In the plugins closure, you can add an entry for each plugin in the project. We keep it simple and only have one plugin, so we add an arbitrarily named closure under it to configure our plugin and set two properties:

  • The id, is the identifier of your plugin, which is used to apply it your plugin:

    plugins {
        id: 'com.praqma.demo'
    }
    
  • The implementationClass points to the entry point’s class, so Gradle can find it. Which, in this case, is com.praqma.demo.DemoPlugin.

The complete configuration looks something like this:

gradlePlugin {
    plugins {
        demoPlugin {
            id = 'com.praqma.demo'
            implementationClass = 'com.praqma.demo.DemoPlugin'
        }
    }
}

Adding tasks

Example found in:
src/main/groovy/com/praqma/demo/greeting/GreetingModule.groovy

I added two tasks here, one showcasing the use of properties from project extensions, the other using project properties. Adding tasks to the plugin differs very little from adding tasks on a regular Gradle project, just call the task method on the project.

Again, splitting tasks into modules is by no means required, it’s just a habit of mine to prevent the plugin class from devolving into a two thousand line abomination.

Adding task types

Example found in:
src/main/groovy/com/praqma/demo/greeting/GreetingTask.groovy
src/main/groovy/com/praqma/demo/greeting/GreetingModule.groovy

Creating a custom task type allows plugin users to base their custom tasks off yours, much like how we base our tasks off the Zip and Copy tasks. To distribute these, simply add have these task classes be a part of your plugin. Just having them there will allow users to define tasks of that type by using its fully qualified name. For example:

import com.praqma.demo.greeting.GreetingTask

task myGreetingTask(type:GreetingTask) {
    message = "Howdy"
}

To prevent your users from having to import the task, add the task class to the project’s ExtraPropertiesExtension when applying the plugin. With this clever trick, your users can access your task by the name you specify here, without having to import it.

project.ext.GreetingTask = com.praqma.demo.greeting.GreetingTask

Adding extensions

Example found in:
src/main/groovy/com/praqma/demo/greeting/GreetingExtension.groovy
src/main/groovy/com/praqma/demo/greeting/GreetingModule.groovy

Extensions expose properties that plugin users can set. They’re great for allowing users to configure values you rely on in the tasks. In the demo plugin, I allow users to configure the greeting used in the helloWorld task by setting the greeting.message property in their build.gradle file.

To add your own extensions to your plugin, create a simple class containing some properties, and add it as a project extension in your plugin’s apply method:

project.extensions.create('greeting', GreetingExtension)

These properties are accessible from the project’s extensions, for example:

project.extensions.<extensionName>.<propertyName>

Testing the plugin

I’ll not dive into unit testing the Gradle plugin, as it’s no different from unit testing in a regular Groovy project, and there are many great resources available on that topic (See the resource section below). I will, however, cover functional tests and how to test your plugin with a local publish.

Functional tests

Example found in:
src/test/groovy/com/praqma/demo/greeting/GreetingModuleTest.groovy

Writing functional tests for a Gradle plugin is child’s play, thanks to the GradleRunner. Using JUnit’s @Rule and @Before annotations makes it trivial to, for each test, set up a temporary directory containing a build.gradle file which applies your plugin. The GradleRunner adds your plugin to the temporary project’s classpath, so it can actually apply your plugin under test. Running your tasks through the GradleRunner grants you access to the build result and text output. This, combined with access to the temporary directory, allows you to check and assert everything ran as expected.

Publishing and testing on your local machine

It is possible to publish your plugin to your machine’s local maven repository. This allows you to apply your local test publication to a Gradle project on your machine, so you can test your changes without publishing them to a shared repository. It requires a tad of configuration, but it’s fairly straightforward.

Publishing to the local maven repository

Apply the maven-publish plugin, which contains the publishToMavenLocal task. This task publishes all your defined publications to your local repository, so let’s set up a publication for our plugin:

publishing {
    publications {
        pluginPublication (MavenPublication) {
            from    components.java
            groupId    project.group
            artifactId    "demo"
            version    project.version
        }
    }
}

To publish the plugin to your local repository in its current state, call gradle publishToMavenLocal.

Applying a locally published plugin

To apply a locally published plugin to your project, you’ll have to add your local maven repository as a trusted repository in your Gradle project. Luckily, Gradle allows you to do this by calling mavenLocal() in the repository closure in your build.gradle file. For example:

buildscript {
    repositories {
        mavenLocal()
    }
    dependencies {
        classpath "com.praqma:demo:1.0.0"
    }
}

apply plugin: 'com.praqma.demo.DemoPlugin'

Now you can run your Gradle builds, and your project will use your local distribution of the plugin.

Distributing the plugin

I’ll discuss publishing both to the public Gradle plugin repository, as well as to an arbitrary Artifactory server. The project contains the necessary configuration for both. Just delete the one you won’t end up using, and tweak the one you will be.

Via the Gradle plugin repository

Configuration

Example found in:
build.gradle

Apply the com.gradle.plugin-publish plugin to your plugin project. Configure it through the pluginBundle closure, where you supply useful information to about your plugin, such as the repository location, a description and relevant tags.

pluginBundle {
    website = 'https://github.com/Praqma/gradle-plugin-bootstrap'
    vcsUrl = 'scm:git@github.com:Praqma/gradle-plugin-bootstrap.git'
    tags = ['demo', 'example', 'quickstart']

    plugins {
        demoPlugin {
            id = 'com.praqma.demo.DemoPlugin'
            displayName = 'Gradle Multi Git plugin'
            description = 'Demo plugin to use as a starting point for custom plugin development'
        }
    }
}

Publishing

The com.gradle.plugin-publish plugin supplies the necessary tasks to publish your plugin to the plugin portal.

Run gradle login in your plugin repository and follow instructions to authorize the machine to publish your plugin.

Run gradle publishPlugins to actually publish your plugin to the portal.

Applying

Once the plugin has been published, it can be applied to other gradle projects through the plugins closure, using the plugin’s id and version. For example:

plugins {
    id 'com.praqma.demo.DemoPlugin', version '1.0.0'
}

Via Artifactory

Configuration

Example found in:
build.gradle

Apply and configure the com.jfrog.artifactory plugin. In the artifactory closure, you’ll configure everything the plugin needs to publish the plugin to your Artifactory server. For example:

artifactory {
    contextUrl = "http://devops.acmeindustries.com/artifactory"
    publish {
        repository {
            repoKey     = 'plugins-release'
            username    = 'joe'
            password    = 's3cr3t-p4ss'
            maven       = true
        }
        defaults{
            publications("pluginPublication")    // Publication defined below
        }
    }
}

We still need to define what we’re publishing, so apply the maven-publish plugin, allowing you to declare a MavenPublication that contains our plugin. For example:

publishing {
    publications {
        pluginPublication (MavenPublication) {
            from    components.java
            groupId    project.group
            artifactId    "demo"
            version    project.version
        }
    }
}

Publishing

Publish your plugin by calling gradle artifactoryPublish.

Applying

To apply the plugin, you’ll have to fetch it first. Register your Artifactory server as a repository, and add your plugin as a dependency. Then apply it like you would any other plugin. Here’s an example configuration from a project applying our plugin:

buildscript {
    repositories {
        maven {
            url = "http://devops.acmeindustries.com/artifactory/plugins-release"
            credentials {
                username = "joe"
                password = "s3cr3t-p4ss"
            }
        }
    }
    dependencies {
        classpath "com.praqma:demo:1.0.0"
    }
}

apply plugin: "com.praqma.demo.DemoPlugin" // Apply with the plugin id

Closing comments

This should cover the basics of getting a custom Gradle plugin up and running. I hope this helps you writing and distributing your own cool Gradle plugins. Thoughts, questions and suggestions are very welcome, just leave them in the comment section below!

References