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 theresultFile
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 afilediff
configuration block. - registering a task of type
FileDiffTask
. Inside the task, we’re simply mapping the properties from theFileDiffExtension
to theFileDiffTask
.
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 abuild.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
Stop reading Gradle articles like these
This article helps you fix a specific problem, but it doesn't teach you the Gradle fundamentals you need to actually help your team succeed.
Instead, follow a step-by-step process that makes getting started with Gradle easy.