建置中的小問題,例如忘記將組態檔宣告為工作的輸入,很容易被忽略。組態檔可能不常變更,或僅在某些其他(正確追蹤的)輸入也變更時才變更。可能發生的最糟情況是您的工作在應該執行時沒有執行。開發人員隨時可以使用 clean 重新執行建置,並以緩慢的重建為代價「修復」他們的建置。最後,沒有人被工作卡住,並且該事件被歸咎於「Gradle 又出問題了」。

對於可快取的工作,不正確的結果會永久儲存,並且稍後可能會回來困擾您;在這種情況下,使用 clean 重新執行也無濟於事。當使用共用快取時,這些問題甚至會跨越機器邊界。在上面的範例中,Gradle 可能最終會為您的工作載入使用不同組態產生的結果。因此,當啟用工作輸出快取時,解決這些建置問題變得更加重要。

建置的其他問題不會導致其產生不正確的結果,但會導致不必要的快取未命中。在本章中,您將了解一些典型的問題以及避免這些問題的方法。解決這些問題還會帶來額外的好處,您的建置將停止「出問題」,並且開發人員可以忘記完全使用 clean 執行建置。

系統檔案編碼

大多數 Java 工具在未指定特定編碼時使用系統檔案編碼。這表示在具有不同檔案編碼的機器上執行相同的建置可能會產生不同的輸出。目前,Gradle 僅在每個工作的基礎上追蹤是否未指定檔案編碼,但它不會追蹤正在使用的 JVM 的系統編碼。這可能會導致不正確的建置。您應該始終設定檔案系統編碼以避免這些問題。

建置腳本是使用 Gradle Daemon 的檔案編碼編譯的。預設情況下,Daemon 也使用系統檔案編碼。

為 Gradle Daemon 設定檔案編碼可減輕上述兩個問題,確保編碼在建置之間保持一致。您可以在 gradle.properties 中執行此操作

gradle.properties
org.gradle.jvmargs=-Dfile.encoding=UTF-8

環境變數追蹤

Gradle 不會追蹤工作中環境變數的變更。例如,對於 Test 工作,結果完全有可能取決於一些環境變數。為了確保僅在建置之間重複使用正確的成品,您需要將環境變數新增為取決於它們的工作的輸入。

絕對路徑也經常作為環境變數傳遞。您需要注意在這種情況下新增為工作輸入的內容。您需要確保絕對路徑在機器之間是相同的。大多數時候,追蹤絕對路徑指向的檔案或目錄的內容是有意義的。如果絕對路徑表示正在使用的工具,則追蹤工具版本作為輸入可能更有意義。

例如,如果您在名為 integTestTest 工作中使用工具,這些工具取決於 LANG 變數的內容,您應該執行此操作

build.gradle.kts
tasks.integTest {
    inputs.property("langEnvironment") {
        System.getenv("LANG")
    }
}
build.gradle
tasks.named('integTest') {
    inputs.property("langEnvironment") {
        System.getenv("LANG")
    }
}

如果您新增條件邏輯來區分 CI 建置與本機開發建置,則必須確保這不會中斷從 CI 載入工作輸出到開發人員機器。例如,以下設定會中斷 Test 工作的快取,因為 Gradle 始終偵測到自訂工作動作中的差異。

build.gradle.kts
if ("CI" in System.getenv()) {
    tasks.withType<Test>().configureEach {
        doFirst {
            println("Running test on CI")
        }
    }
}
build.gradle
if (System.getenv().containsKey("CI")) {
    tasks.withType(Test).configureEach {
        doFirst {
            println "Running test on CI"
        }
    }
}

您應該始終無條件地新增動作

build.gradle.kts
tasks.withType<Test>().configureEach {
    doFirst {
        if ("CI" in System.getenv()) {
            println("Running test on CI")
        }
    }
}
build.gradle
tasks.withType(Test).configureEach {
    doFirst {
        if (System.getenv().containsKey("CI")) {
            println "Running test on CI"
        }
    }
}

這樣,工作在 CI 和開發人員建置上具有相同的自訂動作,並且如果剩餘輸入相同,則可以重複使用其輸出。

行尾符號

如果您在不同的作業系統上建置,請注意,某些版本控制系統會在簽出時轉換行尾符號。例如,Windows 上的 Git 預設使用 autocrlf=true,這會將所有行尾符號轉換為 \r\n。因此,編譯輸出無法在 Windows 上重複使用,因為輸入來源不同。如果跨多個作業系統共用建置快取在您的環境中很重要,那麼在您的建置機器上將 autocrlf=false 設定為關閉對於最佳建置快取使用率至關重要。

當使用符號連結時,Gradle 不會將連結儲存在建置快取中,而是儲存連結目標的實際檔案內容。因此,當您嘗試重複使用大量使用符號連結的輸出時,您可能會遇到困難。目前沒有針對此行為的解決方法。

對於支援符號連結的作業系統,符號連結目標的內容將新增為輸入。如果作業系統不支援符號連結,則實際的符號連結檔案將新增為輸入。因此,具有符號連結作為輸入檔案的工作,例如,具有符號連結作為其執行階段類別路徑一部分的 Test 工作,將不會在 Windows 和 Linux 之間快取。如果需要跨作業系統快取,則不應將符號連結簽入版本控制。

Java 版本追蹤

Gradle 僅追蹤 Java 的主要版本作為編譯和測試執行的輸入。目前,它不會追蹤供應商或次要版本。儘管如此,供應商和次要版本仍可能影響編譯產生的位元組碼。

如果您使用Java 工具鏈,則 Java 主要版本、供應商(如果指定)和實作(如果指定)將自動追蹤為編譯和測試執行的輸入。

如果您使用不同的 JVM 供應商來編譯或執行 Java,我們強烈建議您將供應商新增為對應工作的輸入。這可以使用執行階段 API 來達成,如下面的程式碼片段所示。

build.gradle.kts
tasks.withType<AbstractCompile>().configureEach {
    inputs.property("java.vendor") {
        System.getProperty("java.vendor")
    }
}

tasks.withType<Test>().configureEach {
    inputs.property("java.vendor") {
        System.getProperty("java.vendor")
    }
}
build.gradle
tasks.withType(AbstractCompile).configureEach {
    inputs.property("java.vendor") {
        System.getProperty("java.vendor")
    }
}

tasks.withType(Test).configureEach {
    inputs.property("java.vendor") {
        System.getProperty("java.vendor")
    }
}

關於追蹤 Java 次要版本,存在不同的競爭方面:開發人員擁有快取命中和 CI 上的「完美」結果。基本上有兩種情況您可能想要追蹤 Java 的次要版本:用於編譯和用於執行階段。在編譯的情況下,對於不同的次要版本,產生的位元組碼有時可能會有所不同。但是,位元組碼仍應產生相同的執行階段行為。

Java 編譯避免會將此位元組碼視為相同,因為它會擷取 ABI。

將次要版本號碼視為輸入可能會降低開發人員建置的快取命中機率。根據您的團隊中標準開發環境的普及程度,通常會使用許多不同的 Java 次要版本。

即使不追蹤 Java 次要版本,開發人員也可能由於某些本機編譯的類別檔案而導致快取未命中,這些檔案構成了測試執行的輸入。如果這些輸出進入了此開發人員機器上的本機建置快取,即使是 clean 也無法解決此情況。因此,追蹤 Java 次要版本的選擇是在不同 Java 次要版本之間有時或永遠不重複使用測試執行的輸出之間。

用於執行 Gradle 的 JVM 提供的編譯器基礎架構也由 Groovy 編譯器使用。因此,您可以預期編譯的 Groovy 類別的位元組碼會因與上述相同的原因而產生差異,並且適用相同的建議。

避免變更建置外部的輸入

如果您的建置取決於外部相依性,例如二進位成品或來自網頁的動態資料,則需要確保這些輸入在您的基礎架構中保持一致。跨機器的任何變更都會導致快取未命中。

切勿使用相同的版本號碼但不同的內容重新發佈非變更的二進位相依性:如果外掛程式相依性發生這種情況,您將永遠無法解釋為什麼您看不到機器之間的快取重複使用(因為它們具有該成品的不同版本)。

在建置中設計使用 SNAPSHOT 或其他變更的相依性會違反穩定的工作輸入原則。為了有效地使用建置快取,您應該依賴固定的相依性。您可能想要研究相依性鎖定或切換為使用複合建置

對於取決於不穩定的外部資源(例如,已發佈版本的清單)也是如此。鎖定變更的一種方法是在每次變更時將不穩定的資源簽入原始碼控制,以便建置僅取決於原始碼控制中的狀態,而不取決於不穩定的資源本身。

撰寫建置的建議

檢閱 doFirstdoLast 的用法

在可快取的工作上從建置腳本使用 doFirstdoLast 會將您與建置腳本變更綁定,因為閉包的實作來自建置腳本。如果可能,您應該改為使用獨立的工作。

不建議在 doFirst 中透過執行階段 API 修改輸入或輸出屬性,因為這些變更將不會被偵測到以進行最新檢查和建置快取。更糟的是,當工作未執行時,工作的組態實際上與執行時不同。不要使用 doFirst 來修改輸入,而是考慮使用獨立的工作來配置有問題的工作 - 稱為組態工作。例如,不要執行

build.gradle.kts
tasks.jar {
    val runtimeClasspath: FileCollection = configurations.runtimeClasspath.get()
    doFirst {
        manifest {
            val classPath = runtimeClasspath.map { it.name }.joinToString(" ")
            attributes("Class-Path" to classPath)
        }
    }
}
build.gradle
tasks.named('jar') {
    FileCollection runtimeClasspath = configurations.runtimeClasspath
    doFirst {
        manifest {
            def classPath = runtimeClasspath.collect { it.name }.join(" ")
            attributes('Class-Path': classPath)
        }
    }
}

執行

build.gradle.kts
val configureJar = tasks.register("configureJar") {
    doLast {
        tasks.jar.get().manifest {
            val classPath = configurations.runtimeClasspath.get().map { it.name }.joinToString(" ")
            attributes("Class-Path" to classPath)
        }
    }
}
tasks.jar { dependsOn(configureJar) }
build.gradle
def configureJar = tasks.register('configureJar') {
    doLast {
        tasks.jar.manifest {
            def classPath = configurations.runtimeClasspath.collect { it.name }.join(" ")
            attributes('Class-Path': classPath)
        }
    }
}

tasks.named('jar') { dependsOn(configureJar) }
請注意,當使用組態快取時,不支援從其他工作配置工作。

基於工作結果的建置邏輯

不要根據工作是否已執行來建立建置邏輯。特別是,您不應假設工作的輸出只能在實際執行時才能變更。實際上,從建置快取載入輸出也會變更它們。不要依賴自訂邏輯來處理輸入或輸出檔案的變更,而應利用 Gradle 的內建支援,方法是為您的工作宣告正確的輸入和輸出,並讓 Gradle 決定是否應執行工作動作。出於完全相同的原因,不建議使用 outputs.upToDateWhen,而應替換為正確宣告工作的輸入。

重疊輸出

您已經看到重疊輸出是工作輸出快取的問題。當您將新工作新增至建置或重新配置內建工作時,請確保您不會為可快取的工作建立重疊輸出。如果必須這樣做,您可以新增一個 Sync 工作,然後它會將合併的輸出同步到目標目錄中,同時原始工作保持可快取。

Develocity 將在時間軸和工作輸入比較中顯示快取已針對重疊輸出停用的工作

overlapping outputs input comparison

達成穩定的工作輸入

對於每個可快取的工作,擁有穩定的工作輸入至關重要。在以下章節中,您將了解違反穩定工作輸入的不同情況,並查看可能的解決方案。

不穩定的工作輸入

如果您使用不穩定的輸入(例如時間戳記)作為工作的輸入屬性,那麼 Gradle 無法讓工作可快取。您真的應該仔細思考不穩定的資料對於輸出是否真的至關重要,或者它是否僅用於(例如)稽核目的。

如果不穩定的輸入對於輸出至關重要,那麼您可以嘗試使使用不穩定的輸入的工作執行成本更低。您可以透過將工作拆分為兩個工作來完成此操作 - 第一個工作執行可快取的昂貴工作,第二個工作將不穩定的資料新增至輸出。這樣,輸出保持不變,並且可以使用建置快取來避免執行昂貴的工作。例如,對於建置 jar 檔案,昂貴的部分 - Java 編譯 - 已經是一個不同的工作,而 jar 工作本身(不可快取)則很便宜。

如果它不是輸出的重要部分,那麼您不應將其宣告為輸入。只要不穩定的輸入不影響輸出,就沒有其他需要執行的操作。但是,大多數時候,輸入將成為輸出的一部分。

不可重複的工作輸出

對於相同的輸入產生不同輸出的工作可能會對工作輸出快取的有效使用造成挑戰,如可重複的工作輸出中所見。如果不可重複的工作輸出未被任何其他工作使用,那麼效果非常有限。它基本上表示從快取載入工作可能會產生與在本機執行相同工作不同的結果。如果輸出之間的唯一差異是時間戳記,那麼您可以接受建置快取的效果,或者決定該工作畢竟不可快取。

只要另一個工作取決於不可重複的輸出,不可重複的工作輸出就會導致不穩定的工作輸入。例如,從具有相同內容但修改時間不同的檔案重新建立 jar 檔案會產生不同的 jar 檔案。當在本機重建 jar 檔案時,任何其他將此 jar 檔案作為輸入檔案的工作都無法從快取載入。當消耗建置不是 clean 建置或可快取的工作取決於不可快取的工作的輸出時,這可能會導致難以診斷的快取未命中。例如,在執行增量建置時,磁碟上被視為最新的成品與建置快取中的成品可能不同,即使它們本質上是相同的。然後,取決於此工作輸出的工作將無法從建置快取載入輸出,因為輸入並不完全相同。

穩定的工作輸入章節中所述,您可以使工作輸出可重複,或使用輸入正規化。您已經了解可配置的輸入正規化的可能性。

Gradle 包含一些支援,用於為封存工作建立可重複的輸出。對於 tar 和 zip 檔案,可以將 Gradle 配置為建立可重製的封存。這是透過配置(例如)透過以下程式碼片段的 Zip 工作來完成的。

build.gradle.kts
tasks.register<Zip>("createZip") {
    isPreserveFileTimestamps = false
    isReproducibleFileOrder = true
    // ...
}
build.gradle
tasks.register('createZip', Zip) {
    preserveFileTimestamps = false
    reproducibleFileOrder = true
    // ...
}

使輸出可重複的另一種方法是為具有不可重複輸出的工作啟用快取。如果您可以確保所有建置都使用相同的建置快取,那麼根據建置快取的設計,對於相同的輸入,工作將始終具有相同的輸出。走這條路可能會導致增量建置的快取未命中問題,如上所述。此外,嘗試並行將相同輸出儲存在建置快取中的不同建置之間的競爭條件可能會導致難以診斷的快取未命中。如果可能,您應避免走這條路。

限制不穩定資料的影響

如果針對處理不穩定資料的已描述解決方案都不適用於您,您仍然應該能夠限制不穩定資料對建置快取有效使用的影響。這可以透過稍後將不穩定資料新增至輸出來完成,如不穩定的工作輸入章節中所述。另一個選項是移動不穩定的資料,使其影響更少的工作。例如,將相依性從 compile 移動到 runtime 配置可能已經產生相當大的影響。

有時也可以建置兩個成品,一個包含不穩定的資料,另一個包含不穩定資料的恆定表示。非不穩定的輸出將用於(例如)測試,而不穩定的輸出將發佈到外部儲存庫。雖然這與持續交付「建置成品一次」原則衝突,但有時可能是唯一的選擇。

自訂和第三方工作

如果您的建置包含自訂或第三方工作,您應特別注意這些工作不會影響建置快取的有效性。對於可能沒有可重複的工作輸出的程式碼產生工作也應特別注意。如果程式碼產生器在產生的檔案中包含(例如)時間戳記或取決於輸入檔案的順序,則可能會發生這種情況。其他陷阱可能是工作程式碼中使用 HashMap 或其他沒有順序保證的資料結構。

某些第三方外掛程式甚至可能影響 Gradle 內建工作的可快取性。如果它們透過執行階段 API 將絕對路徑或不穩定的資料等輸入新增至工作,則可能會發生這種情況。在最壞的情況下,當外掛程式嘗試取決於工作結果並且不考慮 FROM-CACHE 時,這可能會導致不正確的建置。