--- 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 ``` **--實作結果--** > ![](https://hackmd.io/_uploads/Hk2RXwsE2.png) ### 查看所有可執行的 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 ``` **--實作結果--** > ![](https://i.imgur.com/zAg9O6S.png) ## 任務結果 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' } ``` > ![](https://hackmd.io/_uploads/B1i2baxB2.png) 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}" } } ``` > ![](https://hackmd.io/_uploads/SJh6MpgS2.png) 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}" } ``` > ![](https://hackmd.io/_uploads/HJxd7pgSn.png) 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}" } } ``` > ![](https://hackmd.io/_uploads/r1hu4axrn.png) ### 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' } ``` > ![](https://hackmd.io/_uploads/BkjTUaxHh.png) :::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')}" } ``` > ![](https://hackmd.io/_uploads/rJFvtpeH3.png) ## 任務屬性 ### 群組分類、描述 - 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 ``` > ![](https://hackmd.io/_uploads/rysAq6lHn.png) ### 任務依賴性 - 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" ``` > ![](https://hackmd.io/_uploads/ryryxAlHn.png) ### 任務排序性 - 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` 時,如果有任務失敗,仍會往下執行** ::: > ![](https://hackmd.io/_uploads/HyXrZRer2.png) ### 最終任務 - finalizedBy * 設定最終閉定要執行的任務,**最算任務發生異常也會執行** > 大部分用在任務結束後的清理資源階段 ```groovy= task("FinalTask") { doLast { println("Final Task working") } } task("TaskX"){ doFirst { throw new StopExecutionException() } doLast { println("TaskX working") } finalizedBy("FinalTask") } ``` > ![](https://hackmd.io/_uploads/BJe80UnZrh.png) ## 任務啟用 & 禁止 ### 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)不會執行 > ![](https://hackmd.io/_uploads/ryddDCxHh.png) ### 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 > ![](https://hackmd.io/_uploads/rJMQ9CgHn.png) ### 跳過任務 - 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' } } ``` > ![](https://hackmd.io/_uploads/r1ucGn-rn.png) ## 其它 ### 任務規則 - addRule * 其實我們上面範例創建的任務都在 **`TaskContainer`** 裡面,並由其進行管理,訪問任務時又透過 `TaskContainer` 進行訪問 `TaskContainer` 繼承於 `NamedDomainObjectCollection` (簡稱 NDOC),NDOC 是一個具有唯一不變名子的域對象的集合,**裡面所有的元素都有 ++唯一不變的名子++ (final 常量),該名子為 String 類型,所以我們可以透過任務名稱訪問 Task** > ![](https://i.imgur.com/UmwiY7p.png) * 若該任務不存在則會拋出異常,如果需要手動處理異常任務,就是增加 **TaskContainer 的 Rule** (`addRule` 函數),當找不到該任務時會執行這個閉包 ```java= // 對 TaskContainer 添加規則 tasks.addRule('規則描述') { String taskName -> // 動態生成任務 task(taskName) { // 創建任務 println "$taskName 該任務不存在,請重新確認" } } task("OkTask"){ dependsOn("MissTask") println 'OkTask Working' } ``` :::info * 直接運行 `gradle MissTask` 是找不到的,因為它是藉由 `addRule` 動態生成的任務 ::: > ![](https://hackmd.io/_uploads/S1ZON2Zrh.png) ### 自定義任務 - 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`