Chapter 15. More about Tasks

In the introductory tutorial (Chapter 6, Build Script Basics) you have learned how to create simple tasks. You have also learned how to add additional behavior to these tasks later on. And you have learned how to create dependencies between tasks. This was all about simple tasks. But Gradle takes the concept of tasks further. Gradle supports enhanced tasks, that is, tasks which have their own properties and methods. This is really different to what you are used to with Ant targets. Such enhanced tasks are either provided by you or are provided by Gradle.

15.1. Defining tasks

We have already seen how to define tasks using a keyword style in Chapter 6, Build Script Basics. There are a few variations on this style, which you may need to use in certain situations. For example, the keyword style does not work in expressions.

Example 15.1. Defining tasks

build.gradle

task(hello) << {
    println "hello"
}

task(copy, type: Copy) {
    from(file('srcDir'))
    into(buildDir)
}

You can also use strings for the task names:

Example 15.2. Defining tasks - using strings for task names

build.gradle

task('hello') <<
{
    println "hello"
}

task('copy', type: Copy) {
    from(file('srcDir'))
    into(buildDir)
}

There is an alternative syntax for defining tasks, which you may prefer to use:

Example 15.3. Defining tasks with alternative syntax

build.gradle

tasks.create(name: 'hello') << {
    println "hello"
}

tasks.create(name: 'copy', type: Copy) {
    from(file('srcDir'))
    into(buildDir)
}

Here we add tasks to the tasks collection. Have a look at TaskContainer for more variations of the create() method.

15.2. Locating tasks

You often need to locate the tasks that you have defined in the build file, for example, to configure them or use them for dependencies. There are a number of ways you can do this. Firstly, each task is available as a property of the project, using the task name as the property name:

Example 15.4. Accessing tasks as properties

build.gradle

task hello

println hello.name
println project.hello.name

Tasks are also available through the tasks collection.

Example 15.5. Accessing tasks via tasks collection

build.gradle

task hello

println tasks.hello.name
println tasks['hello'].name

You can access tasks from any project using the task's path using the tasks.getByPath() method. You can call the getByPath() method with a task name, or a relative path, or an absolute path.

Example 15.6. Accessing tasks by path

build.gradle

project(':projectA') {
    task hello
}

task hello

println tasks.getByPath('hello').path
println tasks.getByPath(':hello').path
println tasks.getByPath('projectA:hello').path
println tasks.getByPath(':projectA:hello').path

Output of gradle -q hello

> gradle -q hello
:hello
:hello
:projectA:hello
:projectA:hello

Have a look at TaskContainer for more options for locating tasks.

15.3. Configuring tasks

As an example, let's look at the Copy task provided by Gradle. To create a Copy task for your build, you can declare in your build script:

Example 15.7. Creating a copy task

build.gradle

task myCopy(type: Copy)

This creates a copy task with no default behavior. The task can be configured using its API (see Copy). The following examples show several different ways to achieve the same configuration.

Example 15.8. Configuring a task - various ways

build.gradle

Copy myCopy = task(myCopy, type: Copy)
myCopy.from 'resources'
myCopy.into 'target'
myCopy.include('**/*.txt', '**/*.xml', '**/*.properties')

This is similar to the way we would normally configure objects in Java. You have to repeat the context (myCopy) in the configuration statement every time. This is a redundancy and not very nice to read.

There is another way of configuring a task. It also preserves the context and it is arguably the most readable. It is usually our favorite.

Example 15.9. Configuring a task - with closure

build.gradle

task myCopy(type: Copy)

myCopy {
   from 'resources'
   into 'target'
   include('**/*.txt', '**/*.xml', '**/*.properties')
}

This works for any task. Line 3 of the example is just a shortcut for the tasks.getByName() method. It is important to note that if you pass a closure to the getByName() method, this closure is applied to configure the task, not when the task executes.

You can also use a configuration closure when you define a task.

Example 15.10. Defining a task with closure

build.gradle

task copy(type: Copy) {
   from 'resources'
   into 'target'
   include('**/*.txt', '**/*.xml', '**/*.properties')
}

15.4. Adding dependencies to a task

There are several ways you can define the dependencies of a task. In Section 6.5, “Task dependencies” you were introduced to defining dependencies using task names. Task names can refer to tasks in the same project as the task, or to tasks in other projects. To refer to a task in another project, you prefix the name of the task with the path of the project it belongs to. Below is an example which adds a dependency from projectA:taskX to projectB:taskY:

Example 15.11. Adding dependency on task from another project

build.gradle

project('projectA') {
    task taskX(dependsOn: ':projectB:taskY') << {
        println 'taskX'
    }
}

project('projectB') {
    task taskY << {
        println 'taskY'
    }
}

Output of gradle -q taskX

> gradle -q taskX
taskY
taskX

Instead of using a task name, you can define a dependency using a Task object, as shown in this example:

Example 15.12. Adding dependency using task object

build.gradle

task taskX << {
    println 'taskX'
}

task taskY << {
    println 'taskY'
}

taskX.dependsOn taskY

Output of gradle -q taskX

> gradle -q taskX
taskY
taskX

For more advanced uses, you can define a task dependency using a closure. When evaluated, the closure is passed the task whose dependencies are being calculated. The closure should return a single Task or collection of Task objects, which are then treated as dependencies of the task. The following example adds a dependency from taskX to all the tasks in the project whose name starts with lib:

Example 15.13. Adding dependency using closure

build.gradle

task taskX << {
    println 'taskX'
}

taskX.dependsOn {
    tasks.findAll { task -> task.name.startsWith('lib') }
}

task lib1 << {
    println 'lib1'
}

task lib2 << {
    println 'lib2'
}

task notALib << {
    println 'notALib'
}

Output of gradle -q taskX

> gradle -q taskX
lib1
lib2
taskX

For more information about task dependencies, see the Task API.

15.5. Ordering tasks

Task ordering is an incubating feature. Please be aware that this feature may change in later Gradle versions.

In some cases it is useful to control the order in which 2 tasks will execute, without introducing an explicit dependency between those tasks. The primary difference between a task ordering and a task dependency is that an ordering rule does not influence which tasks will be executed, only the order in which they will be executed.

Task ordering can be useful in a number of scenarios:

  • Enforce sequential ordering of tasks: eg. 'build' never runs before 'clean'.
  • Run build validations early in the build: eg. validate I have the correct credentials before starting the work for a release build.
  • Get feedback faster by running quick verification tasks before long verification tasks: eg. unit tests should run before integration tests.
  • A task that aggregates the results of all tasks of a particular type: eg. test report task combines the outputs of all executed test tasks.

There are two ordering rules available: "must run after" and "should run after".

By using 'must run after" ordering rule you can specify that taskB must always run after taskA, whenever both taskA and taskB are scheduled for execution. This is expressed as taskB.mustRunAfter(taskA). The 'should run after' ordering rule is similar but less strict as it will be ignored in two situations. Firstly if using that rule introduces an ordering cycle. Secondly when using parallel execution and all dependencies of a task have been satisfied apart from should run after then this task will be run regardless of weather its 'should run after' dependencies have been run or not. You would use 'should run after' rule when ordering preference for faster feedback, where the ordering is helpful but not strictly required.

With these rules present it is still possible to execute taskA without taskB and vice-versa.

Example 15.14. Adding a 'must run after' task ordering

build.gradle

task taskX << {
    println 'taskX'
}
task taskY << {
    println 'taskY'
}
taskY.mustRunAfter taskX

Output of gradle -q taskY taskX

> gradle -q taskY taskX
taskX
taskY

Example 15.15. Adding a 'should run after' task ordering

build.gradle

task taskX << {
    println 'taskX'
}
task taskY << {
    println 'taskY'
}
taskY.shouldRunAfter taskX

Output of gradle -q taskY taskX

> gradle -q taskY taskX
taskX
taskY

In the examples above, it is still possible to execute taskY without causing taskX to run:

Example 15.16. Task ordering does not imply task execution

Output of gradle -q taskY

> gradle -q taskY
taskY

To specify a "must run after" or "should run after" ordering between 2 tasks, you use the Task.mustRunAfter() and Task.shouldRunAfter() methods. These method accept a task instance, a task name or any other input accepted by Task.dependsOn().

Note that "B.mustRunAfter(A)" or "B.shouldRunAfter(A)" does not imply any execution dependency between the tasks:

  • It is possible to execute tasks A and B independently. The ordering rule only has an effect when both tasks are scheduled for execution.
  • When run with --continue, it is possible for B to execute in the event that A fails.

As mentioned before 'should run after' ordering rule will be ignored if it introduces an ordering cycle:

Example 15.17. A 'should run after' task ordering is ignored if it introduces an ordering cycle

build.gradle

task taskX << {
    println 'taskX'
}
task taskY << {
    println 'taskY'
}
task taskZ << {
    println 'taskZ'
}
taskX.dependsOn taskY
taskY.dependsOn taskZ
taskZ.shouldRunAfter taskX

Output of gradle -q taskX

> gradle -q taskX
taskZ
taskY
taskX

15.6. Adding a description to a task

You can add a description to your task. This description is for example displayed when executing gradle tasks.

Example 15.18. Adding a description to a task

build.gradle

task copy(type: Copy) {
   description 'Copies the resource directory to the target directory.'
   from 'resources'
   into 'target'
   include('**/*.txt', '**/*.xml', '**/*.properties')
}

15.7. Replacing tasks

Sometimes you want to replace a task. For example if you want to exchange a task added by the Java plugin with a custom task of a different type. You can achieve this with:

Example 15.19. Overwriting a task

build.gradle

task copy(type: Copy)

task copy(overwrite: true) << {
    println('I am the new one.')
}

Output of gradle -q copy

> gradle -q copy
I am the new one.

Here we replace a task of type Copy with a simple task. When creating the simple task, you have to set the overwrite property to true. Otherwise Gradle throws an exception, saying that a task with such a name already exists.

15.8. Skipping tasks

Gradle offers multiple ways to skip the execution of a task.

15.8.1. Using a predicate

You can use the onlyIf() method to attach a predicate to a task. The task's actions are only executed if the predicate evaluates to true. You implement the predicate as a closure. The closure is passed the task as a parameter, and should return true if the task should execute and false if the task should be skipped. The predicate is evaluated just before the task is due to be executed.

Example 15.20. Skipping a task using a predicate

build.gradle

task hello << {
    println 'hello world'
}

hello.onlyIf { !project.hasProperty('skipHello') }

Output of gradle hello -PskipHello

> gradle hello -PskipHello
:hello SKIPPED

BUILD SUCCESSFUL

Total time: 1 secs

15.8.2. Using StopExecutionException

If the rules for skipping a task can't be expressed with predicate, you can use the StopExecutionException. If this exception is thrown by an action, the further execution of this action as well as the execution of any following action of this task is skipped. The build continues with executing the next task.

Example 15.21. Skipping tasks with StopExecutionException

build.gradle

task compile << {
    println 'We are doing the compile.'
}

compile.doFirst {
    // Here you would put arbitrary conditions in real life. But we use this as an integration test, so we want defined behavior.
    if (true) { throw new StopExecutionException() }
}
task myTask(dependsOn: 'compile') << {
   println 'I am not affected'
}

Output of gradle -q myTask

> gradle -q myTask
I am not affected

This feature is helpful if you work with tasks provided by Gradle. It allows you to add conditional execution of the built-in actions of such a task. [7]

15.8.3. Enabling and disabling tasks

Every task has also an enabled flag which defaults to true. Setting it to false prevents the execution of any of the task's actions.

Example 15.22. Enabling and disabling tasks

build.gradle

task disableMe << {
    println 'This should not be printed if the task is disabled.'
}
disableMe.enabled = false

Output of gradle disableMe

> gradle disableMe
:disableMe SKIPPED

BUILD SUCCESSFUL

Total time: 1 secs

15.9. Skipping tasks that are up-to-date

If you are using one of the tasks that come with Gradle, such as a task added by the Java plugin, you might have noticed that Gradle will skip tasks that are up-to-date. This behaviour is also available for your tasks, not just for built-in tasks.

15.9.1. Declaring a task's inputs and outputs

Let's have a look at an example. Here our task generates several output files from a source XML file. Let's run it a couple of times.

Example 15.23. A generator task

build.gradle

task transform {
    ext.srcFile = file('mountains.xml')
    ext.destDir = new File(buildDir, 'generated')
    doLast {
        println "Transforming source file."
        destDir.mkdirs()
        def mountains = new XmlParser().parse(srcFile)
        mountains.mountain.each { mountain ->
            def name = mountain.name[0].text()
            def height = mountain.height[0].text()
            def destFile = new File(destDir, "${name}.txt")
            destFile.text = "$name -> ${height}\n"
        }
    }
}

Output of gradle transform

> gradle transform
:transform
Transforming source file.

Output of gradle transform

> gradle transform
:transform
Transforming source file.

Notice that Gradle executes this task a second time, and does not skip the task even though nothing has changed. Our example task was defined using an action closure. Gradle has no idea what the closure does and cannot automatically figure out whether the task is up-to-date or not. To use Gradle's up-to-date checking, you need to declare the inputs and outputs of the task.

Each task has an inputs and outputs property, which you use to declare the inputs and outputs of the task. Below, we have changed our example to declare that it takes the source XML file as an input and produces output to a destination directory. Let's run it a couple of times.

Example 15.24. Declaring the inputs and outputs of a task

build.gradle

task transform {
    ext.srcFile = file('mountains.xml')
    ext.destDir = new File(buildDir, 'generated')
    inputs.file srcFile
    outputs.dir destDir
    doLast {
        println "Transforming source file."
        destDir.mkdirs()
        def mountains = new XmlParser().parse(srcFile)
        mountains.mountain.each { mountain ->
            def name = mountain.name[0].text()
            def height = mountain.height[0].text()
            def destFile = new File(destDir, "${name}.txt")
            destFile.text = "$name -> ${height}\n"
        }
    }
}

Output of gradle transform

> gradle transform
:transform
Transforming source file.

Output of gradle transform

> gradle transform
:transform UP-TO-DATE

Now, Gradle knows which files to check to determine whether the task is up-to-date or not.

The task's inputs property is of type TaskInputs. The task's outputs property is of type TaskOutputs.

15.9.2. How does it work?

Before a task is executed for the first time, Gradle takes a snapshot of the inputs. This snapshot contains the set of input files and a hash of the contents of each file. Gradle then executes the task. If the task completes successfully, Gradle takes a snapshot of the outputs. This snapshot contains the set of output files and a hash of the contents of each file. Gradle takes note of any files created, changed or deleted in the output directories of the task. Gradle persists both snapshots for next time the task is executed.

Each time after that, before the task is executed, Gradle takes a new snapshot of the inputs and outputs. If the new snapshots are the same as the previous snapshots, Gradle assumes that the outputs are up to date and skips the task. If they are not the same, Gradle executes the task. Gradle persists both snapshots for next time the task is executed.

15.10. Task rules

Sometimes you want to have a task which behavior depends on a large or infinite number value range of parameters. A very nice and expressive way to provide such tasks are task rules:

Example 15.25. Task rule

build.gradle

tasks.addRule("Pattern: ping<ID>") { String taskName ->
    if (taskName.startsWith("ping")) {
        task(taskName) << {
            println "Pinging: " + (taskName - 'ping')
        }
    }
}

Output of gradle -q pingServer1

> gradle -q pingServer1
Pinging: Server1

The String parameter is used as a description for the rule. This description is shown when running for example gradle tasks.

Rules not just work for calling tasks from the command line. You can also create dependsOn relations on rule based tasks:

Example 15.26. Dependency on rule based tasks

build.gradle

tasks.addRule("Pattern: ping<ID>") { String taskName ->
    if (taskName.startsWith("ping")) {
        task(taskName) << {
            println "Pinging: " + (taskName - 'ping')
        }
    }
}

task groupPing {
    dependsOn pingServer1, pingServer2
}

Output of gradle -q groupPing

> gradle -q groupPing
Pinging: Server1
Pinging: Server2

15.11. Finalizer tasks

Finalizers tasks are an incubating feature (see Section C.1.2, “Incubating”).

Finalizer tasks are automatically added to the task graph when the finalized task is scheduled to run.

Example 15.27. Adding a task finalizer

build.gradle

task taskX << {
    println 'taskX'
}
task taskY << {
    println 'taskY'
}

taskX.finalizedBy taskY

Output of gradle -q taskX

> gradle -q taskX
taskX
taskY

Finalizer task will be executed even if the finalized task fails.

Example 15.28. Task finalizer for a failing task

build.gradle

task taskX << {
    println 'taskX'
    throw new RuntimeException()
}
task taskY << {
    println 'taskY'
}

taskX.finalizedBy taskY

Output of gradle -q taskX

> gradle -q taskX
taskX
taskY

On the other hand, finalizer tasks are not executed if the finalized task didn't do any work, for example due to failed task dependency or if it's considered up to date.

Finalizer tasks are useful in situations where build creates a resource that has to be cleaned up regardless of the build failing or succeeding. An example of such resource is a web container started before an integration test task and which should be always shut down, even if some of the tests fail.

To specify a finalizer task you use the Task.finalizedBy() method. This method accepts a task instance, a task name or any other input accepted by Task.dependsOn().

15.12. Summary

If you are coming from Ant, such an enhanced Gradle task as Copy looks like a mixture between an Ant target and an Ant task. And this is actually the case. The separation that Ant does between tasks and targets is not done by Gradle. The simple Gradle tasks are like Ant's targets and the enhanced Gradle tasks also include the Ant task aspects. All of Gradle's tasks share a common API and you can create dependencies between them. Such a task might be nicer to configure than an Ant task. It makes full use of the type system, is more expressive and easier to maintain.



[7] You might be wondering why there is neither an import for the StopExecutionException nor do we access it via its fully qualified name. The reason is, that Gradle adds a set of default imports to your script. These imports are customizable (see Appendix E, Existing IDE Support and how to cope without it).