Loading...

November 18, 2009

Gradle Goodness: Creating a Gradle Plugin

Caution: Applies to Gradle 0.8

A plugin for Gradle provides reusable build logic packaged as a unit. For example the Java plugin is already provided with Gradle and adds several tasks for working with Java code to a Gradle project. In this post we write our own plugin to add a custom task and a new method to a project. We create a new task mkdirs to create the source directories defined by plugins for a project. The code to create the directories is inspired by the Gradle Cookbook. The task itself looks for a property basePackageDir and apppends it to the source directory names. Finally we add the method makeDirs() to the Gradle project.

How do we achieve this? First we start with a new project and create a buildSrc directory. The buildSrc directory contains the plugin code. Gradle will automatically compile the classes and add it to the project's classpath. To create a new Gradle plugin we define a class that implements the Plugin interface. The interface only contains one method, use(), so that is simple. In the use() method we have access to the project object and plugins, so we can do a lot of things. For our sample we only have to create a new task and add it to the project:

// File: buildSrc/src/main/groovy/com/mrhaki/gradle/MakeDirsPlugin.groovy
package com.mrhaki.gradle

import org.gradle.api.*
import org.gradle.api.plugins.*

class MakeDirsPlugin implements Plugin {

    void use(final Project project, ProjectPluginsContainer plugins) {
        // Closure to create a directory.
        def createDirs = {
            it.mkdirs()
        }

        // Create new task 'mkdirs' and add it to the project.
        project.task('mkdirs') << {
            if (plugins.hasPlugin('java')) {
                project.sourceSets.all.java.srcDirs*.each createDirs
                project.sourceSets.all.resources.srcDirs*.each createDirs
            }
            
            if (plugins.hasPlugin('groovy')) {
                project.sourceSets.all.groovy.srcDirs*.each createDirs
            }
            
            if (plugins.hasPlugin('scala')) {
                project.sourceSets.all.scala.srcDirs*.each createDirs
            }
            
            if (plugins.hasPlugin('war')) {
                createDirs project.webAppDir
            }
        }
        // Assign a description to the task.
        project.tasks.mkdirs.description = "Create source directories."
    }
    
}

And that is all we need to do (for now). We create a build.gradle file to use our plugin:

// File: build.gradle
usePlugin(com.mrhaki.gradle.MakeDirsPlugin)

When we ask Gradle for all available tasks we get our mkdirs task in the results:

$ gradle -t -q
--------------------------------------------------------------
Root Project
--------------------------------------------------------------
:mkdirs - Create source directories.

We can stop now, but we also wanted to use a basePackageDir property and add the method makeDirs() to our project from the plugin. To add properties and methods by a plugin Gradle supports conventions. We create a new convention object with properties and methods and assign it to the project plugins map. Now all properties and methods from the convention object can directly be invoked and accessed from the build script. So we start by creating a convention object, which is just a plain GroovyBean class:

// File: buildSrc/src/main/groovy/com/mrhaki/gradle/MakeDirsPluginConvention.groovy
package com.mrhaki.gradle

import org.gradle.api.*

class MakeDirsPluginConvention {
    String basePackageDir = ''  // Property to hold base package directory name.
    Project project
    
    MakeDirsPluginConvention(Project project) {
        this.project = project 
    }
    
    // Method to create a new directory from the root of the project.
    // (Inspired by mkdir in Java plugin)
    File makeDirs(String dirName) {
        if (!dirName) {
            throw new InvalidUserDataException('You must specify a directory name')
        }
        if (!project) {
            throw new InvalidUserDataException('You must specify a project')
        }
        def newDir = project.file(dirName)
        newDir.mkdirs()
        newDir
    }
}

We change the code for the plugin to use the convention class:

// File: buildSrc/src/main/groovy/com/mrhaki/gradle/MakeDirsPlugin.groovy
package com.mrhaki.gradle

import org.gradle.api.*
import org.gradle.api.plugins.*

class MakeDirsPlugin implements Plugin {

    void use(final Project project, ProjectPluginsContainer plugins) {
        // Assign new convention object to project.
        // makedirs is a key for the plugins map. We can choose the name
        // ourselves here.
        project.convention.plugins.makedirs = new MakeDirsPluginConvention(project)
    
        // Closure to create a directory.
        def createDirs = {
            // Use basePackageDir property.
            def newDir = new File(it, project.convention.plugins.makedirs.basePackageDir)
            newDir.mkdirs()
        }

        // Create new task 'mkdirs' and add it to the project.
        project.task('mkdirs') << {
            if (plugins.hasPlugin('java')) {
                project.sourceSets.all.java.srcDirs*.each createDirs
                project.sourceSets.all.resources.srcDirs*.each createDirs
            }
            
            if (plugins.hasPlugin('groovy')) {
                project.sourceSets.all.groovy.srcDirs*.each createDirs
            }
            
            if (plugins.hasPlugin('scala')) {
                project.sourceSets.all.scala.srcDirs*.each createDirs
            }
            
            if (plugins.hasPlugin('war')) {
                createDirs project.webAppDir
            }
        }
        // Assign a description to the task.
        project.tasks.mkdirs.description = "Create source directories."
    }
    
}

We change our build script to show the usage of our plugin:

// File: build.gradle
usePlugin(com.mrhaki.gradle.MakeDirsPlugin)
usePlugin('war')  // Extra plugin so the task mkdirs from the plugin can do something.

basePackageDir = 'com/mrhaki'  // Property from plugin convention object.
makeDirs 'src/main/config' // Method from plugin convention object.

We run the build with $ gradle mkdirs and all source directories are created including src/main/config. For example we now have the directory src/main/java/com/mrhaki in our project.

Because we created our plugin in the buildSrc directory the plugin is only available for this project. In a next blog post we see how we can reuse our plugin in other projects. We will package the code into a JAR file and upload to a repository. Other projects can define a dependency for our plugin in the repository and use it in the build process.