团队把工程检出到云端 Mac 后,最容易被低估的故障不是编译错误,而是流水线根本找不到预期的 Scheme,或者同名 Scheme 在远程环境里执行了不同的目标。本地 Xcode 会读取开发者目录中的用户配置,干净的 CI 工作区却只认识版本库里的文件。要让构建入口稳定,Scheme 必须像脚本和依赖锁文件一样接受版本控制与验收。
先把 Scheme 当成 CI 接口
一个可供流水线调用的 Scheme 至少定义四件事:构建哪些 Target、使用哪套 Build Configuration、测试哪些 Test Bundle,以及 Archive 时采用什么配置。只在 Xcode 菜单里看见名称,不代表这些信息已经提交。
先检查共享文件是否存在:
find . \( -path "*/xcshareddata/xcschemes/*.xcscheme" \
-o -path "*/xcuserdata/*/xcschemes/*.xcscheme" \) -print
位于 xcuserdata 的 Scheme 属于用户配置,不能作为 CI 入口。应在 Xcode 的 Manage Schemes 中启用 Shared,然后确认文件出现在以下任一路径:
App.xcodeproj/xcshareddata/xcschemes/App.xcscheme
App.xcworkspace/xcshareddata/xcschemes/App.xcscheme
再用 git status 和 git check-ignore -v 确认文件已被跟踪,且没有被过宽的 *.xcuser* 或 xcshareddata 忽略规则误伤。
“本地可见”只说明当前用户目录里有配置;“CI 可用”必须由干净检出目录中的命令行结果证明。
固定 Workspace、Scheme 与配置
流水线不应依赖 Xcode 自动选择工程。存在 CocoaPods、聚合工程或本地 Package 时,打开 .xcodeproj 与 .xcworkspace 的结果可能不同。把三个入口值显式写进 CI 变量:
WORKSPACE="App.xcworkspace"
SCHEME="App"
CONFIGURATION="Release"
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-list
-list 的输出应包含预期 Scheme。随后用 JSON 输出做机器校验,避免只靠人工阅读日志:
xcodebuild -workspace "$WORKSPACE" -list -json > xcode-list.json
python3 - <<'PY'
import json
data = json.load(open("xcode-list.json"))
schemes = data.get("workspace", {}).get("schemes", [])
if "App" not in schemes:
raise SystemExit("Required Xcode scheme is missing")
PY
如果仓库同时保留开发、预发布和生产入口,名称要表达用途,例如 App-Dev、App-Staging、App-Release。不要让脚本通过列表顺序选择第一个 Scheme;新增依赖后,顺序可能改变。
逐项验收 Build、Test 与 Archive
共享成功只解决“看得见”,还没有证明动作正确。建议把验收拆成三层。
检查解析后的构建设置
先导出最终设置,重点核对 PRODUCT_BUNDLE_IDENTIFIER、CONFIGURATION、SDKROOT、SUPPORTED_PLATFORMS 与产物目录:
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-showBuildSettings > build-settings.txt
grep -E "PRODUCT_BUNDLE_IDENTIFIER|CONFIGURATION =|SDKROOT|SUPPORTED_PLATFORMS" \
build-settings.txt
这里读取的是 Scheme、工程配置和命令行参数合并后的结果,比直接检查 project.pbxproj 更接近真实执行。
验证测试入口
测试阶段先构建可测试产物,再执行测试,可以区分“编译入口坏了”和“测试运行失败”:
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-destination "platform=iOS Simulator,name=iPhone 16" \
build-for-testing
模拟器名称必须来自当前机器的 xcrun simctl list devices available,不要把个人电脑上独有的设备写死。若团队需要固定运行时,应同时记录 Xcode 版本与目标 OS 版本。
单独检查归档动作
Archive 可能使用与 Run、Test 不同的配置。先确认 .xcscheme 中 ArchiveAction 的配置,再在受控任务里执行真实归档:
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration Release \
-destination "generic/platform=iOS" \
-archivePath "$PWD/artifacts/App.xcarchive" \
archive
不要用普通 build 成功替代 Archive 验收。资源处理、脚本阶段和归档设置往往只在此处暴露问题。
清除本地状态再做第二次验证
最可靠的检查不是删除 DerivedData,而是在新目录重新克隆。这样能同时暴露未提交 Scheme、本地软链接、未跟踪配置和隐式依赖:
ROOT="$(mktemp -d)"
git clone --no-local . "$ROOT/repo"
cd "$ROOT/repo"
git submodule update --init --recursive
xcodebuild -workspace App.xcworkspace -scheme App -list
若工程依赖生成步骤,应把生成命令放在构建前,并检查生成结果是否稳定。不要把“先用 Xcode 打开一次工程”写进操作手册;这类步骤无法成为无人值守任务,也会掩盖缺失文件。
干净验证至少应覆盖:
| 检查项 | 通过标准 |
|---|---|
| Scheme 可见性 | xcodebuild -list -json 返回指定名称 |
| 文件归属 | Scheme 位于 xcshareddata 且已被 Git 跟踪 |
| 构建配置 | 命令行解析结果与流水线预期一致 |
| 测试入口 | build-for-testing 能生成测试产物 |
| 归档入口 | 通用 iOS 目标可生成 .xcarchive |
| 无交互执行 | 不需要打开 Xcode 或读取个人目录 |
把入口漂移阻断在合并前
Scheme 是 XML 文件,合并冲突常被当作普通文本冲突处理,结果可能保留名称却丢失 Testable 或 BuildActionEntry。可在合并请求中加入轻量检查:验证共享文件存在、禁止提交 xcuserdata,并执行 xcodebuild -list -json。
还要关注 Scheme 的 Pre-actions 与 Post-actions。脚本若引用绝对路径、交互式 Shell 配置或只在某位开发者机器存在的工具,远程执行仍会失败。脚本应从 SRCROOT 等构建变量定位文件,并在缺少依赖时明确退出。
最终的验收标准很简单:从空目录取得仓库,只提供文档中声明的 Xcode 版本和命令,就能枚举 Scheme、解析设置、构建测试产物并完成归档。达到这一点后,G-Mini 上的云端 Mac 才是在执行版本库定义的入口,而不是复现某台开发者电脑的偶然状态。
常见问题
为什么 Xcode 本地能运行,云端 Mac CI 却提示找不到 Scheme?
最常见原因是 Scheme 仍保存在 xcuserdata 中,没有写入 xcshareddata/xcschemes 并提交到版本库。CI 使用全新检出目录,不会取得开发者电脑上的用户级配置。
共享 Scheme 后是否还需要单独验证 Archive 动作?
需要。共享只解决 Scheme 可见性,不保证 Archive 使用了正确的 Release 配置、目标和前置脚本。应在干净检出目录中执行 showBuildSettings,并至少完成一次真实归档验收。
为下一条构建队列准备云端 Mac
比较两档 M4 配置与五个节点,按实际工作周期选择日、周、月或季租用。