Hi all, my name is Hubert A. Klein Ikkink. Not a very common name, right? To make things easier I just picked the first letters of my firstname and surname and came up with haki. So there you have it, now I am also known as Mr. Haki or mrhaki for short. You can read more blog postings at www.mrhaki.com. I am a passionate Groovy and Java developer based in Tilburg, The Netherlands. My goal is to write clean, elegant, user-centered and high quality software. You can find me on Google+ and Twitter. Hubert is a DZone MVB and is not an employee of DZone and has posted 160 posts at DZone. You can read more from them at their website. View Full User Profile

Gradle Goodness: Extending DSL

06.03.2013
| 3192 views |
  • submit to reddit

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 theExtensionContainer, 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 theExtensionContainer. 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 }

                    // 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 followingbuild.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 thebuild/aggregate folder. And in the build/distributions folder we have the file book.zip containing the files.

Code written with Gradle 1.6.



Published at DZone with permission of Hubert Klein Ikkink, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)