一个维护多年的 Xcode 工程往往已经积累了几十条编译警告。此时直接把 SWIFT_TREAT_WARNINGS_AS_ERRORS 打开,主分支会立刻失去可构建性;继续放任不管,新警告又会混进旧噪声。更可行的办法是在云端 Mac 上冻结一份经过审查的警告基线,让 CI 只拒绝本次变更新增的条目。
先定义门禁的边界
警告门禁不是简单统计日志里的 warning: 数量。依赖下载、构建脚本和系统工具都可能输出同样的字样,直接计数既不稳定,也无法告诉开发者应修改哪个文件。
建议把一条警告归一化为“仓库相对路径 + 警告正文”,移除行号、列号、临时目录和时间戳。这样代码上下移动时不会产生新签名,而文件重命名或警告内容改变时仍会触发审查。
| 内容 | 是否进入签名 | 原因 |
|---|---|---|
| 仓库相对路径 | 是 | 标识责任文件 |
| 警告正文 | 是 | 区分问题类型 |
| 行号与列号 | 否 | 修改代码后容易漂移 |
| DerivedData 路径 | 否 | 每次任务可能不同 |
| 外部依赖警告 | 默认否 | 团队通常无法直接修复 |
基线表示“当前已知并被接受的技术债”,不表示这些警告是正确的。每删除一条存量警告,也应同步缩小基线。
生成可重复的警告签名
先固定工作区、Scheme、配置和 SDK。开发机使用 Debug、CI 使用 Release,会让条件编译路径不同,所得基线也没有比较意义。下面的脚本要求调用方明确传入工作区和 Scheme,并将完整输出保存下来。
#!/bin/bash
set -u
WORKSPACE="${WORKSPACE:?set WORKSPACE}"
SCHEME="${SCHEME:?set SCHEME}"
ROOT="$(git rev-parse --show-toplevel)"
OUT="$ROOT/.ci-artifacts"
DERIVED="$OUT/DerivedData"
mkdir -p "$OUT"
rm -rf "$DERIVED"
set +e
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration Release \
-sdk iphoneos \
-derivedDataPath "$DERIVED" \
CODE_SIGNING_ALLOWED=NO \
build 2>&1 | tee "$OUT/xcodebuild.log"
BUILD_STATUS=${PIPESTATUS[0]}
set -e
grep -F "$ROOT/" "$OUT/xcodebuild.log" \
| grep " warning: " \
| sed "s#${ROOT}/##" \
| sed -E 's#:[0-9]+:[0-9]+: warning: # | #' \
| LC_ALL=C sort -u \
> "$OUT/warnings.current"
exit "$BUILD_STATUS"
CODE_SIGNING_ALLOWED=NO 适合只验证编译的任务;需要归档或验证签名的流水线不应照搬。构建失败与警告回归也要分开处理:如果 xcodebuild 本身失败,应优先返回其退出码,而不是用空的警告文件覆盖真实故障。
处理脚本和多行诊断
部分 Run Script 阶段会输出自定义警告,格式未必包含源码坐标。若这些脚本由团队维护,可以为它们约定固定前缀,再建立第二条解析规则。不要用一个宽泛的正则捕获所有输出,否则网络提示和工具升级通知也会进入基线。
Swift 的附加说明行通常没有 warning:,不适合作为独立签名,但应保留在完整日志中。门禁负责快速判断,完整日志负责还原上下文,两者不能互相替代。
审查并提交第一版基线
首次生成的 warnings.current 不应由自动任务直接复制为基线。先按路径分配责任人,删除重复项,确认没有把派生文件、依赖源码或临时目录纳入。审查完成后再执行:
mkdir -p .ci
LC_ALL=C sort -u .ci-artifacts/warnings.current > .ci/xcode-warnings.baseline
git add .ci/xcode-warnings.baseline
git commit -m "Add reviewed Xcode warning baseline"
基线必须与代码一起版本化。任何增加基线的提交都应展示新增签名及原因,不能让失败任务自动“学习”新警告,否则门禁会在最需要拦截时自行放行。
升级 Xcode 时,编译器可能调整诊断措辞。正确流程是在独立分支运行全量构建,区分真实新增问题与文案变化,再一次性更新基线并记录工具链版本,而不是给比较脚本加入无限宽松的模糊匹配。
在 CI 中只阻止新增警告
当前结果和基线都排序后,可用 comm 找出只存在于当前构建的签名:
BASELINE=".ci/xcode-warnings.baseline"
CURRENT=".ci-artifacts/warnings.current"
NEW=".ci-artifacts/warnings.new"
test -f "$BASELINE"
LC_ALL=C sort -u "$BASELINE" -o "$BASELINE"
LC_ALL=C sort -u "$CURRENT" -o "$CURRENT"
LC_ALL=C comm -13 "$BASELINE" "$CURRENT" > "$NEW"
if test -s "$NEW"; then
printf '%s
' "New Xcode warnings detected:"
cat "$NEW"
exit 42
fi
应将 xcodebuild.log、warnings.current 和 warnings.new 一起归档。退出码 42 只是团队内部约定,关键是把“编译失败”和“新增警告”显示为不同原因。开发者看到路径与正文后,才能在一次反馈循环内完成修复。
若多个任务并行构建不同 Target,应分别生成结果,最后合并、排序和去重。不要让多个进程同时写同一个文件,否则截断和交错写入会制造偶发误判。
把基线持续缩小而不是永久冻结
门禁上线后,每次修复旧警告都要删除对应基线行。可以增加一个不阻断构建的检查,找出“基线存在但当前已消失”的条目,提醒提交者顺手清理。这样基线会单向缩小,而不会成为无人理解的历史清单。
稳定运行前还应检查四项:CI 与本地是否使用同一 Xcode 版本;Scheme 是否包含预期 Target;区域设置是否影响工具输出;仓库路径过滤是否覆盖所有自有源码。OnceMac 节点上的执行目录也应固定由任务创建,避免复用上一次构建的日志和 DerivedData。
当基线降到零,可以删除差异比较并启用编译器的警告即错误策略。在此之前,基线门禁提供的是一条现实的过渡路径:不掩盖旧债,也不允许新债继续增长。
常见问题
为什么不直接开启 Treat Warnings as Errors?
已有警告较多时直接启用会让所有构建立即失败。基线门禁先冻结现状,只阻止新增警告,更适合渐进治理;存量归零后再考虑将警告视为错误。
警告行号变化会不会导致误报?
会,因此签名应移除行号和列号,只保留项目内相对路径与警告正文。若编译器文案随 Xcode 版本变化,应在升级验收后单独更新基线。
依赖包产生的警告应该进入基线吗?
通常不应。先按项目根目录过滤,只记录团队维护的源码;必须跟踪的内部依赖可以加入明确的路径白名单,避免第三方噪声掩盖新增问题。
把下一次构建放到独立物理节点上。
选择芯片、内存、存储、节点与租期,获得不与其他客户共享资源的云端 Mac。