增量任务
在 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>
-
描述具有任意 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
直接依赖于另一个任务时,它依赖于另一个任务的结果。当 Gradle 使用 --continue
运行时,依赖于生产者任务的输出(通过任务输入和输出之间的关系)的使用者任务仍然可以在生产者失败后运行。
例如,失败的单元测试将导致测试任务的失败结果。但是,这不妨碍另一个任务读取和处理任务生成的(有效)测试结果。测试报告聚合插件
正是以这种方式使用验证失败的。
即使在生成其他任务可使用的有用输出后仍需要报告失败的任务,验证失败也很有用。
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 可以读取和显示部分(但仍然有效)的结果。 |