By itself, Gradle is a deeply customizable toolkit for creating custom build software. It comes with out-of-the-box conventions that are useful for highly standardized builds, and it exposes a rich set of APIs for introducing novel functionality into a non-commodity build as we saw in Chapter 2. But your ability to customize a Gradle build doesn’t end with plug-ins.
Gradle offers you the ability to modify the execution of that program by hooking a variety of events that occur during the configuration and execution of your build. These hooks are simply blocks of Groovy code that run when tasks are added, when projects are created, and at other times during Gradle’s internal configuration sequence. Also, following the patterns implicit in Groovy’s own metaprogramming APIs, Gradle gives you the ability to create tasks dynamically based on its powerful rules feature. This chapter presents hooks and rules as a means of managing build complexity and decorating the functionality of builds whose source we do not always directly control.
Many readers of this book will be familiar with the Gradle build lifecycle, but a thorough understanding of build hooks requires that we revisit it briefly. Every Gradle build proceeds through three lifecycle phases in precisely the same order. These phases are initialization, configuration, and execution.
During the initialization phase, Gradle starts up and locates the build files it must process. Crucial during this phase is the determination of whether the build is single-project or multi-project. If it is a single project build, Gradle identifies a single build file to pass to the next phase. If it is a multi-project build, Gradle locates potentially many build files for processing in the next phase.
That next phase is configuration. During configuration, Gradle executes each build file as a Groovy script. The effect of configuration is not the actual execution of build actions—that comes next—but rather the creation of a directed acyclic graph (DAG) of task objects.[12] It is during the construction of this graph that many of the hook methods run.
The final phase is execution. During this phase, Gradle identifies the tasks in the task DAG that must be executed, and executes them in dependency order.[13] All build activities (e.g., compiling code, copying files, uploading artifacts, etc.) occur during execution. Some build hooks are evaluated during execution.
In the early 2000s, the paradigm of aspect-oriented programming (AOP) became a marginally popular way to manage the complexity of enterprise Java software. AOP acknowledged that programs often contained individual units of functionality—in the case of an object-oriented language like Java, these are methods—which need to be enhanced with code whose purpose is not directly related to the unit in question. For example, a method that processes a web request might need to make security assertions before it runs, or it might have to emit logging after it runs. A method that writes to the database might have to have a database context set up first and a transaction committed after.
AOP developed its own vocabulary and a small number of Java frameworks to implement it. Gradle build hooks are similar enough to AOP that some of this vocabulary is worth retaining. The code that ran before and after the original method was called advice. The advice code was often described as being orthogonal to the original code, meaning that the purposes of the two blocks of code weren’t correlated in any direct way, even though the execution sequence was. The kinds of code with which one would advise application methods were called cross-cutting concerns. The most common tutorial examples were logging and database transaction management.
In our discussion of Gradle build hooks, we’ll retain the “advice” terminology, and generally focus our examples on the sorts of orthogonal, cross-cutting concerns that have populated AOP texts for the past decade.
The goal of an individual Gradle build file is to set up a Project with all of the settings and tasks it needs to give Gradle useful work to do during the build. The setup takes place during the configuration phase, and the build itself takes place during the execution phase. The project.beforeEvaluate()
and project.afterEvaluate()
methods are hooks to execute blocks of code before and after configuration is run on a particular project.
Example 3-1 is a trivial build that hooks the afterEvaluate
event. After the build has finished evaluating, the hook checks for the presence of a grammars
directory. If the directory exists, the hook creates a task called testGrammars
, ostensibly to perform grammar tests on the contents of the directory. Try running the build with and without a grammars
directory present. Note the output of gradle tasks
in each case.
Example 3-1. Hooking project after-evaluation
afterEvaluate
{
if
(
file
(
'grammars'
).
isDirectory
())
{
println
"'grammars' directory found"
task
testGrammars
<<
{
println
"Running grammar tests"
}
}
else
{
println
"'grammars' directory not found"
}
}
task
helloWorld
{
doLast
{
println
"hello, world"
}
}
The beforeEvaluate()
hook is not particularly useful in this case, since there is no way in a single build file to advise the build to do something before it is evaluated.[14] Before-hooks are only useful in the case of a multiproject build. Example 3-2 has three builds, the parent project and two subprojects. The grammar check is only performed on the subprojects—now as a before-hook—by way of the allprojects
method. The configuration inside the allprojects
closure is applied to all projects, and therefore has a chance to affect the subprojects, since they have not yet been evaluated at the time the closure is run. If you run the example with and without the grammar
directory in all three project directories, you’ll find that the parent project never gains the testGrammars
task, since the beforeEvaluate
hook can’t run on the project in which it is read.
Example 3-2. Hooking project before-evaluation
allprojects
{
beforeEvaluate
{
if
(
project
.
file
(
'grammars'
).
isDirectory
())
{
println
"'grammars' found in ${project.name}"
task
testGrammars
<<
{
println
"Running grammar tests in ${project.name}"
}
}
else
{
println
"'grammars' not found in ${project.name}"
}
}
}
task
helloWorld
{
doLast
{
println
"the parent says hello"
}
}
Note that the check for the file has become project.file()
instead of merely file()
as in the previous example. This causes Gradle to look explicitly in the individual subproject’s directory for the grammars
directory, rather than only at the root project level.
The preceding examples show beforeEvaluate()
and afterEvaluate()
being used globally, to apply to all projects at once using the allprojects
closure. The methods can also be applied to individual project objects just as easily, if a particular subproject needs before-evaluate or after-evaluate processing applied more surgically. However, you might find that you often have a block of processing to apply in a global sense after all projects are evaluated or, if you want to get the hook in earlier in the lifecycle than that, after all projects are loaded. This is what gradle.projectsLoaded()
and gradle.projectsEvaluated()
are for.
The gradle.projectsLoaded()
method takes a closure that runs after all projects are loaded. This event fires at the end of the initialization phase, before any project evaluation has begun. Of course, not much of the Gradle object model is available for examination or configuration at this point in the lifecycle, but useful work is still possible. The gradle
object is passed to the closure as a parameter (named g
in Example 3-3), and can be operated upon as usual. This example shows the buildscript
block being configured with classpath dependencies to support the loading of custom plug-ins in other builds within the project. This buildscript config—often an annoying bit of noise in small builds—is now abstracted away through the work of the projectsLoaded()
hook.
Example 3-3. Hooking the end of the initialization phase in the settings.gradle file
gradle
.
projectsLoaded
{
g
->
g
.
rootProject
.
buildscript
{
repositories
{
mavenCentral
()
}
dependencies
{
classpath
'com.augusttechgroup:gradle-liquibase-plugin:0.7'
classpath
'com.h2database:h2:1.3.160'
}
}
}
The projectsEvaluated()
method similarly takes a closure that runs after all projects have been evaluated.[15] This event fires at the end of the configuration phase, before any build execution has begun. The entire project graph is available for inspection and manipulation at this point, and the caller can be assured that no build activity has taken place at the time the hook runs.
You may want to know when a build is finished, and whether it ran to completion or encountered an error. Certainly your build’s command-line user interface or continuous integration framework is going to give you this feedback, but we can imagine other ways we might want to capture this status and act on it.
If the build fails, it fails because some part of the build threw an exception. A build that runs to completion without throwing an exception is considered successful. This status is reflected in the buildFinished()
closure through the BuildResult
instance passed to it (Example 3-4).
Example 3-4. Hooking the build finished event
gradle
.
buildFinished
{
buildResult
->
println
"BUILD FINISHED"
println
"build failure - "
+
buildResult
.
failure
}
task
succeed
{
doLast
{
println
"hello, world"
}
}
task
fail
{
doLast
{
throw
new
Exception
(
"Build failed"
)
}
}
Running gradle succeed
at the command line produces the output sequence in Example 3-5.
Example 3-5. The output of the succeed task
$
gradle succeed :succeed hello, world BUILD SUCCESSFUL Totaltime
: 1.374 secs BUILD FINISHED build failure - null
Note the last two lines of the output. After Gradle reports the build has finished with Total time: 1.374 secs
, our buildFinished()
hook runs, reporting that that build has finished, and printing the value of buildResult.failure
, which is null.
Running gradle fail
at the command line captures an exception (Example 3-6). Note that the BUILD FINISHED
line is still emitted, even though the build failed. Our hook runs in both cases, but only has a non-null gradle.failure
property when there is a failure to report. This object is an internal Gradle API type that wraps the root-cause exception. We can inspect it to determine the cause of the failure, display it in a automated build console, or simply log it.
Example 3-6. The output of the succeed task
$
gradle fail :fail FAILED FAILURE: Build failed with an exception. * Where: Build file'/Users/tlberglund/Documents/Writing/Gradle/oreilly-gradle-book-examples/hooks-lab/build-finished/build.gradle'
line: 14 * What went wrong: Execution failedfor
task':fail'
. > java.lang.Exception: Build failed * Try: Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. BUILD FAILED Totaltime
: 1.453 secs BUILD FINISHED build failure - org.gradle.api.internal.LocationAwareException: Build file'/Users/tlberglund/Documents/Writing/Gradle/oreilly-gradle-book-examples/hooks-lab/build-finished/build.gradle'
line: 14 Execution failedfor
task':fail'
.
Typically, a task is a specific activity with a specific name. For example, the build
task in a Java project compiles and tests all of the code. It always does that same thing, and it is always called by the same name. It is like a particular method written into a particular class: specific and identifiable.
However, what if your task is less predictable? Cases commonly arise in which you have a certain kind of build activity to carry out that follows a general template but needs to vary with circumstances. For example, you might want a family of tasks that do the same kind of deployment, but to different hosts, or the same kind of binary repository deployment, but with different archives.
The impulse is to want to pass arguments to tasks, which isn’t possible. It’s easy enough to parameterize imperative task activity with a System
property, an environment variable, or even a variable in the build script itself, but those solutions also founder on complexity considerations. They introduce extra coupling of the build to states outside the build script, and their activity is not visible at the level of the task graph—the fundamental data structure that defines a Gradle build and provides opportunities for hooking and tooling integration. In Gradle, wherever possible, we want to expose discrete and specifically named tasks to the build user. An effective way to solve the problem of dynamic task behavior within this constraint is through the use of rules.
Any time the build user references (i.e., executes or tries to configure) a task that doesn’t already exist, Gradle consults the project’s task rules. Task rules are able to respond to the request for a nonexistent task in any way they wish, but it is very common for a rule to create a task on demand based on the name of the requested task. This is useful for creating tasks dynamically based on other build configuration. If you’re familiar with the methodMissing
and propertyMissing
facilities of Groovy or Ruby’s method_missing
, you’ve already got the idea.
The Gradle Java plug-in uses rules to provide tasks for targeted building, binary repository uploading, and cleaning. To see how rules work, let’s take a look at the simplest of these three rules: the clean rule.
There is a general-form clean
task in every Java build, which simply deletes the build
directory. However, we might want to clean only the result of one particular task that contributes to the build. In a Java project, both the compileJava
task and the processResources
task create content in the build
directory: the compile task by putting class files by default under build/classes/main
, and the resources task by putting them by default under build/resources/main
. We can see this after compiling this example project. Here we see the build
directory after running the build
task:
build ├── classes │ └── main │ └── org │ └── gradle │ └── poetry │ └── PoetryEmitter.class ├── libs │ └── java-build-with-resources.jar └── resources └── main ├── chesterton.txt ├── henley.txt ├── shakespeare.txt └── shelley.txt
Java classes have been compiled, static resources have been copied, and a JAR has been created. Simply running clean
at this point will delete the entire build
directory, wiping out all three of those resources indiscriminately. However, running cleanJar
will delete only the JAR file, and running cleanResources
will surgically remove only the build/resources
directory.
Here’s a partial listing of the build directory after running cleanResources
:
build ├── classes │ └── main │ └── org │ └── gradle │ └── poetry │ └── PoetryEmitter.class └── libs └── java-build-with-resources.jar
This simple example may not show particularly advanced behavior—judicious use of the rm
command would do the same thing—but it does illustrate the power of task rules. The possibilities are compelling indeed.
Let’s create a rule to ping an arbitrary server and store the results of the ping. We’ll log them in the build
directory and store them in the extended properties of the task itself, so other tasks in the build can optimize their activity based on the availability of the pinged server.
Pinging a server is a one-line operation in Java (and by extension, in Groovy), but we don’t want to write a specific ping task for every server we might need to contact. Moreover, it isn’t long before we take a one-line operation and start adding handling for various error cases and output formats. This is a very common story for small chunks of code that start off as simple operations, but grow in complexity over time. To keep the complexity manageable, it would be good to have that code in one place, not spread across multiple tasks containing their own customized configuration, as in Example 3-7.
Example 3-7. A rule for checking the HTTP-reachability of an arbitrary server
ext
{
pingLogDir
=
"${buildDir}/reachable"
}
tasks
.
addRule
(
'Rule Usage: ping<Hostname>'
)
{
String
taskName
->
if
(
taskName
.
startsWith
(
'ping'
))
{
task
(
taskName
)
{
ext
.
hostname
=
taskName
-
'ping'
doLast
{
def
url
=
new
URL
(
"http://${ext.hostname}"
)
def
logString
try
{
def
pageContent
=
url
.
text
// Do some regex matching on pageContent
logString
=
"${new Date()}\t${ext.hostname}\tUP\n"
ext
.
up
=
true
}
catch
(
UnknownHostException
e
)
{
logString
=
"${new Date()}\t${ext.hostname}\tUNKNOWN HOST\n"
ext
.
up
=
false
}
catch
(
ConnectException
e
)
{
logString
=
"${new Date()}\t${ext.hostname}\tDOWN\n"
ext
.
up
=
false
}
file
(
pingLogDir
).
mkdirs
()
file
(
"${pingLogDir}/ping.log"
)
<<
logString
}
}
}
}
Presumably, using HTTP for reachability would also be accompanied by some parsing of the message body that was returned. This could easily be added to the script in an imperative way through Groovy’s regular expression facilities; however, this class is already getting a little bit long. We might want to look at a better solution for managing all that imperative code.
Our current rule definition relies on 18 lines of doLast
code, which is probably too much imperative code for a well-factored, maintainable build. We can’t test it, and as of this writing, most tooling does not support Groovy script development inside a Gradle build as well as it supports coding in conventional Groovy classes. The solution to this problem is the Rule
interface.
You may recall that every Gradle build has an embedded Groovy project in it in the buildSrc
directory. If this project exists, it is built prior to running the main build, and all of the classes it generates are made available to the build script classpath. It’s a convenient staging ground to begin creating a Rule
class and some tests for it.
The basic skeleton of a Rule
class is shown in Example 3-8. The interface specifies two methods: a getter for the description, which is used by the tasks
task to document whatever rules are present in the build, and the apply()
method, which will eventually create our new task. This class goes in the buildSrc/src/main/groovy/org/gradle/examples/rules directory of our build.
Example 3-8. The minimal Rule class
import
org.gradle.api.Rule
class
HttpPingRule
implements
Rule
{
String
getDescription
()
{
'Rule Usage: ...'
}
void
apply
(
String
taskName
)
{
}
}
And now our build file becomes radically simpler. All of the imperative code has been removed from the build and placed in a source file where it belongs, so our build looks like Example 3-9 now.
Example 3-9. The build file is much smaller with a class-based rule definition
import
org.gradle.examples.rules.HttpPingRule
tasks
.
addRule
(
new
HttpPingRule
(
project
))
Running gradle tasks
will now reveal the task rule definition after the statically defined tasks are listed.
Fleshing the rule definition out a bit, we might want to add code in the apply()
method to test the name of the task being referenced. (Remember, the point of a task rule is to capture references to tasks that don’t exist, and create them if their name matches some convention.) Adding a bit more functionality to apply()
, we have what we see in Example 3-10.
Example 3-10. A Rule class that creates a task in the apply() method
import
org.gradle.api.Rule
class
HttpPingRule
implements
Rule
{
def
project
HttpPingRule
(
project
)
{
this
.
project
=
project
}
String
getDescription
()
{
'Rule Usage: ping<Hostname>'
}
void
apply
(
String
taskName
)
{
if
(
taskName
.
startsWith
(
'ping'
))
{
project
.
task
(
taskName
)
{
ext
.
hostname
=
taskName
-
'ping'
doLast
{
println
"PING ${ext.hostname}"
}
}
}
}
}
This rule definition can be further expanded with actual network reachability code in place of the println()
call, with that functionality comfortably factored into methods as we see fit. But more importantly, since this rule is now a class, we can test it.
If we place a JUnit or TestNG test in buildSrc/src/test/groovy/org/gradle/examples/rules/HttpPingTests.groovy, the default Gradle unit test support will run the test prior to running our build. Since the topic of unit testing is already covered in Building and Testing with Gradle —and the buildSrc
project works exactly like a normal project in this regard—I will invite you to explore the full example code at your leisure.
The rules we’ve programmed so far have all been task rules, or rules that create new tasks on demand. As it turns out, Gradle’s rule API doesn’t exist specifically at the task level, but rather at the level of the named domain object collection. That is, any named collection of things in Gradle—SourceSets
, configurations, custom domain objects—can be created by rules.
When you have full control over the build source and are writing your own plug-in, often the easiest way to introduce new functionality is simply to code it directly into your custom plug-in. However, at times when build source is not available to you or is not convenient to modify, build hooks and rules provide powerful mechanisms for bringing concepts from aspect-oriented programming and metaprogramming into your build development.
[12] If it is a multi-project build, there is also a DAG of project objects, one for each project.
[13] Dependency order means that if task A depends on task B, task B will run first.
[14] By the time the hook itself is evaluated, it’s too late; evaluation has already taken place.
[15] Recall the discussion of the Gradle lifecycle.
Get Gradle Beyond the Basics now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.