Introducing Incremental Build Support

Task inputs, outputs, and dependencies

Built-in tasks, like JavaCompile declare a set of inputs (Java source files) and a set of outputs (class files). Gradle uses this information to determine if a task is up-to-date and needs to perform any work. If none of the inputs or outputs have changed, Gradle can skip that task. Altogether, we call this behavior Gradle’s incremental build support.

To take advantage of incremental build support, you need to provide Gradle with information about your tasks’ inputs and outputs. It is possible to configure a task to only have outputs. Before executing the task, Gradle checks the outputs and will skip execution of the task if the outputs have not changed. In real builds, a task usually has inputs as well—including source files, resources, and properties. Gradle checks that neither the inputs nor outputs have changed before executing a task.

Often a task’s outputs will serve as the inputs to another task. It is important to get the ordering between these tasks correct, or the tasks will run in the wrong order or not at all. Gradle does not rely on the order that tasks are defined in the build script. New tasks are unordered, therefore execution order can change from build to build. You can explicitly tell Gradle about the order between two tasks by declaring a dependency between one task another, for example consumer.dependsOn producer.

Declaring explicit task dependencies

Let’s take a look at an example project that contains a common pattern. For this project, we need to create a zip file that contains the output from a generator task. The manner in which the generator task creates files is not interesting—it produces files that contain an incrementing number.

build.gradle

apply plugin: 'base'

task generator() {
    doLast {
        def generatedFileDir = file("$buildDir/generated")
        generatedFileDir.mkdirs()
        for (int i=0; i<10; i++) {
            new File(generatedFileDir, "${i}.txt").text = i
        }
    }
}

task zip(type: Zip) {
    dependsOn generator
    from "$buildDir/generated"
}

The build works, but the build script has some issues. The output directory for the generator task is repeated in the zip task, and dependencies of the zip task are explicitly set with dependsOn. Gradle appears to execute the generator task each time, but not the zip task. This is a good time to point out that Gradle’s up-to-date checking is different from other tools, such as Make. Gradle compares the checksum of the inputs and outputs instead of only the timestamp of the files. Even though the generator task runs each time and overwrites all of its output files, the content does not change and the zip task does not need to run again. The checksum of the zip task’s inputs have not changed. Skipping up-to-date tasks lets Gradle avoid unnecessary work and speeds up the development feedback loop.

Declaring task inputs and outputs

Now, let’s understand why the generator task seems to run every time. If we take a look at Gradle’s info-level logging output by running the build with --info, we will see the reason:

Executing task ':generator' (up-to-date check took 0.0 secs) due to:
Task has not declared any outputs.

We can see that Gradle does not know that the task produces any output. By default, if a task does not have any outputs, it must be considered out-of-date. Outputs are declared with the TaskOutputs. Task outputs can be files or directories. Note the use of outputs below:

build.gradle

task generator() {
    def generatedFileDir = file("$buildDir/generated")
    outputs.dir generatedFileDir
    doLast {
        generatedFileDir.mkdirs()
        for (int i=0; i<10; i++) {
            new File(generatedFileDir, "${i}.txt").text = i
        }
    }
}

If we run the build two more times, we will see that the generator task says it is up-to-date after the first run. We can confirm this if we look at the --info output again:

Skipping task ':generator' as it is up-to-date (took 0.007 secs).

But we have introduced a new problem. If we increase the number of files generated (say, from 10 to 20), the generator task does not re-run. We could work around this by doing a clean build each time we need to change that parameter, but this workaround is error-prone.

We can tell Gradle what can impact the generator task and require it to re-execute. We can use TaskInputs to declare certain properties as inputs to the task as well as input files. If any of these inputs change, Gradle will know to execute the task. Note the use of inputs below:

build.gradle

task generator() {
    def fileCount = 10
    inputs.property "fileCount", fileCount
    def generatedFileDir = file("$buildDir/generated")
    outputs.dir generatedFileDir
    doLast {
        generatedFileDir.mkdirs()
        for (int i=0; i<fileCount; i++) {
            new File(generatedFileDir, "${i}.txt").text = i
        }
    }
}

We can check this by examining the --info output after we change the value of the fileCount property:

Executing task ':generator' (up-to-date check took 0.007 secs) due to:
Value of input property 'fileCount' has changed for task ':generator'

Inferring task dependencies

So far, we have only worked on the generator task, but we have not reduced any of the repetition we have in the build script. We have an explicit task dependency and a duplicated output directory path. Let’s try removing the task dependencies by relying on how CopySpec#from evaluates arguments with Project#files. Gradle can automatically add task dependencies for us. This also adds the output of the generator task as inputs to the zip task.

build.gradle

task zip(type: Zip) {
    from generator
}

Inferred task dependencies can be easier to maintain than explicit task dependencies when there is a strong producer-consumer relationship between tasks. When you only need some of the output from another task, explicit task dependencies will usually be cleaner. There is nothing wrong with using both explicit task dependencies and inferred dependencies, if that is easier to understand.

Simplifying with a custom task

We call tasks like generator ad-hoc tasks. They do not have well-defined properties nor predefined actions to perform. It is okay to use ad-hoc tasks to perform simple actions, but a better practice is to move ad-hoc tasks into custom task classes. Custom tasks let you remove a lot of boilerplate and standardize common actions within your build.

Gradle makes it really easy to add new task types. You can start playing around with custom task types directly in your build file. When using annotations like @OutputDirectory, Gradle will create output directories before your task executes, so you do not have to worry about making the directories yourself. Other annotations, like @Input and @InputFiles, have the same effect as manually configuring a task’s TaskInputs.

Try creating a custom task class named Generate that produces the same output as the generator task above. Your build file should look like the following:

build.gradle

task generator(type: Generate) {
    fileCount = 20
}

task zip(type: Zip) {
    from generator
}

Here is our solution:

build.gradle

class Generate extends DefaultTask {
    @Input
    int fileCount = 10

    @OutputDirectory
    File generatedFileDir = project.file("${project.buildDir}/generated")

    @TaskAction
    void perform() {
        for (int i=0; i<fileCount; i++) {
            new File(generatedFileDir, "${i}.txt").text = i
        }
    }
}

Notice that we no longer need to create the output directory manually. The annotation on generatedFileDir takes care of this for us. The annotation on fileCount tells Gradle that this property should be considered an input in the same way we used inputs.property before. Finally, the annotation on perform() defines the action for Generate tasks.

Final notes about incremental builds

When developing your own build scripts, plugins and custom tasks, declaring task inputs and outputs is an important technique to keep in your toolbox. All of the core Gradle tasks use this to great effect. If you would like to learn about other ways to make your tasks incremental at a lower level, take a look at the incubating incremental task support. Incremental tasks provide a fine-grained way of building only what has changed when a task needs to execute.