建置中出現的小問題,例如忘記將設定檔宣告為工作輸入,很容易被忽略。設定檔可能不常變更,或只在其他(正確追蹤的)輸入變更時變更。最糟的情況是工作不會在應該執行時執行。開發人員可以隨時使用 clean
重新執行建置,並以緩慢重建為代價「修復」建置。最後,沒有人會在工作中受阻,而事件則被歸咎於「Gradle 又出問題了」。
使用可快取的工作時,不正確的結果會永久儲存,並可能在稍後回來困擾你;此時使用 clean
重新執行也無濟於事。當使用共用快取時,這些問題甚至會跨越機器界線。在上述範例中,Gradle 可能最終會載入使用不同設定檔產生的工作結果。因此,當啟用工作輸出快取時,解決建置的這些問題變得更加重要。
建置的其他問題不會導致產生不正確的結果,但會造成不必要的快取遺漏。在本章中,您將瞭解一些典型問題以及避免這些問題的方法。修正這些問題將帶來額外好處,您的建置將停止「異常」,而且開發人員可以完全忘記使用 clean
執行建置。
系統檔案編碼
當未指定特定編碼時,大多數 Java 工具會使用系統檔案編碼。這表示在檔案編碼不同的機器上執行相同的建置可能會產生不同的輸出。目前 Gradle 僅在每個任務的基礎上追蹤未指定任何檔案編碼,但不會追蹤使用中 JVM 的系統編碼。這可能會導致建置不正確。您應始終設定檔案系統編碼以避免此類問題。
建置指令碼會使用 Gradle 守護程式精靈的檔案編碼編譯。預設情況下,守護程式精靈也會使用系統檔案編碼。 |
設定 Gradle 守護程式精靈的檔案編碼可透過確保編碼在所有建置中相同來緩解上述兩個問題。您可以在 gradle.properties
中執行此操作
org.gradle.jvmargs=-Dfile.encoding=UTF-8
環境變數追蹤
Gradle 不會追蹤任務的環境變數變更。例如,對於 Test
任務,結果完全有可能取決於幾個環境變數。為確保僅在建置之間重複使用正確的成品,您需要將環境變數新增為依賴它們的任務的輸入。
絕對路徑通常也會傳遞為環境變數。在這種情況下,您需要留意新增什麼作為任務的輸入。您需要確保絕對路徑在各機器之間相同。大多數時候,追蹤絕對路徑指向的檔案或目錄內容是有意義的。如果絕對路徑表示正在使用的工具,則可能改為追蹤工具版本作為輸入更有意義。
例如,如果您在 Test
任務中使用依賴 LANG
變數內容的工具 integTest
,您應該執行下列動作
tasks.integTest {
inputs.property("langEnvironment") {
System.getenv("LANG")
}
}
tasks.named('integTest') {
inputs.property("langEnvironment") {
System.getenv("LANG")
}
}
如果您新增條件式邏輯來區分 CI 建置與在地端開發建置,您必須確保這不會中斷從 CI 載入任務輸出到開發人員機器。例如,下列設定會中斷 Test
任務的快取,因為 Gradle 始終會偵測到自訂任務動作的差異。
if ("CI" in System.getenv()) {
tasks.withType<Test>().configureEach {
doFirst {
println("Running test on CI")
}
}
}
if (System.getenv().containsKey("CI")) {
tasks.withType(Test).configureEach {
doFirst {
println "Running test on CI"
}
}
}
您應始終無條件新增動作
tasks.withType<Test>().configureEach {
doFirst {
if ("CI" in System.getenv()) {
println("Running test on CI")
}
}
}
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 來達成,如下面的程式片段所示。
tasks.withType<AbstractCompile>().configureEach {
inputs.property("java.vendor") {
System.getProperty("java.vendor")
}
}
tasks.withType<Test>().configureEach {
inputs.property("java.vendor") {
System.getProperty("java.vendor")
}
}
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 次要版本,您也可能會因為某些作為測試執行輸入的本地編譯類別檔案而導致開發人員快取遺漏。如果這些輸出已進入此開發人員機器上的本地建置快取,即使執行清除也無法解決這個問題。因此,追蹤 Java 次要版本的選擇在於有時或從不重複使用不同 Java 次要版本之間的輸出以進行測試執行。
JVM 提供的編譯器基礎架構用於執行 Gradle,Groovy 編譯器也會使用。因此,您可以預期編譯的 Groovy 類別的位元組碼會有差異,原因與上述相同,建議也適用。 |
避免變更建置外部的輸入
如果您的建置依賴於外部相依性,例如二進制人工製品或網頁的動態資料,您需要確保這些輸入在整個基礎架構中一致。機器之間的任何差異都會導致快取遺漏。
切勿重新發布非變更二進制相依性,版本號碼相同但內容不同:如果這發生在外掛程式相依性,您將永遠無法解釋為什麼您看不到機器之間的快取重複使用(這是因為它們有該人工製品的不同版本)。
依賴於不穩定的外部資源,例如已發布版本的清單,也是如此。鎖定變更的一種方法是,每當不穩定的資源變更時,將其簽入原始碼控制,以便建置僅依賴於原始碼控制中的狀態,而不是不穩定的資源本身。
撰寫建置的建議
檢閱 doFirst
和 doLast
的用法
在可快取工作的建置指令碼中使用 doFirst
和 doLast
會將您綁定到建置指令碼變更,因為封閉的實作來自建置指令碼。如果可能,您應該改用個別的工作。
不建議透過 doFirst
中的執行時間 API 修改輸入或輸出屬性,因為這些變更不會偵測到最新檢查和建置快取。更糟的是,當工作沒有執行時,工作的組態實際上與執行時不同。不要使用 doFirst
修改輸入,請考慮使用個別的工作來組態有問題的工作,也就是所謂的組態工作。例如,不要執行
tasks.jar {
val runtimeClasspath: FileCollection = configurations.runtimeClasspath.get()
doFirst {
manifest {
val classPath = runtimeClasspath.map { it.name }.joinToString(" ")
attributes("Class-Path" to classPath)
}
}
}
tasks.named('jar') {
FileCollection runtimeClasspath = configurations.runtimeClasspath
doFirst {
manifest {
def classPath = runtimeClasspath.collect { it.name }.join(" ")
attributes('Class-Path': classPath)
}
}
}
do
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) }
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 會在時間軸和工作輸入比較中顯示已停用重疊輸出快取的工作。

達成穩定的工作輸入
對於每個可快取的工作,擁有穩定的工作輸入至關重要。在以下部分中,您將瞭解會違反穩定的工作輸入的不同情況,並檢視可能的解決方案。
不穩定的工作輸入
如果您使用不穩定的輸入(例如時間戳記)作為工作的輸入屬性,則 Gradle 無法採取任何措施讓工作可快取。您應該認真思考不穩定的資料是否對輸出真的必要,或者它僅出於例如稽核目的而存在。
如果變動輸入對輸出至關重要,則可以嘗試使用變動輸入來執行較便宜的任務。你可以將任務分成兩個任務來執行此操作 - 第一個任務執行可快取且昂貴的工作,而第二個任務將變動資料新增至輸出。如此一來,輸出保持不變,且建置快取可用於避免執行昂貴的工作。例如,對於建立 jar 檔案而言,昂貴的部分 - Java 編譯 - 已經是不同的任務,而本身不可快取的 jar 任務則很便宜。
如果它不是輸出中不可或缺的一部分,則不應將其宣告為輸入。只要變動輸入不會影響輸出,就沒有其他事情要做。然而,大多數時候,輸入將會是輸出的一部分。
不可重複的任務輸出
對於相同的輸入產生不同輸出的任務,可能會對任務輸出快取的有效使用構成挑戰,如在 可重複任務輸出 中所見。如果其他任務未使用不可重複的任務輸出,則其影響非常有限。這基本上表示從快取載入任務可能會產生與在本地執行相同任務不同的結果。如果輸出之間唯一的差異是時間戳記,則你可以接受建置快取的影響,或決定任務畢竟不可快取。
一旦另一個任務依賴於不可重複的輸出,不可重複的任務輸出就會導致不穩定的任務輸入。例如,從具有相同內容但修改時間不同的檔案重新建立 jar 檔案,會產生不同的 jar 檔案。任何其他任務依賴此 jar 檔案作為輸入檔案,都無法在本地重新建置 jar 檔案時從快取載入。當使用建置並非乾淨建置,或當可快取任務依賴於不可快取任務的輸出時,這可能會導致難以診斷的快取遺漏。例如,在執行增量建置時,即使磁碟上的成品被視為最新且建置快取中的成品相同,仍有可能不同。然後,依賴於此任務輸出的任務將無法從建置快取載入輸出,因為輸入並不完全相同。
Gradle 包含一些支援,用於為封存任務建立可重複的輸出。對於 tar 和 zip 檔案,可以設定 Gradle 來建立 可重複的封存。這是透過以下程式片段設定 e.g. Zip
任務來完成的。
tasks.register<Zip>("createZip") {
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
// ...
}
tasks.register('createZip', Zip) {
preserveFileTimestamps = false
reproducibleFileOrder = true
// ...
}
讓輸出可重複的另一種方法,是為具有不可重複輸出的任務啟用快取。如果你可以確保所有建置都使用相同的建置快取,則透過建置快取的設計,任務將始終對相同的輸入產生相同的輸出。走上這條路可能會導致增量建置的快取遺漏出現不同的問題,如上所述。此外,不同建置之間的競爭條件,試圖並行將相同的輸出儲存在建置快取中,可能會導致難以診斷的快取遺漏。如果可能,你應避免走上那條路。
限制變動資料的影響
如果您發現沒有任何說明的解決方案能處理不穩定的資料,您仍然可以限制不穩定的資料對有效使用建置快取的影響。這可以透過在稍後將不穩定的資料新增到輸出中來完成,如 不穩定的工作輸入區段 中所述。另一個選項是移動不穩定的資料,讓它影響較少的任務。例如,將相依性從 compile
移到 runtime
組態可能已經產生相當大的影響。
有時也可以建置兩個成品,一個包含不穩定的資料,另一個包含不穩定的資料的常數表示。非不穩定的輸出會用於測試,而會將不穩定的輸出發布到外部儲存庫。雖然這與持續傳遞「建置成品一次」的原則相衝突,但有時可能是唯一的選項。