Engineering guide

远程云端 Mac 的 Xcode 构建缓存调优实战

远程云端 Mac 的 Xcode 构建缓存调优实战

发布前两天临时增加一台远程 Mac,代码拉取只用了几分钟,第一次 Xcode 构建却持续等待;第二次虽然变快,切换分支后又出现旧资源、索引异常或依赖重新解析。问题通常不在机器是否够快,而在 DerivedData、Swift 包下载和构建结果混在默认目录中,既无法判断缓存是否命中,也无法在租期结束前有选择地迁出。

先定义可比较的构建基线

缓存调优前先固定变量。记录 xcodebuild -version、macOS 版本、提交哈希、Scheme、构建配置和目标平台。测试期间不要同时升级依赖、切换 Xcode 或修改编译参数,否则两次耗时没有可比性。

至少执行三组测试:

  1. 删除项目专属 DerivedData 后进行冷构建。
  2. 不改代码,使用相同命令进行暖构建。
  3. 修改一个普通 Swift 文件,再进行增量构建。

每次保存开始时间、结束时间、退出码和结果包。图形界面的进度条适合观察,不适合精确比较;自动化记录应统一走 xcodebuild

缓存的目标不是让某一次构建看起来最快,而是让相同输入得到可预测的耗时和结果。

把缓存按职责拆开

建议在工作目录下建立四类路径:源码、DerivedData、Swift 包下载和结果日志。不要让多个项目共用同一个 DerivedData,也不要让 Debug 与 Release 相互覆盖。项目较多时,可继续加入 Xcode 主版本和提交分支标识。

目录 保存内容 建议策略
Source Git 工作树 按项目独立
DerivedData 索引与中间产物 按版本、Scheme、配置隔离
SourcePackages Swift 包下载与检出 锁文件未变化时复用
Results 结果包与日志 按构建时间归档

Swift 包目录可以复用,但前提是依赖锁文件一致。若锁文件发生变化,先让解析流程正常完成,不要直接把旧检出目录复制到新项目上。DerivedData 对工具链更敏感,切换 Xcode 后应创建新目录,而不是覆盖原目录继续使用。

用固定命令执行构建

下面的脚本把关键路径显式化。将工作区、Scheme 和目标平台替换为项目实际值。结果包路径在执行前必须不存在,因此脚本会先删除同名目录。

#!/bin/bash
set -euo pipefail

ROOT="${HOME}/BuildWorkspace"
SCHEME="Application"
CONFIGURATION="Release"
DERIVED="${ROOT}/DerivedData/${SCHEME}-${CONFIGURATION}"
PACKAGES="${ROOT}/SourcePackages"
RESULT="${ROOT}/Results/${SCHEME}.xcresult"
LOG="${ROOT}/Results/${SCHEME}.log"

mkdir -p "${DERIVED}" "${PACKAGES}" "${ROOT}/Results"
rm -rf "${RESULT}"

xcodebuild \
  -workspace "${ROOT}/Source/Application.xcworkspace" \
  -scheme "${SCHEME}" \
  -configuration "${CONFIGURATION}" \
  -destination "generic/platform=iOS" \
  -derivedDataPath "${DERIVED}" \
  -clonedSourcePackagesDirPath "${PACKAGES}" \
  -resultBundlePath "${RESULT}" \
  build | tee "${LOG}"

先在终端运行 xcodebuild -list,确认工作区确实暴露了目标 Scheme。若项目使用工程文件而非工作区,把 -workspace 改为 -project。不要把两者同时传入。

为缓存目录生成稳定标识

缓存键至少应包含 Xcode 版本、依赖锁文件摘要、Scheme 和配置。分支名可以加入,但不要只用分支名,因为同一分支的依赖可能已经变化。

XCODE_KEY="$(xcodebuild -version | shasum -a 256 | cut -c1-12)"
PACKAGE_KEY="$(shasum -a 256 Package.resolved | cut -c1-12)"
printf '%s-%s-%s-%s
' "${XCODE_KEY}" "${PACKAGE_KEY}" "${SCHEME}" "${CONFIGURATION}"

工作区里若有多个 Package.resolved,应明确选择真正参与构建的文件。找不到锁文件时不要生成一个固定空键,否则不同依赖状态会落入同一目录。

按阶段定位无效重编译

暖构建仍然很慢时,先看耗时落在哪一段。依赖解析反复发生,检查锁文件是否被脚本重写、包目录权限是否正确;编译阶段反复执行,检查编译条件、自动生成文件和构建阶段脚本;链接阶段异常,检查是否每次都改变版本信息或嵌入内容。

最常见的问题是脚本阶段没有声明输入与输出。只要脚本每次改写生成文件的时间戳,下游目标就会被判定为需要重建。修复方法是让脚本仅在内容变化时替换文件,并在 Xcode 构建阶段中填写准确的输入、输出路径。

不要把“清空所有缓存”当作固定步骤。它只能回答缓存是否污染,不能解释污染来自哪里。先单独移动项目的 DerivedData;问题仍在,再处理 Swift 包目录。这样能保留诊断证据,也避免每次重新下载全部依赖。

租期结束前保留真正有用的内容

迁出前先停止正在运行的构建,确认日志已经写完,再打包依赖锁文件、必要的 Swift 包下载、结果包和项目脚本。源码应以代码仓库中的已提交内容为准,工作树里的未提交修改要单独检查。

不建议长期保存整个 DerivedData。它通常体积大,包含索引、对象文件和与具体工具链绑定的中间产物。下一次使用不同版本的 Xcode 时,重新生成往往更可靠。敏感证书、私钥、令牌和临时环境文件不要混入缓存归档;先清理 Shell 历史、临时钥匙串及项目生成的凭据文件,再核对导出包清单。

最后保留一份简短记录:工具链版本、缓存键、最后一次成功提交、构建命令、已导出路径和明确未导出的路径。下一台云端 Mac 按这份记录恢复,比复制整个主目录更容易复现。

常见问题

DerivedData 是否应该在多个 Xcode 版本之间共用?

不应该。应至少按 Xcode 版本、项目、Scheme 和构建配置隔离目录;切换工具链后重新生成 DerivedData,避免旧索引或中间产物造成隐蔽错误。

租期结束前最值得迁出的缓存是什么?

优先迁出依赖锁文件、可复用的 Swift 包下载目录、构建结果包和关键日志。对象文件与索引通常体积大且强依赖工具链,下一台机器重新生成更可靠。

清空全部缓存能否解决构建变慢?

只能用于确认缓存污染,不能作为日常方案。先记录解析依赖、编译、链接和测试各阶段耗时,再定点清理对应目录,才能避免每次都付出完整冷构建成本。

ArmMacs Cloud Mac

按项目周期使用独享物理节点

选择固定芯片、内存、存储、租期与在售节点,库存可用时进入交付流程。

选择机型并订购