工程指南

云端 Mac CI 的 Xcode Scheme 共享与入口验收

云端 Mac CI 的 Xcode Scheme 共享与入口验收

团队把工程检出到云端 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 statusgit 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-DevApp-StagingApp-Release。不要让脚本通过列表顺序选择第一个 Scheme;新增依赖后,顺序可能改变。

逐项验收 Build、Test 与 Archive

共享成功只解决“看得见”,还没有证明动作正确。建议把验收拆成三层。

检查解析后的构建设置

先导出最终设置,重点核对 PRODUCT_BUNDLE_IDENTIFIERCONFIGURATIONSDKROOTSUPPORTED_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 配置与五个节点,按实际工作周期选择日、周、月或季租用。

选择云端 Mac 方案