1. 程式人生 > >Gradle學習(十五)——增量構建

Gradle學習(十五)——增量構建

任何構建工具最重要的一個功能就是防止做重複工作。例如對於編譯程序來說,如果已經執行了一次編譯,那麼就不需要再進行第二次,除非發生了一些會影響輸出的操作,比如原始碼改了或者輸出被刪掉了,編譯會消耗很多時間,如果沒必要去的情況下跳過這步就會節省很多時間。

Gradle是通過增量構建的特性來支援這個功能的,我們來詳細瞭解一下

任務的輸入輸出

在通常情況下,任務接收一些輸入然後產生一些輸出。如果用編譯的例子來講,比如java的編譯,它會接收一些原始檔作為輸入,然後產出class檔案作為輸出,還有一些輸入,比如可以指定是否包含日誌檔案。

就像上圖看到的一樣,輸入最重要的特徵就是可以影響一個或者多個輸出。依賴於原始碼和原始碼所跑在的java執行時的版本都會影響位元組碼的生成,這些都算輸入。但是比如memoryMaximumSize

指定的編譯時最大記憶體的大小是不會影響最終位元組碼生成的,如果按Gradle的術語,memoryMaximumSize應該叫做內部任務屬性。

作為增量構建的一部分,Gradle會去檢查輸入和輸出是否改變,如果沒有改變它就認為任務是up-to-date,並且跳過任務的action。要注意的一點是,除非任務至少有一個輸出,否則增量構建將不起作用。

這對構建者來說是非常容易的:你僅僅要做的就是告訴Gradle哪些是輸入,哪些是輸出。如果任務的一個屬性可以影響輸出,那麼也需要將它設為輸入。還要注意哪些不確定的任務,就是哪些相同輸入都可能產生不同輸出的任務,它們不應該被配置成增量構建。

以下是把任務屬性作為輸入的幾種方法:

自定義任務型別

如果你寫class實現了自定義任務,那麼只需要兩步就可以把它變成增量構建:

  1. 為你的每個輸入輸出通過getter方法建立型別屬性
  2. 給這些屬性加上適當的註解

Gradle主要支援三種輸入輸出

  • 簡單型別
    比如字串或者數字,大部分情況下,這些型別都要實現了Serializable介面
  • 檔案系統型別
    包括標準的File型別,還有Gradle的FileCollection型別的派生類,還有那些可以作為引數傳遞給Project.file(java.lang.Object)Project.files(java.lang.Object[])這兩個方法的任意型別
  • 內嵌型別
    內嵌型別不需要遵照其他兩種型別,但是它有自己的輸入輸出屬性,實際上任務的輸入輸出就內嵌在這些型別中。

想象下你有個任務需要處理各種各樣的模板,比如FreeMarker, Velocity, Moustache等,他們接收模板原始檔然後用一些模型資料組合起來形成模板檔案的填充版本。

這個任務有三個輸入一個輸出:

  • 模板原始檔
  • 資料模型
  • 模板引擎
  • 輸出檔案

官方的例子是用java實現的,程式碼量太冗餘了,這裡講groovy實現的方式,前提是你已經有些groovy的基礎,可以看得懂程式碼。
buildSrc/src/main/groovy/com/lastsweetop/tasks/ProcessTemplates.groovy檔案

@Builder(builderStrategy = SimpleStrategy, prefix = '')
class ProcessTemplates extends DefaultTask {
    @Input
    TemplateEngineType templateEngineType
    @Nested
    TemplateData templateData
    @InputFiles
    FileCollection sourceFiles
    @OutputDirectory
    File outputDir

    @TaskAction
    void processTemplates() {

    }
}

buildSrc/src/main/groovy/com/lastsweetop/tasks/TemplateData.groovy檔案

class TemplateData {
    @Input
    String name
    @Input
    Map<String, String> variables
}

buildSrc/src/main/groovy/com/lastsweetop/tasks/TemplateEngineType.groovy

enum TemplateEngineType {
    FreeMarker,Velocity
}

build.gradle檔案

task processTemplates(type: ProcessTemplates) {
    templateEngineType TemplateEngineType.Velocity
    templateData new TemplateData(name:'1',variables: [:])
    sourceFiles files('src1')
    outputDir file('dst')
}

執行任務:

± % gradle processTemplates
:processTemplates

BUILD SUCCESSFUL in 8s
1 actionable task: 1 executed

再次執行:

± % gradle processTemplates
:processTemplates  UP-TO-DATE

BUILD SUCCESSFUL in 8s
1 actionable task: 1 up-to-date

我們來詳細講解下這個過程中的輸入和輸出:

  • templateEngineType
    代表填充模板的所使用的模板引擎的型別,比如FreeMarker,Velocity,你可以僅僅用一個字串就實現,但我們這裡為了提供更多的型別資訊和安全性的考慮使用了列舉,因為列舉自動實現了Serializable介面,因此我們增加@Input註解可以把它作為簡單型別使用,就像使用String一樣
  • sourceFiles
    需要填充的模板源,可以是單個檔案也可以是多個檔案需要特殊的註解,在這裡因為是輸入,因此我們採用@InputFiles註解,我們稍後的列表中有更多面向檔案的註解
  • templateData
    在這個例子中,我們使用了一個自定義的類來表示模型資料,但是它沒有實現序列化介面,因此我們不能直接用@Input註解,但是沒關係,templateData裡面的兩個屬性一個字串和一個map都實現了序列號介面,我們可以在這兩個屬性上增加@Input註解,然後在templateData上新增@Nested註解,告訴Gradle,這是一個內嵌輸入型別,
  • outputDir
    表示輸出檔案的目錄,和輸入檔案一樣,也有各種各樣的註解,這裡是單獨的輸出目錄,因此使用@OutputDirectory

這些註解的屬性表示,如果模板引擎的型別,需要填充的模板源,模型資料和最後組合的模板和Gradle的上一次構建結果沒有變化的話,本次構建就會跳過執行,這常常可以節省很多時間。

還有一點值得考慮,比如僅僅是一個原始檔更改了,是否整個模板源都需要重新填充,答案是的,一個檔案修改,全部都要重新構建,你可能覺得這個不合理啊,針對本篇所講的例子確實如此,增量構建的範圍就在此,如果想進一步那就是增量任務輸入特性所做的事情了。

讓我們來看看輸入輸出所用的所有註解和他們可以使用附加到的相應的屬性

註解 預期屬性型別 描述
@Input 任何序列化的型別 一個簡單的輸入值
@InputFile File * 一個單獨的輸入檔案(非目錄)
@InputDirectory File * 一個單獨的輸出檔案(非檔案)
@InputFiles Iterable<File>* 輸入檔案或者目錄的迭代
@Classpath Iterable<File>* 表示java的classpath的輸入檔案或者目錄的迭代。它允許任務忽略這個屬性的不相干的改變,比如相同檔案的不同名字,和屬性的註解@PathSensitive(RELATIVE)類似,但是它會忽略附加給classpath的jar的名字,把順序的改變視為classpath的改變,Gradle將檢查jar檔案的內容而忽視無關classpath的改變,比如檔案的名字和日期。
@CompileClasspath Iterable<File>* 表示java的編譯classpath的輸入檔案或者目錄的迭代。它允許忽略不影響classpath中class的API的不相干的改變。可以忽略的改變種類如下:
更改jar或者頂級目錄的路徑
更改jar中實體的順序和時間戳
改變resource和jar的manifests,包括增加或者移除
改變私有元素,比如私有屬性,私有方法,私有內部類
修改程式碼,比如方法體,靜態初始化或者欄位初始化(不包括常量)
debug資訊的改變,比如增加或者減少註釋引起了debug資訊的改變
目錄更改,包括在jar內部的實體的目錄
@OutputFile File * 一個單獨的輸出檔案(非目錄)
@OutputDirectory File * 一個單獨的輸出目錄(非檔案)
@OutputFiles Map<String, File>**
or Iterable<File>*
一個輸出檔案的迭代(非目錄)只有Map時,這些食醋胡才能被快取
@OutputDirectories Map<String, File>**
or Iterable<File>*
一個輸出目錄的迭代(非檔案)只有是Map時,這些輸出才能被快取
@Destroys File or Iterable<File>* 指定一個或者多個檔案需要移除,要注意的是任務只能在輸入輸出或者銷燬兩者選其一,但是不能兩者都存在
@LocalState File or Iterable<File>* 指定一個或者多個檔案來表示任務的本地狀態,如果任務是從快取中載入的,則這些檔案將會被移除
@Nested 任意自定義型別 一個自定義型別,可以不實現序列號介面,但是必須有一個屬性或者欄位被添加了本表格中的任意一個註解,包括@Nested
@Console 任意型別 表示這個屬性既不是輸入也不是輸出,僅僅影響控制檯的輸出資訊,比如增加或減少任務的詳細資訊
@Internal 任意型別 表示這個屬性內部使用,沒有任何輸入輸出

*
事實上File可以是能被Project.file(java.lang.Object)方法接受的任意型別,Iterable<File>可以是能被Project.files(java.lang.Object[])方法接受的任意型別。它包括Callable的例項,比如closure,支援屬性的惰性計算。要注意的是FileCollectionFileTreeIterable<File>
**
和上面類似,可以被Project.file(java.lang.Object)接受的任意型別,Map可以是Callable例項,比如是closure。

可以附加在之上註解之上的註解

註解 描述
@SkipWhenEmpty 如果@InputFiles@InputDirectory註解的檔案列表或者目錄是空的那麼將跳過任務。所有帶該註解的輸入檔案列表或者目錄為空時才會跳過,會產生一個no source的任務結果
@Optional 可用於可選API文件中列出的任何屬性型別註解。該註解禁止對相應屬性進行驗證檢查。
@PathSensitive 可用於輸入檔案的任意屬性,告訴Gradle只考慮檔案的某些部分,比如註解為PathSensitivity.NAME_ONLY,如果只改變檔案的路徑,不改變檔案內容將不會引起out-of-date

屬性的註解是可以繼承的,包括實現的介面的方式。子類可以重寫父類的屬性的註解,比如父類是@InputFile屬性,那麼子類可以更改為@InputDirectory屬性。子型別在屬性上的註解會覆蓋父類的註解和實現介面的註解,父類的註解又優先於實現介面的註解。

ConsoleInternal是兩個特別的註解,他們沒有任何輸入輸出。主要用在使用Java Gradle Plugin Development plugin進行外掛開發時來幫助你檢查你的自定義任務是否新增必要的增量構建註解,防止你忘記。

使用classpath註解

@InputFiles外,與JVM相同的任務Gradle還理解classpath作為輸入的概念,Gradle會分執行時和編譯時兩部分去檢查更改。

@InputFiles不同,對於classpath屬性來說,檔案集合中順序相當重要,但是classpath中jar的檔名和路徑卻可以忽略掉,包括jar內部實體的class和resource的時間戳和順序也可以忽略掉。比如重新建立不同時間戳的jar並不會引起up-to-date

執行時的classpath可以標記為@Classpath,他們可以通過classpath的標準化進行進一步定製,我們後面會講。

帶有@CompileClasspath註解的輸入屬性將會被當做java的編譯時classpath,除了前面的提到的那些,編譯classpath還好忽略除了class檔案外的所有更改,有些情況下class都改變了也不會影響任務的up-to-date,意味著只是更改的具體的實現,但是不影響編譯。

執行時API

使用為自定義任務添加註解的方式轉換成增量構建任務是很方便,但是有時候你沒條件這樣做,比如你訪問不到自定義任務的原始碼,Gradle還提供了額外的api可以讓任何任務都變成增量構建任務。

ad-hoc任務

執行時API通過一組適當的屬性來提供,可以為每個task所用。

  • Task.getInputs() 型別是 TaskInputs
  • Task.getOutputs() 型別是 TaskOutputs
  • Task.getDestroyables() 型別是 TaskDestroyables

這些屬性擁有一些方法允許你設定檔案,目錄或值來組成任務的輸入輸出,執行時API有時候和註解有很多相同的特性,但是不足是,不會去驗證定義的目錄是不是真的目錄,定義的檔案是不是真的檔案。

之前的模板的例子,我們來看下如何改成使用執行時api的ad-hoc任務

task processTemplatesAdHoc {
    inputs.property('engine',TemplateEngineType.FreeMarker)
    inputs.files(fileTree('src1'))
    inputs.property('templateData.name','1')
    inputs.property('templateData.variables',[year:2017])
    outputs.dir('dst1')
    doLast {

    }
}

然後執行任務:

± % gradle processTemplatesAdHoc
:processTemplatesAdHoc

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

首先,你應該編寫自定義的任務還要新增各種屬性,而在這個例子中沒有任何需要儲存原始檔目錄,輸出目錄和其他一些設定的地方,這是為了突出Ad-hoc任務並不需要任務附帶任何狀態,就可以實現和自定義任務在增量構建方面一樣的效果。

所以的輸入輸出都是通過inputsoutputs上的方法進行定義,比如property(),files(),和dir()方法,Gradle會對這些引數進行up-to-date檢查,來確定任務是否需要執行。每個方法都對應一個增量構建的註解,比如inputs.property()對應@Input,而 outputs.dir()對應@OutputDirectory.有一點不同的是file(),files(),dir()dirs()不會校驗給定的路徑是檔案還是目錄。

將會被移除掉檔案列表就可以由destroyables.register()指定。

task removeTempDir {
    destroyables.register("$projectDir/tempDir")
    doLast {
        delete("$projectDir/tempDir")
    }
}

執行時API和註解最大的不同就是沒有@Nested,這就是為什麼需要兩個property(),每個代表一個模板資料的屬性,當然,還可以使用inputs.properties([name:'1',variables: [year: 2018]])這種方式來定義。和註解一樣,只能在銷燬和輸入輸出中二選一,不能一個任務同時設定兩個。

為自定義任務新增執行時api

還有一種情況,就是為那些缺少相應註解的自定義任務新增輸入輸出的定義。比如,假設ProcessTemplatesNoAnnotations是第三方外掛的任務,但是它不支援增量構建,為了給它新增增量構建,你就需要使用執行時API了
ProcessTemplatesNoAnnotations是去掉註解的ProcessTemplates

@Builder(builderStrategy = SimpleStrategy, prefix = '')
class ProcessTemplatesNoAnnotations extends DefaultTask {
    TemplateEngineType templateEngineType
    TemplateData templateData
    FileCollection sourceFiles
    File outputDir

    @TaskAction
    void processTemplates() {

    }
}

對應的任務:

task processTemplatesRuntime(type: ProcessTemplatesNoAnnotations) {
    inputs.property('engine', TemplateEngineType.FreeMarker)
    inputs.files(fileTree('src1'))
    inputs.properties([name: '2', variables: [year: 2018]])
    outputs.dir('dst1')
}

執行任務:

± % gradle processTemplatesRuntime
:processTemplatesRuntime

BUILD SUCCESSFUL in 8s
1 actionable task: 1 executed

再次執行:

± % gradle processTemplatesRuntime
:processTemplatesRuntime  UP-TO-DATE

BUILD SUCCESSFUL in 8s
1 actionable task: 1 up-to-date

使用執行時api有點像使用doLast和doFirst,都是為任務附加些什麼,執行時API附加的是用於增量構建的輸入輸出資訊。要注意的是如果自定義任務已經有增量構建的註解,那麼執行時API所附加的輸入輸出是新增而不是替代。

進一步配置

執行時API的方法僅僅允許你新增輸入輸出,但是面向檔案的輸入輸出將會返回一個TaskInputFilePropertyBuilder例項,基於這個例項你可以附加更多配置資訊。

task processTemplatesRuntimeConf(type: ProcessTemplatesNoAnnotations) {
    //...
   inputs.files(fileTree('src1') {
                include '**/*.fm'
            }).skipWhenEmpty()
    //...
}

執行任務

± % gradle processTemplatesRuntimeConf
:processTemplatesRuntimeConf NO-SOURCE

BUILD SUCCESSFUL in 0s

TaskInputs.files()返回的builder有個skipWhenEmpty(),呼叫她和給屬性增加註解@SkipWhenEmpty一樣的。

現在你已經掌握了註解和執行時API兩種方法,你可以考慮自己喜歡使用哪種了。不過我推薦儘量用註解,實在無法使用註解的情況下再考慮使用執行時API。

定義任務輸入輸出的額外福利

一旦你定義了任務的輸入輸出,Gradle就會去推斷這些屬性。比如,一個任務的輸出正好是一個任務的輸入,這樣是不是會產生依賴關係,不用擔心,Gradle幫你搞定。
我們來看下其他的一些好玩的特性。

推斷任務依賴

想下一個歸檔任務可以打包processTemplates任務的輸出,構建者可能會想這兩者有依賴關係,於是顯式的聲明瞭依賴,其實大可不必,你可以使用下面的方法:

task packageFiles(type: Zip) {
    from processTemplates.outputs
}

執行任務:

± % gradle packageFiles
:processTemplates
:packageFiles

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

Gradle將自己可以推斷出packageFiles任務依賴於processTemplates任務。因為packageFiles任務需要processTemplates任務的輸出作為輸入,我們把這個叫推斷任務依賴。

輸入輸出校驗

增量構建的註解為Gradle提供了足夠的資訊,一遍Gradle可以為這些被註解的屬性提供一些基礎的校驗,這些校驗是在任務執行之前進行的,詳細如下所示:

  • @InputFile 校驗屬性值是否是個正確的檔案路徑(而不是目錄)並且存在
  • @InputDirectory 校驗屬性值是否是個正確的目錄路徑(而不是檔案)並且存在
  • @OutputDirectory 校驗屬性值是否不是一個檔案,並且如果不存在就建立

這些校驗提高了構建的健壯性,可以幫你快速找到輸入輸出的錯誤資訊。

有時候你想禁用掉這些校驗,那麼你就可以使用@Optional註解,它會告訴Gradle這個屬性是可選的,不需要進行校驗。

持續構建

定義任務輸入輸出的另一個福利就是持續構建,它可以知道任務依賴於哪些檔案,一旦這些檔案發生變化,那麼任務就會自動執行。執行時加上--continuous或者-t選型,Gradle就會不斷的檢查是否更新,如果更新則執行任務。

任務並行化

定義任務的輸入輸出的最後一個福利就是,當你給任務增加--parallel選項是,任務利用這些資訊可以知道應該這麼執行。比如,Gradle在執行下一個任務時,會檢查所有任務的輸出防止並行的任務寫入相同的檔案目錄。還有,Gradle可以知道哪些任務在銷燬檔案,這樣可以避免其他並行任務使用這些檔案,或者正在寫入這些檔案。如果一個任務建立了一些檔案,而另一個任務正在基於這些檔案執行,它也可以防止其他任務來銷燬這些檔案。通過定義任務的輸入和輸出提供的資訊,Gradle建立/消費/銷燬之間的關係,防止任務並行時違反這種關係規則。

工作原理

在任務執行第一次之前,Gradle會獲取任務輸入的快照,這個快照包括每個輸入檔案的路徑和內容構成的hash,然後在任務執行之後,Gradle獲取輸出的快照,這些快照包括每個輸出檔案的路徑和內容構成的hash。Gradle會儲存這個兩個快照在下一次任務執行時使用。

在此之後,任務的每一次執行,Gradle都會獲取輸入輸出新的快照。如果新的快照和上一次的相同,Gradle就假定這些任務是up-to-date並且跳過任務,如果不同,就會執行任務,Gradle會為下一次保留快照。

Gradle還好將任務的程式碼作為任務輸入的一部分,當任務的action或者依賴發生並會,Gradle也會任務這個任務過期了,需要再次執行。

Gradle還會考慮檔案屬性是否對順序是敏感的,比如java的classpath。如果這類屬性的快照發生改變,比如更改了檔案的順序,那麼也會認為是過期的,任務需要再次執行。

如果一個任務指定了輸出目錄,那麼往這個目錄中增加了一個檔案,也不會被認為是過期的,因為這對任務的輸出來說是不相干,任務的輸出目錄是可以共享的,如果你不想這樣的話,你可以考慮使用TaskOutputs.upToDateWhen(groovy.lang.Closure)

當構建快取啟用時,任務的輸出還可以被計算為構建快取的key,用於從快取中獲取構建的輸出

進階技術

之前所講的已經涵蓋了增量構建的大部分內容,但有時候你需要一些特殊的處理,我們下面就來講這些進階的技術

新增自己來快取輸入輸出的方法

你可能已經想知道Copy任務的from()方法是如何工作的,它並沒有@InputFiles註解,但是傳給他的檔案都被當做了輸入,而且還支援增量構建。讓我們來解密一下:

實現這個也相當的簡單,其實還是老辦法,只是增加了api而已。自己寫個方法然後往已經添加註解的屬性值裡新增輸入即可。我們可以之前的例子新增sources()方法:

@Builder(builderStrategy = SimpleStrategy, prefix = '')
class ProcessTemplates extends DefaultTask {
    @Input
    TemplateEngineType templateEngineType
    @Nested
    TemplateData templateData
    @InputFiles
    FileCollection sourceFiles = getProject().files()
    @OutputDirectory
    File outputDir

    void sources(FileCollection fileCollection) {
        sourceFiles += fileCollection
    }

    @TaskAction
    void processTemplates() {

    }
}

新增任務:

task processTemplatesOwn(type: ProcessTemplates) {
    templateEngineType TemplateEngineType.Velocity
    templateData new TemplateData(name: '1', variables: [:])
    sources files('src1')
    outputDir file('dst')
}

執行任務:

± % gradle processTemplatesOwn
:processTemplatesOwn

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

也就是說,只要在配置階段你可以任意新增值或者檔案到輸入輸出之中,無論你在哪新增都會被當做輸入輸出。
如果你想將一個任務的輸出新增到進來也是可以的,你需要使用project.files()方法

@Builder(builderStrategy = SimpleStrategy, prefix = '')
class ProcessTemplates extends DefaultTask {
    @Input
    TemplateEngineType templateEngineType
    @Nested
    TemplateData templateData
    @InputFiles
    FileCollection sourceFiles = getProject().files()
    @OutputDirectory
    File outputDir

    void sources(FileCollection fileCollection) {
        sourceFiles += fileCollection
    }

    void sources(Task inputTask) {
        sourceFiles += getProject().files(inputTask)
    }


    @TaskAction
    void processTemplates() {

    }
}

新增任務:

task processTemplatesFromTask(type: ProcessTemplates) {
    templateEngineType TemplateEngineType.Velocity
    templateData new TemplateData(name: '1', variables: [:])
    sources copyTemplate
    outputDir file('dst')
}

執行任務:

± % gradle processTemplatesFromTask
:processTemplatesFromTask

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

這種技術可以讓你的自定義任務更容易使用,構建檔案也可以很簡潔。使用getProject().files()可以自定義任務的內部依賴關係。

最後要提醒的是:如果你需要建立一個獲取原始檔作為輸入的任務,你可以考慮試試內建的SourceTask任務,

連結@OutputDirectory@InputFiles

當你想把一個任務的輸出連結到另一個任務的輸入時,型別往往是匹配的,非常容易建立這種連結。比如File型別的輸出和File型別的輸入。

但是如果你想要@OutputDirectory註解的屬性(File型別)的輸出,連結成另一個任務@InputFiles註解的屬性(FileCollection型別)作為輸入時,這種連結就不起作用了。

我們來看一個例子,利用java編譯任務的輸出,通過destinationDir屬性作為另一個任務Instrument的輸入時.Instrument任務是基於java位元組碼檔案的工具,任務有個輸入屬性classFiles,註解為@InputFiles。示例如下:

@Builder(builderStrategy = SimpleStrategy, prefix = '')
class Instrument extends DefaultTask {
    @InputFiles
    FileCollection classFiles
    @OutputDirectory
    File destinationDir

    @TaskAction
    void action(){

    }
}

定義任務:

task badInstrumentClasses(type: Instrument) {
    classFiles fileTree(compileJava.destinationDir)
    destinationDir file("$buildDir/instrument")
}

執行任務:

± % gradle badInstrumentClasses
:badInstrumentClasses

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

在程式碼層面沒有什麼明顯的問題,但是任務執行的時候你發現java編譯任務沒有執行,在這種情況下你需要顯示得通過dependsOnbadInstrumentClasses任務新增compileJava任務的依賴,使用fileTree()意味著Gradle無法推斷他們之間的依賴關係

有種解決方案就是使用TaskOutputs.files屬性

task instrumentClasses(type: Instrument) {
    classFiles compileJava.outputs.files
    destinationDir file("$buildDir/instrument")
}

執行任務:

± % gradle instrumentClasses
:compileJava
:instrumentClasses

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

還有一種方法就是使用project.files()代替project.fileTree(),示例如下:

task instrumentClasses2(type: Instrument) {
    classFiles files(compileJava)
    destinationDir file("$buildDir/instrument")
}

執行任務:

± % gradle instrumentClasses2
:compileJava
:instrumentClasses2

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

files()可以接收Task作為引數,而fileTree()不可以

這兩種方法的缺點就是把任務輸出裡的所有檔案當做另一個任務的輸入了,如果僅僅是一種輸出檔案那是沒問題的,比如JavaCompile任務,但是有時候你只想將多個輸出中的一個連線到其他任務作為輸入,那可以使用buildBy來指定

task instrumentClassesBuiltBy(type: Instrument) {
    classFiles fileTree(compileJava.destinationDir) {
        builtBy compileJava
    }
    destinationDir file("$buildDir/instrument")
}

自定義up-to-date邏輯

Gradle會自動檢查任務輸出的檔案和目錄,但是如果任務輸出是其他的一些東西怎麼辦,比如也許是對webservice的更新,或者對資料庫表的更新,Gradle是無法檢測到任務是否up-to-date的。

還好有個TaskOutputsupToDateWhen()方法,這個方法可以接收predicate函式,用於檢測任務是否是up-to-date的,示例如下:

task alwaysInstrumentClasses(type: Instrument) {
    classFiles fileTree(compileJava.destinationDir) {
        builtBy compileJava
    }
    destinationDir file("$buildDir/instrument")
    outputs.upToDateWhen {
        false
    }
}

執行任務:

± % gradle alwaysInstrumentClasses
:compileJava
:alwaysInstrumentClasses

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed

然後再次執行

± % gradle alwaysInstrumentClasses
:compileJava UP-TO-DATE
:alwaysInstrumentClasses

BUILD SUCCESSFUL in 0s
2 actionable tasks: 1 executed, 1 up-to-date

閉包{ false }總是認為alwaysInstrumentClasses任務要重新執行,不管輸入輸出是否發生了變化。

當然你可以把更復雜的邏輯寫在這個閉包中,比如判斷資料庫的某條記錄是否存在,或者是否更改過。注意up-to-date檢查的目的是為了節省時間,如果檢查本身就非常耗費時間,那就沒必要新增這些檢查了。

常見的錯誤就是使用upToDateWhen()代替onlyIf(),記住,在於輸入輸出無關的情況下使用onlyIf(),與輸入輸出有關的使用upToDateWhen()

輸入的標準化配置

對於up-to-date檢查和構建快取,Gradle都需要來判斷兩次任務的輸入是否一致,為了達到這個目的,Gradle首先要規範化輸入然後再比較兩者。對於編譯時classpath來說,Gradle會從classpath的class檔案中提取ABI簽名然後在比較兩次任務的簽名。

為實現執行時classpath的規範化,可以定製Gradle的內建策略。所有帶@Classpath註解的輸入都可以被當做執行時classpath。

如果你想在你所有的jar中新增一個build-info.properties檔案,這個檔案包含了一些構建的資訊,比如構建開始的時間戳,用於釋出工件的持續整合任務的ID,這個檔案僅僅是為了審查,對執行測試沒有任何影響。儘管如此,如果有了這個檔案,test任務將永遠不會過期,也不會從構建快取中拉去結果,為了還能使用增量構建和構建快取,你可以使用Project.normalization(org.gradle.api.Action)方法來告訴Gradle在執行時classath中忽略這個檔案。

normalization {
    runtimeClasspath {
        ignore 'build-info.properties'
    }
}

這樣配置的效果就是在進行up-to-date檢查和構建快取計算key時,build-info.properties檔案的更改將會被忽略掉,而且不會改變test任務執行時的操作,test任務仍然會載入build-info.properties檔案,執行時的classpath和以前還是一樣

過期的任務輸出

當Gradle的版本發生變化時,基於原來版本的任務輸出都會被移除掉,以便所有的任務可以基於當前版本的,使得所有任務的環境一致。