Gradle Build Tool

Categories: Programming, Java

Overview

This article gives an introduction to the Java build-tool Gradle, primarily for developers already familiar with Java and Maven.

I prefer to understand the underlying principles of the tools I use, rather than treat them as a “black box” and learn the available features “by rote”. In this case, it means looking at Gradle as a Groovy library, Gradle build-scripts as programs which define and manipulate a runtime project model, and build tasks as closures attached to the project model.

The information below is a somewhat rough collection of notes, and are certainly not a tutorial on Gradle. On the other hand, these are the things I wish I had known before using Gradle for the first time, and after grasping these principles the standard Gradle tutorials made far more sense to me. Hopefully these notes are useful for you too.

I’ve been a Maven user for many years, and my first encounter with Gradle was a moderately steep learning curve. However I came to like Gradle a lot - it is well worth learning.

Maven, Ant, Gradle

Maven is an application which processes a declarative “project model”. Well, not quite, as there are hooks for triggering standard or custom processing logic at various “phases”, but the majority is declarative. The declarative project model allows other tools to at least partially “understand” a project model without executing maven.

Ant is a procedural programming language (in an inconvenient XML syntax) with a set of standard libraries that provide standard functionality needed for a build-tool; “programs” in the ant language can therefore build applications when executed.

Groovy is a general-purpose object-oriented programming language with a nice syntax which runs in a Java Virtual Machine. The syntax has been carefully designed so that, when using appropriately-designed libraries, a program can look almost declarative (ie like a “configuration file) - ie supports defining a domain-specific-language (DSL). Gradle is a set of libraries for the Groovy language which provide features useful for compiling Java applications - and the libraries take advantage of Groovy’s DSL support. The result is that a Gradle file is actually a Groovy program which compiles (and otherwise processes artifacts of) some other Java program while looking more like a declarative “build/configuration file” than a program.

The combination of a readable build-specification with the power of a full programming language is very pleasant to use. Maven provides readable declarations (poms), but is difficult to extend (requires writing custom plugins).

Maven is build around the concept of a “project model” which defines dependencies and processing steps. Gradle also has such a structure - but one that is built by code. Of course the Maven one is also “built by code” - it is just that the code is hidden within the maven program rather than the build-script actually being the code.

A maven commandline usually consists of a target phase or list of phases. Tasks (plugin goals) are associated with phases, thus a maven build results in a graph of tasks to be executed.

A gradle commandline directly specifies a target task (function) or list of tasks - a more natural concept for programmers. Tasks can have pre-tasks and post-tasks associated with them, and thus also results in a graph of tasks being executed. Gradle tasks are simply Groovy closures, so defining custom tasks is natural and easy and can be done from within the build-file itself.

Groovy DSL-related Syntax

One of the interesting things about the Groovy language is that it has been specifically designed to create readable DSLs A function can be separated from its parameter-list by a space rather than having to use a pair of parentheses. The parameter-list may be a single value, or a comma-separated list. In addition, a function-call followed by a function-call are chained together; the second function is assumed to be a method of the object returned by the first call. This means:

  • foo p is equivalent to foo(p)
  • foo p,q is equivalent to foo(p,q)
  • foo p1 bar p2, p3 is equivalent to foo(p1).bar(p2, p3)

Note that a linefeed stops the sequence (unless within a closure), ie

   foo p1 bar p2   // foo(p1).bar(p2);

is very different from:

   foo p1
   bar p2  
   // foo(p1); bar(p2);

Groovy supports “named parameters” (allowing other params to take default values), meaning:

  • foo p:1 bar name:"me" is equivalent to foo(p:1).bar(name:"me") ie method foo has parameter p set to 1, etc.

Another feature allows a call to a method which takes a closure as its last parameter to specify the closure inside or outside the parentheses, supporting:

  • foo {...} // equivalent to foo(someClosure) where the closure is the part in braces
  • foo("p1", {....}) // method foo is passed two parameters: String and closure`
  • foo("p1") {...} // as above, but somewhat cleaner layout, particularly for multiline closures`
  • foo "p1", {...}

but not

  • foo "p1" {...} // items in a parameter-list must be comma-separated

Groovy also supports basing “operator overloading”; specific symbols such as “«” are mapped to method-names (“leftshift” in this case).

  • task compile << {...} is eqivalent to task(compile).leftshift(closure)
  • task test(...) << {...} is equivalent to task(test(...)).leftshift(closure)

And yet another feature is that any closure can be assigned a “delegate object” before it is executed; any calls to functions in the closure become calls to methods on the delegate object. In other words, the caller of the closure can “intercept” any top-level function calls made in the closure by calling closure.delegate=someObj before the closure is executed. A framework that is passed closures (eg groovy) can therefore provide a “context-sensitive” library of functions that the passed-in closures can reference.

Together, these features of Groovy allows Gradle files to look like declarative documents while actually being programs. This allows great expressivity/extensivity. On the negative side, syntax errors in the config file don’t always result in optimal error messages.

Dependency Modelling

Gradle uses a very similar dependency system to Maven, ie (group, artifact, version, scope). A gradle project defines “resolver objects” for dependencies, and all dependencies are obtained (including possibly triggering download) via the defined set of resolvers.

The Gradle standard library includes a maven-resolver which can fetch artifacts from a standard Maven artifact repository. This resolver also handles Maven transitive dependencies, ie when a Gradle project depends on an artifact which is retrieved from a Maven repository then any dependencies in the associated Maven pom are also added as transitive dependencies to the Gradle project.

Alternate resolvers are available for fetching artifacts from Ivy repositories, etc.

Running Gradle

By default, gradle uses file “build.gradle” as its input. If a “settings.gradle” file is present, then this is used to defined “sub-projects”.

Gradle has two phases:

  • first it “runs” the build.gradle file to build its datastructures; code normally has no side-effects at this point other than to update variables in the “project model” - and in particular, to register closures that will be invoked when appropriate.
  • second (after the project model has been built) it executes the relevant tasks (triggering any relevant closures attached to them).

To prove that build.gradle is actually a program, simply add “System.out.println(‘hello,world!’)” somewhere in the build.gradle file, then run a command like “gradle help”..

The entire build.gradle file is executed by Gradle as a closure with an associated “delegate object” to which all top-level method-calls in the closure are applied. This delegate-object provides the “project model”, including:

  • an initial list of tasks
  • a project artifact-name, version, etc
  • a list of dependencies

There are a number of built-in tasks (try “gradle tasks”), including clean, build, test, run, dependencies. File build.gradle can define additional tasks, and optionally attach them as pre-processing or post-processing steps of other tasks. Tasks can be accessed via the “task” method on the “delegate object” (ie via a call to task(taskid) from build.gradle). Each task is also exposed as a top-level method on the delegate-object, eg task(foo) is also available directly as foo.

The project-specific properties such as name, version and dependencies are initially empty, and are populated by parts of the build.gradle file, then read/processed by other parts of build.gradle.

The gradle docs start with some very generic advice. Eventually they move on to more concrete examples at:

Important Top-level Methods

The following methods are provided by the Gradle libraries as standard methods/properties on the “delegate object” - which are therefore directly callable from build.gradle.

Property Assignment and Retrieval

Any top-level “assignment” like:

  • mainClassName = "...."
  • group = "..."

stores that value as a project “attribute”. Some attributes have special meaning to certain plugins; all attributes can later be referenced via ${name}.

There are a whole bunch of “predefined” attributes set up by Gradle itself, such as ${baseName}.

Standard attributes the build.gradle file should set itself include:

  • group: sets artifact group
  • mainClassName: defines what should be written into the MANIFEST.MF as the class to execute when java -jar .. is executed.

buildscript (plugin management)

A gradle project supports “plugins”; these can be explicitly defined in a build.gradle file, and will be downloaded from an artifact repository (as Maven plugins are).

As a plugin loads, it makes calls to the delegate object to register specific tasks and properties, just like code in build.gradle can.

Gradle has separate “environments” for the actual code being built and the plugins used to build it (as Maven does). The “buildscript” config clause defines the custom build-plugins to apply and the repositories to download them from (via nested versions of the standard “repositories” and “apply plugin:” commands).

repositories

This instantiates specific “artifact resolvers”, defines their configuration, and attaches them to the current project-model. In other words, it defines where to fetch artifacts (dependencies) from.

apply plugin

Enables a standard plugin or one loaded via a “buildscript” section.

apply from

Takes a filename, and executes it (effectively merging its contents into the current context). If the “to:” parameter is defined, then it specifies the “context” to which the file’s contents is applied, ie acts as if the file was merged into the specified block.

dependencies

Like maven dependencies: scope, artifactId, exclusions (though Gradle calls a scope a “dependency configuration”).

configurations

Allows specific artifacts to be excluded from certain dependency scopes.

sourceSets

The directories in which sourcecode can be found.

jar

Defines how the classes are turned into a jarfile.

artifacts

Defines the set of artifacts generated by a build.

task

Retrieves a reference to an existing task, or creates (and registers) a new task.

Syntax like

  • task foo(type: Jar) { ... }

invokes foo(type, closure) which returns an object which is then passed to the “task” function, which registers it. The returned objects’s “type” property indicates what category of output artifact it generates. The custom task will only appear in the output of commandline command “gradle tasks” if it has a group property which points to one of the standard Gradle task-groups (eg “build”).

As each task is defined, Gradle makes it available as a property in the current context, eg after task foo is defined, foo.bar() will invoke method bar of task foo.

Syntax like

  • task compile << {...}

invokes task(compile) which returns the existing task with id=compile, and then calls method leftshift on that task object, passing the closure object. The implementation of method task.leftshift attaches the closure to the task as a “before task”; if task “compile” is triggered later, then the closure will be executed before the standard “compile” logic.

A task object has a fixed set of properties. However the property named “ext” is a simple map in which any desired values can be stored - ie for use by the build-script writer.

allprojects

Any of the above elements; will be applied both to the current project and inherited by subprojects.

subprojects

Any of the above elements; will be applied to (inherited by) all subprojects, but not applied to the current project.

Gradle Wrapper

Gradle can generate a “wrapper script” which can be checked in along with your source-code. This “wrapper script” comes in flavours for Windows and Unix; on Unix, this is a standard shell-script ie can be executed on just about any system. Executing the script will dynamically download the appropriate Gradle version, store it under ~/.gradle/wrapper/dists, then use that downloaded version to compile the sourcecode. This ensures that the system is buildable without requiring the user to manually download/install gradle. It also ensures the project is built with the version of Gradle it was designed for.

The security-sensitive should ensure that the wrapper points to a version of Gradle hosted on local resources, as otherwise the script will be downloading and installing a binary from the internet. As alternative, the checksum of the binary can be added to the wrapper properties; a modified binary will then be rejected.

Gradle and Intellij

Gradle can download artifacts from the main Maven repos. However instead of writiing the downloaded artifacts into ~/.m2/repository, it keeps its own cache in ~/.gradle/modules-2. This non-standard location can cause problems with some IDEs, including Intellij.

Gradle will try to download sources along with the binary jars. However if the gradle “repositories” section points first to a ~/.m2/repository (eg to make manually-installed jarfiles available), and that repo does not contain a source-jar, then Gradle will assume that no source-jar is available for that artifact. The fix is for the gradle buildfile to refer to the local repo only after the core maven repo, and then to delete ~/.gradle/modules-2 and rebuild, so artifacts get downloaded again.