工程实践

云端 Mac 的 Xcode 工程文件一致性门禁实战

云端 Mac 的 Xcode 工程文件一致性门禁实战

多人同时修改 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_IDENTIFIERIPHONEOS_DEPLOYMENT_TARGETSWIFT_VERSIONCODE_SIGN_STYLESUPPORTED_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 mini

把下一次构建放到独立物理节点上。

选择芯片、内存、存储、节点与租期,获得不与其他客户共享资源的云端 Mac。

选择配置并租用