Loading...

April 21, 2017

Gradle Goodness: Using Incremental Task Action

Gradle has incremental build support to speed up our builds. This means Gradle checks input and output for a task and if something changed the task is executed, otherwise the task is skipped. In previous posts we learned how to add incremental build support to our tasks with annotations and inputs and outputs property of a task. When we have a task that has an output file for an input file, like with transformations, we can have a more efficient task using an incremental task action. With an incremental task action we have extra information on the files that are handled by the task. We can have different actions based on if an input file is out of date or removed. This way we can handle only the input files that have changed or removed with incremental builds, instead of all the input files.

To create an incremental task action we must have a task action method (annotated with @TaskAction) that has a single argument of type IncrementalTaskInputs. The IncrementalTaskInputs class has the method outOfDate and removed. These methods take an action, that can be implemented with a closure, with an instance of InputFileDetails as argument. We can get to the input file via this instance and use that for our task logic. When an input file is out of date, because the file contents has changed or the output file has been removed, the action we defined for the outOfDate method is invoked. If the input file is removed the action for the method removed is invoked.

In the following example we have a task HtmlConverter with a task action that support incremental builds for input files. When an input file has changed it is processed, otherwise it is skipped. In the build file we create the task convert that uses the HtmlConverter task class:

// Create task to convert text to HTML.
task convert(type: HtmlConverter) {
    sourceDir = file('src/docs/text')
    outputDir = file("${buildDir}/html")
}

import groovy.xml.MarkupBuilder
import groovy.transform.CompileStatic
import groovy.transform.CompileDynamic

/**
 * Simple Gradle task that takes an text
 * input file and converts it to a HTML file.
 */
@CompileStatic
class HtmlConverter extends DefaultTask {

    @InputDirectory
    @PathSensitive(PathSensitivity.RELATIVE)
    File sourceDir

    @OutputDirectory
    File outputDir

    /**
     * Task action that will check if a source file is out of date
     * or removed. If out of date the source file is converted
     * to HTML. If source file is removed the generated 
     * HTML file is removed.
     *
     * @param inputs Used for incremental task action.
     */
    @TaskAction
    void convert(IncrementalTaskInputs inputs) {
        // If the user for example used --rerun-tasks
        // this task is not incremental. Only
        // inputs.outOfDate is executed, so we must first 
        // remove all output files.
        if (!inputs.incremental) {
            project.delete(outputDir.listFiles())
        }

        // Input file has changed, so we convert it.
        inputs.outOfDate { InputFileDetails outOfDate ->
            convertFile(outOfDate.file)
        }

        // Input file is removed, so we remove the
        // output file that was created for the input file.
        inputs.removed { InputFileDetails removed ->
            removeOutputFile(removed.file)
        }
    }

    /**
     * Convert text file to HTML.
     *
     * @param file Text file to convert to HTML.
     */
    private void convertFile(final File file) {
        logger.lifecycle 'Convert file {}', file.name
        final lines = file.readLines()
        final outputWriter = new FileWriter(new File(outputDir, outputFilename(file)))
        writeHtml(lines, outputWriter)
    }

    /**
     * Use first line as title for HTML, rest is body.
     *
     * @param lines Lines to transform to HTML.
     * @param writer Writer to write HTML to.
     */
    @CompileDynamic
    private void writeHtml(final List lines, final Writer writer) {
        final html = new MarkupBuilder(writer)
        html.html {
            head {
                title lines[0]
            }
            body {
                lines[2..-1].each { line ->
                    p line
                }
            }
        }
    }

    /**
     * Remove the output file thas was created for the
     * given input file.
     *
     * @param file Input file to remove output file for.
     */
    private void removeOutputFile(final File file) {
        logger.lifecycle 'Remove HTML for file {}', file.name
        new File(outputDir, outputFilename(file)).delete()
    }

    /**
     * Determine HTML output filename based on base of input filename.
     *
     * @param file Used to create HTML output file name.
     */
    private String outputFilename(final File file) {
        file.name[0..file.name.lastIndexOf('.')] + 'html'
    }

}

In our project we have 3 source files in the directory src/docs/text: sample1.txt, sample2.txt and hello.txt. We run the convert task for the first time and we see all input files are processed:

$ gradle convert
:convert
Convert file hello.txt
Convert file sample1.txt
Convert file sample2.txt

BUILD SUCCESSFUL

Total time: 0.948 secs

Next we change hello.txt and when re-run the task we see only our changed file is processed. If we rename it after the change we can see the hello.html is removed and the new file is processed:

$ echo "Gradle rocks" >> src/docs/text/hello.txt
$ gradle convert
:convert
Convert file hello.txt

BUILD SUCCESSFUL

Total time: 0.793 secs
$ mv src/docs/text/hello.txt src/docs/text/sample.txt
$ gradle convert
:convert
Convert file sample.txt
Remove HTML for file hello.txt

BUILD SUCCESSFUL

Total time: 0.76 secs
$

Written with Gradle 3.5.