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.
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.
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.
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.
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.
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.
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
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.
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.
I would like to thank Malin Ekholm, Johan helmfrid, Adrian Bolboaca and Alexandru Bolboaca for proof reading.