Loading...

May 29, 2013

Gradle Goodness: Extending DSL

Gradle already has a powerful DSL, but Gradle wouldn't be Gradle if we couldn't extend the DSL ourselves. Maybe we have our own naming conventions in our company or we have a special problem domain we want to express in a Gradle build script. We can use the ExtensionContainer, available via project.extensions, to add new concepts to our build scripts. In the Standardizing your enterprise build environment webinar by Luke Daley some examples are shown on how to extend the DSL. Also in the samples folder of the Gradle distribution are examples on how to create a custom DSL.

Let's first create a simple DSL extension. We first define a new class CommonDependencies with methods to define dependencies in a Java project. We want to use these methods with descriptive names in our build scripts. To add the class we use the create() method of the ExtensionContainer. The first argument is a name that needs to be unique within the build. The name can be used together with a configuration block in the script to invoke methods on the class we pass as the second argument. Finally we can pass constructor arguments for the class as last arguments of the create() method.

/**
 * Class for DSL extension. A default repository is added
 * to the project. The use<name>() methods add 
 * dependencies to the project.
 */
class CommonDependencies {
    /** Reference to project, so we can set dependencies/repositories */
    final Project project

    CommonDependencies(final Project project) {
        this.project = project

        // Set mavenCentral() repository for project.
        project.repositories {
            mavenCentral()
        }
    }

    /**
     * Define Spock for testCompile dependency 
     * @param version Version of Spock dependency with default 0.7-groovy-2.0
     */
    void useSpock(final String version = '0.7-groovy-2.0') {
        project.dependencies {
            testCompile "org.spockframework:spock-core:$version"
        }
    }

    /**
     * Define Spring for compile dependency 
     * @param version Version of Spring dependency with default 3.2.3.RELEASE
     */
    void useSpring(final String version = '3.2.3.RELEASE') {
        project.dependencies {
            compile "org.springframework:spring-core:$version"
        }
    }

}

// Add DSL extension 'commonDependencies' with class CommonDependencies 
// passing project as constructor argument.
project.extensions.create('commonDependencies', CommonDependencies, project)

apply plugin: 'java'

// Use new DSL extension. Notice we can use configuration closures just
// like we are used to with other Gradle DSL methods.
commonDependencies {
    useSpock()
    useSpring '3.1.4.RELEASE'
}

// We can still use the Java plugin dependencies configuration.
dependencies {
    compile 'joda-time:joda-time:2.1'
}

We can invoke the dependencies task from the command-line and we see all dependencies are resolved correctly:

$ gradle dependencies
...
compile - Compile classpath for source set 'main'.
+--- org.springframework:spring-core:3.1.4.RELEASE
|    +--- org.springframework:spring-asm:3.1.4.RELEASE
|    \--- commons-logging:commons-logging:1.1.1
\--- joda-time:joda-time:2.1
...
testCompile - Compile classpath for source set 'test'.
+--- org.springframework:spring-core:3.1.4.RELEASE
|    +--- org.springframework:spring-asm:3.1.4.RELEASE
|    \--- commons-logging:commons-logging:1.1.1
+--- joda-time:joda-time:2.1
\--- org.spockframework:spock-core:0.7-groovy-2.0
     +--- junit:junit-dep:4.10
     |    \--- org.hamcrest:hamcrest-core:1.1 -> 1.3
     +--- org.codehaus.groovy:groovy-all:2.0.5
     \--- org.hamcrest:hamcrest-core:1.3
...

We can also use a plugin to extend the Gradle DSL. In the plugin code we use the same project.extensions.create() method so it is more transparent for the user. We only have to apply the plugin to a project and we can use the extra DSL methods in the build script. Let's create a simple plugin that will extend the DSL with the concept of a book and chapters. The following build script shows what we can do after we have applied the plugin:

apply plugin: 'book'

book {
    title 'Groovy Goodness Notebook'
    chapter project(':chapter1')
    chapter project(':chapter2')
}

To achieve this we first create the following directory structure with files:

+ sample
  + buildSrc
     + src/main/groovy/com/mrhaki/gradle
       + Book.groovy
       + BookPlugin.groovy
     + src/main/resources/META-INF/gradle-plugins
       + book.properties
  + book
    + build.gradle
  + chapter1/src/html
    + index.html
  + chapter2/src/html
    + index.html
  + settings.gradle

The Book class will be added as DSL extension. The class has a method to set the title property and a method to add chapters which are Gradle project objects.

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

import org.gradle.api.*

class Book {
    String title
    List<Project> chapters = []

    void title(final String title) {
        this.title = title
    } 

    void chapter(final Project chapter) {
        chapters << chapter
    }
}

Next we create the BookPlugin class. The plugin will add the Book class as DSL extension. But we also create a task aggregate that will visit each chapter that is defined and then copies the content from the scr/html folder in the chapter project to the aggregate folder in the build folder. Finally we add a dist task that will simply archive the contents of the aggregated files.

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

import org.gradle.api.*
import org.gradle.api.tasks.*
import org.gradle.api.tasks.bundling.Zip

class BookPlugin implements Plugin<Project> {
    void apply(Project project) {
        project.configure(project) {
            apply plugin: 'base'

            def book = project.extensions.create 'book', Book

            afterEvaluate {
                // Create task in afterEvaluate, so chapter projects
                // are resolved, otherwise chapters is empty.
                tasks.create(name: 'aggregate') {

                    // Skip task if no chapters are defined.
                    onlyIf { !book.chapters.empty }

                    doFirst {
                        // Copy content in src/html of 'book' directory.
                        copy {
                            from file('src/html')
                            into file("${buildDir}/aggregate")
                        }

                        // Copy content in src/html of chapter directories.
                        book.chapters.each { chapterProject ->
                            copy {
                                from chapterProject.file('src/html')
                                into file("${buildDir}/aggregate/${chapterProject.name}")
                            }
                        }
                    }
                }
            }

            tasks.create(name: 'dist', dependsOn: 'aggregate', type: Zip) {
                from file("${buildDir}/aggregate")
            }
        }        
    }
}

We create the file book.properties to tell Gradle about our new plugin:

# File: buildSrc/src/main/resources/META-INF/gradle-plugins/book.properties
implementation-class=com.mrhaki.gradle.BookPlugin

Our plugin is finished, so we can add a book project and some chapter projects. In the settings.gradle file we define an inclusion for these directories:

// File: settings.gradle
include 'chapter1'
include 'chapter2'
include 'book'

In the chapter directories we can add some sample content in the src/html directories. And in the book folder we create the following build.gradle file:

// File: book/build.gradle
apply plugin: 'book'

book {
    title 'Groovy Goodness Notebook'
    chapter project(':chapter1')
    chapter project(':chapter2')
}

Now from the book folder we can run the aggregate and dist tasks. The end result is that all files from the chapter src/html folder are in the build/aggregate folder. And in the build/distributions folder we have the file book.zip containing the files.

Code written with Gradle 1.6.