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
- Initializing a new Git repo
- Installing gradle wrapper
- Add the groovy plugin file
- Add the
build.gradle
file - Consuming dependencies in the plugin
- Defining META-INF
- Using Extensions
- Listening for afterEvaluate
- Including build.gradle scripts
- Publish the plugin to the local machine for testing
- Consume the plugin from another project
- Adding Gradle Tasks
- Summary
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.