A Gradle plugin written in Java

Filed under: Gradle, Java, Programming, — Tags: Automation, Gradle plugin — Thomas Sundberg — 2015-03-22

Gradle is a build automation system. You write your build script in Groovy. This is different compared to other build system such as Ant or Maven. They both use xml. Using Groovy instead of xml gives you a lot of benefits. You have an entire programming language at your disposal. This mean that you can easily customize the build behaviour.

If you, however, want to be able to do the same thing in many projects, it may be a good idea to write a plugin that you can refer to from other projects. I will show you, step by step, how to implement a Hello World Gradle plugin.

Setup the Gradle infrastructure

Create a directory and create a file called build.gradle in it. Add the content below to this new file.

build.gradle

group = 'se.thinkcode.gradle'
version = '1.0.0-SNAPSHOT'

apply plugin: 'groovy'
apply plugin: 'maven'

sourceCompatibility = 1.7

dependencies {
    compile gradleApi()

    testCompile 'junit:junit:4.12'
}

repositories {
    mavenCentral()
    mavenLocal()
}

This build script (almost) defines the project.

I start with defining a group and version that should be used when the plugin is referred to later.

This is then followed by applying two plugins, the Groovy and maven plugins. The Groovy plugin will allow me to write Groovy code. The Maven plugin will give me access to an install task as well as the Java stack.

I want to tie this project to Java 1.7, so I specify the source code compatibility to be 1.7.

The next section defines the dependencies for this project. I have two dependencies in this example but it is obviously possible to have many more if you need them.

The magic is contained in the gradleApi() dependency. It will give us the things needed to create a Gradle plugin.

The second dependency is the well known testing framework junit. I use it to test the plugin.

The last thing I do in this example is to define that I want access to the Maven repository as well as my local Maven repository. I use The Central Repository to get access to JUnit. I use the local Maven repository to make this plugin available on my computer for a test project I will show you below.

The last part I want to have control over is the name of the project. It is called artifact id in Maven. It would have been nice to be able to define it in build.gradle, but that is not possible. Instead, the name will default to the directory where this project lives. If I want, I can define it to something else by setting the property rootProject.name in settings.gradle as I do below.

settings.gradle

rootProject.name = 'demoPlugin'

This is the infrastructure needed.

Create the Plugin

The plugin is defined in a class called DemoPlugin. My example looks like this:

src/main/java/se/thinkcode/DemoPlugin.java

package se.thinkcode;

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

public class DemoPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        project.getExtensions().create("demoSetting", DemoPluginExtension.class);
        project.getTasks().create("demo", DemoTask.class);
    }
}

A Gradle plugin is an extension to Gradle. The extension is defined in the apply method. It is defined with a name, demoSetting, and a class DemoPluginExtension that will hold default values. The default values will be used if the plugin user doesn't change any values in the configuration section demoSetting in a build script.

A task is also defined. It is called demo and the executing class is defined as DemoTask.

This is all we need to create a new extension and a new task. The magic that makes this a Gradle plugin is the existence of a property file in META-INF/gradle-plugins. The name of the property file will be the name of the plugin to apply in your build script as I will show later.

Let's create a file called se.thinkcode.demo.plugin.properties. It will connect the name se.thinkcode.demo.plugin to the implementation se.thinkcode.DemoPlugin. This will allow you to apply the plugin se.thinkcode.demo.plugin later.

src/main/resources/META-INF/gradle-plugins/se.thinkcode.demo.plugin.properties

implementation-class=se.thinkcode.DemoPlugin

Now it's time to take a look at the implementation of the two classes used in the DemoPlugin above.

Defining the default value holder

The extension class holds the default values that will be used, if the user of the plugin doesn't change any settings. It look like this:

src/main/java/se/thinkcode/DemoPluginExtension.java

package se.thinkcode;

public class DemoPluginExtension {
    private String message = "Default Greeting from Gradle";

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

It is a plain POJO and very uninteresting in many senses. It is, however, interesting to know that we have now left Gradle land and we are in Java POJO land.

Implementing an actual Task

What about the task then, is it anything special? Our implementation looks like this:

src/main/java/se/thinkcode/DemoTask.java

package se.thinkcode;

import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.TaskAction;

public class DemoTask extends DefaultTask {
    @TaskAction
    public void greet() {
        DemoPluginExtension extension = getProject().getExtensions().findByType(DemoPluginExtension.class);
        if (extension == null) {
            extension = new DemoPluginExtension();
        }

        String message = extension.getMessage();
        HelloWorld helloWorld = new HelloWorld(message);
        System.out.println(helloWorld.greet());
    }
}

It inherits a DefaultTask and implements a method with an annotation that defines that this is the method that will be executed when the user calls the task demo. This is the method that actually does something. In our case, it looks for an extension and delegates to a HelloWorld class that will create a message that the user will be greeted with. If the plugin user hasn't added any extension in build.gradle, an instance of the default extension will be created and the default values will be used.

The HelloWorld class could obviously have been skipped in this small case, but I wanted to show you how you can connect your own implementation into a Gradle plugin.

What about testing?

The plugin testing is done using two unit tests written in Groovy. My custom model, HelloWorld, is done using unit tests written in Java.

Testing the plugin can be done like this:

src/test/groovy/se/thinkcode/DemoPluginTest.groovy

package se.thinkcode

import org.junit.Test
import org.gradle.testfixtures.ProjectBuilder
import org.gradle.api.Project
import static org.junit.Assert.*

class DemoPluginTest {
    @Test
    public void demo_plugin_should_add_task_to_project() {
        Project project = ProjectBuilder.builder().build()
        project.getPlugins().apply 'se.thinkcode.demo.plugin'

        assertTrue(project.tasks.demo instanceof DemoTask)
    }
}

I don't do much more than verifying that applying the plugin se.thinkcode.demo.plugin gives me access to a task called demo that is implemented in an instance of DemoTask.

Testing the DemoTask can be done as below::

src/test/groovy/se/thinkcode/DemoTaskTest.groovy

package se.thinkcode

import org.junit.Test
import org.gradle.testfixtures.ProjectBuilder
import org.gradle.api.Project
import static org.junit.Assert.*

class DemoTaskTest {
    @Test
    public void should_be_able_to_add_task_to_project() {
        Project project = ProjectBuilder.builder().build()
        def task = project.task('demo', type: DemoTask)
        assertTrue(task instanceof DemoTask)
    }
}

I verify that I am able to create a task called demo using a class DemoTask and that I get a task of the type DemoTask back.

These two tests verify the wiring of the plugin. The last thing to do is to verify the actual behaviour. It is done in a regular Java unit test that verifies HelloWorld.

src/test/java/se/thinkcode/HelloWorldTest.java

package se.thinkcode;

import org.junit.Test;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;

public class HelloWorldTest {
    @Test
    public void greet_the_user() {
        String greeting = "Hello World!";
        HelloWorld helloWorld = new HelloWorld(greeting);

        String actualGreeting = helloWorld.greet();

        assertThat(actualGreeting, is(greeting));
    }
}

A unit test that verifies that it is possible to return a value given in the constructor. We might have been able to cope without it, but now you know how to incorporate a unit test in this setup.

The files used to build this Gradle plugin

These are all the files I have used to create this plugin:

plugin
|-- build.gradle
|-- settings.gradle
`-- src
    |-- main
    |   |-- java
    |   |   `-- se
    |   |       `-- thinkcode
    |   |           |-- DemoPlugin.java
    |   |           |-- DemoPluginExtension.java
    |   |           |-- DemoTask.java
    |   |           `-- HelloWorld.java
    |   `-- resources
    |       `-- META-INF
    |           `-- gradle-plugins
    |               `-- se.thinkcode.demo.plugin.properties
    `-- test
        |-- groovy
        |   `-- se
        |       `-- thinkcode
        |           |-- DemoPluginTest.groovy
        |           `-- DemoTaskTest.groovy
        `-- java
            `-- se
                `-- thinkcode
                    `-- HelloWorldTest.java

How do you build it?

With all this implemented, how do you actually go about and build the plugin? The answer is, execute

gradle build install

from a command prompt or similar. This will build the project, execute all the tests and finally install it in the local Maven repository.

Next step is to actually use the plugin.

How do you use it?

The last interesting piece is to use the new plugin. It is done from a Gradle project. An example is this:

build.gradle

apply plugin: 'se.thinkcode.demo.plugin'

demoSetting {
    message = "Hi from an extension"
}

buildscript {
    repositories {
        mavenLocal()
    }
    dependencies {
        classpath 'se.thinkcode.gradle:demoPlugin:1.0.0-SNAPSHOT'
    }
}

There are three important parts here.

Firstly, I apply the plugin. That is making sure that Gradle is aware of something called se.thinkcode.demo.plugin.

Secondly I define my own extension, that is changing the message the user will be greeted with. This will override the default values. If you skip this section, the default values will be used instead.

Thirdly I get hold of the dependency that actually implement the plugin. I define that this build script should use the mavenLocal() repository. This is where the plugin was installed when I executed install earlier. I also define the name and version of the dependency that contains the implementation, classpath 'se.thinkcode.gradle:demoPlugin:1.0.0-SNAPSHOT'.

We recognise the group, name and version from earlier.

Executing gradle tasks show us that there is now a task defined that is called demo.

Executing gradle demo should result in something like this:

:demo
Hi from an extension

BUILD SUCCESSFUL

Remove the demoSetting extension from the consumer project and re-run gradle demo. The result is expected to be similar to this:

:demo
Default Greeting from Gradle

BUILD SUCCESSFUL

That it folks, this is one way to build you own Gradle plugin.

Acknowledgements

I would like to thank Malin Ekholm, Johan helmfrid, Adrian Bolboaca and Alexandru Bolboaca for proof reading.

References



(less...)

Pages

About
Events
Why

Categories

Agile
Automation
BDD
Clean code
Continuous delivery
Continuous deployment
Continuous integration
Cucumber
Culture
Design
DevOps
Executable specification
Git
Gradle
Guice
J2EE
JUnit
Java
Javascript
Kubernetes
Linux
Load testing
Maven
Mockito
New developers
Pair programming
PicoContainer
Presentation
Programming
Public speaking
Quality
React
Recruiting
Requirements
Scala
Selenium
Software craftsmanship
Software development
Spring
TDD
Teaching
Technical debt
Test automation
Tools
Web
Windows
eXtreme Programming

Authors

Thomas Sundberg
Adrian Bolboaca

Archives

Meta

rss RSS