跳到内容

高级 Elixir 缓存配置

请先阅读Elixir 缓存设置指南。仅当你的项目包含额外的编译输入、依赖例外或不同的检出路径时,才需要以下选项。

选择依赖#

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 字节也进入键并可能嵌入路径,因此重用可能需要从同一缓存恢复上游闭包,而不是独立重新编译它。

存储和访问#

选项默认用途
remotetrue在本地未命中后使用共享远程缓存。
uploadtrue远程发布新捆绑包。false 仍然允许读取和本地快照。
pathTuist 的缓存目录,位于 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,请使用

Elixir test sharding

来分发确切准备好的构建,而不是期望每个工作节点从每个依赖的缓存命中中重建它。重用完整的构建,尤其是包含原生库的构建,仍然需要兼容的平台、工具链和配置。任意自定义构建在不对其外部输入、输出和所需副作用进行建模的情况下,无法安全地跳过其构建步骤。