同一台云端 Mac 同时跑两条 Xcode 流水线时,偶发的 database is locked、索引数据库损坏或“本地无法复现”的链接错误,往往不是代码变化造成的。先检查两个任务是否共用了默认的 ~/Library/Developer/Xcode/DerivedData。这个目录不只是缓存,还包含构建数据库、索引、模块缓存和任务持续改写的中间产物;把它当成可并发读写的公共缓存,失败只是时间问题。
先确认冲突来自哪里
不要看到失败就立即删除全部缓存。先保存失败任务的完整 xcodebuild 命令、开始时间、工作目录和进程列表,再搜索日志中的 locked、database、unable to attach、malformed 与重复产物提示。
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 版本和目标架构。命中缓存后仍要校验锁文件没有变化。
建立可判定的验收表
修复不能只看一次绿色结果。用同一提交同时启动多轮任务,并核对目录、进程和产物边界。
建议至少同时运行两条完全相同的构建,再运行一组不同分支的构建。前者验证写入隔离,后者用于发现工作目录、模块缓存或产物路径仍然按项目名共享的问题。
保留证据并安全回收
失败任务不应立刻销毁现场。先归档构建日志、结果包、Xcode 版本、锁文件摘要、磁盘剩余空间和任务路径;源码、环境变量及日志中的令牌必须先脱敏。成功任务可以在确认产物已复制到独立发布目录后清理。
清理最好绑定任务根目录,并增加路径前缀校验。若任务被强制终止,后续回收程序应根据任务标识处理遗留目录,而不是清空整台机器的开发者目录。NUMACS 上运行多条流水线时,同样应在控制台确认当前可选配置,再根据并发数量设置任务上限;更高并发不能替代写入隔离。
最终目标不是让错误“更少出现”,而是建立清晰的不变量:每个并行任务只有一个写入者,依赖版本在构建期间保持不变,失败现场可追踪,清理动作不会影响其他任务。满足这四点后,DerivedData 才能从随机故障源变回可管理的构建数据。
常见问题
每个并行任务都必须使用独立 DerivedData 吗?
是。只要任务可能同时运行,就应通过 -derivedDataPath 指向独立目录;串行任务才适合复用同一路径。
隔离 DerivedData 后还能保留缓存收益吗?
可以。应把依赖下载缓存、预构建产物与任务工作目录分层管理,而不是让多个写入者共享完整 DerivedData。
出现 database is locked 后直接重试是否足够?
不足。重试只能掩盖竞争条件,还可能继续读取被其他任务改写的中间产物;应先隔离目录并确认没有残留构建进程。
NUMACS 云端 Mac
把构建任务放到独享物理工作站
两档 Apple Silicon 配置均为独享物理机、非虚拟机,可按天、周、月或季度租用,并覆盖新加坡、日本东京、韩国首尔与香港节点。