多人同时修改 Xcode 工程时,最危险的失败往往不是编译错误,而是 project.pbxproj 已经损坏却仍能通过文本合并。某个文件引用被删掉、Scheme 没有共享,或者关键构建设置被界面操作悄悄改写,问题可能直到归档阶段才暴露。更稳妥的做法,是在云端 Mac 上把工程文件视为独立的构建输入,先做一轮成本较低的一致性检查,再启动依赖解析、测试和归档。
门禁要检查什么
一套实用门禁至少覆盖四层,而且顺序不能颠倒:先检查文本残留,再验证文件格式,然后让 Xcode 真正解析工程,最后比较关键设置。前一层失败时立即退出,不必继续消耗构建时间。
| 层级 | 检查对象 | 失败意味着什么 |
|---|---|---|
| 文本 | 冲突标记、空文件 | 合并尚未完成 |
| 结构 | project.pbxproj |
plist 结构不可解析 |
| 工程 | Project、Target、Scheme | Xcode 无法建立工程模型 |
| 配置 | SDK、部署版本、签名方式 | 设置出现非预期漂移 |
git diff没有冲突,不等于 Xcode 工程有效。版本控制工具只确认文本合并完成,并不知道对象引用、Target 或 Scheme 是否还能被 Xcode 识别。
检查脚本应固定运行目录和 Xcode 路径,不要依赖交互式 Shell 的当前状态。工程中如果同时存在 .xcodeproj 与 .xcworkspace,日常构建应以实际使用的入口为准,但底层 project.pbxproj 仍需单独检查。
先拦截合并残留与格式错误
可以把下面脚本保存为 ci/check_xcode_project.sh。示例假设工程名为 App.xcodeproj,实际使用时通过环境变量覆盖,避免把工程名散落在多个 CI 配置中。
#!/bin/bash
set -euo pipefail
PROJECT_PATH="${PROJECT_PATH:-App.xcodeproj}"
PBXPROJ="${PROJECT_PATH}/project.pbxproj"
test -s "$PBXPROJ" || {
echo "project.pbxproj is missing or empty"
exit 1
}
if grep -nE '^(<<<<<<<|=======|>>>>>>>)' "$PBXPROJ"; then
echo "merge conflict markers found"
exit 1
fi
plutil -lint "$PBXPROJ"
xcodebuild -list -json -project "$PROJECT_PATH" > /tmp/xcode-project-list.json
plutil -lint /tmp/xcode-project-list.json
冲突标记使用行首匹配,避免业务文件名或注释里偶然出现一串等号而误报。plutil 通过后,再调用 xcodebuild -list,因为只有后者会建立 Xcode 工程模型。命令退出非零时,应保留标准错误输出,而不是自动执行“修复”或重新生成工程文件。
Workspace 项目的处理
使用 Workspace 时,再增加一次入口验证:
WORKSPACE_PATH="${WORKSPACE_PATH:-App.xcworkspace}"
xcodebuild -list -json -workspace "$WORKSPACE_PATH" \
> /tmp/xcode-workspace-list.json
plutil -lint /tmp/xcode-workspace-list.json
不要只验证 Workspace 而跳过底层工程。Workspace 能被识别,并不保证其中每个项目文件都没有合并残留。
验证共享 Scheme 与目标集合
CI 需要的 Scheme 必须进入版本控制。可先检查共享 Scheme 文件,再从 xcodebuild -list 的 JSON 输出中确认名称。系统自带的 Python 足以完成解析,不需要额外安装依赖。
SCHEME_NAME="${SCHEME_NAME:-App}"
SCHEME_FILE="${PROJECT_PATH}/xcshareddata/xcschemes/${SCHEME_NAME}.xcscheme"
test -s "$SCHEME_FILE" || {
echo "shared scheme is missing: $SCHEME_NAME"
exit 1
}
python3 - "$SCHEME_NAME" /tmp/xcode-project-list.json <<'PY'
import json
import sys
expected = sys.argv[1]
path = sys.argv[2]
with open(path, encoding="utf-8") as handle:
payload = json.load(handle)
schemes = payload.get("project", {}).get("schemes", [])
if expected not in schemes:
raise SystemExit(f"expected scheme not found: {expected}")
PY
若仓库包含多个应用或扩展,应维护明确的 Scheme 清单,而不是接受“至少存在一个 Scheme”。同时不要把个人目录下的 Scheme 当作 CI 输入;换一台物理节点后,这些未共享文件不会出现。
给关键构建设置建立快照
工程可以成功解析,配置仍可能被误改。建议只选择会改变产物含义的字段建立快照,例如 PRODUCT_BUNDLE_IDENTIFIER、IPHONEOS_DEPLOYMENT_TARGET、SWIFT_VERSION、CODE_SIGN_STYLE 与 SUPPORTED_PLATFORMS。不要保存完整的 -showBuildSettings 输出,其中包含路径和临时目录,跨节点比较会产生大量噪声。
xcodebuild -project "$PROJECT_PATH" \
-scheme "$SCHEME_NAME" \
-configuration Release \
-showBuildSettings |
awk -F ' = ' '
/PRODUCT_BUNDLE_IDENTIFIER =/ ||
/IPHONEOS_DEPLOYMENT_TARGET =/ ||
/SWIFT_VERSION =/ ||
/CODE_SIGN_STYLE =/ ||
/SUPPORTED_PLATFORMS =/ {
gsub(/^[ ]+/, "", $1)
print $1 " = " $2
}
' | LC_ALL=C sort > /tmp/build-settings.current
diff -u ci/build-settings.release /tmp/build-settings.current
首次建立 ci/build-settings.release 后应纳入版本控制。有意调整部署版本或签名方式时,先审阅设置差异,再在同一个变更中更新快照。禁止让 CI 自动覆盖基线,否则任何漂移都会被当成新标准。
接入流水线并处理误报
把一致性门禁放在依赖下载和完整构建之前,并为脚本设置可读的失败阶段。推荐顺序是:检出代码、选择固定 Xcode、运行工程门禁、解析依赖、编译、测试、归档。脚本在本地与云端 Mac 上应使用同一入口,例如:
DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer" \
PROJECT_PATH="App.xcodeproj" \
SCHEME_NAME="App" \
bash ci/check_xcode_project.sh
常见误报来自三个地方。第一,开发者修改了 Scheme 却没有共享;解决办法是提交 xcshareddata/xcschemes,不是在节点上手工创建。第二,快照包含绝对路径;应缩小字段集合。第三,不同任务使用了不同 Xcode 路径;应在门禁开始前输出并核对 xcodebuild -version,同时固定 DEVELOPER_DIR。
当检查失败时,工单或构建记录至少保留提交标识、Xcode 版本、失败命令、标准错误和工程入口。不要上传包含敏感值的完整环境变量。若需要更换物理节点,应在控制台确认当前可选配置,并让新节点从同一仓库重新执行门禁,而不是复制旧节点的临时工程状态。
合并前检查清单
提交门禁前,逐项确认:脚本启用了 set -euo pipefail;工程路径和 Scheme 可通过环境变量覆盖;冲突标记检查只扫描工程文件;Project 与 Workspace 按实际入口分别解析;共享 Scheme 已纳入版本控制;设置快照只保留稳定字段;任何基线更新都经过人工审阅。
这套检查不能替代编译和测试,但能把一类原本在归档阶段出现的问题提前到数秒级步骤。工程文件一旦成为显式、可审阅、可重复执行的输入,团队就不必依赖“某位开发者本地还能打开工程”来判断主分支是否健康。
常见问题
只运行 plutil 就能确认 Xcode 工程可用吗?
不能。plutil 只能发现 project.pbxproj 的结构或语法问题,还应运行 xcodebuild -list 验证 Xcode 能否解析工程,并检查预期 Scheme 是否存在。
工程一致性检查应该在构建前还是合并后执行?
应在每个合并请求中先于完整构建执行,并在主分支构建前再次执行。这样能用较低成本阻止损坏工程进入共享分支。
如何处理团队有意修改的构建设置?
先审阅变更,再更新受版本控制的设置快照。不要自动覆盖基线,否则门禁会失去区分预期变更与意外漂移的能力。
把下一次构建放到独立物理节点上。
选择芯片、内存、存储、节点与租期,获得不与其他客户共享资源的云端 Mac。