工程实践

在云端 Mac 上建立 Xcode 编译警告基线门禁

在云端 Mac 上建立 Xcode 编译警告基线门禁

一个维护多年的 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.logwarnings.currentwarnings.new 一起归档。退出码 42 只是团队内部约定,关键是把“编译失败”和“新增警告”显示为不同原因。开发者看到路径与正文后,才能在一次反馈循环内完成修复。

若多个任务并行构建不同 Target,应分别生成结果,最后合并、排序和去重。不要让多个进程同时写同一个文件,否则截断和交错写入会制造偶发误判。

把基线持续缩小而不是永久冻结

门禁上线后,每次修复旧警告都要删除对应基线行。可以增加一个不阻断构建的检查,找出“基线存在但当前已消失”的条目,提醒提交者顺手清理。这样基线会单向缩小,而不会成为无人理解的历史清单。

稳定运行前还应检查四项:CI 与本地是否使用同一 Xcode 版本;Scheme 是否包含预期 Target;区域设置是否影响工具输出;仓库路径过滤是否覆盖所有自有源码。OnceMac 节点上的执行目录也应固定由任务创建,避免复用上一次构建的日志和 DerivedData。

当基线降到零,可以删除差异比较并启用编译器的警告即错误策略。在此之前,基线门禁提供的是一条现实的过渡路径:不掩盖旧债,也不允许新债继续增长。

常见问题

为什么不直接开启 Treat Warnings as Errors?

已有警告较多时直接启用会让所有构建立即失败。基线门禁先冻结现状,只阻止新增警告,更适合渐进治理;存量归零后再考虑将警告视为错误。

警告行号变化会不会导致误报?

会,因此签名应移除行号和列号,只保留项目内相对路径与警告正文。若编译器文案随 Xcode 版本变化,应在升级验收后单独更新基线。

依赖包产生的警告应该进入基线吗?

通常不应。先按项目根目录过滤,只记录团队维护的源码;必须跟踪的内部依赖可以加入明确的路径白名单,避免第三方噪声掩盖新增问题。

独享物理 Mac mini

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

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

选择配置并租用