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 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 {
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 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.