增量任務

在 Gradle 中,實作一個任務,當其輸入和輸出已經 UP-TO-DATE 時略過執行,這要歸功於 增量組建 功能,簡單又有效率。

然而,有時只有少數輸入檔案自上次執行後變更,最好避免重新處理所有未變更的輸入。這種情況常見於將輸入檔案轉換為輸出檔案,且是一對一轉換的任務中。

若要最佳化您的組建流程,您可以使用增量任務。此方法可確保僅處理過期的輸入檔案,進而改善組建效能。

實作增量任務

若要讓任務增量處理輸入,該任務必須包含一個增量任務動作。

這是一個任務動作方法,只有一個 InputChanges 參數。該參數會告知 Gradle 動作只想要處理變更的輸入。

此外,任務需要宣告至少一個增量檔案輸入屬性,方法是使用 @Incremental@SkipWhenEmpty

build.gradle.kts
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)
    }
}
build.gradle
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"
    }
}

若要查詢輸入檔案屬性的增量變更,該屬性必須始終傳回相同的執行個體。達成此目的最簡單的方法是使用下列其中一種屬性類型:RegularFilePropertyDirectoryPropertyConfigurableFileCollection

您可以在 Lazy Configuration 中進一步了解 RegularFilePropertyDirectoryProperty

增量工作任務動作可以使用 InputChanges.getFileChanges() 找出特定基於檔案的輸入屬性變更了哪些檔案,無論其類型是 RegularFilePropertyDirectoryPropertyConfigurableFileCollection

此方法傳回 FileChanges 型別的 Iterable,而後者又可以查詢下列內容

下列範例示範一個具有目錄輸入的增量工作任務。它假設目錄包含文字檔案集合,並將它們複製到輸出目錄,反轉每個檔案中的文字

build.gradle.kts
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())
            }
        }
    }
}
build.gradle
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 可以明智地判斷需要處理哪些輸入檔案,這是一個稱為增量執行的概念。

在這個情況下,InputChanges.getFileChanges() 方法,在 org.gradle.work.InputChanges 類別中提供與已 ADDEDREMOVEDMODIFIED 的指定屬性相關的所有輸入檔案的詳細資料。

然而,有許多情況 Gradle 無法判斷需要處理哪些輸入檔案(即非增量執行)。範例包括

  • 沒有先前執行的歷史記錄可用。

  • 您正在使用不同版本的 Gradle 進行建置。目前,Gradle 沒有使用來自不同版本的任務記錄。

  • 新增到任務的 upToDateWhen 準則傳回 false

  • 自上次執行以來,輸入屬性已變更。

  • 自上次執行以來,非增量輸入檔案屬性已變更。

  • 自上次執行以來,一個或多個輸出檔案已變更。

在這些情況下,Gradle 會將所有輸入檔案報告為 ADDED,而 getFileChanges() 方法會傳回組成指定輸入屬性的所有檔案的詳細資料。

您可以使用 InputChanges.isIncremental() 方法檢查任務執行是否為增量執行。

正在執行的增量任務

考慮第一次針對一組輸入執行的 IncrementalReverseTask 執行個體。

在這種情況下,所有輸入都將被視為 ADDED,如下所示

build.gradle.kts
tasks.register<IncrementalReverseTask>("incrementalReverse") {
    inputDir = file("inputs")
    outputDir = layout.buildDirectory.dir("outputs")
    inputProperty = project.findProperty("taskInputProperty") as String? ?: "original"
}
build.gradle
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() 傳回。

以下範例修改一個檔案的內容並在執行增量任務之前新增另一個檔案

build.gradle.kts
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.")
    }
}
build.gradle
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
各種變異任務(updateInputsremoveInput 等)僅用於展示增量任務的行為。它們不應視為您自己的建置指令碼中應該具有的任務或任務實作類型。

當現有的輸入檔案被移除時,重新執行任務會導致該檔案由 InputChanges.getFileChanges() 傳回,並標示為 REMOVED

下列範例在執行增量任務之前移除現有檔案之一

build.gradle.kts
tasks.register<Delete>("removeInput") {
    delete("inputs/3.txt")
}
build.gradle
tasks.register('removeInput', Delete) {
    delete 'inputs/3.txt'
}
$ gradle -q removeInput incrementalReverse
Executing incrementally
REMOVED: 3.txt

輸出檔案被刪除(或修改)時,Gradle 無法判斷哪些輸入檔案已過時。在這種情況下,給定屬性的所有輸入檔案的詳細資料都會由 InputChanges.getFileChanges() 傳回。

下列範例從建置目錄中移除輸出檔案之一。但所有輸入檔案都被視為 ADDED

build.gradle.kts
tasks.register<Delete>("removeOutput") {
    delete(layout.buildDirectory.file("outputs/1.txt"))
}
build.gradle
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 會驗證 URL 是否能透過建立 HTTP 呼叫並檢查回應碼來解析。要驗證的 URL 可透過屬性 url 設定。屬性的 setter 方法會使用 @Option 註解

UrlVerify.java
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 類型的任務執行個體,並透過公開的選項提供命令列中的值

build.gradle.kts
tasks.register<UrlVerify>("verifyUrl")
build.gradle
tasks.register('verifyUrl', UrlVerify)
$ gradle -q verifyUrl --url=http://www.google.com/
Verifying URL 'http://www.google.com/'

選項支援的資料類型

Gradle 限制了可宣告命令列選項的資料類型。

命令列的使用方式會因類型而異

booleanBooleanProperty<Boolean>

描述值為 truefalse 的選項。
在命令列中傳遞選項會將值視為 true。例如,--foo 等於 true
如果沒有選項,則使用屬性的預設值。對於每個布林選項,會自動建立相反的選項。例如,會為提供的選項 --foo 建立 --no-foo,並為 --no-bar 建立 --bar。名稱以 --no 開頭的選項為停用選項,並將選項值設定為 false。只有當任務尚不存在具有相同名稱的選項時,才會建立相反的選項。

DoubleProperty<Double>

描述具有雙精度值的選項。
在命令列中傳遞選項也需要一個值,例如,--factor=2.2--factor 2.2

IntegerProperty<Integer>

描述具有整數值的選項。
在命令列中傳遞選項也需要一個值,例如,--network-timeout=5000--network-timeout 5000

LongProperty<Long>

描述具有長整數值的選項。
在命令列中傳遞選項也需要一個值,例如,--threshold=2147483648--threshold 2147483648

StringProperty<String>

描述具有任意字串值的選項。
在命令列中傳遞選項也需要一個值,例如,--container-id=2x94held--container-id 2x94held

enumProperty<enum>

將選項描述為列舉類型。
在命令列中傳遞選項也需要一個值,例如,--log-level=DEBUG--log-level debug
值不區分大小寫。

List<T> 其中 TDoubleIntegerLongStringenum

描述可以採用給定類型多個值的選項。
選項的值必須作為多個宣告提供,例如,--image-id=123 --image-id=456
目前不支援其他表示法,例如逗號分隔清單或以空白字元分隔的多個值。

ListProperty<T>SetProperty<T> 其中 TDoubleIntegerLongStringenum

描述可以採用給定類型多個值的選項。
選項的值必須作為多個宣告提供,例如,--image-id=123 --image-id=456
目前不支援其他表示法,例如逗號分隔清單或以空白字元分隔的多個值。

DirectoryPropertyRegularFileProperty

描述具有檔案系統元素的選項。
在命令列中傳遞選項也需要一個代表路徑的值,例如,--output-file=file.txt--output-dir outputDir
相對路徑會相對於擁有此屬性實例的專案專案目錄解析。請參閱 FileSystemLocationProperty.set()

記錄選項可用的值

理論上,屬性類型 StringList<String> 的選項可以接受任何任意值。此類選項可接受的值可以使用註解 OptionValues 以程式方式記錄

@OptionValues('file')

此註解可以指定給任何傳回受支援資料類型之一的 List 的方法。您需要指定選項識別碼,以表示選項與可用值之間的關係。

在命令列中傳遞選項不支援的值不會導致建置失敗或擲回例外。您必須在任務動作中實作此類行為的客製化邏輯。

以下範例示範如何針對單一任務使用多個選項。任務實作提供選項 output-type 的可用值清單

UrlProcess.java
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
    }
}

列出命令列選項

使用註解 OptionOptionValues 的命令列選項會自動記錄。

您會看到 已宣告的選項 及其 可用值 反映在 help 任務的控制台輸出中。輸出會按字母順序呈現選項,但布林值停用選項除外,這些選項會顯示在啟用選項之後

$ 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
     -

限制

目前宣告命令列選項的支援有一些限制。

  • 命令列選項只能透過註解為自訂任務宣告。沒有用於定義選項的程式化等效項。

  • 無法在全域宣告選項,例如,在專案層級或作為外掛的一部分。

  • 在命令列上指定選項時,公開選項的任務需要明確寫出,例如,gradle check --tests abc 無法運作,即使 check 任務依賴於 test 任務。

  • 如果您指定的任務選項名稱與內建 Gradle 選項的名稱衝突,請在呼叫任務之前使用 -- 分隔符號來參考該選項。如需更多資訊,請參閱 區分任務選項與內建選項

驗證失敗

通常,在任務執行期間引發的例外狀況會導致立即終止組建的失敗。任務的結果會是 FAILED,組建的結果會是 FAILED,且不會執行進一步的任務。在 使用 --continue 旗標執行 時,Gradle 會在遇到任務失敗後繼續執行組建中其他已要求的任務。不過,任何依賴於失敗任務的任務都不會執行。

有一種特殊類型的例外狀況,當下游任務僅依賴於失敗任務的輸出時,其行為會有所不同。任務可以引發 VerificationException 的子類型,以指出任務已以受控方式失敗,且其輸出對使用者而言仍然有效。當任務使用 dependsOn 直接依賴於另一個任務時,它會依賴於該任務的結果。當 Gradle 使用 --continue 執行時,依賴於生產者任務輸出(透過任務輸入和輸出之間的關係)的使用者任務可以在使用者失敗後仍然執行。

例如,失敗的單元測試會導致測試任務的失敗結果。不過,這不會阻止其他任務讀取和處理任務產生的(有效)測試結果。驗證失敗正是 測試報告聚合外掛 以這種方式使用的。

驗證失敗也對需要在產生其他任務可使用的有用輸出後報告失敗的任務很有用。

build.gradle.kts
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)
    }
}
build.gradle
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 可以讀取並顯示部分(但仍有效)的結果。