# 高级 Elixir 缓存配置

请先阅读<.localized_link href="/guides/features/cache/elixir">Elixir 缓存设置指南</.localized_link>。仅当你的项目包含额外的编译输入、依赖例外或不同的检出路径时，才需要以下选项。

## 选择依赖

`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 自身的引导依赖在冷工作节点上也能正常编译。结构上的符合条件并不能证明任意编译代码是封闭的。

## 声明额外输入

源码树、锁定的依赖标识、上游源码、BEAM 和资源、依赖的运行时/可选/应用标志、已解析的应用元数据、明确的有效应用配置、编译器选项（包括词法分析器和解析器选项）、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、编译器字节码、操作系统/版本、架构、字长和字节序。有效上下文会在每次查找之前被捕获。每个依赖的输入模型可以在一次准备过程中共享用于查找规划；编译会清除该计划，而恢复会使已安装应用的记录失效。分阶段安装和发布始终检查新的输入，而不重用查找计划。查找未命中也会在编译前捕获一个新的键。输入发生变化时，会拒绝分阶段命中的结果，而不是安装基于早期输入构建的代码。

键使用确定性的术语编码和排序的文件系统遍历。无序的构建器声明和声明的输入集会被排序和去重；有序的编译器选项保留其顺序。文件内容、名称、可执行位和缺失的路径都很重要，而文件系统时间戳不重要。文件声明不建模所有权、时间戳或其他文件系统元数据；排除那些从这些值派生输出的构建器。上游编译器清单作为增量记账被排除；故意读取其内容的代码必须将它们声明为额外输入。正则表达式配置使用其源码和选项，从不使用 VM 本地编译的引用。其他不透明的运行时值会绕过缓存。更新的键模型会使旧条目失效，无需迁移。

确定性键编码与确定性编译器输出是分开的。如果在最近的 Elixir 版本上，上游 BEAM 元数据在其他方面相同的构建之间发生变化，请考虑在 SDK 引导、种子生成和缓存消费之前设置`ERL_COMPILER_OPTIONS='[deterministic]'`。将其与任何现有的编译器选项合并，并在更改设置时重新构建依赖输出一遍。SDK 不会自动设置它；其确切值仍然是键的一部分。完整的上游字节仍然会被哈希，不会被规范化或剥离。这并不能保证跨检出或任意构建器的可重现性。该编译器选项还会从`module_info(:compile)`中省略源码/选项，因此在采用之前请验证覆盖率、调试和源码发现工具。编译器标志无法修复无序的编译时生成器。SDK 需要 Mint 1.10.2 或更高版本，包括其安全回溯补丁。此版本对用于生成其 HTTP/2 帧函数的映射进行了排序；在升级 SDK 时更新旧的 Mint 锁定条目。即使启用了`deterministic`，其他依赖和编译器阶段仍可能产生不同的引导 BEAM。

仅当词法分析器和解析器输出在锁定的 Git 树或 Hex 包文件清单中不存在时，才将其视为派生文件。随附的`.erl`文件和不透明的目录/子模块内容仍然是普通输入；重新生成的随附文件必须通过相同的 staging 后输入协调。来自 OTP 的`parsetools`的生成器模板也会进入工具链身份。在并发构建之间保持依赖源码检出的隔离：生成器和本机构建器可能会写入`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` 资源具体化到恢复的应用程序中。因此，原生库、端口可执行文件、头文件和生成的资源可以在冷检出后存活，而无需修改其依赖源。可执行模式和包含的相对资源符号链接会被保留；转义、绝对和循环链接会被拒绝。所有上游应用程序资源都进入下游键，包括普通的 `priv` 资源和自定义输出目录，而不仅仅是原生库或 BEAM。直接读取生产者源路径的代码不会被重定位；具有应用程序工件之外强制副作用的构建器仍然需要这些步骤。

在审计其输入和输出之前，保持自定义构建器被排除。合同不会消除冷 SDK 引导编译、64 MiB 工件限制或对兼容构建环境的需求。

## 跨检出路径的重用

检出路径默认参与键的生成。如果你的依赖不依赖于生产者路径，可以选择在不同检出位置之间重用：

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

嵌入检出路径的配置值（例如在 `config/config.exs` 中由 `__DIR__` 构建的路径）不会改变可移植键。Elixir 工具链的安装位置也不会影响：归档文件和 rebar3 按其内容计数。

缓存的 BEAM 保留生产者调试/源路径以及编译时的 `__DIR__`、`__ENV__.file` 和 `File.cwd!()` 值。可移植重用不会重写它们。资源链接指向当前的源检出。上游 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 配置。设置会保留其他别名和 Tuist 选项；它会拒绝自定义的编译操作，而不是静默地移除它们。
- 保留生成的编译别名。直接 `mix tuist.compile` 任务发现可能会在缓存检查之前编译依赖。
- 对于选定的受支持依赖，已经最新且有效的输出计为原因 `up_to_date` 的本地命中。它们不会事后发布，也不携带工件密钥、传输字节数、恢复延迟或预估节省时间。要测试快照恢复，请在依赖填充缓存后，使用 `mix deps.clean NAME --build` 仅清理该受支持依赖的构建，然后再次编译。
- 匹配 `MIX_ENV`、`MIX_TARGET`、编译器设置和声明的输入。除非启用了可移植复用，否则路径更改也会更改密钥。
- 不受支持的依赖显示为 **未缓存**，而非未命中。它们被排除在命中率之外。既没有查找也没有最新本地命中的构建具有不可用的命中率，而非 0%。
- 缺少凭据、端点不可用和快照损坏会回退到编译。编译器失败仍会导致构建失败。
- CAS 记录可能在构建报告之后到达。刷新以查看新摄入的传输。本地命中和旧版构建没有远程 CAS 输出。

服务器需要支持编译报告契约 `0.2` 才能记录 `up_to_date` 本地命中。如果较旧的服务器明确拒绝缓存数据或架构枚举值，包将重试一次报告，但不包含这些新的复用观察结果。编译详情和其他缓存结果会被保留；新的本地复用在服务器升级之前仍然不可用。先部署服务器可以避免此额外请求。契约版本不协商支持，身份验证失败、传输失败和其他验证消息不会重试。枚举错误在较旧的服务器上不会标识其字段；保留的无效字段在单次重试中仍然会失败。

## 手动编译集成

如果设置无法安全地编辑你的项目配置，请手动将此别名和这些函数添加到你的 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>
来分发确切准备好的构建，而不是期望每个工作节点从每个依赖的缓存命中中重建它。重用完整的构建，尤其是包含原生库的构建，仍然需要兼容的平台、工具链和配置。任意自定义构建在不对其外部输入、输出和所需副作用进行建模的情况下，无法安全地跳过其构建步骤。
