工程卷宗

云端 Mac 的 Xcode 静态分析基线与增量门禁

云端 Mac 的 Xcode 静态分析基线与增量门禁

团队把 xcodebuild analyze 接进云端 Mac 后,最常见的结果不是立即发现零缺陷,而是一次出现几十条历史诊断。若把告警总数直接设为失败条件,流水线会长期变红;若只把日志留作附件,又没人持续查看。更可执行的办法是先建立经过审阅的基线,再让每次合并只对新增问题负责。

先固定分析任务的输入

静态分析依赖工程配置、编译条件和实际参与编译的源文件。分析前应固定 Xcode 选择、工作区、Scheme、配置与目标平台,并确保 CI 使用与归档任务相同的依赖锁文件。不要用开发者个人 Scheme,也不要在脚本中自动选择“最新”工具链。

分析任务应使用独立目录,避免与编译、测试任务同时改写 DerivedData:

set -o pipefail

ROOT="$PWD"
OUT="$ROOT/ci-artifacts/analyze"
DERIVED="$ROOT/.derived/analyze"
RESULT="$OUT/AppAnalyze.xcresult"

rm -rf "$OUT" "$DERIVED"
mkdir -p "$OUT" "$DERIVED"

xcodebuild analyze \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Debug \
  -destination 'generic/platform=iOS' \
  -derivedDataPath "$DERIVED" \
  -resultBundlePath "$RESULT" \
  CODE_SIGNING_ALLOWED=NO \
  | tee "$OUT/xcodebuild.log"

CODE_SIGNING_ALLOWED=NO 只适用于不需要签名才能完成的分析目标。若工程脚本明确读取签名相关构建设置,应先修正脚本的输入边界,而不是盲目追加更多覆盖参数。

基线不是“这些问题可以忽略”的名单,而是迁移期间对已知事实的快照。新增问题仍应立即失败,旧问题则需要负责人和清理计划。

保存完整结果而不是只数 warning

直接执行 grep -c warning: 很方便,但不适合作为长期门禁。日志中的路径、行号和格式可能变化,同一诊断也可能打印多次。CI 应完整保存 .xcresult 和原始日志,再从结构化结果生成一份用于比较的诊断清单。

不同 Xcode 版本的 xcresulttool 子命令可能调整,因此先记录帮助信息并在节点镜像更新时复核解析脚本:

xcrun xcresulttool help > "$OUT/xcresulttool-help.txt"
xcrun xcresulttool get \
  --legacy \
  --path "$RESULT" \
  --format json > "$OUT/xcresult.json"

如果当前工具链不接受 --legacy,应按该版本帮助信息调整命令,并把解析器变更与工具链升级放在同一个合并请求中。不要在脚本失败时退回日志计数并继续放行,否则门禁会悄悄失效。

建议为每条诊断保留以下字段:

字段 用途
规则或问题类型 区分空指针、资源泄漏等类别
仓库相对路径 避免节点工作目录进入基线
函数或符号名 行号变化后仍能辅助定位
规范化消息 去除临时目录和不稳定数字
严重级别 支持按风险设置处理策略

用稳定指纹生成可审阅基线

诊断指纹不应包含绝对路径、DerivedData 路径或单独的行号。较稳妥的组合是“问题类型 + 仓库相对路径 + 符号名 + 规范化消息”。行号可以保留作展示,但不参与主键,否则在文件顶部增加一行就会把所有旧问题识别为新增。

规范化时避免过度合并

可以删除工作目录前缀、临时 UUID 和连续空白,却不要删除变量名、调用名或资源类型。两条发生在同一文件的不同缺陷若被压成同一指纹,后出现的问题会被旧基线遮住。

基线文件建议采用排序后的 JSON Lines,每行一条记录,并纳入版本控制:

{"fingerprint":"sha256:…","type":"AnalyzeWarning","path":"Sources/Cache.swift","symbol":"load()","message":"Potential leak of an object"}

首次建立基线时,应由代码所有者逐条确认。确认内容至少包括:诊断确实来自当前主分支、暂时无法立即修复、已有跟踪事项,并且没有把解析失败或重复记录写进基线。

在合并请求中只比较集合差异

每次任务生成 current.jsonl 后,以指纹集合与 baseline.jsonl 比较。current - baseline 是新增问题,应使任务失败;baseline - current 是已经消失的问题,应提示维护者删除对应基线项,而不是继续保留无效记录。

门禁输出要短而可操作。对每个新增问题打印类型、相对路径、行号、符号和消息,并附上完整 xcresult 的产物位置。控制台日志只展示摘要,避免数千行分析轨迹掩盖真正差异。

区分三种失败

分析脚本至少要区分以下状态:

  1. xcodebuild analyze 本身失败,例如依赖缺失或 Scheme 不可用;
  2. 结果解析失败,例如工具链升级后 JSON 结构变化;
  3. 分析成功,但发现了基线之外的新诊断。

前两种属于基础设施或配置错误,不能显示成“零新增缺陷”。只有分析与解析都成功,集合差异为空时,任务才可以通过。

让基线持续缩小

基线长期不变,最终会成为另一份无人维护的忽略列表。可以按模块指定负责人,在修改相关文件时顺手修复附近诊断;也可以设定每次迭代清理少量高风险项。修复后必须同步删除基线记录,下一次运行会验证问题是否真的消失。

日常检查可按下面的顺序执行:

  • 确认工具链、Scheme、配置和目标平台没有漂移;
  • 确认分析目录与其他任务隔离;
  • 确认 xcresult、日志和规范化清单均已生成;
  • 抽查指纹中没有绝对路径或临时标识;
  • 新增诊断阻断合并,消失诊断提示收缩基线;
  • 工具链升级时先在独立分支重建并审阅差异。

在 OakVM 上运行这类任务时,关键不是选择更激进的阈值,而是保持同一物理节点上的工具链、脚本和结果格式可追踪。只要解析失败不会被当成成功、基线变更必须经过审阅,静态分析就能从一份容易被忽略的长日志,变成稳定的增量质量门禁。

常见问题

为什么不直接要求 Xcode 静态分析零告警?

已有工程可能积累了历史告警,直接要求清零会让门禁长期不可用。先固化基线并只阻断新增缺陷,随后按模块逐步削减基线,更容易持续执行。

静态分析结果应该保存哪些文件?

至少保存完整的 xcresult、构建日志、规范化后的诊断清单和本次使用的基线文件。完整结果用于复查上下文,清单与基线用于稳定比较。

分析任务可以与普通编译共用 DerivedData 吗?

不建议。为 analyze 任务使用独立 DerivedDataPath,可以避免并行任务争抢中间文件,也能减少缓存状态对诊断结果的干扰。

OAKVM BUILD NODE

在独享物理节点上运行下一次构建

选择 OakVM M4 或 OakVM M4 Pro,按天、周、月或季租用非虚拟机的云端 Mac。节点实际可用状态以控制台实时返回为准。

选择配置并租用