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:

  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
}
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:

  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 '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.

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:

  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()
}
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 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"
    }
}
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 really help your team succeed.

Instead, follow a step-by-step process that makes getting started with Gradle easy.

Download this Free Quick-Start Guide to building simple Java projects with Gradle.

  • Learn to create and build Java projects in Gradle.
  • Understand the Gradle fundamentals.