定义 tasks

我们已经在第 6 章学习了定义任务的形式 (keyword 形式). 当然也会有一些定义形式的变化来适应某些特殊的情况. 比如下面的例子中任务名被括号括起来了. 这是因为之前定义简单任务的形式 (keyword 形式) 在表达式里是不起作用的.
例子 15.1. 定义 tasks
build.gradle

  1. task(hello) << {
  2. println "hello"
  3. }
  4. task(copy, type: Copy) {
  5. from(file('srcDir'))
  6. into(buildDir)
  7. }

你也可以使用 strings 来定义任务的名字:
例子 15.2. 例子 tasks - 使用 strings 来定义任务的名字
build.gradle

  1. task('hello') <<
  2. {
  3. println "hello"
  4. }
  5. task('copy', type: Copy) {
  6. from(file('srcDir'))
  7. into(buildDir)
  8. }

还有另外一种语法形式来定义任务, 更加直观:
例子 15.3. 另外一种语法形式
build.gradle

  1. tasks.create(name: 'hello') << {
  2. println "hello"
  3. }
  4. tasks.create(name: 'copy', type: Copy) {
  5. from(file('srcDir'))
  6. into(buildDir)
  7. }

这里实际上我们把任务加入到 tasks collection 中. 可以看一看 TaskContainer 来深入了解下.

定位 tasks

你经常需要在构建文件里找到你定义的 tasks, 举个例子, 为了配置它们或者使用它们作为依赖. 有许多种方式都可以来实现定位. 首先, 每一个任务都必须是一个 project 的有效属性, 并使用任务名来作为属性名:
例子 15.4. 通过属性获取 tasks
build.gradle

  1. task hello
  2. println hello.name
  3. println project.hello.name

Tasks 也可以通过 tasks collection 来得到.
例子 15.5. 通过 tasks collection 获取 tasks
build.gradle

  1. task hello
  2. println tasks.hello.name
  3. println tasks['hello'].name

你也可以使用 tasks.getByPath() 方法通过任务的路径来使用任何 project 里的任务. 你可以通过使用任务的名字, 任务的相对路径或者绝对路径作为 getByPath() 方法的输入.
例子 15.6. 通过路径获取 tasks
build.gradle

  1. project(':projectA') {
  2. task hello
  3. }
  4. task hello
  5. println tasks.getByPath('hello').path
  6. println tasks.getByPath(':hello').path
  7. println tasks.getByPath('projectA:hello').path
  8. println tasks.getByPath(':projectA:hello').path

gradle -q hello 的输出

  1. > gradle -q hello
  2. :hello
  3. :hello
  4. :projectA:hello
  5. :projectA:hello

配置 tasks

举一个例子, 让我们看一看 Gradle 自带的 Copy task. 为了创建一个 Copy task, 你需要在你的构建脚本里先声明它:
例子 15.7. 创建一个 copy task
build.gradle

  1. task myCopy(type: Copy)

它创建了一个没有默认行为的 copy task. 这个 task 可以通过它的 API 来配置(参考 Copy). 接下来例子展示了不同的实现方法.
补充说明一下, 这个 task 的名字是 “myCopy”, 但是它是 “Copy” 类(type). 你可以有许多同样 type 不同名字的 tasks. 这个在实现特定类型的所有任务的 cross-cutting concerns 时特别有用.
例子 15.8. 配置一个任务 - 不同的方法
build.gradle

  1. Copy myCopy = task(myCopy, type: Copy)
  2. myCopy.from 'resources'
  3. myCopy.into 'target'
  4. myCopy.include('**/*.txt', '**/*.xml', '**/*.properties')

这个我们通过 Java 配置对象是一样的形式. 但是你每次都必须在语句里重复上下文 (myCopy). 这种方式可能读起来并不是那么的漂亮.
下面一种方式就解决了这个问题. 是公认的最具可读性的方式.
例子 15.9. 配置一个任务 - 通过闭包 closure
build.gradle

  1. task myCopy(type: Copy)
  2. myCopy {
  3. from 'resources'
  4. into 'target'
  5. include('**/*.txt', '**/*.xml', '**/*.properties')
  6. }

上面例子里的第三行是 tasks.getByName() 方法的一个简洁的写法. 特别要注意的是, 如果你通过闭包的形式来实现 getByName() 方法, 这个闭包会在 task 配置的时候执行而不是在 task 运行的时候执行.
你也可以直接在定义 task 时使用闭包.
例子 15.10. 通过定义一个任务

  1. build.gradle
  2. task copy(type: Copy) {
  3. from 'resources'
  4. into 'target'
  5. include('**/*.txt', '**/*.xml', '**/*.properties')
  6. }

请不要忘了构建的各个阶段.
一个任务有配置和动作. 当使用 << 时, 你只是简单的使用捷径定义了动作. 定义在配置区域的代码只会在构建的配置阶段执行, 而且不论执行哪个任务. 可以参考第 55 章, The Build Lifecycle for more details about the build lifecycle.

给 task 加入依赖

有许多种加入依赖的方式. 在 6.5 小节, “任务依赖”里, 你已经学习了如何使用任务的名称定义依赖. 任务名称可以指向同一个项目里的任务, 或者其他项目里的任务. 为了指向其他项目, 你必须在任务的名称前加入项目的路径. 下面的例子给 projectA:taskX 加入依赖 projectB:taskY :
例子 15.11. 从另外一个项目给任务加入依赖
build.gradle

  1. project('projectA') {
  2. task taskX(dependsOn: ':projectB:taskY') << {
  3. println 'taskX'
  4. }
  5. }
  6. project('projectB') {
  7. task taskY << {
  8. println 'taskY'
  9. }
  10. }

gradle -q taskX 的输出

  1. > gradle -q taskX
  2. taskY
  3. taskX

除了使用任务名称, 你也可以定义一个依赖对象y:
例子 15.12. 通过任务对象加入依赖
build.gradle

  1. task taskX << {
  2. println 'taskX'
  3. }
  4. task taskY << {
  5. println 'taskY'
  6. }
  7. taskX.dependsOn taskY

gradle -q taskX 的输出

  1. > gradle -q taskX
  2. taskY
  3. taskX

更加先进的用法, 你可以通过闭包定义一个任务依赖. 闭包只能返回一个单独的 Task 或者 Task 对象的 collection, 这些返回的任务就将被当做依赖. 接下来的例子给 taskX 加入了一个复杂的依赖, 所有以 lib 开头的任务都将在 taskX 之前执行:
例子 15.13. 通过闭包加入依赖
build.gradle

  1. task taskX << {
  2. println 'taskX'
  3. }
  4. taskX.dependsOn {
  5. tasks.findAll { task -> task.name.startsWith('lib') }
  6. }
  7. task lib1 << {
  8. println 'lib1'
  9. }
  10. task lib2 << {
  11. println 'lib2'
  12. }
  13. task notALib << {
  14. println 'notALib'
  15. }

gradle -q taskX 的输出

  1. > gradle -q taskX
  2. lib1
  3. lib2
  4. taskX

给 tasks 排序

任务的排序功能正在测试和优化. 请注意, 这项功能在 Gradle 之后的版本里可能会改变.

在某些情况下, 我们希望能控制任务的的执行顺序, 这种控制并不是向上一张那样去显示地加入依赖关系. 最主要的区别是我们设定的排序规则不会影响那些要被执行的任务, 只是影响执行的顺序本身. 好吧, 我知道可能有点抽象.
我们来看看以下几种有用的场景:

  • 执行连续的任务: eg. ‘build’ 从来不会在 ‘clean’ 之前执行.
  • 在 build 的一开始先运行构建确认 (build validations): eg. 在正式的发布构建前先确认我的证书是正确的.
  • 在运行长时间的检测任务前先运行快速的检测任务来获得更快的反馈: eg. 单元测试总是应该在集成测试之前被执行.
  • 一个聚集 (aggregates) 某种特定类型的所有任务结果的任务: eg. 测试报告任务 (test report task) 包含了所有测试任务的运行结果.

目前, 有 2 种可用的排序规则: “must run after”“should run after”.
当你使用 “must run after” 时即意味着 taskB 必须总是在 taskA 之后运行, 无论 taskA 和 taskB 是否将要运行:

  1. taskB.mustRunAfter(taskA)

“should run after” 规则其实和 “must run after” 很像, 只是没有那么的严格, 在 2 种情况下它会被忽略:

  1. 使用规则来阐述一个执行的循环.
  2. 当并行执行并且一个任务的所有依赖除了 “should run after” 任务其余都满足了, 那么这个任务无论它的 “should run after” 依赖是否执行, 它都可以执行. (编者: 翻译待商榷, 提供具体例子)

总之, 当要求不是那么严格时, “should run after” 是非常有用的.
即使有目前的这些规则, 我们仍可以执行 taskA 而不管 taskB, 反之亦然.
例子 15.14. 加入 ‘must run after’
build.gradle

  1. task taskX << {
  2. println 'taskX'
  3. }
  4. task taskY << {
  5. println 'taskY'
  6. }
  7. taskY.mustRunAfter taskX

gradle -q taskY taskX 的输出

  1. > gradle -q taskY taskX
  2. taskX
  3. taskY

例子 15.15. 加入 ‘should run after’
build.gradle

  1. task taskX << {
  2. println 'taskX'
  3. }
  4. task taskY << {
  5. println 'taskY'
  6. }
  7. taskY.shouldRunAfter taskX

gradle -q taskY taskX 的输出

  1. > gradle -q taskY taskX
  2. taskX
  3. taskY

在上面的例子里, 我们仍可以直接执行 taskY 而不去 taskX :
例子 15.16. 任务排序不影响任务执行
gradle -q taskY 的输出

  1. > gradle -q taskY
  2. taskY

为了在 2 个任务间定义 “must run after” 或者 “should run after” 排序, 我们需要使用 Task.mustRunAfter() 和 Task.shouldRunAfter() 方法. 这些方法接收一个任务的实例, 任务的名字或者任何 Task.dependsOn()可以接收的输入.
注意 “B.mustRunAfter(A)” 或者 “B.shouldRunAfter(A)” 并不影响任何任务间的执行依赖:

  • tasks A 和 B 可以被独立的执行. 排序规则只有当 2 个任务同时执行时才会被应用.
  • 在运行时加上 —continue, 当 A 失败时 B 仍然会执行.

之前提到过, “should run after” 规则在一个执行循环中将被忽略:
例子 15.17. ‘should run after’ 任务的忽略
build.gradle

  1. task taskX << {
  2. println 'taskX'
  3. }
  4. task taskY << {
  5. println 'taskY'
  6. }
  7. task taskZ << {
  8. println 'taskZ'
  9. }
  10. taskX.dependsOn taskY
  11. taskY.dependsOn taskZ
  12. taskZ.shouldRunAfter taskX

gradle -q taskX 的输出

  1. > gradle -q taskX
  2. taskZ
  3. taskY
  4. taskX

给 task 加入描述

你可以给你的任务加入一段描述性的文字. 它将会在任务执行的时候显示出来.
例子 15.18. 给任务加入描述
build.gradle

  1. task copy(type: Copy) {
  2. description 'Copies the resource directory to the target directory.'
  3. from 'resources'
  4. into 'target'
  5. include('**/*.txt', '**/*.xml', '**/*.properties')
  6. }

替换 tasks

有时候你想要替换一个任务. 举个例子, 如果你想要互换一个通过 java 插件定义的任务和一个自定义的不同类型的任务:
例子 14.19. 覆写一个任务
build.gradle

  1. task copy(type: Copy)
  2. task copy(overwrite: true) << {
  3. println('I am the new one.')
  4. }

gradle -q copy 的输出

  1. > gradle -q copy
  2. I am the new one.

这种方式将用你自己定义的任务替换一个 Copy 类型的任务, 因为它使用了同样的名字. 但你定义一个新的任务时, 你必须设置 overwrite 属性为 true. 否则的话 Gradle 会抛出一个异常, task with that name already exists.

跳过 tasks

Gradle 提供了好几种跳过一个任务的方式.

1. 使用判断条件 (predicate)

你可以使用 onlyIf() 方法来为一个任务加入判断条件. 就和 Java 里的 if 语句一样, 任务只有在条件判断为真时才会执行. 你通过一个闭包来实现判断条件. 闭包像变量一样传递任务, 如果任务应该被执行则返回真, 反之亦然. 判断条件在任务执行之前进行判断.
例子 15.20. 使用判断条件跳过一个任务
build.gradle

  1. task hello << {
  2. println 'hello world'
  3. }
  4. hello.onlyIf { !project.hasProperty('skipHello') }

gradle hello -PskipHello 的输出

  1. > gradle hello -PskipHello
  2. :hello SKIPPED
  3. BUILD SUCCESSFUL
  4. Total time: 1 secs

2. 使用 StopExecutionException

如果想要跳过一个任务的逻辑并不能被判断条件通过表达式表达出来, 你可以使用 StopExecutionException. 如果这个异常是被一个任务要执行的动作抛出的, 这个动作之后的执行以及所有紧跟它的动作都会被跳过. 构建将会继续执行下一个任务.
例子 15.21. 通过 StopExecutionException 跳过任务
build.gradle

  1. task compile << {
  2. println 'We are doing the compile.'
  3. }
  4. compile.doFirst {
  5. // Here you would put arbitrary conditions in real life.
  6. // But this is used in an integration test so we want defined behavior.
  7. if (true) { throw new StopExecutionException() }
  8. }
  9. task myTask(dependsOn: 'compile') << {
  10. println 'I am not affected'
  11. }

gradle -q myTask 的输出

  1. > gradle -q myTask
  2. I am not affected

如果你直接使用 Gradle 提供的任务, 这项功能还是十分有用的. 它允许你为内建的任务加入条件来控制执行. [6]

3. 激活和注销 tasks

每一个任务都有一个已经激活的标记(enabled flag), 这个标记一般默认为真. 将它设置为假, 那它的任何动作都不会被执行.
例子 15.22. 激活和注销 tasks
build.gradle

  1. task disableMe << {
  2. println 'This should not be printed if the task is disabled.'
  3. }
  4. disableMe.enabled = false

gradle disableMe 的输出

  1. > gradle disableMe
  2. :disableMe SKIPPED
  3. BUILD SUCCESSFUL
  4. Total time: 1 secs

跳过 up-to-date 的任务

如果你正在使用一些附加的任务, 比如通过 Java 插件加入的任务, 你可能会注意到 Gradle 会跳过一些任务, 这些任务后面会标注 up-to-date. 代表这个任务已经运行过了或者说是最新的状态, 不再需要产生一次相同的输出. 不仅仅是这些内建任务, 其实你在运行自己的任务时, 也会碰到这种情况.

1. 声明一个任务的输入和输出

让我们先看一个例子. 这里我们的任务会根据一个 XML 文件生成好几个输出文件. 让我们运行这个任务 2 次.
例子 15.23. A generator task
build.gradle

  1. task transform {
  2. ext.srcFile = file('mountains.xml')
  3. ext.destDir = new File(buildDir, 'generated')
  4. doLast {
  5. println "Transforming source file."
  6. destDir.mkdirs()
  7. def mountains = new XmlParser().parse(srcFile)
  8. mountains.mountain.each { mountain ->
  9. def name = mountain.name[0].text()
  10. def height = mountain.height[0].text()
  11. def destFile = new File(destDir, "${name}.txt")
  12. destFile.text = "$name -> ${height}\n"
  13. }
  14. }
  15. }

gradle transform 的输出

  1. > gradle transform
  2. :transform
  3. Transforming source file.

gradle transform 的输出

  1. > gradle transform
  2. :transform
  3. Transforming source file.

这里 Gradle 执行了这个任务两次, 即使什么都没有改变, 它也没有跳过这个任务. 这个例子里的任务, 它的行为是通过闭包定义的. Gradle 并不知道闭包会做什么, 也并不能自动指出是否这个任务是 up-to-date. 为了使用 Gradle 的 up-to-date 检测, 你需要定义任务的输入和输出.
每个任务都有输入和输出属性, 你需要使用这些属性来声明任务的输入和输出. 下面的例子中, 我们将声明 XML 文件作为输入, 并且把输出放在一个指定的目录. 让我们运行这个任务 2 次.
例子 15.24. 声明任务的输入和输出
build.gradle

  1. task transform {
  2. ext.srcFile = file('mountains.xml')
  3. ext.destDir = new File(buildDir, 'generated')
  4. inputs.file srcFile
  5. outputs.dir destDir
  6. doLast {
  7. println "Transforming source file."
  8. destDir.mkdirs()
  9. def mountains = new XmlParser().parse(srcFile)
  10. mountains.mountain.each { mountain ->
  11. def name = mountain.name[0].text()
  12. def height = mountain.height[0].text()
  13. def destFile = new File(destDir, "${name}.txt")
  14. destFile.text = "$name -> ${height}\n"
  15. }
  16. }
  17. }

gradle transform 的输出

  1. > gradle transform
  2. :transform
  3. Transforming source file.

gradle transform 的输出

  1. > gradle transform
  2. :transform UP-TO-DATE

现在, Gradle 就能够检测出任务是否是 up-to-date 状态.
任务的输入属性是 TaskInputs 类型. 任务的输出属性是 TaskOutputs 类型.
一个任务如果没有定义输出的话, 那么它永远都没用办法判断 up-to-date. 对于某些场景, 比如一个任务的输出不是文件, 或者更复杂的场景, TaskOutputs.upToDateWhen()) 方法会计算任务的输出是否应被视为最新.
总而言之, 如果一个任务只定义了输出, 如果输出不变的话, 它就会被视为 up-to-date.

2. 它是如何工作的?

当一个任务是首次执行时, Gradle 会取一个输入的快照 (snapshot). 该快照包含组输入文件和每个文件的内容的散列. 然后当 Gradle 执行任务时, 如果任务成功完成,Gradle 会获得一个输出的快照. 该快照包含输出文件和每个文件的内容的散列. Gradle 会保留这两个快照用来在该任务的下一次执行时进行判断.
之后, 每次在任务执行之前, Gradle 都会为输入和输出取一个新的快照, 如果这个快照和之前的快照一样, Gradle 就会假定这个任务已经是最新的 (up-to-date) 并且跳过任务, 反之亦然.
需要注意的是, 如果一个任务有指定的输出目录, 自从该任务上次执行以来被加入到该目录的任务文件都会被忽略, 并且不会引起任务过时 (out of date). 这是因为不相关任务也许会共用同一个输出目录. 如果这并不是你所想要的情况, 可以考虑使用 TaskOutputs.upToDateWhen())

Task 规则

有时候也想要一个任务的行为是基于已经定义好的取值范围或者特定规则, 下面的例子就提供了一种很直观漂亮的方式:
例子 15.25. 任务规则
build.gradle

  1. tasks.addRule("Pattern: ping<ID>") { String taskName ->
  2. if (taskName.startsWith("ping")) {
  3. task(taskName) << {
  4. println "Pinging: " + (taskName - 'ping')
  5. }
  6. }
  7. }

gradle -q pingServer1 的输出

  1. > gradle -q pingServer1
  2. Pinging: Server1

这里的 String 参数就是用来定义规则的.
规则并不只是在通过命令行使用任务的时候执行. 你也可以基于规则来创建依赖关系:
例子 15.26. 基于规则的任务依赖
build.gradle

  1. tasks.addRule("Pattern: ping<ID>") { String taskName ->
  2. if (taskName.startsWith("ping")) {
  3. task(taskName) << {
  4. println "Pinging: " + (taskName - 'ping')
  5. }
  6. }
  7. }
  8. task groupPing {
  9. dependsOn pingServer1, pingServer2
  10. }

gradle -q groupPing 的输出

  1. > gradle -q groupPing
  2. Pinging: Server1
  3. Pinging: Server2

如果你运行 “gradle -q tasks”, 你并不能找到名叫 “pingServer1” 或者 “pingServer2” 的任务, 但是这个脚本仍然会执行这些任务.

终止 tasks

终止任务是一个正在开发的功能.

这里的终止任务并不是指终止一个任务, 而是指一个无论运行结果如何最后都会被执行的任务.
例子 15.27. 加入一个任务终止器
build.gradle

  1. task taskX << {
  2. println 'taskX'
  3. }
  4. task taskY << {
  5. println 'taskY'
  6. }
  7. taskX.finalizedBy taskY

gradle -q taskX 的输出

  1. > gradle -q taskX
  2. taskX
  3. taskY

即使要终止的任务失败了, 终止任务仍会继续执行.
例子 14.28. 当任务失败时k
build.gradle

  1. task taskX << {
  2. println 'taskX'
  3. throw new RuntimeException()
  4. }
  5. task taskY << {
  6. println 'taskY'
  7. }
  8. taskX.finalizedBy taskY

gradle -q taskX 的输出

  1. > gradle -q taskX
  2. taskX
  3. taskY

另外, 如果要终止的任务并没有被执行 (比如上一节讲的 up-to-date) 那么终止任务并不会执行.
当构建创建了一个资源, 无论构建失败或成功时这个资源必须被清除的时候, 终止任务就非常有用.
要使用终止任务, 你必须使用 Task.finalizedBy()) 方法. 一个任务的实例, 任务的名称, 或者任何 Task.dependsOn() 可以接收的输入都可以作为这个任务的输入.