增量任務

在 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

您可以在 延遲配置 中了解更多關於 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 透過進行 HTTP 呼叫並檢查回應碼來驗證 URL 是否可以解析。要驗證的 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
     -

限制

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

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

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

  • 在命令列上指派選項時,需要明確拼寫出公開選項的任務,例如,即使 check 任務依賴於 test 任務,gradle check --tests abc 也不起作用。

  • 如果您指定的任務選項名稱與內建 Gradle 選項的名稱衝突,請在使用 -- 定界符後再呼叫您的任務以參考該選項。如需更多資訊,請參閱 消除任務選項與內建選項的歧義

驗證失敗

通常,在任務執行期間擲回的例外狀況會導致失敗,並立即終止建置。任務的結果將為 FAILED,建置的結果將為 FAILED,並且不會執行進一步的任務。當使用 --continue 旗標執行時,Gradle 將在遇到任務失敗後繼續執行建置中的其他請求任務。但是,任何依賴於失敗任務的任務將不會執行。

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

例如,失敗的單元測試將導致測試任務的結果失敗。但是,這不會阻止另一個任務讀取和處理任務產生的(有效)測試結果。驗證失敗正是以這種方式被 Test Report Aggregation Plugin 使用。

驗證失敗也適用於即使在產生其他任務可消耗的有用輸出後仍需要回報失敗的任務。

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 可以讀取和顯示部分(但仍然有效)結果。