跳至內容

模組快取

必要條件
  • 一個已產生的專案。模組快取會讀取 Tuist 在 Project.swift 背後維護的專案圖,以計算目標的雜湊值並替換其二進位檔。

Tuist 模組快取提供了一種強大的方式,透過將你的模組快取為二進位檔(.xcframeworks)並在不同環境之間共用,來最佳化建置時間。此功能讓你能够利用先前產生的二進位檔,減少重複編譯的需求,並加速開發流程。

與 Xcode 快取搭配使用

模組快取與 Xcode 快取 互補,因為它們在不同的粒度層級上運作。模組快取在建置執行前,以預先建置的 .xcframeworks 取代整個模組,而 Xcode 快取則在建置期間重複使用編譯輸出。

編譯快取設定不屬於模組快取雜湊的一部分,因此開啟或關閉 Xcode 快取會保留你已預熱的二進位檔。在 Xcode 27 及更新版本中,隨附的前綴對應設定會被納入雜湊計算。請參閱模組快取雜湊。

預熱#

Tuist 有效利用雜湊值來偵測相依性圖中每個目標的變更。利用這些資料,它會建置並指派唯一識別碼給衍生自這些目標的二進位檔。在產生圖表時,Tuist 會無縫地以對應的二進位版本取代原始目標。

這項稱為 「預熱」 的作業,會產生供本機使用或透過 Tuist 與團隊成員及 CI 環境共用的二進位檔。預熱快取的流程簡單明瞭,只需執行一個指令即可啟動:

bash
tuist cache

該指令會重複使用二進位檔以加速流程。

如果某個二進位檔上傳失敗,指令仍會上傳其餘檔案,然後以非零狀態結束並列出未上傳的目標。機器本機快取中的二進位檔視為已快取,因此若要從同一台機器再次上傳它們,請在預熱前執行 tuist clean binaries。傳遞 --no-upload 以僅將二進位檔儲存於本機快取。

目的地選擇#

使用 --destination simulator 或 --destination device 來針對單一目的地進行快取、產生與測試:

bash
tuist cache --destination simulator
tuist generate --destination simulator
tuist test --destination simulator
tuist cache --destination device
tuist generate --destination device
tuist test --destination device -- -destination 'platform=iOS,id=DEVICE_IDENTIFIER'

預設值為 all,這會保留你的目標所支援的所有目的地,包括 Mac Catalyst。後續的 tuist cache 會重複使用已快取的切片,並僅建置缺少的 SDK。一般產生流程會在缺少任何必要切片時保留模組的原始碼。

限制產生會將來源目標限制為選定的目的地,並保留原生 macOS 目標(包括巨集)。兩種限制模式皆不包含 Mac Catalyst。使用 --destination all 重新產生以還原所有目的地。

tuist test --destination 僅使用和下載所選目的地的切片,因此模擬器測試執行可以使用僅限模擬器或完整預熱的二進位檔。當傳遞給 xcodebuild(在 -- 之後)的目的地不相符時會失敗,且 --destination device 需要裝置 -destination。選擇性測試的雜湊值不依賴目的地,因此具有不同 --destination 值的執行會共用測試結果。

逐切片快取支援採用標準架構的 iOS 和 macOS。其他平台、自訂架構和不支援的產品則維持完整工件快取。舊版模組快取一律下載完整工件,且在預熱時需要 --destination all。

遠端上傳准入#

現代模組快取使用 REAPI。當忙碌的快取暫時以 RESOURCE_EXHAUSTED 拒絕上傳時,Tuist 會以抖動方式退避,遵守標準的 google.rpc.RetryInfo 最小延遲,並在有限的等待預算內重試。每次呼叫的上傳准入最多允許六次重試。每個 blob 上傳作業在其存在檢查、批次上傳和串流寫入之間共用一個 60 秒的累積退避預算;並行等待各自消耗預算,因此重複拒絕不會讓每個排队的 blob 再獲得一分鐘的重試時間。成功的傳輸不會消耗此預算,也不會在預算耗盡時被取消。動作結果發布擁有自己的有限預算。指數延遲從一秒開始,在比例抖動前逐漸增加到 30 秒。如果伺服器要求的延遲超過剩餘預算,Tuist 會失敗而非早於指示進行重試。取消會中斷退避,而快取讀取保留短暫重試,以免過載導致產生停滯。

這些預設值也有助於沒有重試提示的較舊 Kura 版本。較長的重試能吸收暫時性的競爭,而非持續性的過載:如果拒絕持續發生,請降低暖身作業的快取並行限制或增加伺服器容量。請參閱Kura 上傳規模評估以取得 Pod 記憶體估算和准入指標。

組態選擇#

在未傳遞 --configuration 的情況下預熱快取時,Tuist 會依下列順序選擇要使用的建置組態:

  1. 你的 manifest 中 Project.Options.generationOptions 所設定的 defaultConfiguration(如果有)。
  2. 否則,選擇變體為 debug 的第一個建置組態,依名稱按字母順序排序。

若要為特定組態預熱快取,請明確傳遞該組態:

bash
tuist cache --configuration Release

用法#

預設情況下,當 Tuist 指令需要產生專案時,如果快取中有可用的二進位等效項目,它們會自動以這些項目取代相依性。此外,如果你指定要專注的目標清單,Tuist 也會以已快取的二進位檔取代任何相依目標(前提是它們可用)。對於偏好不同做法的使用者,可以選擇使用特定旗標完全退出此行為:

bash
tuist generate # Only dependencies
tuist generate Search # Dependencies + Search dependencies
tuist generate Search Settings # Dependencies, and Search and Settings dependencies
tuist generate --cache-profile none # No cache at all
bash
tuist test
Warning

二進位快取是一項專為開發工作流程設計的功能,例如在模擬器或裝置上執行應用程式,或執行測試。它不適用於發行版建置。在歸檔應用程式時,請使用 --cache-profile none 產生包含原始碼的專案。

快取設定檔#

Tuist 支援快取設定檔,以控制在產生專案時以多積極的程度用已快取的二進位檔取代目標。

  • 內建設定檔:
    • only-external:僅取代外部相依性(系統預設)
    • all-possible:盡可能取代更多目標(包括內部目標)
    • none:永不以已快取的二進位檔取代

在 tuist generate 上使用 --cache-profile 選擇設定檔:

bash
# Built-in profiles
tuist generate --cache-profile all-possible
# Custom profiles (defined in Tuist Config)
tuist generate --cache-profile development
# Use config default (no flag)
tuist generate
# Focus on specific targets (implies all-possible)
tuist generate MyModule AnotherTarget
# Disable binary replacement entirely
tuist generate --cache-profile none
已棄用的旗標

--no-binary-cache 旗標已棄用。請改用 --cache-profile none。該已棄用的旗標仍可用於向後相容。

解析有效行為時的優先順序(從高到低):

  1. --cache-profile none
  2. 目標焦點(將目標傳遞給 generate)→ 設定檔 all-possible
  3. --cache-profile <value>
  4. 組態預設值(如果已設定)
  5. 系統預設值(only-external)

支援的產品#

只有下列目標產品可由 Tuist 快取:

  • Framework(靜態與動態),包括相依於 XCTest 或 Swift Testing 的測試支援 Framework
  • 程式庫(靜態與動態),包括相依於 XCTest 或 Swift Testing 的測試支援程式庫
  • Bundle
  • Swift Macros

測試 Bundle 仍被排除在模組快取之外。這表示 unitTests 和 uiTests 目標不會以二進位檔形式快取,但測試所相依的一般 Framework 和程式庫目標即使連結了 XCTest 或 Swift Testing,仍可被快取。

已快取的程式庫 .xcframework 會保留產生專案匯入它們所需的詮釋資料,包括 Swift 模組,以及當來源目標宣告公開標頭時的公開 C/Objective-C 標頭。

上游相依性

當某個目標不可快取時,其上游目標也會變成不可快取。例如,如果你有相依圖 A > B,其中 A 相依於 B,若 B 不可快取,A 也會不可快取。

分析#

在專案儀表板中開啟 模組快取 → 模組,找出經常未命中的模組。選擇你想要改善的環境,例如 CI,以及日期範圍。概覽會顯示快取命中、未命中,以及有未命中的模組;開啟模組會顯示其歷史記錄,以及指派給每次觀察的原因。

未命中 下拉式選單會為單一原因選擇計數與說明。圖表會顯示所有四個原因的分布情況。在模組頁面上,使用歷史記錄的 原因 篩選器來檢查個別發生情況。將滑鼠懸停在原因徽章上,或使用鍵盤聚焦該徽章,即可查看其定義。

未命中原因#

原因意義
已變更模組自身的比較輸入已變更。這些包括檔案雜湊、建置設定、已解析的組態,以及編譯器識別碼。原始碼編輯只是可能的原因之一。
上游模組自身的比較輸入保持不變,但其相依性或外部套件的雜湊已變更。模組的原始檔可能完全未更動。
冷啟動沒有先前的模組觀察可供比較,或者回報的輸入無法解釋未命中,且沒有符合條件的證據證明先前遠端可用性。冷啟動並不能證明該模組從未快取過。
已驅逐精確的快取金鑰先前在同一專案、同一記錄的快取端點上有過遠端命中,但現在未命中。

對於標記為 已驅逐 的未命中,該構件先前已從遠端快取下載,但這次無法重複使用。驅逐是可能的原因,不過存取或下載失敗也可能導致未命中。Tuist 僅在找到先前的遠端命中時才會指派此標籤;先前的未命中和本地命中並不足夠。

例如:

情境分類
模組未命中,且沒有先前的觀察或符合條件的遠端命中冷啟動
A 變更;B 相依於 A,而 C 相依於 B;三個金鑰都變更A 為已變更;B 和 C 為上游
預熱後編譯器版本變更,原始碼和設定未變更當兩次觀察都回報編譯器輸入時,受影響的未命中為已變更
精確金鑰已從遠端下載,但在其構件被驅逐後未命中當先前的命中符合證據條件時,為已驅逐
金鑰重複未命中,且從未成功預熱可能維持冷啟動狀態

原因用於描述觀察結果;它們並非附加在金鑰上的永久標籤。例如,編譯器升級後的第一次未命中可能是已變更,而該新金鑰後續的未命中若從未在遠端被觀察到可用,則可能是冷啟動。

提升快取命中率#

  1. 選擇一致的基準。 從 CI 和具有代表性的日期範圍開始。依未命中次數排序模組,然後考慮其命中率、相依模組和建置成本。修復經常未命中且昂貴的相依性,比提升小型模組的命中率更能節省時間。進行變更後,比較相同環境和類似工作負載的結果。
  2. 調查已變更的未命中。 開啟相關的執行記錄,並比較其完整金鑰和回報的輸入。將預熱和使用任務對齊至預期的 Xcode/編譯器版本、組態和目標目的地。在有意圖的變更後再次預熱。如果易變的產生檔案或依賴環境的設定導致原本穩定的模組失效,請調查這些輸入。將影響二進位相容性的輸入保留在雜湊中。
  3. 追蹤上游未命中至已變更的相依性。 檢查其歷史記錄以找出直接變更。預熱產生的金鑰可以恢復重複使用。如果經常變更的實作導致許多昂貴的相依模組失效,請考慮如效率所述,採用較小的模組或穩定的介面。
  4. 檢查冷啟動未命中的預熱涵蓋範圍。 驗證預熱是否選取了所需的模組、使用了使用任務的組態和環境,並成功上傳構件。檢查實際 CI 任務使用的完整金鑰。如果預熱和使用請求不同的金鑰,在假設快取被驅逐之前,請檢查雜湊輸入。即使模組的原始碼未變更,仍可能出現重複未命中且先前沒有成功上傳的情況。
  5. 利用已驅逐未命中的證據。 確認使用執行的金鑰和端點,然後檢查使用執行的快取警告和預熱任務的上傳結果。必要時檢查保留或驅逐設定。重新預熱所需的構件,並驗證後續是否命中。如果仍然未命中,請將相關的執行連結和日誌提供給支援團隊。

若要進行雜湊比較,請在您打算預熱和使用的每個相關環境中,使用相應的組態執行以下命令:

bash
tuist hash cache --configuration Debug --verbose

比較模組的完整雜湊和元件區塊。在相同的未變更機器上重複執行命令通常應產生相同的結果;比較實際的預熱和使用環境以調查不符之處。這兩個命令都不需要命中快取。如需進一步檢查,請參閱雜湊診斷。

效率#

透過二進位快取所能達到的效率水準,很大程度上取決於圖形結構。為了達到最佳效果,我們建議採取以下措施:

  1. 避免過度巢狀的相依圖形。圖形越淺層越好。
  2. 使用協定/介面目標來定義相依性,而非實作目標,並從最上層目標注入實作的相依性。
  3. 將經常修改的目標拆分為較小的目標,以降低其變更的可能性。

上述建議屬於模組化架構的一部分,我們建議以此方式建構專案,不僅能最大化二進位快取的效益,也能充分利用 Xcode 的功能。

建議設定#

我們建議設定一個 CI 任務,在 main 分支的每次提交中執行以預熱快取。這將確保快取始終包含main中變更的二進位檔,以便本地和 CI 分支建置能夠在其基礎上進行增量建置。

保持快取預熱隔離

在專用的 CI 步驟中執行tuist cache,且不要有後續依賴所產生工作區的步驟。由於tuist cache會為了快取建置目的而修改工作區,任何需要工作區的 CI 步驟都應先執行tuist generate以取得全新且可用的工作區。

快取預熱使用二進位檔

tuist cache命令也會利用二進位快取來加速預熱過程。

以下是一些常見工作流程的範例:

開發人員開始開發新功能#

  1. 他們從main建立新分支。
  2. 他們執行tuist generate。
  3. Tuist 從main拉取最新的二進位檔,並使用它們產生專案。

開發人員推送變更至上游#

  1. CI 流水線將執行xcodebuild build或tuist test來建置或測試專案。
  2. 工作流程將從main拉取最新的二進位檔,並使用它們產生專案。
  3. 接著,它將增量建置或測試專案。

組態#

快取並行限制#

將TUIST_CACHE_CONCURRENCY_LIMIT設定為正整數,以限制下載和上傳。例如,減少小型自託管快取在預熱任務中的負載:

bash
TUIST_CACHE_CONCURRENCY_LIMIT=2 tuist cache

未設定時,REAPI 上傳使用八個傳輸任務。當任何 blob 需要 ByteStream 時,下載使用八個任務;僅限批次計畫則使用 32 個任務。舊版 HTTP 快取路徑預設為 100;該預設不適用於 REAPI。無效值(包括先前文件記載的none)會保留預設值,而非移除限制。使用unset TUIST_CACHE_CONCURRENCY_LIMIT還原預設值。

限制適用於每次傳輸作業,而非全域適用於所有作業或 CI 任務。三個同時進行的預熱任務,若限制為二,可能對同一快取同時進行六個上傳。消費者任務可以保留變數未設定,以維持預設的下載平行處理能力。

快取預熱暫存目錄#

預設情況下,tuist cache會為建置中繼檔案和組合好的快取構件建立臨時暫存目錄,並在快取預熱完成後將其移除。若要將這些檔案保留在您管理的目錄下,請設定TUIST_CACHE_WARM_SCRATCH_DIRECTORY。

路徑可以是絕對路徑,也可以是相對於目前工作目錄的相對路徑。若目錄不存在,Tuist 會建立該目錄。若目錄已存在,則必須是空目錄。

設定此環境變數後,命令執行完畢時,Tuist 會保留目錄及其內容。呼叫者須在下一次快取預熱前負責清理該目錄。若未設定此變數,Tuist 會繼續使用並移除臨時目錄。

當需要預熱外部建置目標時,Tuist 會拒絕使用由呼叫者擁有的暫存目錄。外部建置腳本自行控制其輸出位置,因此 Tuist 無法保證這些輸出會留在暫存目錄內。

程式碼涵蓋率#

模組快取中的目標是預先建置的二進位檔,只有在該二進位檔建置時啟用了覆蓋率計數器,測試執行才會測量其程式碼。本節說明快取如何處理覆蓋率;若要端到端設定覆蓋率,請參閱程式碼覆蓋率。在Tuist.swift中為你的專案啟用覆蓋率:

swift
import ProjectDescription
let tuist = Tuist(
project: .tuist(
generationOptions: .options(
coverage: .coverage(enabled: true)
)
)
)

啟用enabled後,每個命令和每個環境都會讀取專案自有目標的單一插樁版快取,因此在 CI 上的一次預熱即可同時服務 CI 和本機機器:

命令你專案的目標外部套件
tuist cache(Debug 組態)預熱插樁二進位檔預熱普通二進位檔
tuist cache(Release 組態)預熱普通二進位檔預熱普通二進位檔
tuist hash cache列印插樁金鑰列印普通金鑰
tuist generate讀取插樁二進位檔讀取普通二進位檔
tuist test讀取插樁二進位檔讀取普通二進位檔

此選項適用於 Xcode 視為 Debug 的每種組態。Release 建置從不進行插樁。Tuist 也會在這些組態中連結剖析執行階段(CLANG_COVERAGE_MAPPING_LINKER_ARGS),因此即使未收集涵蓋率的建置仍會連結插樁二進位檔,並在其自動產生的 Scheme 以及你定義但未設定coverage的 Scheme 之測試動作中啟用涵蓋率。

你定義的 Scheme 會遵循此選項,除非它自行設定涵蓋率:.options(coverage: false)會保持關閉,而.options(coverage: true)會保持開啟,無論選項為何。測試計畫自行決定涵蓋率,且-enableCodeCoverage仍會覆寫執行,如同在 Xcode 中一樣。讀取插樁二進位檔但未收集涵蓋率的執行會正常建置和測試。當執行收集涵蓋率,但你專案的目標以普通二進位檔形式來自快取時,Tuist 會警告其涵蓋率未被測量。

TUIST_COVERAGE_ENABLED環境變數(1或0)優先於coverage: .coverage(enabled:)。請為整個環境(例如 CI 工作流程)設定此變數,而非僅針對單一命令,以便預熱快取的命令與讀取快取的命令保持一致。

prefixMapping(見Xcode 快取)有自己的覆寫機制:TUIST_COVERAGE_PREFIX_MAPPING環境變數(1或0)的優先順序高於coverage: .coverage(prefixMapping:)。

覆寫為關閉會讀取普通變體,而啟用涵蓋率預熱的快取不會填充該變體,因此這些目標會從原始碼建置。Release 預熱始終預熱普通二進位檔。

KEEP THE VALUES THE SAME EVERYWHERE

enabled和prefixMapping會變更你專案目標的建置設定和快取金鑰,以及其測試目標的選擇性測試雜湊。在共享快取的每台機器上將它們設定為相同的值,且不要從環境衍生它們(例如從Environment.isCI):CI 和本機機器之間不同的值會導致它們擁有不同的金鑰。環境變數亦然:使用TUIST_COVERAGE_ENABLED=0的執行不會重用啟用涵蓋率之執行的二進位檔或選擇性測試結果。

請注意:

  • 插樁二進位檔較大:靜態封存檔和動態函式庫約大 20–30%。Debug 建置的執行速度相同。
  • 無論是否啟用此選項,Xcode 的涵蓋率報告從不顯示來自快取的程式碼。Tuist 的涵蓋率上傳會測量它。若要在 Xcode 中查看目標的涵蓋率,請使用tuist generate <target>聚焦該目標,使其從原始碼建置。

疑難排解#

未對我的目標使用二進位檔#

確保雜湊值在不同環境與執行中具確定性。如果專案參考了環境(例如透過絕對路徑),就可能發生這種情況。你可以使用diff命令,比較兩次連續執行tuist generate所產生的專案,或跨環境與執行的專案。

同時確認目標並未直接或間接相依於無法快取的目標。

缺少符號#

使用原始碼時,Xcode 的建置系統透過 Derived Data 可以解析未明確宣告的相依性。然而,當你依賴二進位快取時,必須明確宣告相依性;否則,當找不到符號時,你很可能會看到編譯錯誤。為了除錯此問題,我們建議使用tuist inspect dependencies --only implicit命令,並在 CI 中設定它以防止隱式連結的回歸。