All about the Gradle task graph

All about the Gradle task graph

A powerful Gradle feature is its ability to setup dependencies between tasks, creating a task graph or tree. That’s great because 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. Really common examples within a Java project include:

  1. compiling code with the compileJava task
  2. building a jar file with the jar task
  3. 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
}

So when we run ./gradlew taskB we would 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’re seeing 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:

  1. 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.
  2. 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 and assemble tasks separately, the build 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 run build 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 '1.0.5'
}

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)
`--- :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)
          `--- :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)
               `--- :processTestResources       (org.gradle.language.jvm.tasks.ProcessResources)

Cool! The output shows the same 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 mainly allows you to:

  1. get all tasks in the graph
  2. get dependencies of a specific task
  3. 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()
}

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.

What’s a closure? It’s a way of defining a block of code in a way that can be passed around as variable and executed later 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)
}

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 assemble 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"
    }
}

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.

Gradle icon

Want to learn more about Gradle?
Check out the full selection of Gradle tutorials.

Get going with Gradle course
All about the Gradle task graph

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to top

Get the newsletter

Found this article helpful? Subscribe for monthly updates.

✅ All of my latest articles for the month
✅ Access to video tutorials
✅ Exclusive tips not found on my website