同一提交在开发者电脑上没有改动,进入云端 Mac CI 后却让 Generated/ 出现几十行差异;更麻烦的是,工程仍能编译,于是旧常量、漏掉的资源键或过期 mock 被带进产物。处理这类问题不能只追加一次生成命令,而要把生成器版本、输入集合、输出目录和验收规则一起纳入构建契约。
先确认漂移来自哪里
SwiftGen、Sourcery 一类工具的输出由四部分共同决定:可执行文件版本、配置与模板、输入文件、运行环境。只锁定配置文件并不够。开发机 PATH 中的工具可能比 CI 新,文件遍历顺序可能随目录状态变化,区域设置也可能改变排序或日期格式。
先在本地和 CI 各记录一次最小现场:
set -euo pipefail
sw_vers
xcodebuild -version
swiftgen --version
sourcery --version
printf 'LANG=%s
' "${LANG:-unset}"
printf 'PWD=%s
' "$PWD"
git status --short
这里的目标不是收集越多信息越好,而是确认生成过程实际读取了什么。若模板会写入当前时间、绝对路径或随机标识,应先删除这些非确定输入,否则每次运行都会产生合理但无意义的差异。
“能够再次生成”不等于“能够生成相同结果”。门禁判断的是输入相同时产物是否逐字节一致。
建立可复现的生成基线
生成目录应由工具完全拥有,不要混放人工维护的 Swift 文件。每次执行前删除旧目录,可以发现已从输入中移除、却因增量生成而残留的类型。配置、模板和输入目录都要使用仓库根目录下的确定路径,避免依赖调用者当前所在位置。
建议把责任整理成下表:
| 项目 | 固定方式 | CI 验收 |
|---|---|---|
| 生成器 | 锁定版本并打印版本 | 与仓库声明一致 |
| 配置与模板 | 纳入版本控制 | 工作区无临时修改 |
| 输入 | 明确目录和扩展名 | 排序后集合稳定 |
| 输出 | 使用专属目录 | 先清空再生成 |
| 环境 | 固定区域设置 | 不写时间和绝对路径 |
工具安装方式可以因团队而异,但版本声明必须只有一个来源。不要让脚本写一个版本、开发文档写另一个版本,再由云端 Mac 读取 PATH 中的第三个版本。
把差异检查放在编译之前
将生成步骤放到编译前,失败反馈更快,也不会让编译错误掩盖真正原因。下面的脚本假设所有生成文件都位于 Generated/,并且这些文件已提交到仓库:
set -euo pipefail
repo_root="$(git rev-parse --show-toplevel)"
cd "$repo_root"
export LANG=C
export LC_ALL=C
rm -rf Generated
mkdir -p Generated build
swiftgen config run --config swiftgen.yml
sourcery --config sourcery.yml
find Generated -type f -print |
LC_ALL=C sort |
while IFS= read -r file; do
shasum -a 256 "$file"
done > build/generated.sha256
git diff --exit-code -- Generated
git status --short --untracked-files=all Generated
git diff 只能看到已跟踪文件的变化,所以还要检查未跟踪文件。实际门禁中应在 git status 有输出时显式失败,并把差异补丁与 generated.sha256 保存为构建附件。哈希清单不能替代差异检查,但能回答“这次 CI 到底生成了哪一组文件”。
区分三种失败
若只有格式变化,检查模板换行、工具版本和格式化器执行顺序;若新增或删除文件,检查输入集合及清空逻辑;若内容值变化,检查资源、接口描述或模板参数。分类后再修复源头,不要直接在 CI 中覆盖产物并继续编译。
避开 Xcode 构建阶段的并发陷阱
多个 target 共用同一个生成目录时,把生成命令分别塞进每个 Run Script Phase,可能同时写入同一文件。结果有时只是重复执行,有时会得到截断文件或短暂缺失。更稳妥的方式是把生成设为 CI 的独立前置步骤,完成后再启动所有 xcodebuild 任务。
如果必须放在 Xcode 构建阶段,应只保留一个所有者,并声明完整的输入、输出文件列表。生成脚本本身不要修改工程文件,也不要在读取生成目录的同时清空它。并行构建需要独立输出时,可按任务标识创建临时目录,验收通过后再以一次原子替换发布到固定位置。
另一个常见误区是生成后立即运行格式化工具,而本地提交前钩子采用相反顺序。生成、格式化、差异检查必须保持同一顺序,并由同一入口脚本执行。
合并前与故障时的检查清单
提交生成器或输入变更时,代码审查至少核对以下项目:
- 生成器版本声明是否同步更新。
- 配置、模板和输入变更是否出现在同一提交。
- 删除输入后,对应生成文件是否确实消失。
- 产物中是否混入时间、用户名或绝对路径。
- 本地连续执行两次后,第二次是否保持零差异。
- CI 是否在编译前检查已跟踪与未跟踪文件。
- 并行任务是否写入彼此隔离的目录。
- 失败时是否保留版本、差异补丁和哈希清单。
排查时不要先执行自动修复并覆盖现场。先保存 git status、工具版本与差异,再从干净检出复跑生成入口。若干净检出仍不一致,问题属于环境或未锁定输入;若只有复用工作区失败,则重点检查旧文件、缓存和并发写入。最终标准很简单:相同提交在任意云端 Mac 作业中连续生成两次,第二次都应没有可观察差异。
常见问题
生成的 Swift 文件应该提交到仓库吗?
如果本地开发、代码审查或下游目标需要直接读取生成文件,就应提交,并在 CI 中重新生成后执行差异检查;如果不提交,则必须把生成器、模板和输入完整纳入构建依赖。
为什么本地生成正常,云端 Mac CI 仍然出现差异?
优先检查生成器版本、区域设置、输入文件排序、模板换行和工作目录。任何隐式读取全局环境的行为都可能改变输出,不能只比较最终是否编译成功。
在独享物理节点上运行下一项任务
选择节点与计费周期,使用固定的 M4、16GB RAM 和 256GB SSD 配置开始部署。实际可用性以控制台实时返回为准。