Xcode 26 官方要求运行在 macOS Sequoia 15.6 或更高版本上;如果远程 Mac 已满足系统条件,却出现 build database locked,优先怀疑构建任务或路径冲突,而不是先重装 Xcode。(developer.apple.com)
症状:两个 Job 几乎同时执行 xcodebuild,日志末尾出现 build database locked,但最后一行不足以说明真正的占用者。
最快解法:先停止共享同一构建目录的并发任务,再为每个 Job 分配独立的 DerivedData;确认没有残留进程后仍失败,才清理受影响项目的构建数据库,并用单任务 Archive 复验。
这篇文章适合通过 SSH 在远程 Mac 上运行 xcodebuild、断线或重试后遇到数据库锁定的独立开发者,也适合允许多个 Job 同时构建同一项目的小型团队。
如果你负责一台 Mac 上的 Build、Test、Archive 调度,下面的重点是建立证据链,而不是收集一串“万能清缓存”命令。
先把失败现场保存下来:不要只看最后一行
假设脱敏后的工作目录为 /Users/runner/work/App,两个任务分别在 10:14:02 和 10:14:05 启动。你需要保留完整命令、当前目录、Scheme、工作区文件、DerivedData 路径,以及第一个有效错误,而不是只复制:
error: build database is locked
建议把现场整理成下面这种记录:
Job A
start: 10:14:02
cwd: /Users/runner/work/App
command: xcodebuild -workspace Redacted.xcworkspace \
-scheme RedactedApp \
-configuration Release build \
-derivedDataPath /Users/runner/build/shared-derived
Job B
start: 10:14:05
cwd: /Users/runner/work/App
command: xcodebuild -workspace Redacted.xcworkspace \
-scheme RedactedApp \
-configuration Release test \
-derivedDataPath /Users/runner/build/shared-derived
同时标明是哪个进程发起了 Build、Test、Archive,是否由 Run Script、发布脚本或依赖项目再次调用 xcodebuild。Xcode 的构建系统会根据 Scheme、Target Dependencies 和构建设置生成任务图,并在依赖允许时并行执行;因此“看到并行”本身不是错误,真正危险的是多个独立 Job 共享同一个可写目录。(developer.apple.com)
⚠️ 不要一开始执行
rm -rf ~/Library/Developer/Xcode/DerivedData/*。这会抹掉其他项目的诊断线索,也可能让你无法判断问题究竟来自并发、路径还是异常中断。
第一步:确认是不是两个进程正在争用同一数据库
在远程 Mac 上重新登录后,先列出相关进程:
pgrep -alf 'xcodebuild|xcbuild|xctest|swiftc'
再查看进程启动时间、父进程和完整参数:
ps -axo pid,ppid,lstart,command | \
grep -E 'xcodebuild|xcbuild|xctest' | grep -v grep
你要找的不是“有没有 Xcode 进程”,而是以下关系:
- 同一项目、同一 Scheme,两个 Job 同时运行;
- 一个旧的
xcodebuild仍由 SSH 会话、Runner 或后台脚本托管; - CI 重试已启动新任务,但旧任务没有收到取消信号;
- 一个 Archive 正在写入,另一个 Test 或 Build 使用了相同的中间目录;
- 不同系统用户分别启动了任务,却把输出指向同一路径。
不要直接使用会终止所有 Xcode 相关任务的命令。先根据 PPID 和启动时间判断哪个 Job 应保留,再让无效任务正常退出;只有在明确确认进程已失去控制、且不会再写入 xcresult、xcarchive 或上传目录时,才按父子关系终止。
终止顺序也有实际影响:正在运行的 Test 可能只留下不完整的 .xcresult,Archive 可能没有完成 Info.plist 或 dSYM 写入,上传脚本则可能已经拿到一个不完整文件。保留任务应继续观察日志,失效任务先尝试发送正常终止信号,等待其子进程退出后再处理目录。
第二步:核对 DerivedData、OBJROOT、SYMROOT 是否真的分开
很多 CI 配置表面上使用了不同的 Job 名称,实际却解析到相同路径。-derivedDataPath 只解决显式传入的 DerivedData;项目或脚本仍可能通过 OBJROOT、SYMROOT、CONFIGURATION_BUILD_DIR 或自定义变量写入共享位置。Apple 的 Build Settings 文档将 OBJROOT 定义为中间文件路径,并将 SYMROOT、CONFIGURATION_BUILD_DIR 用于构建产物位置。(developer.apple.com)
先让 xcodebuild 输出最终解析后的设置:
xcodebuild \
-workspace Redacted.xcworkspace \
-scheme RedactedApp \
-showBuildSettings \
| egrep 'DERIVED|OBJROOT|SYMROOT|CONFIGURATION_BUILD_DIR|ARCHIVE_PATH'
如果使用 .xcconfig、Shell 变量或 CI 模板,还要检查:
env | egrep 'BUILD|DERIVED|OBJROOT|SYMROOT|ARCHIVE|RESULT'
grep -R --line-number -E 'derivedDataPath|OBJROOT|SYMROOT|archivePath|resultBundlePath' .
每个 Job 应至少具备以下隔离:
JOB_ROOT="/Users/runner/build/${CI_JOB_ID}"
DERIVED="$JOB_ROOT/DerivedData"
RESULT="$JOB_ROOT/Results/App.xcresult"
ARCHIVE="$JOB_ROOT/Archives/App.xcarchive"
mkdir -p "$JOB_ROOT" "$DERIVED" "$JOB_ROOT/Results" "$JOB_ROOT/Archives"
xcodebuild \
-workspace Redacted.xcworkspace \
-scheme RedactedApp \
-configuration Release \
build \
-derivedDataPath "$DERIVED" \
-resultBundlePath "$RESULT"
源码工作区可以只读共享,最终 Artifacts 也可以由流水线统一收集,但中间产物不应共享。DerivedData、对象文件、模块缓存和构建数据库属于可重新生成内容;xcresult、xcarchive、dSYM、签名后的导出包和上传日志则应按 Job 单独保存。
第三步:排除 Run Script 或依赖项目触发的嵌套构建
如果进程列表只显示一个主 xcodebuild,仍可能存在嵌套构建。常见来源包括:
- Run Script 内部再次执行
xcodebuild build; - 发布脚本为了生成另一个产品,复用了主 Job 的 DerivedData;
- 跨项目依赖没有被声明为 Target Dependency,而是由脚本手工触发;
- 包构建工具或自定义生成器启动了第二个构建命令;
- 脚本没有声明输入、输出,导致 Xcode 每次都运行并重复写入文件。
Apple 要求 Run Script 提供输入和输出文件信息,以便构建系统安排任务顺序;缺少这些声明时,脚本可能每次构建都执行,并引入不正确的构建行为。(developer.apple.com)
你可以在项目文件和脚本中搜索:
grep -R --line-number -E 'xcodebuild|xcrun|archivePath|derivedDataPath' \
--exclude-dir=.git .
修改前先写清楚三个边界:
- 主
xcodebuild负责哪些 Target 和产品; - 脚本只能读取哪些输入,生成哪些输出;
- 如果子构建失败,主任务如何回退、日志保存在哪里。
不要把“关闭并行构建”作为默认修复。Scheme 的 Dependency Order 本身支持按照依赖关系并行构建,Manual Order 反而会让任务串行化并降低资源利用率。(developer.apple.com) 只有当日志证明项目的依赖声明错误、脚本输出无法准确描述,且短期必须降低风险时,才可临时限制并行;长期仍应修正调用边界。
没有活跃进程后,怎样逐级清理残留状态?
只有在确认相关 xcodebuild、测试进程和嵌套脚本均已退出后,才进入清理阶段。可以用下面的检查确认目标路径是否仍被打开:
lsof +D "/Users/runner/build/失效Job/DerivedData" 2>/dev/null
清理范围应从小到大:
- 只删除失效 Job 的临时 DerivedData;
- 仍失败时,删除该项目专属的构建数据库或项目构建目录;
- 重新创建 Job 工作区,并重复同一条
xcodebuild命令; - 最后才考虑用户级缓存,而且必须先保存诊断资料。
保留这些内容:
- 源码和提交版本;
- 证书、Provisioning Profile 等签名资产;
- 已完成的
xcarchive; - dSYM;
.xcresult;- 完整标准输出、错误输出和进程时间线。
删除 DerivedData 不等于删除已经独立保存的 Archive 或签名资产,但如果你的 Archive 仍放在 DerivedData 下面,或者脚本用临时目录保存 dSYM,清理就可能带来不可逆损失。Archive 是包含调试信息的构建包,Apple 的分发流程要求先创建 Archive,再使用 xcodebuild exportArchive 导出分发包。(developer.apple.com)
独立构建路径的 CI 写法,应该通过什么标准?
不要只验证“命令返回成功”。两个并行 Job 应分别生成自己的结果包,并且在删除其中一个 Job 的临时目录后,另一个 Job 的结果仍可读取。
测试阶段可以显式输出独立的 .xcresult:
xcodebuild \
-workspace Redacted.xcworkspace \
-scheme RedactedApp \
test \
-derivedDataPath "$DERIVED" \
-resultBundlePath "$RESULT"
Apple 文档说明,xcodebuild test 会生成包含测试会话、覆盖率和日志的 .xcresult;将它放在 Job 专属目录,才能在并行和失败重试时追踪真实结果。(developer.apple.com)
Archive 阶段则使用独立的归档路径:
xcodebuild \
-workspace Redacted.xcworkspace \
-scheme RedactedApp \
-configuration Release \
-destination "generic/platform=iOS" \
archive \
-derivedDataPath "$DERIVED" \
-archivePath "$ARCHIVE"
验证 Archive 时,不要把“生成了目录”当成通过。至少检查归档结构、产品文件、签名信息、dSYM 和日志是否完整,再进入导出或上传。对于持续集成,还应固定 Package.resolved,并按 Apple 的 CI 建议考虑使用 -disableAutomaticPackageResolution,避免构建过程中出现与数据库锁定无关的依赖变化。(developer.apple.com)
独立路径与共享路径,哪种方案适合你的远程 Mac?
| 决策维度 | 多个 Job 共用路径 | 每个 Job 独立路径 |
|---|---|---|
| DerivedData | ❌ 容易互相写入构建数据库和中间文件 | ✅ 通过 Job ID 隔离 |
| 并行 Build / Test | ❌ 锁冲突和结果覆盖风险高 | ✅ 可并发,问题更容易归属 |
| 磁盘占用 | ✅ 可能较低,但依赖共享缓存 | ⚠️ 需要为每个任务预留临时空间 |
| 失败重试 | ❌ 旧状态容易污染新任务 | ✅ 可删除单个 Job 后重跑 |
| Archive 与 dSYM | ⚠️ 容易被脚本覆盖或误清理 | ✅ 可按版本和 Job 独立留存 |
| 适用场景 | 单任务、严格串行且有锁机制 | CI 并行、SSH 断线恢复、多人共用主机 |
如果你只有一台远程 Mac,路径隔离仍然是最低要求;但它不能替代进程控制。主机需要能识别取消的 Job、结束孤儿进程,并在重启后重新创建目录。Apple 对构建设置的解析还存在层级继承,命令行传入的设置优先级最高,因此排查时应以 xcodebuild -showBuildSettings 的最终值为准,而不是只看 Xcode 图形界面的某个字段。(developer.apple.com)
| 验收阶段 | 执行动作 | 通过标准 | 未通过时的动作 |
|---|---|---|---|
| 单任务 Build | 使用独立 -derivedDataPath 执行 Build |
无锁错误,日志完整 | 检查路径解析和残留进程 |
| 并行 Test | 启动两个不同 Job,各自输出 .xcresult |
两个结果包均可打开且不互相覆盖 | 检查共享的 OBJROOT、脚本和测试目录 |
| 真实 Archive | 只保留一个 Archive 任务 | xcarchive、dSYM 和签名信息完整 |
回退到项目专属清理,不清全局缓存 |
| SSH 断线重连 | 断开会话后重新登录并检查进程 | Job 状态可判断,日志仍持续保存 | 增加进程托管和取消逻辑 |
| 主机重启恢复 | 重启后重新创建工作目录并重跑 | 新 Job 不读取失效 Job 的中间目录 | 拆分 Runner 或更换独立构建环境 |
FAQ:几个容易误判的锁定场景
Xcode 26 build database locked 通常是哪里发生冲突?
最常见的排查方向不是缓存本身,而是两个 xcodebuild 进程同时写入同一个 DerivedData、OBJROOT 或 Build Products 目录。SSH 断线后旧任务仍在后台运行、CI 重试未取消,以及脚本再次启动构建,也会造成相同现象。
两个 xcodebuild 任务能不能共用同一个 DerivedData?
不建议把并行 Job 的 DerivedData 设为同一路径。即使源码和 Scheme 相同,两个任务仍可能同时更新构建数据库、中间文件和模块缓存。正确做法是按 Job ID 分配独立的 -derivedDataPath,同时隔离 xcresult 和 xcarchive。
SSH 断线后怎样确认旧的 Xcode 构建还在运行?
重新登录远程 Mac 后,先用 pgrep、ps 和 lsof 查看 xcodebuild、测试进程及其父子关系,不要直接执行会杀掉所有 Xcode 任务的命令。确认旧任务属于已取消或已失效的 Job 后,再按进程树逐级终止。
CI 并行构建怎样设置独立的 derivedDataPath?
在流水线变量中生成每个 Job 唯一的工作目录,例如 $RUNNER_TEMP/ios-build/$CI_JOB_ID,并在每次 Build、Test、Archive 命令中显式传入 -derivedDataPath。Archive、xcresult 和日志也应使用独立文件名,避免只隔离中间目录。
删除 DerivedData 会不会影响 Archive 和签名文件?
删除受影响 Job 的 DerivedData 通常不会直接删除源码、证书、Provisioning Profile 或已经保存到其他位置的 xcarchive,但它会移除中间产物。清理前必须确认签名资产、Archive、dSYM 和 xcresult 已独立留存,并用同一命令重新验证。
结论:把锁错误当成调度问题处理
Xcode 26 build database locked 的可靠修复顺序是:保存完整失败现场,确认并发进程,核对最终解析路径,排除隐藏嵌套构建,确认没有残留任务后再局部清理,最后依次通过单任务 Build、并行 Test 和真实 Archive。
如果你需要继续完善远程 Mac 的持续构建流程,可以先参考 远程 Mac 使用场景,再结合 Xcode 安装与远程环境排查文章检查系统版本、权限和开发工具链。对于需要长期运行 SSH 构建、断线恢复和独立工作目录的团队,也可以查看 KVMFLUX 的 Mac 方案与套餐。
你现有环境如果能完成“并行 Job、SSH 断线重连、主机重启恢复”三项测试,就不必因为一次锁错误立即更换方案;但如果它无法提供独立工作目录、完整进程控制或稳定常驻运行,继续把所有 Build、Test 和 Archive 挤在同一台临时主机上,维护成本会持续转化为失败重试和人工清理。此时,使用 KVMFLUX 的远程 Mac 作为专用构建环境,至少能把主机独占、持续在线和路径隔离纳入部署设计;是否租用,仍应以你的并发量、长期运行时间和是否需要物理设备接口来决定。