远程云端 Mac 上的 Xcode 构建突然出现 Cycle inside ... 或 Multiple commands produce ...,最容易做错的第一步是立刻删除 DerivedData。这样可能暂时改变报错顺序,却会同时抹掉定位线索。更可靠的做法是固定代码、Scheme、配置和 Xcode 路径,保存完整日志,再判断问题属于 Target 依赖循环,还是多个 Build Phase 争用同一输出路径。
先把故障稳定复现下来
开始排查前暂停自动拉取代码、依赖升级和并行构建,避免工作区在两次实验之间变化。记录当前提交、Xcode 版本与实际选择的开发者目录:
set -o pipefail
git rev-parse HEAD
xcodebuild -version
xcode-select -p
mkdir -p .diagnostics
xcodebuild \
-project App.xcodeproj \
-scheme App \
-configuration Debug \
-destination 'generic/platform=iOS Simulator' \
-showBuildTimingSummary \
build 2>&1 | tee .diagnostics/build.log
若工程使用 Workspace,将 -project App.xcodeproj 换成 -workspace App.xcworkspace。不要同时传入两者。随后提取关键位置,但保留原始日志供上下文核对:
grep -nE \
'Cycle inside|cycle in dependencies|Multiple commands produce|will be run during every build' \
.diagnostics/build.log
日志中的最后一条命令通常只是发现冲突的位置,不一定是制造循环的起点。应沿 Xcode 输出的依赖链向上查找第一个重复 Target、脚本或产物路径。
区分两类看起来相似的错误
依赖循环表示 A 必须等待 B,而 B 又直接或间接等待 A。重复产物则表示两个命令都宣称负责同一个文件。两者可能同时出现,但修复方式不同。
| 日志特征 | 优先检查 | 常见根因 |
|---|---|---|
Cycle inside |
Target Dependencies、隐式依赖 | App 与 Framework 相互依赖 |
Multiple commands produce |
Copy Files、Compile Sources、Run Script | 同一文件被复制或生成两次 |
| 脚本每次都运行 | Run Script 输入输出 | 未声明依赖,或关闭依赖分析 |
| Archive 才失败 | Embed 阶段、归档脚本 | Debug 与 Release 阶段配置不一致 |
先用命令确认本次实际构建的 Target 和配置,而不是凭 Xcode 界面中的当前选项猜测:
xcodebuild -list -json -project App.xcodeproj \
> .diagnostics/project-list.json
xcodebuild -showBuildSettings \
-project App.xcodeproj \
-scheme App \
-configuration Debug \
> .diagnostics/build-settings.txt
沿 Target 与 Build Phase 追依赖链
在 Xcode 的 Target Dependencies 中,从报错链首尾涉及的 Target 开始检查。App 可以依赖 Framework,但 Framework 不应为了访问 App 中的类型再依赖 App。共享代码应下沉到独立模块,或者通过协议和注入解除反向引用。
接着逐个查看 Build Phases,重点核对以下关系:
检查重复归属
同一个源文件不应同时出现在两个会生成相同目标文件的 Compile Sources 项中。同一个 Framework 也不应既由系统生成的嵌入阶段处理,又被自定义 Copy Files 再复制一次。对于代码生成器,要明确生成目录,避免把输出写回已由其他 Target 编译的源码目录。
检查隐式依赖
如果脚本通过固定路径读取另一个 Target 的构建产物,却没有显式声明依赖,构建顺序可能随并行度变化。不要用关闭并行构建来掩盖问题;它只是让错误较难触发,并没有修复图结构。
给 Run Script 写清输入与输出
Run Script 是循环高发区。脚本读取配置文件、生成 Swift 文件或复制资源时,应在阶段中填写 Input Files 与 Output Files,或者使用文件列表。脚本本身还要保证输出由单一阶段负责。
例如,生成版本文件的脚本可以先验证必要变量,并采用临时文件加原子替换:
set -euo pipefail
input="${SRCROOT}/Config/version.txt"
output="${DERIVED_FILE_DIR}/GeneratedVersion.swift"
tmp="${output}.tmp"
test -f "$input"
mkdir -p "$(dirname "$output")"
version="$(tr -d '
' < "$input")"
printf 'enum GeneratedVersion { static let value = "%s" }
' \
"$version" > "$tmp"
if test -f "$output" && cmp -s "$tmp" "$output"; then
rm "$tmp"
else
mv "$tmp" "$output"
fi
对应阶段应把 $(SRCROOT)/Config/version.txt 声明为输入,把 $(DERIVED_FILE_DIR)/GeneratedVersion.swift 声明为输出。生成文件放在派生目录,比直接修改仓库文件更容易确定所有权,也能减少并行任务互相覆盖。
用最小改动完成验收
每次只改一条依赖、一个阶段或一个输出路径,然后重新保存日志。一次删除多个引用虽然可能让构建通过,却很难确认真正根因,也可能漏掉归档配置中的同类问题。
修复后按以下顺序验收:
- 执行一次干净构建,确认完整图可以从零建立。
- 不清理地连续构建两次,确认脚本能正确跳过未变化输入。
- 分别检查 Debug 与 Release;发布工程还应执行一次 Archive。
- 比较两次日志,确认没有新的重复输出和无条件脚本警告。
- 将 Target 依赖调整、阶段输入输出及原因写入代码评审说明。
清理 DerivedData 可以作为最后的隔离实验,但不能作为修复结论。真正稳定的 Xcode 工程应在不同并行度、干净构建与增量构建下得到同一依赖顺序;这样云端 Mac 的交互式开发与无人值守任务才不会因调度变化随机失败。
常见问题
删除 DerivedData 能修复 Xcode 构建循环吗?
通常不能。删除 DerivedData 只能排除旧中间产物影响,Target 依赖、脚本阶段和重复输出造成的构建图循环仍会再次出现。
Run Script 阶段为什么容易造成构建图问题?
脚本若读取其他阶段的产物却没有声明输入,或与 Copy Files 阶段写入同一路径,Xcode 就无法建立稳定的先后关系。
修复后应该如何验收?
至少执行一次干净构建和两次不清理的连续构建,并核对日志中不再出现循环、重复输出及每次都无条件运行的脚本警告。
把下一项远程 Mac 任务放到独享节点运行
按任务规模选择两档在售配置、五个节点与日、周、月、季周期。下单前可完整核对配置和 USD 金额。