---
title: 'Gradle 腳本、Task'
disqus: kyleAlien
---
Gradle 腳本、Task
===
## OverView of Content
所有 Gradle 的建構工作都是由 Task 組合完成
[TOC]
## Gradle Task 初見
在介紹 Gradle Task 之前,我們先看一個簡單的 Gradle Task,稍微有個映像,之後在學習會比較有方向(知道自己要學的東西是啥)
### Hello Task
* 以下會先編譯一個簡單的 gradle 的腳本,取名為 `build.gradle` 檔案
:::info
* **腳本取名方式**
> 該取名為預設讀取,當 gradle 指令執行時,會默認加載當前目錄下的 `build.gradle` 檔案,如同 Maven 的 `pom.xml` 檔案
:::
使用 task 閉包,任務名取為 hello,當任務執行完後執行 `doLast` 閉包
```groovy=
// 名為 hello 的任務
tasks.register("Hello") {
doLast {
println "Hello gradle"
}
}
```
運行任務
```groovy=
// -q 只接受 quiet 以上即別的輸出
// 指定 task 名稱 => Hello 並運行
gradle -q Hello
```
**--實作結果--**
> 
### 查看所有可執行的 Tasks
* 使用指令 gradlew tasks,就可以看所有的任務
```groovy=
// 查看所有可執行的 Tasks
gradlew tasks
```
輸出結果
```typescript=
> Task :tasks
------------------------------------------------------------
Tasks runnable from root project
------------------------------------------------------------
Build Setup tasks
-----------------
init - Initializes a new Gradle build.
wrapper - Generates Gradle wrapper files.
Help tasks
----------
buildEnvironment - Displays all buildscript dependencies declared in root project 'android-gradle-book-code'.
components - Displays the components produced by root project 'android-gradle-book-code'. [incubating]
dependencies - Displays all dependencies declared in root project 'android-gradle-book-code'.
dependencyInsight - Displays the insight into a specific dependency in root project 'android-gradle-book-code'.
dependentComponents - Displays the dependent components of components in root project 'android-gradle-book-code'. [incubating]
help - Displays a help message.
model - Displays the configuration model of root project 'android-gradle-book-code'. [incubating]
outgoingVariants - Displays the outgoing variants of root project 'android-gradle-book-code'.
projects - Displays the sub-projects of root project 'android-gradle-book-code'.
properties - Displays the properties of root project 'android-gradle-book-code'.
tasks - Displays the tasks runnable from root project 'android-gradle-book-code'.
To see all tasks and more detail, run gradle tasks --all
To see more detail about a task, run gradle help --task <task>
```
* 可以看到上面的建構類 (Build Setup tasks) `init`、`wrapper`;也可以使用指令 **==`gradle help --task <任務名稱>`==** 查看任務
```groovy=
// 查看 hello 任務
gradle help --task hello
```
**--實作結果--**
> 
## 任務結果
Gradle 執行任務後,會返回任務的 **結果標籤**,這些結果標籤分別代表了不同意思
### 結果標籤
* 任務執行後的結果標籤有以下幾個
1. **`EXECUTED` or `no label`**:**任務被執行**
- Task 有動作,是執行的一部分
- Task 無動作,但相對的依賴任務有被執行
2. **`UP-TO-DATE`**:**任務輸出沒有改變**
- 任務的輸入、輸出沒有改變
- 任務的輸出沒有改變,但任務有被執行
- 任務沒有被執行,但依賴項目有被執行(依賴項是最新、跳過、或是任務有緩存)
- 任務沒被執行,也沒依賴項
3. **`FROM-CACHE`**:**任務的輸出從緩存而來**
- 取用任務已有的輸出
4. **`SKIPPED`**:**任務未執行**
- 任務已被排除
- 使用 `onlyIf` 並返回 false
5. **`NO-SOURCE`**:**任務不須執行**
- 任務有輸入、輸出但沒有 source
> eg. source like `JavaCompile`
## [Task](https://docs.gradle.org/7.5.1/dsl/org.gradle.api.Task.html) 創建方式
[**創建任務**](https://docs.gradle.org/current/javadoc/org/gradle/api/Task.html#leftShift(groovy.lang.Closure)) 的方法有很多種,但是都是依賴 `Project` 對象提供的方法、`TaskContainer` 的 Create 方法,接下來會重點介紹幾種
:::success
* [**Project**](https://docs.gradle.org/7.5.1/dsl/org.gradle.api.Project.html) 對象中關於 Task 的源碼最終都是調用了 `TaskContainer` 對象的 create 方法
> **Project 中也有 TaskContainer 的屬性,也就是 tasks**
```groovy=
tasks.create("Hello") {
doLast {
println 'Hello Task'
}
}
```
:::
### Task 創建
1. **名稱創建**
**方法原型:`Task task(String name) throws InvalidUserDataException`**,它接收 String 作為任務名稱,並返回 Task 類型
```groovy=
// 1. 創建名為 CreateTask_1 的任務
def nameCreate = task(CreateTask_1)
nameCreate.doFirst {
println 'use name create task, doFirst'
}
nameCreate.doLast {
println 'use name create task, doLast'
}
```
> 
2. **名稱 + Map 對象**
task 建構函數中多個一個 Map 參數,它用於創建 Task 配置;以下提供一些 task 可用的 1Map Key
**方法原型:`Task task(Map<String, ?> args, String name) throws InvalidUserDataException`**
| Map Key | 描述 | 默認值 |
| -------- | -------- | -------- |
| type | 基於一個存在的任務創建,與 Java 的概念類似 | DefaultTask |
| overwrite | 是否替換存在的 Task,與 type 配合使用 | false |
| dependsOn | 任務依賴 | [] |
| action | 添加到任務中的一個閉包、Action | null |
| description | 任務描述 | null |
| group | 任務分組 | null |
```groovy=
// Use Map, Task name
// Task 名稱:CreateTask_2_1
// 群組名稱為:group:BasePlugin.BUILD_GROUP
task(group:BasePlugin.BUILD_GROUP, "CreateTask_2_1") {
doLast {
println 'Use Map params to create task'
}
}
// 相反參數,也是 Okay (因為透過名稱指定)
task("CreateTask_2_2", group:BasePlugin.BUILD_GROUP) {
doLast {
println 'Use Map params to create task, Params converter'
}
}
// 指定多個類型
task([group:BasePlugin.BUILD_GROUP, description:'MapMapUse'], "CreateTask_2_3") {
doLast {
println "Use Map params to create task, multiple Map, description: ${description}"
}
}
```
> 
3. **任務名稱 + 閉包 + 指定 Property**
由於 Map 創建的配置 Key 有限制,更為方便的是使用 **閉包創建 並在內部指定 Property 數值**
| Task Property | 描述 | 默認值 |
| -------- | -------- | -------- |
| type | 基於一個存在的任務創建,與 Java **繼承**的概念類似 | DefaultTask |
| overwrite | 是否替換存在的 Task,與 type 配合使用 | false |
| dependsOn | 任務依賴 | [] |
| action | 添加到任務中的一個閉包、Action | null |
| description | 任務描述 | null |
| group | 任務分組 | null |
```groovy=
// task To
task ("To") {
println 'To, Hello'
}
// task Do
task ("Do") {
println 'Do, World'
}
task ("nameClosure") {
// 將 key 寫在內部
dependsOn To, Do
description 'name + closure'
doLast {
println "Use name and closure to make task, description: ${description}"
}
```
> 
4. 名稱 + Map + 閉包指定 Property
上面兩個方法也可以混合使用,同時使用 `Map 指定` + `閉包指定 Property`
```groovy=
// name + map + closure
task (dependsOn: ["To", "Do"], "NMC"){
description 'name + map + closure'
doLast {
println "Use name & map &closure to make task, description: ${description}"
}
}
```
> 
### TaskContainer 創建任務
* 在 `Project` 物件中有一個 `tasks` 屬性 (其類型就是 `TaskContainer`),透過它的 `register` 也可以創建任務
範例如下
```groovy=
tasks.register('hello') {
doLast {
println 'hello'
}
}
```
## 任務訪問
**在創建任務 `Project` 時會自動將任務名稱轉為屬性**,我們可以直接操作該屬性
:::info
* 其實透過 `Project` 最終都會加入到 `TaskContainer` 中,**`TaskContainer` 存有所有任務的集合**
可以透過 `Project` 來取得`TaskContainer`
```groovy=
Project.getTasks()
```
:::
### 名子訪問 - TaskContainer
* **TaskContainer 存有任務的 ++集合++**,所以可已透過 Project 的 **==tasks 屬性==**,透過 `[task name]` 訪問任務
1. 使用操作符 `[]`
```groovy=
task("visitTask_1") {
doLast {
println 'use task name visit task'
}
}
// tasks(TaskContainer 型態) 內有所有任務的集合
tasks['visitTask_1'].doLast {
println 'use tasks [] visit tasks'
}
```
> 
:::success
* `[]` 在 Groovy 中是一個 **操作符**,Groovy 的操作符都有對應的方法重載,`a[b]` 其實是對應了 `a.getAt(b)` 這個方法
所以 `tasks['visitTask_1']` 就是 tasks.getAt('visitTask_1'),若是查看 **源碼則是調用 ++`findByName(String name)` 實現++**
> findByName('visitTask_1')
:::
2. 使用 `named` 訪問任務
```groovy=
tasks.named("visitTask_1") {
println "use tasks `named` visit tasks, ${it.name}"
}
```
### 路徑訪問 - [TaskContainer](https://docs.gradle.org/7.5.1/dsl/org.gradle.api.tasks.TaskContainer.html) 尋找 Task
* 通過路徑的訪問有以下兩種方式,必且它也是透過 `Project` 提供的 `tasks` 變量訪問
| 訪問方式 | api 使用 | 無任務時 | 範例 |
| - | - | - | - |
| Name | getByName | 拋出 UnknownTaskException | tasks.getByName('') |
| Name | findByName | 返回 null | tasks.findByName('') |
| Path | getByPath | 拋出 UnknownTaskException | tasks.getByPath('') |
| Path | findByPath | 返回 null | tasks.findByPath('') |
:::info
**透過路徑訪問時**,參數值可以是 ^1.^ 任務名訪問,也可以是 ^2.^ 任務路徑
:::
1. **任務名訪問**
```groovy=
// use name to find Task (名稱找尋)
task("visitTask_2")
tasks['visitTask_2'].doLast {
// find
println "find task: ${tasks.findByName('visitTask_2')}"
// get
println "get task: ${tasks.getByName('visitTask_2')}"
// find 找尋空任務
println "find empty task: ${tasks.findByName('visitTask_999')}"
// get 找尋空任務,會拋出錯誤
//println "get empty task: ${tasks.getByName('visitTask_999')}"
}
```
2. **任務路徑**
```groovy=
// use path to find Task (路徑找尋,在路徑前需使用 `:` 符號)
task("visitTask_3")
tasks['visitTask_3'].doLast {
// find,路徑之間使用 `:`
println "find task: ${tasks.findByPath(':visitTask_3')}"
// get,路徑之間使用 `:`
println "get task: ${tasks.getByPath(':visitTask_3')}"
// find 找尋空任務
println "find empty task: ${tasks.findByPath(':visitTask_999')}"
// get 找尋空任務,會拋出錯誤
// println "get empty task: ${tasks.getByPath(':visitTask_999')}"
}
```
> 
## 任務屬性
### 群組分類、描述 - group、description
* 多個任務可以透過 group 將群組分配
> 對任務的描述 & 分組可以更好的管理任務,之後可以用利於查看任務細節
```java=
task ("randomTask_1") {
// 分配群組為 group
group = 'Hello'
description = 'Task one'
doLast {
println "group: $group, description: $description"
}
}
task ("randomTask_2") {
// 分配群組為 group
group = 'Hello'
description = 'Task two'
doLast {
println "group: $group, description: $description"
}
}
task ("randomTask_3") {
// 分配群組為 group
group = 'Hello'
description = 'Task three'
doLast {
println "group: $group, description: $description"
}
}
```
Gradle CLI 運行這三個任務
```shell=
gradle randomTask_1 randomTask_2 randomTask_3
```
> 
### 任務依賴性 - dependsOn
* **`dependsOn` 屬性**:Gradle 中的每個任務都可以有相互依賴性
1. `dependsOn` 可以設置任務的依賴
```groovy=
tasks.register("Apple") {
doLast {
println "Apple task"
}
}
tasks.register("Banana") {
dependsOn("Apple")
doLast {
println "Banana task."
}
}
```
使用 gradle CLI 執行 `Banana` 任務
```shell=
gradle -q "Banana"
```
> 
### 任務排序性 - mustRunAfter、shouldRunAfter
* **任務排序性**:`tasks` 有兩個屬性可以設置,^1.^**`mustRunAfter` 屬性**^2.^`shouldRunAfter` 屬性,**這些設置 ==不代表任務之間的任何依賴關係==**
> 與任務依賴性 (`dependsOn`) 相比,**`dependsOn` 在 ++並行++ 執行任務的狀況下無法確保其順序性**
常用於以下幾點
1. 任務執行的排序
2. 在建構早期運行驗證
3. 快速任務接續在耗時任務之後時
4. 聚合特定類型的任務結果
> 測試報告運行在測試之後
範例如下:
```groovy=
def taskX = tasks.register('taskX') {
doLast {
println 'taskX'
}
}
def taskY = tasks.register('taskY') {
doLast {
println 'taskY'
}
}
taskY.configure {
mustRunAfter taskX
}
```
:::warning
* 與 `dependsOn` 不同,**在執行任務時,必須呼叫所有任務名**
* 設定任務的順序,**配合 `--continue` 時,如果有任務失敗,仍會往下執行**
:::
> 
### 最終任務 - finalizedBy
* 設定最終閉定要執行的任務,**最算任務發生異常也會執行**
> 大部分用在任務結束後的清理資源階段
```groovy=
task("FinalTask") {
doLast {
println("Final Task working")
}
}
task("TaskX"){
doFirst {
throw new StopExecutionException()
}
doLast {
println("TaskX working")
}
finalizedBy("FinalTask")
}
```
> 
## 任務啟用 & 禁止
### Flag 設置 - enabled
* 在 Project 的每一個 task 都有一個屬性,用於啟用 & 禁止任務 (默認為啟用),該屬性為 **==enabled==**,啟動時為 true,反知禁止
```groovy=
task("prohibited") {
enabled = false
doLast {
println 'prohibited Task'
}
}
task("open") {
enabled = true
doLast {
println 'open Task'
}
}
task("MyTask") {
dependsOn("prohibited", "open")
doLast {
println 'Finish MyTask'
}
}
```
被禁止的任務(enabled = false)不會執行
> 
### onlyIf 表達式
* onlyIf 表達式又稱為斷言,**Task 有一個 `onlyIf` 方法,它接受一個閉包作為參數,如果 ++閉包返回 true 則執行該任務,若反為 false 則不執行++**
定義了 3 個打包的渠道,一個渠道對應一個任務 `Debug`、`TestMode`、`Release`,而**任務是否執行透過 task.onlyIf 做出判斷 (當然可操控 enabled)**
```groovy=
def PACKAGE_APP = 'all'
def DEBUG_APP = 'debug'
def TEST_APP = 'test'
def RELEASE_APP = 'release'
task("DeBug") {
// 任務內容
doLast {
println 'DeBug APK'
}
onlyIf {
List<String> lists = [PACKAGE_APP, DEBUG_APP];
def res = decideProject(lists);
println "Package debug : $res"
res
}
}
task("TestMode") {
// 任務內容
doLast {
println 'Test APK'
}
onlyIf {
def res = decideProject([PACKAGE_APP, TEST_APP]);
println "Package TestMode : $res"
res
}
}
task("Release") {
// 任務內容
doLast {
println 'Release APK'
}
onlyIf {
def res = decideProject([PACKAGE_APP, RELEASE_APP]);
println "Package Release : $res"
res
}
}
// 讓使用者調用的 build
task("build") {
dependsOn "DeBug", "TestMode", "Release"
description '多渠道打包'
println 'build task: $description'
}
def decideProject(List<String> item) {
def result = false
if(project.hasProperty('build_apps')) { // 自定義,輸入屬性 build_apps
Object typeIn = project.property('build_apps'); // 取出輸入
println "typeIn : ${typeIn}"
item.each {
if(it.equals(typeIn)) {
result = true
}
}
} else {
result = true // 預設全打包
}
result // 省去 return
}
```
這邊使用到指令 **輸入屬性**,在腳本中判斷是否有需要做多渠道打包的判斷式 (預設不判斷),若需要判斷則透過使用者輸入屬性、數值
> `-P<屬性>=<數值>`,可以看作為 key=value,-PK=V
> 
### 跳過任務 - StopExecutionException
* 一般來說 Gradle 在建構過程中拋出任何錯誤都會中斷建構,但我們 **如果是拋出 `StopExecutionException` 類,那就不會中斷建構**
```groovy=
tasks.register('occur_exception') {
doFirst {
// Here you would put arbitrary conditions in real life.
throw new StopExecutionException()
}
doLast {
println 'We are doing the compile.'
}
}
tasks.register('myTask') {
dependsOn('occur_exception')
doLast {
println 'I am not affected'
}
}
```
> 
## 其它
### 任務規則 - addRule
* 其實我們上面範例創建的任務都在 **`TaskContainer`** 裡面,並由其進行管理,訪問任務時又透過 `TaskContainer` 進行訪問
`TaskContainer` 繼承於 `NamedDomainObjectCollection` (簡稱 NDOC),NDOC 是一個具有唯一不變名子的域對象的集合,**裡面所有的元素都有 ++唯一不變的名子++ (final 常量),該名子為 String 類型,所以我們可以透過任務名稱訪問 Task**
> 
* 若該任務不存在則會拋出異常,如果需要手動處理異常任務,就是增加 **TaskContainer 的 Rule** (`addRule` 函數),當找不到該任務時會執行這個閉包
```java=
// 對 TaskContainer 添加規則
tasks.addRule('規則描述') { String taskName ->
// 動態生成任務
task(taskName) { // 創建任務
println "$taskName 該任務不存在,請重新確認"
}
}
task("OkTask"){
dependsOn("MissTask")
println 'OkTask Working'
}
```
:::info
* 直接運行 `gradle MissTask` 是找不到的,因為它是藉由 `addRule` 動態生成的任務
:::
> 
### 自定義任務 - DefaultTask
* `DefaultTask` 是 Gradle 中的一個抽像類,**它實現了 Task 接口**,DefaultTask 提供了一些默認的行為和屬性,使得創建和定義自定義任務更加方便
> Task 是 Gradle 中用於執行特定構建操作的基本單元
範例如下
1. 定義一個抽象任務,**繼承於 `DefaultTask`**,並透過註解來標示行為、對外提供的屬性
```groovy=
abstract class GreetingToFileTask extends DefaultTask {
// 標記執行該任務時會產出的檔案
@OutputFile
abstract RegularFileProperty getDestination()
// 執行任務時,該函數也會被執行
@TaskAction
def greet() {
def settingDest = getDestination().get()
if (settingDest == null) {
throw new Exception("Set destination element first.")
}
def file = settingDest.asFile
file.parentFile.mkdirs()
file.write 'Hello!'
}
}
```
2. 定義一個任務,被將抽象任務的 Class 傳入,在這之後的 Closure 內就可以使用抽象任務的屬性
```groovy=
tasks.register('greet', GreetingToFileTask) {
def greetingFile = objects.fileProperty()
greetingFile.set(layout.buildDirectory.file('hello.txt'))
// 使用 GreetingToFileTask 的屬性
destination = greetingFile
doLast {
def file = greetingFile.get().asFile
println "${file.text} (file: ${file.name})"
}
}
```
## Appendix & FAQ
###### tags: `Gradle`