增量任务
在 Gradle 中,由于增量构建功能,实现一个当其输入和输出已经是UP-TO-DATE
时跳过执行的任务是简单而高效的。
然而,有时自上次执行以来只有少数输入文件发生了变化,最好避免重新处理所有未更改的输入。这种情况在将输入文件一对一转换为输出文件的任务中很常见。
为了优化你的构建过程,你可以使用增量任务。这种方法确保只处理过时的输入文件,从而提高构建性能。
实现增量任务
为了让任务增量处理输入,该任务必须包含一个增量任务动作。
这是一个只有一个InputChanges参数的任务动作方法。该参数告诉 Gradle 该动作只希望处理更改的输入。
此外,任务需要通过使用@Incremental
或@SkipWhenEmpty
声明至少一个增量文件输入属性
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)
}
}
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"
}
}
要查询输入文件属性的增量更改,该属性必须始终返回相同的实例。最简单的方法是使用以下属性类型之一: 你可以在延迟配置中了解更多关于 |
增量任务动作可以使用InputChanges.getFileChanges()
来查找给定基于文件的输入属性(无论是RegularFileProperty
、DirectoryProperty
还是ConfigurableFileCollection
类型)中哪些文件已更改。
该方法返回一个类型为FileChanges的Iterable
,该Iterable又可以查询以下内容
以下示例演示了一个具有目录输入的增量任务。它假定该目录包含一组文本文件,并将它们复制到输出目录,同时反转每个文件中的文本
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())
}
}
}
}
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 可以智能地确定哪些输入文件需要处理,这个概念称为增量执行。
在这种情况下,org.gradle.work.InputChanges
类中可用的InputChanges.getFileChanges()
方法提供了与给定属性相关联的所有输入文件的详细信息,这些文件已被ADDED
、REMOVED
或MODIFIED
。
然而,在许多情况下,Gradle 无法确定哪些输入文件需要处理(即非增量执行)。例如包括
-
没有可用的先前执行历史记录。
-
你正在使用不同版本的 Gradle 进行构建。目前,Gradle 不使用来自不同版本的任务历史记录。
-
添加到任务的
upToDateWhen
条件返回false
。 -
自上次执行以来,输入属性已更改。
-
自上次执行以来,非增量输入文件属性已更改。
-
自上次执行以来,一个或多个输出文件已更改。
在这些情况下,Gradle 会将所有输入文件报告为ADDED
,并且getFileChanges()
方法将返回构成给定输入属性的所有文件的详细信息。
你可以使用InputChanges.isIncremental()
方法检查任务执行是否是增量的。
增量任务的实际应用
考虑一个针对一组输入首次执行的IncrementalReverseTask
实例。
在这种情况下,所有输入都将被视为ADDED
,如下所示
tasks.register<IncrementalReverseTask>("incrementalReverse") {
inputDir = file("inputs")
outputDir = layout.buildDirectory.dir("outputs")
inputProperty = project.findProperty("taskInputProperty") as String? ?: "original"
}
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()
返回。
以下示例修改了一个文件的内容并在运行增量任务之前添加了另一个文件
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.")
}
}
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
各种变异任务(updateInputs 、removeInput 等)仅用于演示增量任务的行为。它们不应被视为你自己的构建脚本中应该拥有的任务或任务实现类型。 |
当现有输入文件被删除时,重新执行任务会导致该文件由InputChanges.getFileChanges()
作为REMOVED
返回。
以下示例在执行增量任务之前删除了一个现有文件
tasks.register<Delete>("removeInput") {
delete("inputs/3.txt")
}
tasks.register('removeInput', Delete) {
delete 'inputs/3.txt'
}
$ gradle -q removeInput incrementalReverse Executing incrementally REMOVED: 3.txt
当输出文件被删除(或修改)时,Gradle 无法确定哪些输入文件已过时。在这种情况下,给定属性的所有输入文件的详细信息由InputChanges.getFileChanges()
返回。
以下示例从构建目录中删除一个输出文件。然而,所有输入文件都被视为ADDED
tasks.register<Delete>("removeOutput") {
delete(layout.buildDirectory.file("outputs/1.txt"))
}
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
。
以下示例在运行incrementalReverse
任务时将项目属性taskInputProperty
设置为新值。该项目属性用于初始化任务的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注释
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
类型的任务实例,并通过暴露的选项从命令行提供了一个值
tasks.register<UrlVerify>("verifyUrl")
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
。
缺少选项将使用属性的默认值。对于每个布尔选项,会自动创建一个相反的选项。例如,为提供的选项--foo
创建--no-foo
,为--no-bar
创建--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>
,其中T
是Double
、Integer
、Long
、String
、enum
-
描述一个可以接受给定类型多个值的选项。
选项的值必须作为多个声明提供,例如--image-id=123 --image-id=456
。
目前不支持其他表示法,例如逗号分隔列表或由空格字符分隔的多个值。 ListProperty<T>
,SetProperty<T>
,其中T
是Double
、Integer
、Long
、String
、enum
-
描述一个可以接受给定类型多个值的选项。
选项的值必须作为多个声明提供,例如--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
的可用值列表
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
}
}
列出命令行选项
使用Option和OptionValues注释的命令行选项是自文档化的。
$ 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
不起作用,即使check
任务依赖于test
任务。 -
如果你指定的任务选项名称与内置 Gradle 选项的名称冲突,请在调用任务之前使用
--
分隔符来引用该选项。有关更多信息,请参见区分任务选项与内置选项。
验证失败
通常,任务执行期间抛出的异常会导致失败,从而立即终止构建。任务的结果将是FAILED
,构建的结果将是FAILED
,并且不会执行任何后续任务。当使用--continue
标志运行时,Gradle 在遇到任务失败后将继续运行构建中请求的其他任务。但是,任何依赖于失败任务的任务将不会执行。
有一种特殊类型的异常,当后续任务仅依赖于失败任务的输出时,其行为不同。任务可以抛出VerificationException的子类型,以指示它以受控方式失败,以便其输出仍然对消费者有效。当一个任务直接使用dependsOn
依赖于另一个任务时,它依赖于另一个任务的结果。当 Gradle 带有--continue
运行,消费者任务依赖于生产者任务的输出(通过任务输入和输出之间的关系)时,即使生产者失败,消费者任务仍然可以运行。
例如,失败的单元测试将导致测试任务的失败结果。但是,这并不妨碍另一个任务读取和处理该任务产生的(有效)测试结果。Test Report Aggregation Plugin
正是以这种方式使用验证失败的。
验证失败对于即使在产生可供其他任务使用的有用输出后仍需要报告失败的任务也很有用。
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)
}
}
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 | 任务失败:任务抛出VerificationException 并在此处失败。 |
4 | 继续修改输出:由于异常停止任务,此行永远不会运行。 |
5 | 消费输出:postProcess 任务依赖于process 任务的输出,因为它将该任务的输出用作自己的输入。 |
6 | 使用部分结果:设置--continue 标志后,尽管process 任务失败,Gradle 仍会运行请求的postProcess 任务。postProcess 可以读取并显示部分(但仍然有效)结果。 |