# 進階 Elixir 快取設定

請先參閱 <.localized_link href="/guides/features/cache/elixir">Elixir 快取設定指南</.localized_link>。以下選項僅在專案有額外編譯輸入、相依例外或不同 checkout 路徑時才需要。

## 選擇相依套件

`cache: true` 會自動快取支援的相依套件。關鍵字清單具有相同的自動選擇機制，並可提供選用覆寫：

```elixir
cache: [exclude: [:a_dependency_with_compile_time_side_effects]]
```

如要改為將快取限制為特定相依套件：

```elixir
cache: [dependencies: [:nimble_options, :nimble_parsec]]
```

`dependencies: :all` 明確選擇所有支援的相依套件；`dependencies: []` 則不選擇任何套件。`exclude:` 優先於任一選擇。省略 `cache`，或將其設為 `false` 或 `nil`，會停用快取。

支援的相依套件會透過 Hex 或 Git 取得，並使用標準 Mix 編譯器流程搭配 Elixir/Erlang 原始碼，或使用 rebar3 的一般純 BEAM 編譯。當上游相依套件使用 rebar3 時，Mix 相依套件也可以重複使用構件。Rebar 設定、原始碼/標頭檔以及 rebar3 可執行檔指紋都會納入鍵值。封裝的 `ebin/*.app` 中繼資料和被動式 Makefile 也受支援。Rebar 設定腳本在查找前已由 Mix 評估；其原始碼與有效設定會進入鍵值。應用程式原始碼腳本（`*.app.src.script`）稍後由 rebar3 評估，並需要經審核的建構合約，建構外掛程式與編譯鉤子亦然。缺少的 rebar3 啟動器會在指紋計算前安裝，使用 Mix 的正常安裝器；冷態工作節點仍需要存取該安裝器。

自訂追蹤器、原始碼符號連結、umbrella 根目錄以及包含本機相依套件的鏈結都能正常編譯。Tuist 自身的 bootstrap 相依套件在冷態工作節點上也能正常編譯。結構上的符合資格並不證明任意編譯程式碼具備封閉性。

## 宣告額外輸入

原始碼樹、鎖定的相依身分、上游來源、BEAM 與資源、相依執行階段/選用/應用程式旗標、已解析的應用程式中繼資料、明確的有效應用程式設定、編譯器選項（包括 lexer 與 parser 選項）、Mix 環境與目標，以及工具鏈身分都會自動納入。相依專屬的 `env:` 與 `system_env:` 會進入鍵值，並在該環境下評估設定。應用程式預設值透過原始碼輸入進入，而非 VM 載入狀態。

對於在編譯時期讀取原始碼樹以外檔案或額外環境變數的相依套件，請宣告這些輸入：

```elixir
cache: [
  inputs: ["schemas", "config/compiler-input.json"],
  env: ["YOUR_COMPILER_OPTION"]
]
```

Elixir 巨集與模組主體可以讀取任意檔案、環境、周邊程式碼路徑、網路回應與時間。Tuist 不會自動發現這些讀取行為。變更未宣告的輸入可能產生過時的輸出。排除具有時間/網路依賴編譯或必須在每次建構執行的副作用的相依套件；在需要更嚴格控制時，使用 `dependencies:` 建立經審核的相依允許清單。

標準編譯器/執行階段變數（`ERL_COMPILER_OPTIONS`、`ELIXIR_ERL_OPTIONS`、`ERL_AFLAGS`、`ERL_FLAGS`、`ERL_ZFLAGS`、`SOURCE_DATE_EPOCH`、`ERL_LIBS`、`MIX_PATH` 與 `MIX_ARCHIVES`）會自動捕捉。工具鏈身分包括 Elixir 建構版本、可用的完整 OTP 版本、ERTS、編譯器位元組碼、作業系統/版本、架構、字組大小與位元組順序。有效上下文會在每次查找前捕捉。每個相依的輸入模型可在一次準備工作中共用以供查找規劃；編譯會清除計畫，而還原會使已安裝應用程式的記錄失效。分階段安裝與發布一律檢查全新輸入，不會重複使用查找計畫。查找未命中也會在編譯前捕捉全新鍵值。輸入變更會拒絕分階段命中，而非安裝為先前輸入建構的程式碼。

鍵值使用確定性項編碼與排序檔案系統遍歷。無序的建構宣告與已宣告輸入集會排序並去重；有序編譯器選項保留其順序。檔案內容、名稱、可執行位元與缺失路徑會影響結果，檔案系統時間戳記則不會。檔案宣告不建模擁有權、時間戳記或其他檔案系統中繼資料；請排除從這些值衍生輸出的建構器。上游編譯器清單被排除為增量記帳；刻意讀取其內容的程式碼必須將其宣告為額外輸入。Regex 設定使用其原始碼與選項，絕不使用 VM 本機編譯參考。其他不透明執行階段值會繞過快取。更新的鍵值模型會讓舊條目失效，無需遷移。

確定性鍵值編碼與確定性編譯器輸出是分開的。如果在近期 Elixir 版本上，上游 BEAM 中繼資料在其他方面相同的建構之間發生變化，請考慮在 SDK bootstrap、種子與快取消耗前設定 `ERL_COMPILER_OPTIONS='[deterministic]'`。將其與任何現有編譯器選項合併，並在變更設定時重建相依輸出一次。SDK 不會自動設定它；其確切值仍是鍵值的一部分。完整的上游位元組仍會被雜湊，不會標準化或剔除。這不是跨 checkout 或任意建構器的可複現性保證。該編譯器選項也會從 `module_info(:compile)` 省略原始碼/選項，因此在採用前請驗證涵蓋範圍、除錯與原始碼探索工具。編譯器旗標無法修復無序的編譯時期產生器。SDK 需要 Mint 1.10.2 或更新版本，包括其安全性回溯修補。此版本會對用於產生其 HTTP/2 框函式的映射進行排序；升級 SDK 時請更新較舊的 Mint 鎖定條目。即使啟用 `deterministic`，其他相依套件與編譯器階段仍可能產生不同的 bootstrap BEAM。

Lexer 與 parser 輸出僅在它們不存在於鎖定的 Git 樹或 Hex 套件檔案清單時，才會被視為衍生檔案。隨附的 `.erl` 檔案與不透明目錄/子模組內容仍為一般輸入；重新產生的隨附檔案必須通過相同的分階段後輸入調和。來自 OTP 的 `parsetools` 的產生器範本也會進入工具鏈身分。在併發建構之間保持相依原始碼 checkout 隔離：產生器與原生建構器可能會寫入 `deps/`。巢狀/自訂 rebar 產生器佈局可能仍需要經審核的 `generated` 宣告。

## 原生與自訂建構器

對於經審核的 Mix、rebar3 或 Make 建構器，請宣告每個相依套件的合約：

```elixir
cache: [
  builders: [
    bcrypt_elixir: [
      platform: "build-image@sha256:YOUR_IMMUTABLE_IMAGE_DIGEST",
      outputs: ["priv/bcrypt_nif.so"],
      tools: ["cc", "make"],
      env: ["YOUR_BUILDER_OPTION"]
    ]
  ]
]
```

`platform` 識別相容的原生建構環境。使用涵蓋編譯器、SDK、系統標頭檔與共享程式庫的不可變映像檔／工具鏈身分，而非如 `linux` 等可重複使用的標籤。Elixir/OTP 與常見的平台檢查仍然適用。這是對建構器的明確審核，而非自動探索原生輸入或必要的副作用。無效的明確建構器合約會引發可操作的設定錯誤，而不會靜默地停用快取。

`outputs` 列出相對於相依套件應用程式建構目錄的必要路徑。列出執行時期實際需要的程式庫／可執行檔。即使 Mix 認為 BEAM 是最新的，缺少輸出仍會觸發還原或編譯；不完整的快照會被拒絕。使用建構器的 `env` 宣告額外的環境變數，並使用其 `inputs` 宣告外部檔案／目錄，這些路徑相對於使用該相依套件的專案。原生鍵值會自動包含標準建構環境變數，例如 `PATH`、`CC`、`CXX`、`CFLAGS`、`LDFLAGS`、`SDKROOT` 與 `RUSTFLAGS`，並使用相依套件的有效 `system_env` 覆寫。已解析的 Mix 專案設定也會進入原生建構器鍵值；不透明的函式值設定無法被快取。

選用 `tools` 名稱會在該有效 `PATH` 上解析；相對工具路徑則相對於相依套件原始碼目錄。可執行位元組與解析後的路徑會進入鍵值，而無法使用的已宣告工具會繞過快取。啟動器或編譯器包裝器的指紋並不涵蓋其 SDK、子行程或系統程式庫：不可變的 `platform` 與額外的 `inputs` 必須涵蓋這些項目。`MAKEFLAGS` 會按原樣保留，因為自訂建構器可以讀取它。繼承的 GNU make jobserver 識別碼因此可能導致不同執行之間的安全快取未命中；請在隔離的快取工作中使用穩定的建構旗標，而不要假設這些值會從鍵值中移除。

某些建構器會在 `deps/` 下產生檔案，而不僅是在 `_build/`。在 `generated` 中列出相對於該相依套件原始碼目錄的那些路徑，例如 `generated: ["priv/native"]`。只有那些產生的路徑才會從原始碼輸入中排除。不要排除原始資源或原始碼檔案：它們必須保持為輸入。路徑是字面意義，不含萬用字元或遍歷。合約、平台、額外輸入、環境值與任何自訂相依套件編譯命令都會參與鍵值。`compile: false` 仍然是選擇退出編譯的方式。

經審核的快照會將基於原始碼的 `ebin`、`priv`、`include` 與 `src` 資源實體化到還原的應用程式中。因此，原生程式庫、連接埠可執行檔、標頭檔與產生的資源能在冷 checkout 中存活，無需修改其相依套件原始碼。可執行模式與包含的相對資源符號連結會被保留；逸出、絕對與循環連結會被拒絕。所有上游應用程式資源都會進入下游鍵值，包括一般的 `priv` 資源與自訂輸出目錄，而不僅是原生程式庫或 BEAM。直接讀取生產者原始碼路徑的程式碼不會被重新定位；具有應用程式產物之外必要副作用的建構器仍需要這些步驟。

在審核其輸入與輸出之前，請保持自訂建構器被排除。合約不會移除冷 SDK bootstrap 編譯、64 MiB 產物限制，或對相容建構環境的需求。

## 跨 checkout 路徑重用

Checkout 路徑預設會參與鍵值。如果你的相依套件不依賴生產者路徑，請選擇在不同 checkout 位置之間重用：

```elixir
cache: [portable: true]
```

嵌入 checkout 路徑的設定值（例如在 `config/config.exs` 中由 `__DIR__` 建構的路徑）不會改變可攜式鍵值。Elixir 工具鏈的安裝位置也不會影響：歸檔與 rebar3 按其內容計算。

快取的 BEAM 會保留生產者除錯／原始碼路徑以及編譯時期的 `__DIR__`、`__ENV__.file` 與 `File.cwd!()` 值。可攜式重用不會重寫它們。資源連結指向目前的原始碼 checkout。上游 BEAM 位元組也會進入鍵值並可能嵌入路徑，因此重用可能需要從同一個快取還原上游閉包，而不是獨立重新編譯它。

## 儲存與存取

| 選項 | 預設值 | 用途 |
| --- | --- | --- |
| `remote` | `true` | 在本機未命中後使用共享的遠端快取。 |
| `upload` | `true` | 遠端發布新捆綁包。`false` 仍允許讀取與本機快照。 |
| `path` | Tuist 的快取目錄，位於 `mix/dependencies/v1` 之下 | 選擇不同的本機儲存區。 |
| `endpoint` | 自動發現 | 覆寫帳戶的 HTTPS REAPI 端點。 |

`upload: false` 控制客戶端行為，而非授權。專案快取權限由伺服器強制執行。只能讀取專案快取的憑證會自動跳過上傳。專案 `endpoint:` 選項與發現的端點需要 HTTPS，但迴環（`localhost`、`127.0.0.1`、`::1`）開發端點除外。

非空的 `TUIST_CACHE_ENDPOINT` 會覆寫 `endpoint:` 與發現機制。託管執行器使用它來存取其私有的叢集內快取。此由營運者控制的環境覆寫也允許在非迴環主機上使用 HTTP。HTTP 會以未加密方式傳送專案範圍的快取權杖與產物，因此請仅在受信任的網路上使用。無效的覆寫會回退至本機快照，而不會嘗試發現。

本機快照與遠端捆綁包限制為 64 MiB；較大的產物會編譯但不會被快取。本機儲存區沒有自動驅逐機制；請在需要時移除其內容。遠端產物使用 Kura 現有的保留、驅逐與專案清理規則。

> [!IMPORTANT]
> 編譯器清單與 BEAM 可以嵌入生產者路徑與編譯時期值，包括密鑰。專案快取讀者可以存取這些產物。快取寫入者是受信任的程式碼生產者：摘要檢查偵測損壞，而非惡意的可執行 BEAM。不要將快取寫入憑證暴露給不受信任的 PR 工作，或在這些憑證可用的情況下編譯不受信任的相依套件。

## 疑難排解：缺少命中

- 安裝套件後執行 `mix tuist.setup` 並提交產生的 Mix 設定。Setup 會保留其他別名與 Tuist 選項；它會拒絕自訂的編譯動作，而不會靜默地移除它們。
- 保留產生的編譯別名。直接 `mix tuist.compile` 工作發現可能會在快取檢查之前先編譯相依套件。
- 對於已選取且受支援的相依套件，其已經最新且有效的輸出會以原因 `up_to_date` 計為本機命中。它們不會事後發布，也不帶有任何產物金鑰、傳輸位元組數、還原延遲或預估節省時間。若要測試快照還原，請在相依套件填入快取後，使用 `mix deps.clean NAME --build` 僅清除該受支援相依套件的建置，然後再次編譯。
- 比對 `MIX_ENV`、`MIX_TARGET`、編譯器設定與宣告的輸入。除非啟用可攜式重用，否則路徑變更也會改變金鑰。
- 不受支援的相依套件會顯示為 **未快取**，而非未命中。它們不納入命中率計算。若一次建置既沒有查詢也沒有最新的本機命中，則命中率為不可用，而非 0%。
- 缺少憑證、端點不可用或快照損壞時，會回退至編譯。編譯器失敗仍會導致建置失敗。
- CAS 記錄可能在建置報告之後才到達。重新整理以查看新匯入的傳輸。本機命中與舊版建置沒有遠端 CAS 輸出。

伺服器需要支援編譯報告合約 `0.2` 才能記錄 `up_to_date` 本機命中。如果較舊的伺服器明確拒絕快取資料或結構描述列舉值，套件會在不包含這些新重用觀察的情況下重試報告一次。編譯詳細資訊與其他快取結果會保留；新的本機重用功能在伺服器升級前仍然不可用。先部署伺服器可避免這額外的請求。合約版本不會協商支援，且驗證失敗、傳輸失敗與其他驗證訊息不會重試。列舉錯誤在較舊的伺服器上不會指明其欄位；保留的無效欄位在單次重試中仍會失敗。

## 手動編譯整合

如果 setup 無法安全地編輯你的專案設定，請手動將此別名與這些函式加入你的 Mix 專案模組，並保留任何自訂編譯動作：

```elixir
# In aliases/0:
compile: &compile/1

# In the same Mix project module:
defp compile(args) do
  if "--no-compile" not in args do
    compile_tuist_ex()
  end

  Mix.Task.run("deps.loadpaths", ["--no-compile", "--no-deps-check"])
  Mix.Task.run("tuist.compile", args)
end

defp compile_tuist_ex do
  deps =
    ["tuist_ex"]
    |> Mix.Dep.filter_by_name(Mix.Dep.load_and_cache(), include_children: true)
    |> Enum.reject(&(Mix.Dep.ok?(&1) and &1.scm.fetchable?()))
    |> Enum.map(&Atom.to_string(&1.app))

  if deps != [], do: Mix.Task.run("deps.compile", deps)
end
```

這些函式會在其他相依套件之前編譯並載入 Tuist，包括在冷啟動平行建置時；當 Tuist 及其相依套件已編譯，或命令以 `--no-compile` 執行時，則會跳過該工作。相同的函式也適用於 Hex、Git 與路徑安裝；不需要腳本路徑。

如果你的別名以 `deps.compile tuist_ex --include-children` 開頭，每次 `mix compile` 都會重建 Tuist 的相依套件，並可能重新編譯你的整個專案。請再次執行 `mix tuist.setup` 以取代它。

## 運作方式

Elixir 快取使用 Bazel 所用的共享 REAPI ActionCache/CAS 管線、驗證與 Kura 簽署的分析資料，而非獨立的 Mix 儲存服務。模型化的輸入金鑰會參照一個 ActionResult，其中包含完整編譯捆綁包的原始位元組 SHA-256 摘要。Tuist 會在分階段安裝前驗證其檔案、應用程式中繼資料、宣告的 BEAM 與 SCM 清單。損壞的項目會在成功編譯後修復。

在 OTP 28 及更高版本中，遠端工作階段會為至少 1 KiB 的 ByteStream 傳輸協商 zstd 壓縮。不可壓縮的上傳、較舊的 OTP 執行時期與不支援壓縮的伺服器會使用身份傳輸。摘要、產物限制與回報的位元組數均指未壓縮的內容。解碼後的位元組在安裝前會進行邊界檢查與驗證。每個相依套件仍保持為單一 CAS 捆綁包，避免對小型產物發出每檔案或分塊請求。

遠端憑證／連線會延遲解析；閒置建置與本機命中（包括已經最新的輸出）不會聯絡遠端端點。每次退出時都會關閉一個工作階段，包括編譯器失敗的情況。客戶端 RPC 有 30 秒的截止時間。Mix 仍負責鎖定檢查、本機相依套件編譯與不支援的工作。

儀表板的延遲／位元組數會保留本機與遠端的來源區別，並將不可用的值與測量的零區分開來。遠端計時測量客戶端 CAS 傳輸，而本機計時包括驗證與安裝；兩者都不包括編譯。CAS 輸出使用成功的 Kura 簽署傳輸，而非本機副本或動作輸入金鑰。吞吐量是位元組數除以測量時間；壓縮大小不可用。預估避免的編譯時間是生產者的原始編譯持續時間，而非測量的端到端建置節省時間。

獨立且就緒的相依套件會透過同一個 HTTP/2 連線，以最多四個並發請求的視窗取得。這將解碼後的捆綁包緩衝限制為四個產物，每個產物都受 64 MiB 限制。金鑰規劃、Mix 狀態與安裝仍保持序列化。下游金鑰會等待上游產物；預取的位元組僅在其確切輸入金鑰下消耗，並在遠端安裝前重新檢查輸入。已經最新的輸出與本機快照不會引發猜測性的遠端請求。針對選定的快取未命中，編譯與上傳仍保持串行；並行性並不保證每次建置都會更快。

## 每個相依套件都能跳過編譯嗎？

相容的命中可以跳過受支援相依套件的編譯，但 `cache: true` 是故障開放模式，並非零編譯保證。缺少／被驅逐的項目、不支援的建置工具與冷啟動 SDK 引導仍可能觸發編譯。高命中率本身並不代表實際掛鐘時間加速。

對於「建置一次／測試多次」的 CI，請使用
<.localized_link href="/guides/features/test-sharding/elixir">Elixir test sharding</.localized_link>
來分發精確準備好的建置，而不是期望每個工作節點從每個相依套件的快取命中重建它。重用完整的建置，尤其是包含原生程式庫的建置，仍然需要相容的平台、工具鏈與設定。任意的自訂建置若未對其外部輸入、輸出與必要的副作用進行建模，就無法安全地跳過其建置步驟。
