增量任务

UP-TO-DATE在 Gradle 中,由于增量构建功能,实现在输入和输出已经存在时跳过执行的任务既简单又高效。

然而,有时自上次执行以来只有少数输入文件发生了变化,最好避免重新处理所有未更改的输入。这种情况在一对一地将输入文件转换为输出文件的任务中很常见。

要优化构建过程,您可以使用增量任务。这种方法可确保仅处理过时的输入文件,从而提高构建性能。

实施增量任务

对于增量处理输入的任务,该任务必须包含增量任务操作。

这是一个具有单个InputChanges参数的任务操作方法。该参数告诉 Gradle 该操作只想处理更改的输入。

此外,该任务需要使用@Incremental或声明至少一个增量文件输入属性@SkipWhenEmpty

build.gradle.kts
public class IncrementalReverseTask : DefaultTask() {

    @get:Incremental
    @get:InputDirectory
    val inputDir: DirectoryProperty = project.objects.directoryProperty()

    @get:OutputDirectory
    val outputDir: DirectoryProperty = project.objects.directoryProperty()

    @get:Input
    val inputProperty: RegularFileProperty = project.objects.fileProperty() // File input property

    @TaskAction
    fun execute(inputs: InputChanges) { // InputChanges parameter
        val msg = if (inputs.isIncremental) "CHANGED inputs are out of date"
                  else "ALL inputs are out of date"
        println(msg)
    }
}
build.gradle
class IncrementalReverseTask extends DefaultTask {

    @Incremental
    @InputDirectory
    def File inputDir

    @OutputDirectory
    def File outputDir

    @Input
    def inputProperty // File input property

    @TaskAction
    void execute(InputChanges inputs) { // InputChanges parameter
        println inputs.incremental ? "CHANGED inputs are out of date"
                                   : "ALL inputs are out of date"
    }
}

要查询输入文件属性的增量更改,该属性必须始终返回相同的实例。实现此目的的最简单方法是使用以下属性类型之一:RegularFilePropertyDirectoryPropertyConfigurableFileCollection

您可以在延迟配置中了解有关RegularFileProperty和 的更多信息。DirectoryProperty

增量任务操作可用于InputChanges.getFileChanges()找出给定的基于文件的输入属性(类型为或 )RegularFileProperty的文件已更改。DirectoryPropertyConfigurableFileCollection

该方法返回FileChangesIterable类型,进而可以查询以下内容:

以下示例演示了具有目录输入的增量任务。它假设该目录包含文本文件的集合,并将它们复制到输出目录,反转每个文件中的文本:

build.gradle.kts
abstract class IncrementalReverseTask : DefaultTask() {
    @get:Incremental
    @get:PathSensitive(PathSensitivity.NAME_ONLY)
    @get:InputDirectory
    abstract val inputDir: DirectoryProperty

    @get:OutputDirectory
    abstract val outputDir: DirectoryProperty

    @get:Input
    abstract val inputProperty: Property<String>

    @TaskAction
    fun execute(inputChanges: InputChanges) {
        println(
            if (inputChanges.isIncremental) "Executing incrementally"
            else "Executing non-incrementally"
        )

        inputChanges.getFileChanges(inputDir).forEach { change ->
            if (change.fileType == FileType.DIRECTORY) return@forEach

            println("${change.changeType}: ${change.normalizedPath}")
            val targetFile = outputDir.file(change.normalizedPath).get().asFile
            if (change.changeType == ChangeType.REMOVED) {
                targetFile.delete()
            } else {
                targetFile.writeText(change.file.readText().reversed())
            }
        }
    }
}
build.gradle
abstract class IncrementalReverseTask extends DefaultTask {
    @Incremental
    @PathSensitive(PathSensitivity.NAME_ONLY)
    @InputDirectory
    abstract DirectoryProperty getInputDir()

    @OutputDirectory
    abstract DirectoryProperty getOutputDir()

    @Input
    abstract Property<String> getInputProperty()

    @TaskAction
    void execute(InputChanges inputChanges) {
        println(inputChanges.incremental
            ? 'Executing incrementally'
            : 'Executing non-incrementally'
        )

        inputChanges.getFileChanges(inputDir).each { change ->
            if (change.fileType == FileType.DIRECTORY) return

            println "${change.changeType}: ${change.normalizedPath}"
            def targetFile = outputDir.file(change.normalizedPath).get().asFile
            if (change.changeType == ChangeType.REMOVED) {
                targetFile.delete()
            } else {
                targetFile.text = change.file.text.reverse()
            }
        }
    }
}
属性的类型inputDir、其注释以及execute()用于getFileChanges()处理自上次构建以来已更改的文件子集的操作。如果相应的输入文件已被删除,则该操作将删除目标文件。

如果由于某种原因,任务以非增量方式执行(--rerun-tasks例如,通过使用 运行),则所有文件都将报告为ADDED,无论之前的状态如何。在这种情况下,Gradle 会自动删除之前的输出,因此增量任务必须只处理给定的文件。

对于像上面的示例这样的简单转换器任务,任务操作必须为任何过时的输入生成输出文件,并为任何已删除的输入删除输出文件。

一个任务可能只包含一个增量任务操作。

哪些输入被认为是过时的?

当之前执行过某个任务,并且执行后唯一的更改是增量输入文件属性时,Gradle 可以智能地确定需要处理哪些输入文件,这一概念称为增量执行。

在这种情况下,类InputChanges.getFileChanges()中可用的方法org.gradle.work.InputChanges提供与给定属性关联的所有输入文件的详细信息,这些输入文件已被ADDEDREMOVEDMODIFIED

然而,很多情况下Gradle无法确定哪些输入文件需要处理(即非增量执行)。示例包括:

  • 没有先前执行的可用历史记录。

  • 您正在使用不同版本的 Gradle 进行构建。目前,Gradle 不使用不同版本的任务历史记录。

  • upToDateWhen添加到任务中的条件返回false

  • 自上次执行以来,输入属性已更改。

  • 自上次执行以来,非增量输入文件属性已更改。

  • 自上次执行以来,一个或多个输出文件已更改。

在这些情况下,Gradle 会将所有输入文件报告为ADDED,并且该getFileChanges()方法将返回构成给定输入属性的所有文件的详细信息。

您可以使用该方法检查任务执行是否是增量的InputChanges.isIncremental()

正在执行的增量任务

IncrementalReverseTask考虑第一次针对一组输入执行的实例。

在这种情况下,将考虑所有输入ADDED,如下所示:

build.gradle.kts
tasks.register<IncrementalReverseTask>("incrementalReverse") {
    inputDir = file("inputs")
    outputDir = layout.buildDirectory.dir("outputs")
    inputProperty = project.findProperty("taskInputProperty") as String? ?: "original"
}
build.gradle
tasks.register('incrementalReverse', IncrementalReverseTask) {
    inputDir = file('inputs')
    outputDir = layout.buildDirectory.dir("outputs")
    inputProperty = project.properties['taskInputProperty'] ?: 'original'
}

构建布局:

.
├── build.gradle
└── inputs
    ├── 1.txt
    ├── 2.txt
    └── 3.txt
$ gradle -q incrementalReverse
Executing non-incrementally
ADDED: 1.txt
ADDED: 2.txt
ADDED: 3.txt

当然,当任务没有任何改变地再次执行时,那么整个任务就是UP-TO-DATE,并且任务动作不会被执行:

$ gradle incrementalReverse
> Task :incrementalReverse UP-TO-DATE

BUILD SUCCESSFUL in 0s
1 actionable task: 1 up-to-date

当以某种方式修改输入文件或添加新的输入文件时,重新执行任务会导致InputChanges.getFileChanges().

以下示例在运行增量任务之前修改一个文件的内容并添加另一个文件:

build.gradle.kts
tasks.register("updateInputs") {
    val inputsDir = layout.projectDirectory.dir("inputs")
    outputs.dir(inputsDir)
    doLast {
        inputsDir.file("1.txt").asFile.writeText("Changed content for existing file 1.")
        inputsDir.file("4.txt").asFile.writeText("Content for new file 4.")
    }
}
build.gradle
tasks.register('updateInputs') {
    def inputsDir = layout.projectDirectory.dir('inputs')
    outputs.dir(inputsDir)
    doLast {
        inputsDir.file('1.txt').asFile.text = 'Changed content for existing file 1.'
        inputsDir.file('4.txt').asFile.text = 'Content for new file 4.'
    }
}
$ gradle -q updateInputs incrementalReverse
Executing incrementally
MODIFIED: 1.txt
ADDED: 4.txt
各种突变任务(updateInputsremoveInput等)仅用于演示增量任务的行为。它们不应被视为您应该在自己的构建脚本中拥有的任务类型或任务实现。

InputChanges.getFileChanges()当删除现有输入文件时,重新执行任务会导致as返回该文件REMOVED

以下示例在执行增量任务之前删除现有文件之一:

build.gradle.kts
tasks.register<Delete>("removeInput") {
    delete("inputs/3.txt")
}
build.gradle
tasks.register('removeInput', Delete) {
    delete 'inputs/3.txt'
}
$ gradle -q removeInput incrementalReverse
Executing incrementally
REMOVED: 3.txt

当删除(或修改)输出文件时,Gradle 无法确定哪些输入文件已过期。在这种情况下,给定属性的所有输入文件的详细信息都由InputChanges.getFileChanges().

以下示例从构建目录中删除输出文件之一。但是,所有输入文件都被视为ADDED

build.gradle.kts
tasks.register<Delete>("removeOutput") {
    delete(layout.buildDirectory.file("outputs/1.txt"))
}
build.gradle
tasks.register('removeOutput', Delete) {
    delete layout.buildDirectory.file("outputs/1.txt")
}
$ gradle -q removeOutput incrementalReverse
Executing non-incrementally
ADDED: 1.txt
ADDED: 2.txt
ADDED: 3.txt

我们要介绍的最后一个场景涉及修改非基于文件的输入属性时会发生什么。在这种情况下,Gradle 无法确定该属性如何影响任务输出,因此任务以非增量方式执行。这意味着给定属性的所有InputChanges.getFileChanges()输入文件都由 返回,并且它们都被视为ADDED.

以下示例taskInputProperty在运行任务时将项目属性设置为新值incrementalReverse。该项目属性用于初始化任务的属性,如本节第一个示例inputProperty中所示。

这是本例中的预期输出:

$ gradle -q -PtaskInputProperty=changed incrementalReverse
Executing non-incrementally
ADDED: 1.txt
ADDED: 2.txt
ADDED: 3.txt

命令行选项

有时,用户希望在命令行而不是构建脚本上声明公开任务属性的值。如果属性值更改更频繁,则在命令行上传递属性值特别有用。

任务API支持标记属性的机制,以在运行时自动生成具有特定名称的相应命令行参数。

步骤 1. 声明命令行选项

要公开任务属性的新命令行选项,请使用Option注释属性的相应 setter 方法:

@Option(option = "flag", description = "Sets the flag")

选项需要强制标识符。您可以提供可选的描述。

任务可以公开与类中可用属性一样多的命令行选项。

选项也可以在任务类的超级接口中声明。如果多个接口声明相同的属性但具有不同的选项标志,则它们都将用于设置该属性。

在下面的示例中,自定义任务UrlVerify通过进行 HTTP 调用并检查响应代码来验证是否可以解析 URL。要验证的 URL 可以通过 属性进行配置url。该属性的 setter 方法用@Option注释:

网址验证.java
import org.gradle.api.tasks.options.Option;

public class UrlVerify extends DefaultTask {
    private String url;

    @Option(option = "url", description = "Configures the URL to be verified.")
    public void setUrl(String url) {
        this.url = url;
    }

    @Input
    public String getUrl() {
        return url;
    }

    @TaskAction
    public void verify() {
        getLogger().quiet("Verifying URL '{}'", url);

        // verify URL by making a HTTP call
    }
}

为任务声明的所有选项都可以通过运行任务和选项呈现为控制台输出help--task

步骤 2. 在命令行上使用选项

命令行上的选项有一些规则:

  • 该选项使用双破折号作为前缀,例如--url。单个破折号不符合任务选项的有效语法。

  • 选项参数直接跟在任务声明之后,例如verifyUrl --url=http://www.google.com/

  • 可以在命令行上紧跟任务名称的任意顺序声明多个任务选项。

在前面的示例的基础上,构建脚本创建一个类型的任务实例UrlVerify,并通过公开的选项从命令行提供一个值:

build.gradle.kts
tasks.register<UrlVerify>("verifyUrl")
build.gradle
tasks.register('verifyUrl', UrlVerify)
$ gradle -q verifyUrl --url=http://www.google.com/
Verifying URL 'http://www.google.com/'

选项支持的数据类型

Gradle 限制了可用于声明命令行选项的数据类型。

命令行的使用因类型而异:

boolean, Boolean,Property<Boolean>

true描述带有值或 的选项false
在命令行上传递选项会将值视为true。例如,--foo等于true
如果缺少该选项,则使用该属性的默认值。对于每个布尔选项,都会自动创建一个相反的选项。例如,--no-foo是为提供的选项创建的--foo--bar是为 创建的--no-bar。名称以 开头的选项--no是禁用选项,并将选项值设置为false。仅当任务不存在同名选项时才会创建相反的选项。

Double,Property<Double>

描述具有双精度值的选项。
在命令行上传递选项还需要一个值,例如--factor=2.2--factor 2.2

Integer,Property<Integer>

描述具有整数值的选项。
在命令行上传递选项还需要一个值,例如--network-timeout=5000--network-timeout 5000

Long,Property<Long>

描述具有长值的选项。
在命令行上传递选项还需要一个值,例如--threshold=2147483648--threshold 2147483648

String,Property<String>

描述具有任意字符串值的选项。
在命令行上传递选项还需要一个值,例如--container-id=2x94held--container-id 2x94held

enum,Property<enum>

将选项描述为枚举类型。
在命令行上传递选项还需要一个值,例如--log-level=DEBUG--log-level debug
该值不区分大小写。

List<T>哪里,,,,TDoubleIntegerLongStringenum

描述可以采用给定类型的多个值的选项。
选项的值必须作为多个声明提供,例如--image-id=123 --image-id=456.
目前不支持其他表示法,例如逗号分隔的列表或由空格字符分隔的多个值。

ListProperty<T>SetProperty<T>哪里,,,,TDoubleIntegerLongStringenum

描述可以采用给定类型的多个值的选项。
选项的值必须作为多个声明提供,例如--image-id=123 --image-id=456.
目前不支持其他表示法,例如逗号分隔的列表或由空格字符分隔的多个值。

DirectoryProperty,RegularFileProperty

描述带有文件系统元素的选项。
在命令行上传递选项还需要一个表示路径的值,例如--output-file=file.txt--output-dir outputDir
相对路径是相对于拥有此属性实例的项目的项目目录进行解析的。看FileSystemLocationProperty.set()

记录选项的可用值

理论上,属性类型String或选项List<String>可以接受任何任意值。可以借助注释OptionValues以编程方式记录此类选项的可接受值:

@OptionValues('file')

可以将此注释分配给返回List受支持数据类型之一的任何方法。您需要指定一个选项标识符来指示选项和可用值之间的关系。

在命令行上传递选项不支持的值不会使构建失败或引发异常。您必须在任务操作中实现此类行为的自定义逻辑。

下面的示例演示了对单个任务使用多个选项。任务实现提供了选项的可用值列表output-type

UrlProcess.java
import org.gradle.api.tasks.options.Option;
import org.gradle.api.tasks.options.OptionValues;

public abstract class UrlProcess extends DefaultTask {
    private String url;
    private OutputType outputType;

    @Input
    @Option(option = "http", description = "Configures the http protocol to be allowed.")
    public abstract Property<Boolean> getHttp();

    @Option(option = "url", description = "Configures the URL to send the request to.")
    public void setUrl(String url) {
        if (!getHttp().getOrElse(true) && url.startsWith("http://")) {
            throw new IllegalArgumentException("HTTP is not allowed");
        } else {
            this.url = url;
        }
    }

    @Input
    public String getUrl() {
        return url;
    }

    @Option(option = "output-type", description = "Configures the output type.")
    public void setOutputType(OutputType outputType) {
        this.outputType = outputType;
    }

    @OptionValues("output-type")
    public List<OutputType> getAvailableOutputTypes() {
        return new ArrayList<OutputType>(Arrays.asList(OutputType.values()));
    }

    @Input
    public OutputType getOutputType() {
        return outputType;
    }

    @TaskAction
    public void process() {
        getLogger().quiet("Writing out the URL response from '{}' to '{}'", url, outputType);

        // retrieve content from URL and write to output
    }

    private static enum OutputType {
        CONSOLE, FILE
    }
}

列出命令行选项

使用注释OptionOptionValues的命令行选项是自记录的。

您将看到声明的选项及其可用值反映在任务的控制台输出中help。输出按字母顺序呈现选项,但布尔禁用选项除外,这些选项出现在启用选项之后:

$ gradle -q help --task processUrl
Detailed task information for processUrl

Path
     :processUrl

Type
     UrlProcess (UrlProcess)

Options
     --http     Configures the http protocol to be allowed.

     --no-http     Disables option --http.

     --output-type     Configures the output type.
                       Available values are:
                            CONSOLE
                            FILE

     --url     Configures the URL to send the request to.

     --rerun     Causes the task to be re-run even if up-to-date.

Description
     -

Group
     -

局限性

目前对声明命令行选项的支持存在一些限制。

  • 只能通过注释为自定义任务声明命令行选项。没有用于定义选项的编程等效项。

  • 选项不能全局声明,例如,在项目级别或作为插件的一部分。

  • 当在命令行上分配选项时,需要明确说明暴露该选项的任务,例如,即使该任务依赖于该任务gradle check --tests abc,也不起作用。checktest

  • 如果您指定的任务选项名称与内置 Gradle 选项的名称冲突,请--在调用任务之前使用分隔符来引用该选项。有关详细信息,请参阅消除任务选项与内置选项的歧义

验证失败

通常,任务执行期间引发的异常会导致失败并立即终止构建。任务的结果将为FAILED,构建的结果将为FAILED,并且不会执行进一步的任务。当使用标志运行--continue时,Gradle 在遇到任务失败后将继续运行构建中的其他请求的任务。但是,任何依赖于失败任务的任务都不会被执行。

有一种特殊类型的异常,当下游任务仅依赖于失败任务的输出时,其行为会有所不同。任务可以抛出VerificationException的子类型来指示它以受控方式失败,以便其输出对使用者仍然有效。当一个任务直接依赖于另一个任务的结果时,它依赖于另一个任务的结果dependsOn。当 Gradle 使用 运行时--continue,依赖于生产者任务输出(通过任务输入和输出之间的关系)的消费者任务在消费者失败后仍然可以运行。

例如,失败的单元测试将导致测试任务失败。但是,这并不能阻止另一个任务读取和处理该任务生成的(有效)测试结果。验证失败正是以这种方式使用的Test Report Aggregation Plugin

验证失败对于即使在产生其他任务可消耗的有用输出之后也需要报告失败的任务也很有用。

build.gradle.kts
val process = tasks.register("process") {
    val outputFile = layout.buildDirectory.file("processed.log")
    outputs.files(outputFile) (1)

    doLast {
        val logFile = outputFile.get().asFile
        logFile.appendText("Step 1 Complete.") (2)
        throw VerificationException("Process failed!") (3)
        logFile.appendText("Step 2 Complete.") (4)
    }
}

tasks.register("postProcess") {
    inputs.files(process) (5)

    doLast {
        println("Results: ${inputs.files.singleFile.readText()}") (6)
    }
}
build.gradle
tasks.register("process") {
    def outputFile = layout.buildDirectory.file("processed.log")
    outputs.files(outputFile) (1)

    doLast {
        def logFile = outputFile.get().asFile
        logFile << "Step 1 Complete." (2)
        throw new VerificationException("Process failed!") (3)
        logFile << "Step 2 Complete." (4)
    }
}

tasks.register("postProcess") {
    inputs.files(tasks.named("process")) (5)

    doLast {
        println("Results: ${inputs.files.singleFile.text}") (6)
    }
}
$ gradle postProcess --continue
> Task :process FAILED

> Task :postProcess
Results: Step 1 Complete.
2 actionable tasks: 2 executed

FAILURE: Build failed with an exception.
1 注册输出process任务将其输出写入日志文件。
2 修改输出:任务在执行时写入其输出文件。
3 任务失败:任务抛出 aVerificationException并在此时失败。
4 继续修改输出:由于异常停止任务,该行永远不会运行。
5 使用输出:由于使用任务的输出作为其自己的输入,因此postProcess任务取决于任务的输出。process
6 使用部分结果:设置标志后,尽管任务失败,--continueGradle 仍会运行请求的任务。可以读取并显示部分(尽管仍然有效)结果。postProcessprocesspostProcess