增量任务
在 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
,它可以进一步查询以下信息:
以下示例演示了一个具有目录输入的增量任务。它假设该目录包含一个文本文件集合,并将它们复制到输出目录,同时反转每个文件中的文本。
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 -
限制
目前,声明命令行选项的支持存在一些限制。
-
命令行选项只能通过注解为自定义任务声明。没有定义选项的编程等价物。
-
选项不能全局声明,例如,在项目级别或作为插件的一部分。
-
在命令行上指定选项时,必须明确写出公开该选项的任务,例如,即使
check
任务依赖于test
任务,gradle check --tests abc
也无法工作。 -
如果您指定的任务选项名称与内置 Gradle 选项的名称冲突,请在调用任务之前使用
--
分隔符来引用该选项。有关更多信息,请参阅 区分任务选项和内置选项。
验证失败
通常,任务执行期间抛出的异常会导致失败,从而立即终止构建。任务结果将是 FAILED
(失败),构建结果将是 FAILED
(失败),并且不会执行后续任务。当使用 --continue
标志运行时,Gradle 在遇到任务失败后将继续运行构建中请求的其他任务。但是,任何依赖于失败任务的任务将不会执行。
有一种特殊类型的异常,当下游任务仅依赖于失败任务的输出时,其行为会有所不同。任务可以抛出 VerificationException 的子类型,以表明它以受控方式失败,但其输出对消费者仍然有效。当任务使用 dependsOn
直接依赖于另一个任务时,它依赖于该任务的结果。当使用 --continue
运行 Gradle 时,依赖于生产者任务输出(通过任务输入和输出之间的关系)的消费者任务即使在生产者失败后仍然可以运行。
例如,失败的单元测试将导致测试任务的结果为失败。然而,这并不会阻止另一个任务读取和处理该任务生成的(有效)测试结果。测试报告聚合插件
正是以这种方式使用验证失败的。
验证失败对于那些即使在产生可供其他任务使用的有用输出后仍需要报告失败的任务也非常有用。
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 可以读取并显示部分(但仍然有效)结果。 |