A powerful feature of the Gradle build tool is its ability to setup dependencies between tasks, creating a task graph or tree.
The task graph means you only need to run the task you care about, and any other required tasks get run automatically. In this article, you’ll learn all about the Gradle task graph, how to add tasks to it, and how to print it out.
Tasks and task dependencies
A Gradle task is a unit of work which needs to get done in your build. Common examples within a Java project include:
- compiling code with the
compileJava
task - building a jar file with the
jar
task - building an entire project with the
build
task
Tasks vary from doing a huge amount to the tiniest amount of work. The clever thing is that those tasks which seemingly do a lot, like build
, consist only of dependencies on other tasks.
Defining task dependencies
As a quick reminder, if we have two tasks taskA and taskB which print their name, then we can say that taskB depends on taskA using the dependsOn
function.
task taskA() {
doLast {
print name
}
}
task taskB() {
doLast {
print name
}
dependsOn taskA
}
task("taskA") {
doLast {
println(name)
}
}
task("taskB") {
doLast {
println(name)
}
dependsOn(tasks.named("taskA"))
}
So when we run ./gradlew taskB
we get this output, showing that taskA is run followed by taskB.
> Task :taskA
taskA
> Task :taskB
taskB
BUILD SUCCESSFUL in 1s
This simple concept, scaled up to include chains of many tasks, is how the common tasks we use every day in Gradle are created.
The Gradle task graph
A task graph is the structure which is formed from all the dependencies between tasks in a Gradle build. Continuing with our example of the build
task in a project with the java
plugin applied, its task graph looks like this.
What you see here is all the different tasks that make up the build
task. The dotted lines represent a dependsOn
relationship between tasks. So looking at the top section, build
depends on assemble
, which depends on jar
, which depends on classes
, which depends on both compileJava
and processResources
.
So build
really is the big daddy task. It also depends on check
and all the testing related tasks beneath that.
You can see in the diagram that tasks fall into one of two categories:
- tasks which perform an action - for example, the
jar
task has an action associated with it which goes and creates a jar file. These types of tasks may or may not depend on other tasks. - aggregate tasks - these tasks are there just to provide a convenient way for you to execute a grouping of functionality. For example, rather than you having to run the
check
andassemble
tasks separately, thebuild
task just aggregates them together.
So build
doesn’t actually do anything? Not really, it’s a bit lazy like that. It just depends on other tasks that do the real work.
Printing the task graph
The benefits of understanding the task graph structure are:
- you can run whichever task you want within it: if you only need to create a
jar
file, there’s no need to runbuild
which also runs the tests. This saves you time since running fewer tasks is usually quicker. - it can help debug task related issues: if you’ve got a complex task graph, perhaps with your own custom tasks, then understanding the task graph is key to solving questions like “Why isn’t myAwesomeTask running?”
Sound good, so how do we print the task graph? Well, Gradle itself doesn’t support this functionality, but fortunately there are several plugin that do. The best one I’ve found is the gradle-taskinfo plugin.
Let’s apply it to a simple Java project in our build.gradle.
plugins {
id 'java'
id 'org.barfuin.gradle.taskinfo' version '2.1.0'
}
plugins {
java
id("org.barfuin.gradle.taskinfo") version "2.1.0"
}
It exposes a new task tiTree
, which we run along with the task whose task tree we’re interested in.
./gradlew tiTree build
Which prints this output.
> Task :tiTree
:build (org.gradle.api.DefaultTask)
+--- :assemble (org.gradle.api.DefaultTask)
| `--- :jar (org.gradle.api.tasks.bundling.Jar)
| +--- :classes (org.gradle.api.DefaultTask)
| | +--- :compileJava (org.gradle.api.tasks.compile.JavaCompile)
| | `--- :processResources (org.gradle.language.jvm.tasks.ProcessResources)
| `--- :compileJava (org.gradle.api.tasks.compile.JavaCompile)
`--- :check (org.gradle.api.DefaultTask)
`--- :test (org.gradle.api.tasks.testing.Test)
+--- :classes (org.gradle.api.DefaultTask)
| +--- :compileJava (org.gradle.api.tasks.compile.JavaCompile)
| `--- :processResources (org.gradle.language.jvm.tasks.ProcessResources)
+--- :compileJava (org.gradle.api.tasks.compile.JavaCompile)
+--- :compileTestJava (org.gradle.api.tasks.compile.JavaCompile)
| +--- :classes (org.gradle.api.DefaultTask)
| | +--- :compileJava (org.gradle.api.tasks.compile.JavaCompile)
| | `--- :processResources (org.gradle.language.jvm.tasks.ProcessResources)
| `--- :compileJava (org.gradle.api.tasks.compile.JavaCompile)
`--- :testClasses (org.gradle.api.DefaultTask)
+--- :compileTestJava (org.gradle.api.tasks.compile.JavaCompile)
| +--- :classes (org.gradle.api.DefaultTask)
| | +--- :compileJava (org.gradle.api.tasks.compile.JavaCompile)
| | `--- :processResources (org.gradle.language.jvm.tasks.ProcessResources)
| `--- :compileJava (org.gradle.api.tasks.compile.JavaCompile)
`--- :processTestResources (org.gradle.language.jvm.tasks.ProcessResources)
Cool! The output shows a similar structure as the diagram from earlier (funny that 😉). The plugin also prints us the type of task, for example we can see that compileJava
is a task of type org.gradle.api.tasks.compile.JavaCompile
.
Thanks to Barfuin for this awesome plugin, which you can learn more about over on GitLab.
Navigating the task graph programmatically
If you want to get your hands on the Gradle task graph yourself during your build, thankfully that’s pretty straightforward with the org.gradle.api.execution.TaskExecutionGraph
interface.
It helps you to:
- get all tasks in the graph
- get dependencies of a specific task
- add a listener to be executed before or after tasks are executed
Let’s try a few examples within a Gradle project which has the java
plugin applied.
Getting all tasks in the task graph
When using the task graph we have to define a closure to be called when the task graph is ready, otherwise we get a Task information is not available error. Within that closure we can print out the list of all tasks in the graph by calling getAllTasks
project.gradle.taskGraph.whenReady {
println project.gradle.taskGraph.getAllTasks()
}
project.gradle.taskGraph.whenReady {
println(project.gradle.taskGraph.allTasks)
}
When we run ./gradlew build
it outputs this.
[task ':compileJava', task ':processResources', task ':classes', task ':jar', task ':assemble', task ':compileTestJava', task ':processTestResources', task ':test
Classes', task ':test', task ':check', task ':build']
BUILD SUCCESSFUL in 859ms
This contains all the tasks from the task graph diagrams earlier on.
Querying task dependencies
The getDependencies
function takes a task as input and returns its direct dependencies. Let’s change the closure passed to whenReady
to the following.
project.gradle.taskGraph.whenReady {
println project.gradle.taskGraph.getDependencies(build as Task)
}
project.gradle.taskGraph.whenReady {
println(project.gradle.taskGraph.getDependencies(tasks.getByName("build")))
}
Executing ./gradlew build
now prints this.
[task ':assemble', task ':check']
BUILD SUCCESSFUL in 893ms
Which shows that the direct dependencies of the build
task are assembl
e and check
.
Adding a task listener
Finally, let’s define a closure to be executed after every task is run, using the afterTask
function.
project.gradle.taskGraph.whenReady {
project.gradle.taskGraph.afterTask { task ->
println "Doing important stuff after $task"
}
}
project.gradle.taskGraph.whenReady {
project.gradle.taskGraph.afterTask {
println("Doing important stuff after $this")
}
}
When we run ./gradlew jar
we get this output.
> Task :compileJava UP-TO-DATE
Doing important stuff after task ':compileJava'
> Task :processResources UP-TO-DATE
Doing important stuff after task ':processResources'
> Task :classes UP-TO-DATE
Doing important stuff after task ':classes'
> Task :jar UP-TO-DATE
Doing important stuff after task ':jar'
BUILD SUCCESSFUL in 798ms
3 actionable tasks: 3 up-to-date
Our closure was called after every task that got executed.
For full details about these functions and more, check out the docs for the TaskExecutionGraph
.
Wrap up
You just learnt about tasks, and how the dependencies between them form the Gradle task graph.
The task graph can be nicely visualised using the taskinfo
plugin, which helps us understand the task graph for a specific task.
For even more control, Gradle offers the TaskExecutionGraph
interface allowing us to hook in custom logic where we need to.
Stop reading Gradle articles like these
This article helps you fix a specific problem, but it doesn't teach you the Gradle fundamentals you need to actually help your team succeed.
Instead, follow a step-by-step process that makes getting started with Gradle easy.