工程记录

云端 Mac 并行 Xcode 构建的 DerivedData 锁冲突治理

云端 Mac 并行 Xcode 构建的 DerivedData 锁冲突治理

同一台云端 Mac 同时跑两条 Xcode 流水线时,偶发的 database is locked、索引数据库损坏或“本地无法复现”的链接错误,往往不是代码变化造成的。先检查两个任务是否共用了默认的 ~/Library/Developer/Xcode/DerivedData。这个目录不只是缓存,还包含构建数据库、索引、模块缓存和任务持续改写的中间产物;把它当成可并发读写的公共缓存,失败只是时间问题。

先确认冲突来自哪里

不要看到失败就立即删除全部缓存。先保存失败任务的完整 xcodebuild 命令、开始时间、工作目录和进程列表,再搜索日志中的 lockeddatabaseunable to attachmalformed 与重复产物提示。

pgrep -alf 'xcodebuild|XCBuildService|swift-frontend'
find "$HOME/Library/Developer/Xcode/DerivedData" -name build.db -print
grep -Eini 'locked|database|malformed|multiple commands produce' build.log

如果两个任务的 -derivedDataPath 相同,或者都没有显式设置该参数,就已经找到首要风险。还要检查流水线超时后是否只结束了外层脚本,留下 xcodebuild 或编译子进程继续写目录。

重试成功不能证明问题已解决。竞争窗口缩短后,第二次构建可能恰好通过,但共享写入关系仍然存在。

区分锁冲突与普通编译失败

源码编译错误通常稳定落在同一文件和行号;锁冲突更常随并发时序变化,在解析依赖、生成模块或链接阶段随机出现。若关闭并行任务后连续通过,而恢复并发就失败,应优先审计目录所有权,而不是修改业务代码。

为每个任务分配独立工作区

可靠做法是让仓库检出目录、DerivedData、结果包和临时目录都带任务唯一标识。标识可来自流水线任务号;本地验证时可退化为进程号。

set -euo pipefail

JOB_KEY="${CI_JOB_ID:-local-$$}"
ROOT="${RUNNER_TEMP:-$HOME/ci-work}/$JOB_KEY"
DERIVED_DATA="$ROOT/DerivedData"
RESULT_BUNDLE="$ROOT/TestResults.xcresult"

mkdir -p "$DERIVED_DATA"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Debug \
  -derivedDataPath "$DERIVED_DATA" \
  -resultBundlePath "$RESULT_BUNDLE" \
  build-for-testing

test -d "$RESULT_BUNDLE"

路径必须由任务创建,也只能由该任务删除。不要用项目名作为唯一目录名,因为同一项目的两个分支仍会碰撞。清理脚本也不应执行模糊匹配,例如删除所有名称包含 App 的 DerivedData。

把依赖解析与编译分开

隔离 DerivedData 后,如果多个任务仍同时修改同一个依赖检出目录,问题会转移到包解析阶段。先执行依赖解析,再在正式构建中禁止自动改变依赖版本。仓库应提交经过审核的锁文件,流水线不得静默更新它。

PACKAGES="$ROOT/SourcePackages"

xcodebuild \
  -resolvePackageDependencies \
  -workspace App.xcworkspace \
  -scheme App \
  -clonedSourcePackagesDirPath "$PACKAGES"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -derivedDataPath "$DERIVED_DATA" \
  -clonedSourcePackagesDirPath "$PACKAGES" \
  -disableAutomaticPackageResolution \
  build

需要缓存时,应缓存下载结果或经过验证的不可变快照,而不是让多个任务写同一份活动目录。缓存键至少包含锁文件摘要、Xcode 版本和目标架构。命中缓存后仍要校验锁文件没有变化。

建立可判定的验收表

修复不能只看一次绿色结果。用同一提交同时启动多轮任务,并核对目录、进程和产物边界。

| 检查项 | 通过条件 | 失败时处理 | |---|---|---| | DerivedData 路径 | 每个并行任务均唯一 | 修正任务标识与目录拼接 | | 依赖锁文件 | 构建前后摘要一致 | 禁止自动解析并检查脚本 | | 残留进程 | 任务结束后无所属编译进程 | 修正超时与退出信号处理 | | 结果包 | 每个任务独立生成且可读取 | 分离 `resultBundlePath` | | 清理范围 | 仅删除当前任务根目录 | 移除通配符和共享目录删除 |

建议至少同时运行两条完全相同的构建,再运行一组不同分支的构建。前者验证写入隔离,后者用于发现工作目录、模块缓存或产物路径仍然按项目名共享的问题。

保留证据并安全回收

失败任务不应立刻销毁现场。先归档构建日志、结果包、Xcode 版本、锁文件摘要、磁盘剩余空间和任务路径;源码、环境变量及日志中的令牌必须先脱敏。成功任务可以在确认产物已复制到独立发布目录后清理。

清理最好绑定任务根目录,并增加路径前缀校验。若任务被强制终止,后续回收程序应根据任务标识处理遗留目录,而不是清空整台机器的开发者目录。NUMACS 上运行多条流水线时,同样应在控制台确认当前可选配置,再根据并发数量设置任务上限;更高并发不能替代写入隔离。

最终目标不是让错误“更少出现”,而是建立清晰的不变量:每个并行任务只有一个写入者,依赖版本在构建期间保持不变,失败现场可追踪,清理动作不会影响其他任务。满足这四点后,DerivedData 才能从随机故障源变回可管理的构建数据。

常见问题

每个并行任务都必须使用独立 DerivedData 吗?

是。只要任务可能同时运行,就应通过 -derivedDataPath 指向独立目录;串行任务才适合复用同一路径。

隔离 DerivedData 后还能保留缓存收益吗?

可以。应把依赖下载缓存、预构建产物与任务工作目录分层管理,而不是让多个写入者共享完整 DerivedData。

出现 database is locked 后直接重试是否足够?

不足。重试只能掩盖竞争条件,还可能继续读取被其他任务改写的中间产物;应先隔离目录并确认没有残留构建进程。

NUMACS 云端 Mac

把构建任务放到独享物理工作站

两档 Apple Silicon 配置均为独享物理机、非虚拟机,可按天、周、月或季度租用,并覆盖新加坡、日本东京、韩国首尔与香港节点。

选择设备并下单