How to write a custom Gradle plugin in groovy
by Riley MacDonald, December 28, 2017

While working on an embedded software library for Android I noticed several projects (all in their own repositories) had duplicated code in their build.gradle files. I wanted to encapsulate this duplicated code into a single location that could be applied to any interested project. After some investigation I decided to write my own Gradle plugin which I could apply to each project. This would remove the duplicated code completely. Here’s the complete process I followed to accomplish this.

What’s covered in this post

Initialize a new git repository
Switch to new directory to hold the plugin (see my post on using the -c argument with mkdir):

# Create the directory
$ mkdir -c ~/Documents/myGradlePlugin
 
# Initialize the git repo and add the new remote
~/Documents/myGradlePlugin $ git init
~/Documents/myGradlePlugin $ git remote add origin http://your.git.repo.url

Add the Gradle wrapper
The Gradle wrapper isn’t required but it’s best practice, especially if you’re planning to open source the plugin. You’ll need gradle installed on your machine. If you don’t have it follow these instructions.

~/Documents/myGradlePlugin $ gradle wrapper
Starting a Gradle Daemon (subsequent builds will be faster)
 
BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed

Add the groovy plugin file
The plugin (in this case written in groovy) should live in a directory similar to a Java application src/main/package. Let’s add the plugin file with basic functionality now:

# Create the directory for the plugin file
~/Documents/myGradlePlugin $ mkdir -c src/main/groovy/com/example/pluginName/
 
# Create the groovy plugin file
~/Documents/myGradlePlugin/src/main/groovy/com/example/pluginName $ touch PluginFileName.groovy

Open your preferred IDE and begin writing your plugin. Gradle plugins must implement Plugin and import a few classes. Also be sure the package is declared correctly at the top of the file:

1
2
3
4
5
6
7
8
9
10
11
package com.example.pluginName
 
import org.gradle.api.Plugin
import org.gradle.api.Project
 
class PluginFileName implements Plugin<Project> {
  @Override void apply(Project project) {
    // the meat and potatoes of the plugin
    println "PluginFileName: WORKS!"
  }
}

The apply() method is invoked when the plugin is applied (by the client) using apply plugin:. Within the apply() function you’ll have access to the applying Project. You can do anything groovy you would normally do within a build.gradle file here (add tasks, extension properties, etc) by using a slightly different syntax.

Adding the build.gradle file
Next you need to add and configure a build.gradle in the root of the project. This file declares the location of the groovy plugin file, the version of the plugin, how it’s published and a few more things. The file should look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apply plugin: 'groovy'
apply plugin: 'java-gradle-plugin'
apply plugin: 'maven-publish'
 
group = 'com.example.pluginName'
version = '1.0.0'
 
gradlePlugin {
  plugins {
    demoPlugin {
      id = project.group
      implementationClass = 'com.example.pluginName.PluginFileName'
    }
  }
}
 
dependencies {
  compile localGroovy()
  compile gradleApi()
}
 
publishing {
  publications {
    pluginPublication (MavenPublication) {
      from            components.java
      groupId         project.group
      artifactId      "PluginFileName"
      version         project.version
    }
  }
}

Consuming dependencies in the plugin
When developing a Gradle plugin it’s possible to apply other plugins internally in order to consume or deliver additional functionality. To accomplish this the plugin can be applied to the root build.gradle of the plugin you’re developing. Projects which apply your plugin will not inherit these plugins. You can however apply any plugin to the applying project that is available on the classpath.

Root build.gradle:

apply plugin: 'c'

Your Plugins main implementation Plugin class:

1
2
3
@Override void apply(Project project) {   
  project.plugins.apply('c')
}

Defining META-INF
It’s best practice to define a META-INF resource so Gradle can find the Plugins implementation class. The file should be stored in resources with the same package/path as your Plugins implementation class with properties appended: src/main/resources/META-INF/gradle-plugins/com.example.pluginName.properties. The file should look something like this:

implementation-class:com.example.pluginName.PluginClass

Using Extensions
Creating an extension object allows applying projects to customize the functionality of the plugin. An extension object can be created using the following syntax:

// args: name, class reference
def extension = project.extensions.create("exampleExtension", ExampleExtension)

ExampleExtension (which is located in the same package as the referring class) will look something like this:

1
2
3
4
5
6
7
8
9
10
class ExampleExtension {
  // Stuff
  def doThing = false
 
  // Things
  def collectionOfSettings = [
    'gigaWatts': 1.21,
    'speed'     : 88
  ]
}

The extension values can then be queried by the Plugin:

1
2
3
if (extension.doThing) {
  // ...
}

These extension values can also be referenced or overridden by the applying project (after applying the plugin via build.gradle) using a closure defined by the “name” argument above:

1
2
3
4
5
exampleExtension {
  doThing = true
 
  collectionOfThings.gigaWatts = 0
}

Waiting for the applying project
In many cases plugins need to wait for the applying project to finish being evaluated (apply other plugins for example). This can be accomplished by using a closure such as afterEvaluate:

1
2
3
4
5
project.subprojects { subProject ->
  afterEvaluate {
    // ...
  }
}

Including build.gradle scripts
When initially writing my first Gradle plugin I attempted to migrate existing build.gradle scripts into the plugin for inclusion (using apply from: "something.gradle"). Unfortunately including build.gradle scripts is not preferred. These scripts should instead be converted into Groovy, Java or Kotlin classes.

Publish the plugin to the local maven repository
Everything should be in place for the plugin to be published to your local machine. You can run ./gradlew tasks to get a list of available tasks. Among those tasks should be publishToMavenLocal. This is the task which will deploy the plugin as a jar to your machines local m2 repository (located at ~/.m2/repository).

# Publish the plugin
~/Documents/myGradlePlugin $ ./gradlew publishToMavenLocal
 
BUILD SUCCESSFUL in 2s
10 actionable tasks: 10 executed
 
# Verify the plugin was deployed appropriately
~/Documents/myGradlePlugin $ cd ~/.m2/repository/com/example/pluginName/PluginFileName
~/.m2/repository/com/example/pluginName/PluginFileName $ ls -la
drwxr-xr-x  4 riley.macdonald  wheel  136 Dec 28 15:53 1.0.0
-rw-r--r--  1 riley.macdonald  wheel  339 Dec 28 15:53 maven-metadata-local.xml

I found any errors I made along the way to be descriptive enough to work through, for example:

Execution failed for task ':publishPluginMavenPublicationToMavenLocal'.
> Failed to publish publication 'pluginMaven' to repository 'mavenLocal'
   > Invalid publication 'pluginMaven': groupId cannot be empty.

If you’ve made it this far the plugin is ready to be consumed by a project! Note: If you want to deploy your plugin to an artifact repository try using the com.gradle.plugin-publish plugin!

Consuming the plugin in another project
Navigate to the root build.gradle in the project you want to apply the plugin to. Only the following is required:

1
2
3
4
5
6
7
8
9
buildscript {
  repositories {
    mavenLocal() // or artifact repository url
  }
  dependencies {
    classpath 'com.example.pluginName:PluginFileName:1.0.0'
  }
}
apply plugin: 'com.example.pluginName'

If your project builds you’re all set!

Adding Gradle Tasks
In most cases you’ll want to add a new task to the applying project so it can perform it. This is accomplished defining the task name, class, group and description (this example assumes the applying project is adding tasks to its subprojects):

1
2
3
4
5
6
7
8
subProject.task("exampleTask", type: DefaultTask) {
  group = "Example Plugin"
  description = "A description of what the tasks does"
 
  dependsOn anotherTask
 
  // ...
}

You can verify this functionality by executing ~/.gradlew tasks from the root of the applying project. The task should appear in the returned list. You should be able to execute the task or make another task depend on this new custom task. In this example I used DefaultTask. It’s possible to create or use a subclass of DefaultTask to further encapsulate plugin logic.

Summary
Hopefully you’ve found this process as simple as I did. After working through a few errors I was able to apply my plugin to encapsulate my duplicated code. Be sure to rename pluginName, PluginFileName and the packages to your liking. It’s worth noting that a pluginName closure will also be added to your project where you can configure the plugin on a project level see the gradle docs regarding this.

Open the comment form

Leave a comment:

Comments will be reviewed before they are posted.

User Comments:

Be the first to leave a comment on this post!