任何建置工具的重要部分都是避免執行已經完成的工作。考慮編譯的過程。一旦您的原始檔編譯完成,除非影響輸出的內容有所變更,例如原始檔修改或輸出檔移除,否則不應重新編譯。而編譯可能需要花費大量時間,因此在不需要時略過此步驟可以節省很多時間。

Gradle 透過稱為「增量建置」的功能,立即支援此行為。您幾乎肯定已經看到它在運作。當您執行任務,而任務在主控台輸出中標記為 UP-TO-DATE,這表示增量建置正在運作。

增量建置如何運作?如何確定您的工作支援增量執行?讓我們來看看。

工作輸入和輸出

在最常見的情況下,工作會取得一些輸入並產生一些輸出。我們可以將 Java 編譯過程視為工作的範例。Java 原始檔作為工作的輸入,而產生的類別檔(也就是編譯結果)則是工作的輸出。

taskInputsOutputs
圖 1. 工作輸入和輸出的範例

輸入的一個重要特徵是它會影響一個或多個輸出,如您從前一個圖中所見。會根據原始檔的內容和您想要在上面執行程式碼的 Java 執行時間的最低版本來產生不同的位元組碼。這讓它們成為工作輸入。但是編譯是否有 500MB 或 600MB 的最大可用記憶體(由 memoryMaximumSize 屬性決定)並不會影響產生哪些位元組碼。在 Gradle 術語中,memoryMaximumSize 僅是內部工作屬性。

作為增量建置的一部分,Gradle 會測試自上次建置以來是否有任何工作輸入或輸出已變更。如果沒有,Gradle 可以將工作視為最新,因此略過執行其動作。另外請注意,除非工作至少有一個工作輸出,否則增量建置不會運作,儘管工作通常也至少有一個輸入。

這對建置作者來說很簡單:您需要告訴 Gradle 哪些工作屬性是輸入,哪些是輸出。如果工作屬性會影響輸出,請務必將其註冊為輸入,否則工作在尚未更新時會被視為最新。相反地,如果屬性不會影響輸出,請勿將其註冊為輸入,否則工作可能會在不需要時執行。另外請小心非決定性工作,它們可能會為完全相同的輸入產生不同的輸出:這些工作不應設定為增量建置,因為最新檢查無法運作。

現在讓我們看看如何將工作屬性註冊為輸入和輸出。

透過註解宣告輸入和輸出

如果您將自訂工作實作為類別,則只需兩個步驟即可讓它與增量建置搭配運作

  1. 為每個工作輸入和輸出建立型別屬性(透過 getter 方法)

  2. 將適當的註解新增到每個屬性

註解必須置於 getter 或 Groovy 屬性上。置於 setter 或 Java 欄位(沒有對應的註解 getter)上的註解會被忽略。

Gradle 支援四種主要的輸入和輸出類別

  • 簡單值

    字串和數字等項目。更一般地說,簡單值可以具有實作 `Serializable` 的任何類型。

  • 檔案系統類型

    這些類型包含 `RegularFile`、`Directory` 和標準 `File` 類別,但也包含 Gradle 的 FileCollection 類型的衍生類別,以及任何可以傳遞給 Project.file(java.lang.Object) 方法(用於單一檔案/目錄屬性)或 Project.files(java.lang.Object...) 方法的任何其他項目。

  • 相依性解析結果

    這包括 ResolvedArtifactResult 類型(用於人工製品的元資料)和 ResolvedComponentResult 類型(用於相依性圖表)。請注意,它們僅支援包覆在 `Provider` 中。

  • 巢狀值

    自訂類型,不符合其他兩個類別,但有自己的屬性作為輸入或輸出。實際上,任務輸入或輸出會巢狀在這些自訂類型中。

舉例來說,假設您有一個任務可以處理不同類型的範本,例如 FreeMarker、Velocity、Moustache 等。它會取得範本原始檔,並將它們與一些模型資料結合,以產生範本檔的已填入版本。

這個任務會有三個輸入和一個輸出

  • 範本原始檔

  • 模型資料

  • 範本引擎

  • 輸出檔的寫入位置

當您撰寫自訂任務類別時,可以透過註解輕鬆地將屬性註冊為輸入或輸出。為了示範,以下是範例任務實作,其中包含一些適當的輸入和輸出,以及其註解

buildSrc/src/main/java/org/example/ProcessTemplates.java
package org.example;

import java.util.HashMap;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileSystemOperations;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.*;

import javax.inject.Inject;

public abstract class ProcessTemplates extends DefaultTask {

    @Input
    public abstract Property<TemplateEngineType> getTemplateEngine();

    @InputFiles
    public abstract ConfigurableFileCollection getSourceFiles();

    @Nested
    public abstract TemplateData getTemplateData();

    @OutputDirectory
    public abstract DirectoryProperty getOutputDir();

    @Inject
    public abstract FileSystemOperations getFs();

    @TaskAction
    public void processTemplates() {
        // ...
    }
}
buildSrc/src/main/java/org/example/TemplateData.java
package org.example;

import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;

public abstract class TemplateData {

    @Input
    public abstract Property<String> getName();

    @Input
    public abstract MapProperty<String, String> getVariables();
}
gradle processTemplates 的輸出
> gradle processTemplates
> Task :processTemplates

BUILD SUCCESSFUL in 0s
3 actionable tasks: 3 up-to-date
gradle processTemplates 的輸出(再次執行)
> gradle processTemplates
> Task :processTemplates UP-TO-DATE

BUILD SUCCESSFUL in 0s
3 actionable tasks: 3 up-to-date

這個範例有很多值得討論的地方,因此讓我們逐一探討每個輸入和輸出屬性

  • templateEngine

    表示處理來源範本時要使用的引擎,例如 FreeMarker、Velocity 等。您可以將其實作為字串,但在這種情況下,我們選擇自訂列舉,因為它提供更豐富的類型資訊和安全性。由於列舉會自動實作 Serializable,因此我們可以將其視為簡單值,並使用 @Input 註解,就像我們使用 String 屬性一樣。

  • sourceFiles

    任務將處理的來源範本。單一檔案和檔案集合需要自己的特殊註解。在這種情況下,我們處理的是輸入檔案集合,因此我們使用 @InputFiles 註解。稍後您會在表格中看到更多以檔案為導向的註解。

  • templateData

    針對此範例,我們使用自訂類別來表示模型資料。但是,它沒有實作 Serializable,因此我們無法使用 @Input 註解。這沒有問題,因為 TemplateData 中的屬性(字串和具有可序列化類型參數的雜湊映射)是可序列化的,並且可以用 @Input 註解。我們在 templateData 上使用 @Nested,讓 Gradle 知道這是一個具有巢狀輸入屬性的值。

  • outputDir

    產生檔案的目錄。與輸入檔案一樣,輸出檔案和目錄有數個註解。表示單一目錄的屬性需要 @OutputDirectory。您很快就會了解其他註解。

這些有註解的屬性表示,如果自 Gradle 上次執行任務後,來源檔案、範本引擎、模型資料或產生檔案都沒有變更,Gradle 就會略過任務。這通常可以節省大量時間。您可以了解 Gradle 如何偵測 變更

這個範例特別有趣,因為它會處理來源檔案集合。如果只變更一個來源檔案,會發生什麼事?任務會再次處理所有來源檔案,還是只處理已修改的檔案?這取決於任務實作。如果是後者,則任務本身是漸進式的,但那是我們在此討論的不同功能。Gradle 會透過其 漸進式任務輸入 功能協助任務實作者處理這項工作。

現在您已經看到一些輸入和輸出註解的實際應用,讓我們來看看所有可用的註解,以及您應該在何時使用它們。下表列出可用的註解,以及您可以與每個註解搭配使用的對應屬性類型。

表格 1. 漸進式建置屬性類型註解
註解 預期的屬性類型 說明

任何 Serializable 類型或相依關係解析結果類型

簡單的輸入值或相依關係解析結果

檔案*

單一輸入檔案(非目錄)

檔案*

單一輸入目錄(非檔案)

Iterable<File>*

輸入檔案和目錄的 Iterable

Iterable<File>*

表示 Java 類別路徑的輸入檔案和目錄的 Iterable。這允許任務忽略對屬性的不相關變更,例如相同檔案的不同名稱。這類似於註解屬性 @PathSensitive(RELATIVE),但它會忽略直接新增到類別路徑的 JAR 檔案名稱,並且會將檔案順序的變更視為類別路徑的變更。Gradle 會檢查類別路徑上 JAR 檔案的內容,並忽略不影響類別路徑語意的變更(例如檔案日期和條目順序)。另請參閱使用類別路徑註解

注意:@Classpath 註解是在 Gradle 3.2 中引入的。為了與較早版本的 Gradle 相容,類別路徑屬性也應該使用 @InputFiles 註解。

Iterable<File>*

表示 Java 編譯類別路徑的輸入檔案和目錄的 Iterable。這允許任務忽略不影響類別路徑中類別 API 的不相關變更。另請參閱使用類別路徑註解

下列類別路徑變更類型將會被忽略

  • JAR 或頂層目錄路徑的變更。

  • JAR 中的時間戳記和條目順序的變更。

  • 資源和 JAR 清單的變更,包括新增或移除資源。

  • 私人類別元素的變更,例如私人欄位、方法和內部類別。

  • 程式碼的變更,例如方法主體、靜態初始化程式和欄位初始化程式(常數除外)。

  • 偵錯資訊的變更,例如當對註解的變更影響類別偵錯資訊中的行號時。

  • 目錄的變更,包括 JAR 中的目錄條目。

注意:@CompileClasspath 註解是在 Gradle 3.4 中引入的。為了與 Gradle 3.3 和 3.2 相容,編譯類別路徑屬性也應該使用 @Classpath 註解。為了與 3.2 之前的 Gradle 版本相容,屬性也應該使用 @InputFiles 註解。

檔案*

單一輸出檔案(非目錄)

檔案*

單一輸出目錄(非檔案)

Map<String, File>** 或 Iterable<File>*

輸出檔案的 Iterable 或 Map。使用檔案樹會關閉任務的快取

Map<String, File>** 或 Iterable<File>*

輸出目錄的 Iterable。使用檔案樹會關閉任務的快取

FileIterable<File>*

指定此任務移除的一個或多個檔案。請注意,任務可以定義輸入/輸出或可毀滅項目,但不能同時定義。

FileIterable<File>*

指定一個或多個代表任務本地狀態的檔案。從快取載入任務時,這些檔案會被移除。

任何自訂類型

自訂類型,可能未實作 Serializable,但至少有一個欄位或屬性標記為此表格中的其中一個註解。它甚至可以是另一個 @Nested

任何類型

表示屬性既不是輸入也不是輸出。它僅以某種方式影響任務的控制台輸出,例如增加或減少任務的詳細程度。

任何類型

表示屬性在內部使用,但既不是輸入也不是輸出。

任何類型

表示屬性已被另一個屬性取代,應忽略為輸入或輸出。

FileIterable<File>*

@InputFiles@InputDirectory 搭配使用,告訴 Gradle 如果對應的檔案或目錄為空,以及所有其他使用此註解宣告的輸入檔案,則略過任務。由於所有使用此註解宣告的輸入檔案都為空而略過的任務將導致不同的「無來源」結果。例如,控制台輸出中會發出 NO-SOURCE

暗示 @Incremental

Provider<FileSystemLocation>FileCollection

@InputFiles@InputDirectory 搭配使用,指示 Gradle 追蹤註解檔案屬性的變更,因此變更可透過 @InputChanges.getFileChanges() 查詢。增量任務需要此功能。

任何類型

Optional API 文件中列出的任何屬性類型註解一起使用。此註解會停用對應屬性的驗證檢查。有關更多詳細資訊,請參閱 驗證區段

FileIterable<File>*

與任何輸入檔案屬性一起使用,以告知 Gradle 僅將檔案路徑的特定部分視為重要。例如,如果屬性加上註解 @PathSensitive(PathSensitivity.NAME_ONLY),則在不變更檔案內容的情況下移動檔案,不會使工作過期。

FileIterable<File>*

@InputFiles@InputDirectory 一起使用,以指示 Gradle 僅追蹤目錄內容的變更,而不追蹤目錄本身的差異。例如,在目錄結構中移除、重新命名或新增一個空目錄,不會使工作過期。

FileIterable<File>*

@InputFiles@InputDirectory@Classpath 一起使用,以指示 Gradle 在計算最新檢查或建置快取金鑰時,要標準化換行符號。例如,在 Unix 換行符號和 Windows 換行符號之間切換檔案 (或反之亦然),不會使工作過期。

File 可以是 Project.file(java.lang.Object) 接受的任何類型,而 Iterable<File> 可以是 Project.files(java.lang.Object…​) 接受的任何類型。這包括 Callable 的執行個體,例如封閉,允許延遲評估屬性值。請注意,類型 FileCollectionFileTreeIterable<File>

與上述類似,File 可以是 Project.file(java.lang.Object) 接受的任何類型。Map 本身可以包裝在 Callable 中,例如封閉。

註解會從所有父類型繼承,包括實作的介面。屬性類型註解會覆寫在父類型中宣告的任何其他屬性類型註解。這樣一來,可以在子工作類型中將 @InputFile 屬性轉換為 @InputDirectory 屬性。

在類型覆寫中宣告的屬性註解會類似於超級類別和任何實作介面所宣告的註解。超級類別註解優先於在實作介面中宣告的註解。

表格中的 ConsoleInternal 註解是特殊情況,因為它們不會宣告任務輸入或任務輸出。那麼為什麼要使用它們?這是為了讓您可以利用 Java Gradle Plugin Development 外掛程式 來協助您開發和發佈自己的外掛程式。此外掛程式會檢查您的自訂任務類別的任何屬性是否缺少增量建置註解。這可以防止您在開發期間忘記新增適當的註解。

使用相依性解析結果

相依性解析結果可以用兩種方式作為任務輸入。首先,使用 ResolvedComponentResult 來使用已解析的元資料圖形。其次,使用 ResolvedArtifactResult 來使用已解析的成品平面集合。

可以從 Configuration 的輸入解析結果中延遲取得已解析的圖形,並將其連接到 @Input 屬性

任務宣告
@Input
public abstract Property<ResolvedComponentResult> getRootComponent();
任務組態
Configuration runtimeClasspath = configurations.getByName("runtimeClasspath");

task.getRootComponent().set(
    runtimeClasspath.getIncoming().getResolutionResult().getRootComponent()
);

可以從 Configuration 的輸入成品中延遲取得已解析的成品集合。由於 ResolvedArtifactResult 類型包含元資料和檔案資訊,因此在連接到 @Input 屬性之前,需要將執行個體轉換為僅元資料

任務宣告
@Input
public abstract ListProperty<ComponentArtifactIdentifier> getArtifactIds();
任務組態
Configuration runtimeClasspath = configurations.getByName("runtimeClasspath");
Provider<Set<ResolvedArtifactResult>> artifacts = runtimeClasspath.getIncoming().getArtifacts().getResolvedArtifacts();

task.getArtifactIds().set(artifacts.map(new IdExtractor()));

static class IdExtractor
    implements Transformer<List<ComponentArtifactIdentifier>, Collection<ResolvedArtifactResult>> {
    @Override
    public List<ComponentArtifactIdentifier> transform(Collection<ResolvedArtifactResult> artifacts) {
        return artifacts.stream().map(ResolvedArtifactResult::getId).collect(Collectors.toList());
    }
}

圖形和平面結果都可以結合並擴充已解析的檔案資訊。這都示範在 具有相依性解析結果輸入的任務 範例中。

使用類別路徑註解

除了 @InputFiles,對於與 JVM 相關的任務,Gradle 了解類別路徑輸入的概念。當 Gradle 尋找變更時,執行時間和編譯類別路徑都會以不同的方式處理。

與使用 @InputFiles 註解的輸入屬性相反,對於類別路徑屬性,檔案集合中條目的順序很重要。另一方面,類別路徑本身中目錄和 jar 檔案的名稱和路徑會被忽略。類別路徑中 jar 檔案內類別檔案和資源的時間戳記和順序也會被忽略,因此重新建立具有不同檔案日期的 jar 檔案不會使任務過期。

執行時期類別路徑標記為 @Classpath,並透過 類別路徑正規化 提供進一步自訂。

註解為 @CompileClasspath 的輸入屬性被視為 Java 編譯類別路徑。除了上述一般類別路徑規則之外,編譯類別路徑會忽略對所有內容的變更,但類別檔案除外。Gradle 使用 Java 編譯避免 中描述的相同類別分析,進一步篩選不會影響類別 ABI 的變更。這表示只觸及類別實作的變更不會使任務過期。

巢狀輸入

當分析 @Nested 任務屬性以取得已宣告的輸入和輸出子屬性時,Gradle 會使用實際值的類型。因此,它可以找出執行時期子類型宣告的所有子屬性。

當將 @Nested 加入 Provider 時,Provider 的值會被視為巢狀輸入。

當將 @Nested 加入可迭代物件時,每個元素會被視為一個獨立的巢狀輸入。可迭代物件中的每個巢狀輸入都會被指定一個名稱,預設為美元符號,後面加上可迭代物件中的索引,例如 $2。如果可迭代物件的元素實作 Named,則名稱會用作屬性名稱。可迭代物件中元素的順序對於可靠的最新檢查和快取至關重要,除非所有元素都實作 Named。不允許多個具有相同名稱的元素。

當將 @Nested 加入地圖時,則會為每個值加入一個巢狀輸入,並使用金鑰作為名稱。

巢狀輸入的類型和類別路徑也會被追蹤。這可確保巢狀輸入實作的變更會導致建置過期。透過此方式,也可以將使用者提供的程式碼加入為輸入,例如透過註解 @Action 屬性,並加上 @Nested。請注意,此類動作的任何輸入都應該被追蹤,不論是透過動作上的註解屬性,還是透過手動向任務註冊。

使用巢狀輸入可為任務提供更豐富的建模和可延伸性,例如 Test.getJvmArgumentProviders() 所示。

這讓我們可以建模 JaCoCo Java 代理,因此宣告必要的 JVM 參數,並提供輸入和輸出給 Gradle

JacocoAgent.java
class JacocoAgent implements CommandLineArgumentProvider {
    private final JacocoTaskExtension jacoco;

    public JacocoAgent(JacocoTaskExtension jacoco) {
        this.jacoco = jacoco;
    }

    @Nested
    @Optional
    public JacocoTaskExtension getJacoco() {
        return jacoco.isEnabled() ? jacoco : null;
    }

    @Override
    public Iterable<String> asArguments() {
        return jacoco.isEnabled() ? ImmutableList.of(jacoco.getAsJvmArg()) : Collections.<String>emptyList();
    }
}

test.getJvmArgumentProviders().add(new JacocoAgent(extension));

為此,JacocoTaskExtension 需要有正確的輸入和輸出註解。

這種方法適用於 Test JVM 參數,因為 Test.getJvmArgumentProviders() 是註解為 @NestedIterable

還有其他任務類型可以使用這種巢狀輸入

同樣地,這種建模方式可供自訂工作使用。

執行時期驗證

在執行建置時,Gradle 會檢查工作類型是否已宣告適當的註解。它會嘗試找出問題,例如註解用於不相容的類型,或用於設定器等。未註解為輸入/輸出註解的任何取得器也會被標記。這些問題會導致建置失敗,或在執行工作時轉變為不建議使用的警告。

具有驗證警告的工作會在沒有任何最佳化的情況下執行。具體來說,它們永遠無法

  • 是最新的,

  • 建置快取載入或儲存在建置快取中,

  • 與其他工作並行執行,即使已啟用並行執行

  • 遞增執行。

在執行無效的工作之前,檔案系統狀態的記憶體中表示(虛擬檔案系統也會失效。

透過執行時期 API 宣告輸入和輸出

自訂工作類別是一種將您自己的建置邏輯帶入遞增建置領域的簡單方法,但您並不總是擁有該選項。這就是為什麼 Gradle 也提供可與任何工作一起使用的替代 API,我們接下來將探討。

當您無法存取自訂工作類別的來源時,無法新增我們在前一節中涵蓋的任何註解。幸運的是,Gradle 提供了適用於此類情境的執行時期 API。它也可供臨時工作使用,如您接下來將看到的。

宣告臨時工作的輸入和輸出

此執行時期 API 是透過幾個適當命名的屬性提供的,這些屬性可供每個 Gradle 工作使用

這些物件有方法可讓您指定構成任務輸入和輸出的檔案、目錄和值。事實上,執行時期 API 幾乎與註解具有相同的功能。

它缺少下列等效項

讓我們採用先前的範本處理範例,看看如何將其視為使用執行時期 API 的臨時任務

範例 4. 臨時任務
build.gradle.kts
tasks.register("processTemplatesAdHoc") {
    inputs.property("engine", TemplateEngineType.FREEMARKER)
    inputs.files(fileTree("src/templates"))
        .withPropertyName("sourceFiles")
        .withPathSensitivity(PathSensitivity.RELATIVE)
    inputs.property("templateData.name", "docs")
    inputs.property("templateData.variables", mapOf("year" to "2013"))
    outputs.dir(layout.buildDirectory.dir("genOutput2"))
        .withPropertyName("outputDir")

    doLast {
        // Process the templates here
    }
}
build.gradle
tasks.register('processTemplatesAdHoc') {
    inputs.property('engine', TemplateEngineType.FREEMARKER)
    inputs.files(fileTree('src/templates'))
        .withPropertyName('sourceFiles')
        .withPathSensitivity(PathSensitivity.RELATIVE)
    inputs.property('templateData.name', 'docs')
    inputs.property('templateData.variables', [year: '2013'])
    outputs.dir(layout.buildDirectory.dir('genOutput2'))
        .withPropertyName('outputDir')

    doLast {
        // Process the templates here
    }
}
gradle processTemplatesAdHoc 的輸出
> gradle processTemplatesAdHoc
> Task :processTemplatesAdHoc

BUILD SUCCESSFUL in 0s
3 actionable tasks: 3 executed

與先前相同,有很多需要討論的地方。首先,您真的應該為此撰寫自訂任務類別,因為它是一個具有多個組態選項的非平凡實作。在此情況下,沒有任務屬性來儲存根源資料夾、輸出目錄的位置或任何其他設定。這是故意要強調執行時期 API 不需要任務有任何狀態。在增量建置方面,上述臨時任務的行為將與自訂任務類別相同。

所有輸入和輸出定義都是透過 inputsoutputs 上的方法來完成,例如 property()files()dir()。Gradle 會對引數值執行最新檢查,以判斷任務是否需要再次執行。每個方法都對應到其中一個增量建置註解,例如 inputs.property() 對應到 @Input,而 outputs.dir() 對應到 @OutputDirectory

任務移除的檔案可以透過 destroyables.register() 來指定。

build.gradle.kts
tasks.register("removeTempDir") {
    val tmpDir = layout.projectDirectory.dir("tmpDir")
    destroyables.register(tmpDir)
    doLast {
        tmpDir.asFile.deleteRecursively()
    }
}
build.gradle
tasks.register('removeTempDir') {
    def tempDir = layout.projectDirectory.dir('tmpDir')
    destroyables.register(tempDir)
    doLast {
        tempDir.asFile.deleteDir()
    }
}

執行時期 API 和註解之間一個顯著的差異是缺少直接對應到 @Nested 的方法。這就是範例使用兩個 property() 宣告來表示範本資料的原因,每個 TemplateData 屬性各一個。當您在執行時期 API 中使用巢狀值時,應該使用相同的技術。任何給定的任務都可以宣告可毀滅項目或輸入/輸出,但不能同時宣告這兩者。

精細組態

執行時期 API 方法只允許您宣告輸入和輸出本身。不過,以檔案為導向的方法會傳回一個建構函式 (型別為 TaskInputFilePropertyBuilder),讓您提供有關這些輸入和輸出的其他資訊。

您可以在其 API 文件中瞭解建構器提供的全部選項,但我們將在此處展示一個簡單範例,讓您了解可以執行的操作。

假設我們不希望在沒有來源檔案的情況下執行 processTemplates 任務,無論是否為乾淨建置。畢竟,如果沒有來源檔案,任務就沒有任何可執行的動作。建構器允許我們像這樣進行設定

build.gradle.kts
tasks.register("processTemplatesAdHocSkipWhenEmpty") {
    // ...

    inputs.files(fileTree("src/templates") {
            include("**/*.fm")
        })
        .skipWhenEmpty()
        .withPropertyName("sourceFiles")
        .withPathSensitivity(PathSensitivity.RELATIVE)
        .ignoreEmptyDirectories()

    // ...
}
build.gradle
tasks.register('processTemplatesAdHocSkipWhenEmpty') {
    // ...

    inputs.files(fileTree('src/templates') {
            include '**/*.fm'
        })
        .skipWhenEmpty()
        .withPropertyName('sourceFiles')
        .withPathSensitivity(PathSensitivity.RELATIVE)
        .ignoreEmptyDirectories()

    // ...
}
gradle clean processTemplatesAdHocSkipWhenEmpty 的輸出
> gradle clean processTemplatesAdHocSkipWhenEmpty
> Task :processTemplatesAdHocSkipWhenEmpty NO-SOURCE

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

TaskInputs.files() 方法會傳回一個建構器,其中包含一個 skipWhenEmpty() 方法。呼叫此方法等同於使用 @SkipWhenEmpty 註解屬性。

現在您已經看過註解和執行時期 API,您可能會想知道應該使用哪個 API。我們的建議是盡可能使用註解,而且有時值得建立自訂任務類別,以便您可以使用它們。執行時期 API 更適用於無法使用註解的情況。

宣告自訂任務類型的輸入和輸出

另一種類型的範例涉及為自訂任務類別的執行個體註冊額外的輸入和輸出。例如,假設 ProcessTemplates 任務也需要讀取 src/headers/headers.txt(例如,因為它包含在其中一個來源中)。您希望 Gradle 知道這個輸入檔案,以便它可以在這個檔案的內容變更時重新執行任務。使用執行時期 API,您可以執行此操作

build.gradle.kts
tasks.register<ProcessTemplates>("processTemplatesWithExtraInputs") {
    // ...

    inputs.file("src/headers/headers.txt")
        .withPropertyName("headers")
        .withPathSensitivity(PathSensitivity.NONE)
}
build.gradle
tasks.register('processTemplatesWithExtraInputs', ProcessTemplates) {
    // ...

    inputs.file('src/headers/headers.txt')
        .withPropertyName('headers')
        .withPathSensitivity(PathSensitivity.NONE)
}

像這樣使用執行時期 API 有點像是使用 doLast()doFirst() 將額外的動作附加到任務,但在此情況下,我們附加的是關於輸入和輸出的資訊。

如果任務類型已經使用增量建置註解,則使用相同屬性名稱註冊輸入或輸出會導致錯誤。

宣告任務輸入和輸出的好處

一旦你宣告任務的正式輸入和輸出,Gradle 便可以推論出這些屬性的資訊。例如,如果一個任務的輸入設定為另一個任務的輸出,這表示第一個任務依賴於第二個任務,對吧?Gradle 知道這一點,並且可以採取行動。

我們將接著探討這個功能,以及 Gradle 從輸入和輸出中得知資訊而衍生的其他一些功能。

推論的任務依賴關係

考慮一個封裝 processTemplates 任務輸出的封存任務。建置作者會看到封存任務顯然需要 processTemplates 先執行,因此可能會新增明確的 dependsOn。不過,如果你像這樣定義封存任務

build.gradle.kts
tasks.register<Zip>("packageFiles") {
    from(processTemplates.map { it.outputDir })
}
build.gradle
tasks.register('packageFiles', Zip) {
    from processTemplates.map { it.outputDir }
}
gradle clean packageFiles 的輸出
> gradle clean packageFiles
> Task :processTemplates
> Task :packageFiles

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

Gradle 會自動讓 packageFiles 依賴於 processTemplates。它可以這樣做,因為它知道 packageFiles 的其中一個輸入需要 processTemplates 任務的輸出。我們稱這為推論的任務依賴關係。

上述範例也可以寫成

build.gradle.kts
tasks.register<Zip>("packageFiles2") {
    from(processTemplates)
}
build.gradle
tasks.register('packageFiles2', Zip) {
    from processTemplates
}
gradle clean packageFiles2 的輸出
> gradle clean packageFiles2
> Task :processTemplates
> Task :packageFiles2

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

這是因為 from() 方法可以接受任務物件作為參數。在幕後,from() 使用 project.files() 方法來包裝參數,而這反過來會將任務的正式輸出公開為檔案集合。換句話說,這是一個特例!

輸入和輸出驗證

增量建置註解提供了足夠的資訊,讓 Gradle 能對註解屬性執行一些基本驗證。特別是,它在任務執行前對每個屬性執行下列動作

  • @InputFile - 驗證屬性有值,且路徑對應到存在的檔案(不是目錄)。

  • @InputDirectory - 與 @InputFile 相同,但路徑必須對應到目錄。

  • @OutputDirectory - 驗證路徑與檔案不符,並在路徑不存在時建立目錄。

如果一個任務在某個位置產生輸出,而另一個任務透過將該位置視為輸入來使用該輸出,則 Gradle 會檢查使用者任務是否依賴於產生者任務。當產生者和使用者任務同時執行時,建置會失敗,以避免擷取不正確的狀態。

此類驗證可提升建置的健全性,讓你能夠快速找出與輸入和輸出相關的問題。

偶爾您會想要停用一些驗證,特別是當輸入檔案可能有效地不存在時。這就是 Gradle 提供 @Optional 注解的原因:您使用它來告訴 Gradle 特定的輸入是可選的,因此如果對應的檔案或目錄不存在,則建置不應失敗。

持續建置

定義任務輸入和輸出的另一個好處是持續建置。由於 Gradle 知道任務依賴哪些檔案,因此如果其任何輸入變更,它可以自動再次執行任務。在執行 Gradle 時透過 --continuous-t 選項啟用持續建置,您會將 Gradle 置於持續檢查變更並在遇到此類變更時執行所要求任務的狀態。

您可以在 持續建置 中找到更多關於此功能的資訊。

任務並行性

定義任務輸入和輸出的最後一個好處是 Gradle 可以使用此資訊來決定在使用「--parallel」選項時如何執行任務。例如,Gradle 會在選擇要執行的下一個任務時檢查任務的輸出,並避免同時執行寫入相同輸出目錄的任務。類似地,Gradle 會使用有關任務會刪除哪些檔案的資訊(例如由 Destroys 注解指定),並避免在另一個任務正在執行時執行會移除一組檔案的任務,而該任務會使用或建立相同的檔案(反之亦然)。它還可以確定已執行建立一組檔案的任務,而使用這些檔案的任務尚未執行,並且會避免在中間執行會移除這些檔案的任務。透過這種方式提供任務輸入和輸出資訊,Gradle 可以推斷任務之間的建立/使用/刪除關係,並可以確保任務執行不會違反這些關係。

它是如何運作的?

在任務第一次執行之前,Gradle 會對輸入進行指紋辨識。此指紋包含輸入檔案的路徑和每個檔案內容的雜湊。然後 Gradle 執行任務。如果任務成功完成,Gradle 會對輸出進行指紋辨識。此指紋包含輸出檔案的集合和每個檔案內容的雜湊。Gradle 會保留兩個指紋以供下次執行任務時使用。

在那之後的每次,在執行任務之前,Gradle 會對輸入和輸出進行新的指紋辨識。如果新的指紋與先前的指紋相同,Gradle 會假設輸出是最新的,並略過任務。如果它們不同,Gradle 會執行任務。Gradle 會保留兩個指紋以供下次執行任務時使用。

如果檔案的統計資料(例如 `lastModified` 和 `size`)沒有變更,Gradle 會重複使用前一次執行時檔案的指紋。這表示 Gradle 在檔案統計資料沒有變更時不會偵測到變更。

Gradle 也將任務的程式碼視為任務輸入的一部分。當任務、其動作或其相依性在執行之間變更時,Gradle 會將任務視為過時。

Gradle 會了解檔案屬性(例如持有 Java 類別路徑的屬性)是否對順序敏感。在比較此類屬性的指紋時,即使檔案順序變更,也會導致任務過時。

請注意,如果任務有指定的輸出目錄,自上次執行以來新增到該目錄的任何檔案都會被忽略,而且不會導致任務過時。這是為了讓不相關的任務可以共用輸出目錄,而不會互相干擾。如果您因為某些原因不想要這種行為,請考慮使用 TaskOutputs.upToDateWhen(groovy.lang.Closure)

另請注意,變更不可用檔案的可用性(例如將中斷的符號連結目標修改為有效檔案,或反之亦然),將會由最新檢查偵測並處理。

任務的輸入也用於計算 建置快取金鑰,用於在啟用時載入任務輸出。如需更多詳細資料,請參閱 任務輸出快取

為了追蹤任務、任務動作和巢狀輸入的實作,Gradle 會使用類別名稱和包含實作的類別路徑的識別碼。有些情況下,Gradle 無法精確追蹤實作

未知類別載入器

當載入實作的類別載入器不是由 Gradle 建立時,就無法判斷類別路徑。

Java lambda

Java lambda 類別會在執行階段以非決定性的類別名稱建立。因此,類別名稱不會識別 lambda 的實作,而且會在不同的 Gradle 執行之間變更。

當無法精確追蹤任務、任務動作或巢狀輸入的實作時,Gradle 會停用任務的任何快取。這表示任務永遠不會是最新的,也不會從 建置快取載入。

進階技術

到目前為止,您在本節中所看到的一切將涵蓋您會遇到的絕大多數使用案例,但有些場景需要特殊處理。我們將在接下來介紹其中幾個場景,並提供適當的解決方案。

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

您是否曾想過 Copy 任務的 from() 方法如何運作?它未註解為 @InputFiles,但傳遞給它的任何檔案都會視為任務的正式輸入。發生了什麼事?

實作相當簡單,您可以對自己的任務使用相同的技術來改善其 API。撰寫您的方法,以便它們將檔案直接新增到適當的註解屬性。舉例來說,以下是將 sources() 方法新增到我們先前介紹的客製化 ProcessTemplates 類別的方法

build.gradle.kts
tasks.register<ProcessTemplates>("processTemplates") {
    templateEngine = TemplateEngineType.FREEMARKER
    templateData.name = "test"
    templateData.variables = mapOf("year" to "2012")
    outputDir = layout.buildDirectory.dir("genOutput")

    sources(fileTree("src/templates"))
}
build.gradle
tasks.register('processTemplates', ProcessTemplates) {
    templateEngine = TemplateEngineType.FREEMARKER
    templateData.name = 'test'
    templateData.variables = [year: '2012']
    outputDir = file(layout.buildDirectory.dir('genOutput'))

    sources fileTree('src/templates')
}
ProcessTemplates.java
public abstract class ProcessTemplates extends DefaultTask {
    // ...
    @SkipWhenEmpty
    @InputFiles
    @PathSensitive(PathSensitivity.NONE)
    public abstract ConfigurableFileCollection getSourceFiles();

    public void sources(FileCollection sourceFiles) {
        getSourceFiles().from(sourceFiles);
    }

    // ...
}
gradle processTemplates 的輸出
> gradle processTemplates
> Task :processTemplates

BUILD SUCCESSFUL in 0s
3 actionable tasks: 3 executed

換句話說,只要您在組態階段將值和檔案新增到正式任務輸入和輸出,它們就會被視為此類,無論您在哪裡新增它們。

如果我們也想要支援任務作為引數,並將其輸出視為輸入,我們可以直接使用 TaskProvider,如下所示

build.gradle.kts
val copyTemplates by tasks.registering(Copy::class) {
    into(file(layout.buildDirectory.dir("tmp")))
    from("src/templates")
}

tasks.register<ProcessTemplates>("processTemplates2") {
    // ...
    sources(copyTemplates)
}
build.gradle
def copyTemplates = tasks.register('copyTemplates', Copy) {
    into file(layout.buildDirectory.dir('tmp'))
    from 'src/templates'
}

tasks.register('processTemplates2', ProcessTemplates) {
    // ...
    sources copyTemplates
}
ProcessTemplates.java
    // ...
    public void sources(TaskProvider<?> inputTask) {
        getSourceFiles().from(inputTask);
    }
    // ...
gradle processTemplates2 的輸出
> gradle processTemplates2
> Task :copyTemplates
> Task :processTemplates2

BUILD SUCCESSFUL in 0s
4 actionable tasks: 4 executed

此技術可以讓您的客製化任務更容易使用,並產生更簡潔的建置檔案。作為額外的好處,我們使用 TaskProvider 表示我們的客製化方法可以設定推論的任務相依性。

最後要注意的一件事:如果您正在開發一個將來源檔案集合作為輸入的任務,就像這個範例一樣,請考慮使用內建的 SourceTask。它可以讓您不必實作我們放入 ProcessTemplates 的一些管道。

當您想要將一個任務的輸出連結到另一個任務的輸入時,類型通常會相符,而且一個簡單的屬性指派會提供該連結。例如,可以將 File 輸出屬性指派給 File 輸入。

很不幸地,當您想要將任務中的 @OutputDirectory(類型為 File)的檔案變成另一個任務的 @InputFiles 屬性(類型為 FileCollection)的來源時,此方法就會失效。由於這兩個具有不同的類型,所以屬性指派無法運作。

舉例來說,假設您想要使用 Java 編譯任務的輸出(透過 destinationDir 屬性)作為客製化任務的輸入,該任務會對一組包含 Java 位元組碼的檔案進行編制。這個客製化任務(我們稱之為 Instrument)有一個註解為 @InputFilesclassFiles 屬性。您最初可能會嘗試像這樣組態任務

build.gradle.kts
plugins {
    id("java-library")
}

tasks.register<Instrument>("badInstrumentClasses") {
    classFiles.from(fileTree(tasks.compileJava.flatMap { it.destinationDirectory }))
    destinationDir = layout.buildDirectory.dir("instrumented")
}
build.gradle
plugins {
    id 'java-library'
}

tasks.register('badInstrumentClasses', Instrument) {
    classFiles.from fileTree(tasks.named('compileJava').flatMap { it.destinationDirectory }) {}
    destinationDir = file(layout.buildDirectory.dir('instrumented'))
}
gradle clean badInstrumentClasses 的輸出
> gradle clean badInstrumentClasses
> Task :clean UP-TO-DATE
> Task :badInstrumentClasses NO-SOURCE

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

這段程式碼看起來沒有明顯的錯誤,但從主控台輸出中可以看到編譯任務不見了。在這種情況下,您需要透過 dependsOninstrumentClassescompileJava 之間加入明確的任務相依性。使用 fileTree() 表示 Gradle 無法自行推斷任務相依性。

一種解決方案是使用 TaskOutputs.files 屬性,如下例所示

build.gradle.kts
tasks.register<Instrument>("instrumentClasses") {
    classFiles.from(tasks.compileJava.map { it.outputs.files })
    destinationDir = layout.buildDirectory.dir("instrumented")
}
build.gradle
tasks.register('instrumentClasses', Instrument) {
    classFiles.from tasks.named('compileJava').map { it.outputs.files }
    destinationDir = file(layout.buildDirectory.dir('instrumented'))
}
gradle clean instrumentClasses 的輸出
> gradle clean instrumentClasses
> Task :clean UP-TO-DATE
> Task :compileJava
> Task :instrumentClasses

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

或者,您可以使用 project.files()project.layout.files()project.objects.fileCollection() 取代 project.fileTree(),讓 Gradle 存取適當的屬性本身

build.gradle.kts
tasks.register<Instrument>("instrumentClasses2") {
    classFiles.from(layout.files(tasks.compileJava))
    destinationDir = layout.buildDirectory.dir("instrumented")
}
build.gradle
tasks.register('instrumentClasses2', Instrument) {
    classFiles.from layout.files(tasks.named('compileJava'))
    destinationDir = file(layout.buildDirectory.dir('instrumented'))
}
gradle clean instrumentClasses2 的輸出
> gradle clean instrumentClasses2
> Task :clean UP-TO-DATE
> Task :compileJava
> Task :instrumentClasses2

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

請記住,files()layout.files()objects.fileCollection() 可以將任務作為引數,而 fileTree() 則不行。

這種方法的缺點是來源任務的所有檔案輸出都變成目標任務的輸入檔案,在此情況下為 instrumentClasses。只要來源任務只有一個基於檔案的輸出(例如 JavaCompile 任務),這就沒問題。但是,如果您必須在多個輸出屬性中連結只有一個輸出,則需要使用 builtBy 方法明確告訴 Gradle 哪個任務會產生輸入檔案

build.gradle.kts
tasks.register<Instrument>("instrumentClassesBuiltBy") {
    classFiles.from(fileTree(tasks.compileJava.flatMap { it.destinationDirectory }) {
        builtBy(tasks.compileJava)
    })
    destinationDir = layout.buildDirectory.dir("instrumented")
}
build.gradle
tasks.register('instrumentClassesBuiltBy', Instrument) {
    classFiles.from fileTree(tasks.named('compileJava').flatMap { it.destinationDirectory }) {
        builtBy tasks.named('compileJava')
    }
    destinationDir = file(layout.buildDirectory.dir('instrumented'))
}
gradle clean instrumentClassesBuiltBy 的輸出
> gradle clean instrumentClassesBuiltBy
> Task :clean UP-TO-DATE
> Task :compileJava
> Task :instrumentClassesBuiltBy

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

當然,您也可以透過 dependsOn 加入明確的任務相依性,但上述方法提供了更多的語意意義,說明了為什麼 compileJava 必須事先執行。

停用最新檢查

Gradle 會自動處理輸出檔案和目錄的最新檢查,但如果任務輸出完全是其他東西怎麼辦?也許是更新網路服務或資料庫表格。或者,有時您有必須一直執行的任務。

這就是 Task 上的 doNotTrackState() 方法派上用場的地方。可以這樣使用它來完全停用任務的最新檢查,如下所示

build.gradle.kts
tasks.register<Instrument>("alwaysInstrumentClasses") {
    classFiles.from(layout.files(tasks.compileJava))
    destinationDir = layout.buildDirectory.dir("instrumented")
    doNotTrackState("Instrumentation needs to re-run every time")
}
build.gradle
tasks.register('alwaysInstrumentClasses', Instrument) {
    classFiles.from layout.files(tasks.named('compileJava'))
    destinationDir = file(layout.buildDirectory.dir('instrumented'))
    doNotTrackState("Instrumentation needs to re-run every time")
}
gradle clean alwaysInstrumentClasses 的輸出
> gradle clean alwaysInstrumentClasses
> Task :compileJava
> Task :alwaysInstrumentClasses

BUILD SUCCESSFUL in 0s
4 actionable tasks: 1 executed, 3 up-to-date
gradle alwaysInstrumentClasses 的輸出
> gradle alwaysInstrumentClasses
> Task :compileJava UP-TO-DATE
> Task :alwaysInstrumentClasses

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

如果您正在撰寫自己的任務,且該任務總是應該執行,則您也可以在任務類別上使用 @UntrackedTask 註解,而不是呼叫 Task.doNotTrackState()

整合自行進行最新檢查的外部工具

有時您會想要整合外部工具,例如 Git 或 Npm,這兩個工具都會自行進行最新檢查。在這種情況下,讓 Gradle 也進行最新檢查並沒有什麼意義。您可以透過在包裝工具的任務上使用 @UntrackedTask 註解來停用 Gradle 的最新檢查。或者,您可以使用執行時期 API 方法 Task.doNotTrackState()

例如,假設您想要實作一個複製 Git 儲存庫的任務。

範例 17. Git clone 任務
buildSrc/src/main/java/org/example/GitClone.java
@UntrackedTask(because = "Git tracks the state") (1)
public abstract class GitClone extends DefaultTask {

    @Input
    public abstract Property<String> getRemoteUri();

    @Input
    public abstract Property<String> getCommitId();

    @OutputDirectory
    public abstract DirectoryProperty getDestinationDir();

    @TaskAction
    public void gitClone() throws IOException {
        File destinationDir = getDestinationDir().get().getAsFile().getAbsoluteFile(); (2)
        String remoteUri = getRemoteUri().get();
        // Fetch origin or clone and checkout
        // ...
    }

}
build.gradle.kts
tasks.register<GitClone>("cloneGradleProfiler") {
    destinationDir = layout.buildDirectory.dir("gradle-profiler") // <3
    remoteUri = "https://github.com/gradle/gradle-profiler.git"
    commitId = "d6c18a21ca6c45fd8a9db321de4478948bdf801b"
}
build.gradle
tasks.register("cloneGradleProfiler", GitClone) {
    destinationDir = layout.buildDirectory.dir("gradle-profiler") (3)
    remoteUri = "https://github.com/gradle/gradle-profiler.git"
    commitId = "d6c18a21ca6c45fd8a9db321de4478948bdf801b"
}
1 將任務宣告為未追蹤。
2 使用輸出目錄來執行外部工具。
3 在您的建置中新增任務並設定輸出目錄。

設定輸入正規化

對於最新檢查和 建置快取,Gradle 需要判斷兩個任務輸入屬性是否具有相同值。為了執行此操作,Gradle 首先會正規化兩個輸入,然後比較結果。例如,對於編譯類別路徑,Gradle 會從類別路徑上的類別中萃取 ABI 簽章,然後在最後一次 Gradle 執行和目前的 Gradle 執行之間比較簽章,如 Java 編譯迴避 中所述。

正規化適用於類別路徑上的所有 zip 檔案(例如 jar、war、aar、apk 等)。這讓 Gradle 能夠辨識兩個 zip 檔案在功能上是否相同,即使 zip 檔案本身可能因為元資料(例如時間戳記或檔案順序)而略有不同。正規化不僅適用於類別路徑上的 zip 檔案,也適用於巢狀在目錄中或類別路徑上其他 zip 檔案中的 zip 檔案。

可以自訂 Gradle 內建的執行時期類別路徑正規化策略。所有使用 @Classpath 註解的輸入都被視為執行時期類別路徑。

假設您想要將檔案 build-info.properties 新增至所有產生的 jar 檔案,其中包含有關建置的資訊,例如建置開始時的時間戳記或用於識別發布成品的 CI 工作的 ID。此檔案僅用於稽核目的,且不會影響執行測試的結果。不過,此檔案是 test 工作的執行時期類別路徑的一部分,且會在每次建置呼叫時變更。因此,test 永遠不會是最新的,也無法從建置快取中提取。為了再次從增量建置中受益,您可以使用 Project.normalization(org.gradle.api.Action) (在使用專案中) 讓 Gradle 在執行時期類別路徑中忽略此檔案,以專案層級的方式進行設定。

build.gradle.kts
normalization {
    runtimeClasspath {
        ignore("build-info.properties")
    }
}
build.gradle
normalization {
    runtimeClasspath {
        ignore 'build-info.properties'
    }
}

如果將此類檔案新增至 jar 檔案是您在建置中所有專案都會執行的動作,且您想要為所有使用者篩選此檔案,您應該考慮在 慣例外掛程式 中設定此類標準化,以便在子專案之間共用。

此設定的效果是,build-info.properties 的變更會在最新檢查和 建置快取 金鑰計算中被忽略。請注意,這不會變更 test 工作的執行時期行為,亦即任何測試仍可以載入 build-info.properties,且執行時期類別路徑仍與之前相同。

屬性檔案標準化

預設情況下,屬性檔案 (亦即以 .properties 副檔名結尾的檔案) 會標準化,以忽略註解、空白和屬性順序的差異。Gradle 會載入屬性檔案,並在最新檢查或建置快取金鑰計算期間僅考慮個別屬性,藉此執行此動作。

不過,有時某些屬性會對執行時期產生影響,而其他屬性則不會。如果變更的屬性不會對執行時期類別路徑產生影響,則可能需要將其從最新檢查和 建置快取 金鑰計算中排除。然而,排除整個檔案也會排除對執行時期產生影響的屬性。在此情況下,可以選擇性地從執行時期類別路徑上的任何或所有屬性檔案中排除屬性。

可使用 RuntimeClasspathNormalization 中所述的模式,將忽略屬性的規則套用至特定檔案組。如果檔案符合規則,但無法載入為屬性檔案(例如,因為格式不正確或使用非標準編碼),則會將其納入最新或建置快取金鑰計算中,視為一般檔案。換句話說,如果無法將檔案載入為屬性檔案,則空白、屬性順序或註解的任何變更都可能導致任務過期或快取遺漏。

build.gradle.kts
normalization {
    runtimeClasspath {
        properties("**/build-info.properties") {
            ignoreProperty("timestamp")
        }
    }
}
build.gradle
normalization {
    runtimeClasspath {
        properties('**/build-info.properties') {
            ignoreProperty 'timestamp'
        }
    }
}
build.gradle.kts
normalization {
    runtimeClasspath {
        properties {
            ignoreProperty("timestamp")
        }
    }
}
build.gradle
normalization {
    runtimeClasspath {
        properties {
            ignoreProperty 'timestamp'
        }
    }
}

Java META-INF 標準化

由於執行時期影響,無法完全忽略 jar 檔案 META-INF 目錄中的檔案。

META-INF 中的清單檔案會標準化,以忽略註解、空白和順序差異。清單屬性名稱不分大小寫且順序不敏感。清單屬性檔案會根據 屬性檔案標準化 標準化。

build.gradle.kts
normalization {
    runtimeClasspath {
        metaInf {
            ignoreAttribute("Implementation-Version")
        }
    }
}
build.gradle
normalization {
    runtimeClasspath {
        metaInf {
            ignoreAttribute("Implementation-Version")
        }
    }
}
build.gradle.kts
normalization {
    runtimeClasspath {
        metaInf {
            ignoreProperty("app.version")
        }
    }
}
build.gradle
normalization {
    runtimeClasspath {
        metaInf {
            ignoreProperty("app.version")
        }
    }
}
build.gradle.kts
normalization {
    runtimeClasspath {
        metaInf {
            ignoreManifest()
        }
    }
}
build.gradle
normalization {
    runtimeClasspath {
        metaInf {
            ignoreManifest()
        }
    }
}
build.gradle.kts
normalization {
    runtimeClasspath {
        metaInf {
            ignoreCompletely()
        }
    }
}
build.gradle
normalization {
    runtimeClasspath {
        metaInf {
            ignoreCompletely()
        }
    }
}

提供自訂最新邏輯

Gradle 會自動處理輸出檔案和目錄的最新檢查,但如果任務輸出完全是其他內容呢?也許是更新網路服務或資料庫表格。在這種情況下,Gradle 沒有辦法知道如何檢查任務是否最新。

這就是 TaskOutputs 上的 upToDateWhen() 方法的用武之地。它採用一個謂詞函數,用於判斷任務是否是最新的。例如,您可以從資料庫讀取資料庫架構的版本號。或者,您可以檢查資料庫表格中的特定記錄是否存在或是否已變更。

請注意,最新檢查應該可以節省您的時間。不要新增會花費與任務標準執行時間一樣多或更多時間的檢查。事實上,如果任務最終會頻繁執行,因為它很少是最新的,那麼可能根本不值得進行任何最新檢查,如 停用最新檢查 中所述。請記住,如果任務在執行任務圖形中,您的檢查將始終執行。

一個常見的錯誤是使用 upToDateWhen() 而不是 Task.onlyIf()。如果您想根據與任務輸入和輸出無關的某些條件略過任務,則應使用 onlyIf()。例如,在您希望在設定或未設定特定屬性時略過任務的情況下。

過時的任務輸出

當 Gradle 版本變更時,Gradle 會偵測到需要移除使用舊版 Gradle 執行的任務的輸出,以確保最新版本的任務從已知的乾淨狀態開始。

過時輸出目錄的自動清理僅已針對來源集的輸出(Java/Groovy/Scala 編譯)實作。