跳到内容

模块缓存

要求
  • 一个生成的项目。模块缓存读取 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 cache --destination device
tuist generate --destination device

默认值是 all,它会保留你的目标支持的每个目标平台,包括 Mac Catalyst。后续的 tuist cache 会重用缓存的分片并仅构建缺失的 SDK。常规生成会在缺少任何所需分片时保留模块的源代码。

受限生成将源目标限制为所选的目标平台,并保留原生 macOS 目标,包括宏。两种受限模式都不包括 Mac Catalyst。使用 --destination all 重新生成以恢复所有目标平台。

逐分片缓存支持具有标准架构的 iOS 和 macOS。其他平台、自定义架构和不支持的产品保留完整构件缓存。旧版模块缓存需要 --destination all。

远程上传准入#

现代模块缓存使用 REAPI。当繁忙的缓存暂时以 RESOURCE_EXHAUSTED 拒绝上传时,Tuist 会以抖动方式进行退避,遵守标准的 google.rpc.RetryInfo 最小延迟,并在有限的等待预算内重试。每次调用最多允许六次重试。每个 blob 上传操作在其存在性检查、批量上传和流式写入之间共享一个 60 秒的累计退避预算;并发等待各自消耗预算,因此反复拒绝不会给每个排队的 blob 再提供一分钟的重试时间。成功的传输不消耗此预算,并且在预算耗尽时不会被取消。动作结果发布有其自己的有限预算。指数延迟从一秒开始,在向比例抖动过渡之前增长至 30 秒。如果服务器要求的延迟超过剩余预算,Tuist 会失败而不是比指示更早地重试。取消会中断退避,缓存读取保留短重试,以便过载不会停滞生成。

这些默认值也有助于没有重试提示的旧版 Kura。更长的重试吸收临时争用,而非持续过载:如果拒绝持续存在,请减少热作业上的缓存并发限制或增加服务器容量。请参阅Kura 上传大小调整以获取 pod 内存估计和准入指标。

配置选择#

在不传递 --configuration 的情况下预热缓存时,Tuist 按以下顺序选择要使用的构建配置:

  1. 清单的 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 缓存:

  • 框架(静态和动态),包括依赖于 XCTest 或 Swift Testing 的测试支持框架
  • 库(静态和动态),包括依赖于 XCTest 或 Swift Testing 的测试支持库
  • Bundle
  • Swift Macros

测试 Bundle 仍被排除在模块缓存之外。这意味着 unitTests 和 uiTests 目标不会作为二进制文件进行缓存,但测试所依赖的常规框架和库目标即使链接了 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 无法保证这些输出保留在临时目录内。

代码覆盖率#

EARLY ACCESS

模块缓存的代码覆盖率功能处于早期访问阶段。仅当设置 TUIST_FEATURE_FLAG_COVERAGE=1 时,命令才会读取和预热下文所述的插桩二进制文件。

模块缓存中的目标是预构建的二进制文件,只有在使用覆盖率计数器构建该二进制文件时,测试运行才会测量其代码。在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),因此即使不收集覆盖率的构建也会链接插桩二进制文件,并在其自动生成的方案以及你定义的未设置 coverage 的方案中的测试操作中开启覆盖率。

你定义的方案将遵循该选项,除非它自行设置覆盖率:.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 中设置它以防止隐式链接回归。