Gradle Logo

Introduction

Following my previous post on Groovy, I've decided to write a bit about one of my new favorite build tools, Gradle! Gradle is a build tool geared towards languages that run on the JVM, providing first class support for Java, Groovy, and Scala. There are also plugins for C/C++, however I have not taken the time to evaluate Gradle for such use. At its core, Gradle is a Domain Specific Language based in Groovy. Executing our builds on Groovy gives us access to all the power the language has to offer allowing for greater expressiveness and (hopefully) increased readability, re-usability, etc. However, with this additional power it easier to go amok if kept unchecked. I strongly recommend maintaining the viewpoint that build scripts should not be treated as code. Do not overcomplicate the build with unnecessary complexity - an unwieldy build process can do more than add bugs to a feature, it can fracture the foundation the rest of your application rests on. Even if bugs are not introduced directly to the finished product, overly complicated build files have a tendency to harm developer productivity on a larger scope. Builds that 'mysteriously fail' can disrupt things like continuous integration and harm momentum for everyone on the team.

The user guide on Gradle's website is just enough to wet an appetite, but leaves much of its magic hidden behind the scenes. Pure theorists advocating DSLs would claim familiarizing yourself with the patterns and features provided by the framework is adequate. However, this viewpoint is only suitable for those who have the luxury of starting a project from scratch and strictly adhere to pre-defined conventions. Unfortunately, this is hardly the case in the real world. Most projects have some baggage: perhaps a previous build script written in ant, maven, make, or even no build process defined at all. DSLs when understood fully can be a wonderful productivity gain in the long run, but lean too much on the magic of "it just works!" without questioning why, will come to bite you in the ass sooner or later. I hope to demystify some of the things I found confusing at first here for you.

Installation

I recommend installing Gradle via the Groovy Version Manager.


$ curl -s get.gvmtool.net | bash
$ gvm install groovy
$ gvm install gradle

Gradle can then be executed with gradle <task names>
By default Gradle looks for a file named build.gradle.

Key Concepts

In a nutshell, Gradle takes the step by step proceduralism of Ant, strips away all the gouge-my-eyes-out xml, replaces it with the power and flexibility of Groovy, hijacks the dependency management of Maven and Ivy, and tops it off with an a la carte approach to convention over configuration through plugins. You are never mandated to use these conventions. Even if you do decide to use them, you are free to modify the conventions to your heart's desire, without Gradle throwing a temper tantrum like Maven.

Gradle gives you a clean slate by default. Aside from a few standard tasks that assist in helping to construct and debug the build file itself, there are no conventions mandated or standard build steps you must follow.

There are two phases to every build:

  1. Configuration Phase
  2. Execution Phase

During the configuration phase, tasks are announced to the build system. These tasks get added to a Directed Acyclic Graph (DAG). Tasks frequently spell out dependencies on each other. When executing a task from Gradle, any dependencies that task has are guaranteed to execute before it. After the configuration phase, the graph is checked for cycles, if any are found the build fails as this would lead to an infinite loop.

During the execution phase, the tasks specified on the command line as arguments to gradle are set as goals. The task graph is scanned for all ancestors of the goals and executed in an order to maintain dependent relationships, e.g. 'compile' should come before 'jar' should come before 'run'.

Closures Everywhere

In most cases, Gradle shies away from requiring usage Groovy's advanced syntactic features and prefers to seamlessly construct objects and methods for you. There are two exceptions: closures and automatic accessors are made use of aggressively. A Brief recap - closures are blocks of code* that maintain access to the scope (access to variables) in which they are defined. In Groovy, accessor methods are created for properties of classes for you. Property access is automatically routed through these accessors as well.

* A block of code between braces '{}' when the braces themselves are "unbound" i.e. do not immediately follow class definitions, looping keywords, or conditionals

For example, a Toaster class with a timer property could provide getTimer() and setTimer(). Using these methods looks deceptively like direct property access (but it's not!)


class Toaster {
  def timer
}

Toaster toaster = new Toaster()
toaster.timer = new Timer() //implicitly calls burner.setTimer(new Timer())
//...
Timer timer = toaster.timer //implicitly calls burner.getTimer()

Closures are inescapable, in fact the body of your build.gradle file executes within a closure provided to the root Project object itself!

Creating Tasks

To create a new task use the task keyword. This will create both a task object and a task method with the same name. The task method accepts a ConfigurationClosure which gets executed within the task's scope, giving you direct access to all of its properties within that closure.

One of the more confusing aspects of Gradle is the difference between creating a task using << or not


task sayHello << {
	println "Hello"
}
Creates a task named sayHello, and then appends a block of code to be executed to the task during the execution phase

task sayHello {
	inputs.file 'foo.txt'
	outputs.dir 'myoutput'
	println "Hello"
}

Creates a task named hello, modifies its inputs and outputs properties (more on that later) and prints out "Hello" during the configuration phase. The configuration phase will always occur, therefore you will always see "Hello" get printed out, which is more than likely not what we want!

It is also common to use Groovy's map based constructors in task definition:


task sayHello(dependsOn: makeFriends) << {
	println "Hello"
}

All tasks have a type which should extend from DefaultTask. When type is left unspecified, by default task creation is of type DefaultTask. Other task types are built in such as JavaCompile, Copy, Jar, JavaExec, Exec, and many more. It is also possible to define your own task types.

DefaultTask allows you to specify inputs and outputs. These standard properties are used by gradle to determine if a task can be skipped. Like make, if the timestamp on the outputs is newer than the timestamp on the inputs, then Gradle assumes no changes have been made and thus the output would be identical. This can be a huge time saver for many projects!

Many properties on tasks are objects that accept configuration closures themselves. This is why I like to say Gradle plays "Russian dolls" with its closures - nesting them within each other sometimes many layers deep.


task sayHello(dependsOn: makeFriends, type: Copy) {
	from 'foo.txt'
	into 'somedir'
	
	doLast {
		println "Copied the files"
	}
}

The Project Object

Several key objects are created for us by the Gradle environment to track the state of a build in execution. Gradle makes many of these objects available to us both explicitly and implicitly. Key amongst them is the Project object. When accessing a method or property anywhere in the build script a chain of scopes is inspected for the matching declaration. Starting within the current scope and recursively winding up the scope chain to the Project object. Much of the basic functionality provided by specialized task types are provided in a procedural manner by the Project object itself. For example, we don't always have to make a new task to copy files around like so:


task copyFiles(type: Copy) {
	from 'myfile.txt'
	into 'mydir'
}

We could instead choose to perform a copy with the Project's copy method via closure scope unwinding


task copyAll(dependsOn: sayHello) {
    inputs.files 'file1', 'file2'
    outputs.dir 'mydir'
    outputs.dir 'myotherdir'

    doLast {
        copy {
            from 'file1'
            into 'mydir'
        }   
        copy {
            from 'file2'
            into 'myotherdir'
        }   
    }   
}

It is easy to overlook the doLast declaration with tasks that only copy files, especially when the task is essential to your build (and therefore always gets run), but the doLast declaration necessary if you wish to have incremental build support. Without doLast the copying will always be performed during the configuration phase!

I strongly recommend reading up on the official JavaDocs for the Project Object. The JavaDocs have also proven themselves to be invaluable in finding which properties are available on the various task types. Pay special attention to which properties are actually not properties at all, but instead methods returning configurable domain objects. For instance this does not work: inputs = files('file1', 'file2') because inputs is defined as a method on DefaultTask returning a configurable TaskInputs domain object that in turn has a files method on it (also not to be confused with the files method on the root Project object).

Plugins

Gradle provides many convention based plugins at our disposal. A plugin is simply a set of standard tasks, "convention" properties, and rules (rules are an advanced feature not discussed here). Of the most common are the java and eclipse plugins. The java plugin adds a standard build chain to your configuration. If you have code generation to perform, I strongly recommend placing it before the javaCompile task. The eclipse plugin can generate an eclipse project for you including the dependencies laid out in your build and other properties specific to eclipse like project natures. Plugins for other IDEs exist as well, but I have not made use of them.

For the java plugin, pay special attention to the dependency graph and think carefully about where your tasks should be inserted. Remember dependencies of the tasks defined by the plugin are just as configurable as the ones you create. This is especially handy if you have code that needs to be generated. I've found generated code can either get it's own sourceSet or at least get its own folder and attached as a srcDir to the main source set. javaCompile put your generation tasks close to the javaCompile task (immediately before or after), leaving your build process clean and simple to flow into the plugin's standard lifecycle. Of course your mileage may vary and more drastic changes to the lifecycle may be called for in other circumstances!

Tying It Together

If you understand the basics of tasks, their different types, closures, and the Properties object. You should be well set to understand plugins and their usage within a Gradle build. I found the user guide's documentation on plugins to be well written and self contained, so I wont duplicate it here. Hop over to the Gradle User guide and read up on the java plugin if you haven't already.

In this example we have the main and generated source sets. When the java plugin lifecycle compiles the code we ensure that the main source set gets compiled first through compileGeneratedJava.dependsOn(generate) which in turn depends on compileJava. I also threw in a task to run the generated code, this could have been achieved more easily with the application plugin, but I wanted to demonstrate a bit more about task types. The final jar includes only the generated code, again for demonstration purposes.


apply plugin: 'java'

sourceSets {
	generated
}

task generate(type: JavaExec, dependsOn: compileJava) {
	main = 'com.toastedbits.gradletest.generator.Generator'
	classpath = sourceSets.main.runtimeClasspath
}

compileGeneratedJava.dependsOn(generate)

task runGenerated(type: JavaExec, dependsOn: compileGeneratedJava) {
	main = 'com.toastedbits.gradletest.generated.Generated'
	classpath = sourceSets.generated.runtimeClasspath
}

jar {
	from sourceSets.generated.output
	excludes = ['com/toastedbits/gradletest/generator/**']
	manifest {
		attributes 'Main-Class':'com.toastedbits.gradletest.generated.Generated'
	}
}

The source can be downloaded from my GitHub page.

Conclusion

I left out a couple bits about Gradle concerning dependency management and artifact publishing. Perhaps I'll have more on that in a future article. For now, this article should have covered all the low level concepts that allow you to get started without feeling like your Gradle build is an artificial intelligent life form.

When first learning Gradle (and Groovy) it is easy to get lost between the dynamism and infrastructure. So many things happen seemingly by magic, but once demystified a bit these tools can simplify your build immensely. I was able to reduce one of the ant build files at work down to a quarter of its original size! Videos of presentations on Gradle that I've seen demonstrate more drastic reduction when migrating away from Maven. My advice, avoid maven and ant all together. Let Gradle rock your build!