工程文章

云端 Mac 用 ibtool 预检 Storyboard 与 XIB

云端 Mac 用 ibtool 预检 Storyboard 与 XIB

一个 Storyboard 在开发机上能正常打开,合并后却让远程归档任务跑到中段才失败。此时重新下载依赖、启动模拟器或清理 DerivedData 都没有帮助,因为真正的问题可能只是失效的 outlet、错误的自定义类模块,或某个 XIB 使用了当前部署目标不支持的属性。对云端 Mac 上的持续集成来说,更经济的做法是先用 ibtool 独立编译界面资源,再决定是否进入完整构建。

把界面资源检查放到构建前面

ibtool 随 Xcode 提供,应该通过 xcrun 调用,而不是在脚本里写死工具路径。它能读取 Storyboard 和 XIB,输出 errors、warnings 与 notices,并把源文件编译为 storyboardcnib。这个阶段不需要启动模拟器,适合放在依赖解析之后、xcodebuild archive 之前。

先确认执行节点实际使用的开发者目录:

set -euo pipefail

xcode-select -p
xcrun --find ibtool
xcodebuild -version
xcrun ibtool --version

这些信息应写入任务日志。若团队在同一台云端 Mac 上保留多个 Xcode,流水线必须显式设置 DEVELOPER_DIR,并在任务结束后清除该环境变量,避免后续作业继承错误工具链。

界面文件能被图形编辑器打开,不等于它能在指定 Xcode、部署目标和模块上下文中成功编译。CI 检查的目标是复现发布工具链,而不是复现某位开发者的桌面状态。

扫描并独立编译 Storyboard 与 XIB

下面的脚本扫描仓库中的界面文件,并为每个输入创建独立输出目录。排除构建目录和依赖目录很重要,否则可能重复检查生成物或第三方资源。

#!/usr/bin/env bash
set -euo pipefail

root="${1:-$PWD}"
target="${IPHONEOS_DEPLOYMENT_TARGET:-16.0}"
work="${TMPDIR:-/tmp}/ibtool-preflight"
log="$work/ibtool.log"

rm -rf "$work"
mkdir -p "$work/out"
: > "$log"

find "$root" \
  \( -path "*/DerivedData/*" -o -path "*/build/*" -o -path "*/Pods/*" \) -prune \
  -o \( -name "*.storyboard" -o -name "*.xib" \) -print0 |
while IFS= read -r -d '' source; do
  relative="${source#"$root"/}"
  safe_name="$(printf '%s' "$relative" | tr '/ ' '__')"

  case "$source" in
    *.storyboard)
      output="$work/out/${safe_name%.storyboard}.storyboardc"
      ;;
    *.xib)
      output="$work/out/${safe_name%.xib}.nib"
      ;;
  esac

  printf 'Checking %s
' "$relative" | tee -a "$log"
  xcrun ibtool \
    --errors \
    --warnings \
    --notices \
    --minimum-deployment-target "$target" \
    --compile "$output" "$source" 2>&1 | tee -a "$log"
done

脚本使用临时目录,不会污染仓库。失败时应保留 ibtool.log,成功时可以删除编译结果,只上传日志摘要。不要把临时输出提交到版本库,也不要让多个并行任务共用固定目录。

分清独立预检与项目构建的边界

独立调用 ibtool 能发现 XML 损坏、部分连接异常、属性不兼容和编译错误,但它不了解完整 target 的所有构建设置。自定义视图控制器属于哪个模块、某个资源是否加入 Copy Bundle Resources,以及条件编译后的类是否存在,仍要交给 xcodebuild 验证。

建议把检查分成两层:

层级 输入 主要发现 失败处理
快速预检 Storyboard、XIB 文件损坏、属性不兼容、基础连接错误 立即停止
项目构建 workspace、project、Scheme 模块解析、资源归属、链接与签名上下文 保存完整构建日志

如果独立预检通过而项目构建失败,应优先检查 Target Membership、Module 字段、类名重构记录和部署目标继承关系。反过来,若预检已经失败,就没有必要继续执行耗时更长的归档。

把本地化文件纳入同一门禁

Storyboard 本地化常见两种形式:每种语言维护独立资源,或使用 Base Internationalization 配合 .strings。前者容易发生对象 ID 漂移,后者容易留下已经删除的键。先用 plutil 检查 .strings 的基本格式:

find . -name "*.strings" -print0 |
while IFS= read -r -d '' file; do
  plutil -lint "$file"
done

对于 Base Storyboard,可以生成一份当前键集合,再与仓库中的本地化文件比较:

mkdir -p "${TMPDIR:-/tmp}/ibtool-strings"
xcrun ibtool \
  --generate-strings-file "${TMPDIR:-/tmp}/ibtool-strings/Main.strings" \
  "App/Base.lproj/Main.storyboard"
plutil -lint "${TMPDIR:-/tmp}/ibtool-strings/Main.strings"

生成文件只用于比较,不应直接覆盖译文。较稳妥的流程是提取键名、排序后做差异检查:新增键进入待翻译清单,删除键形成清理提示,已有键的文本仍由本地化流程维护。这样既能发现缺项,也不会把现有翻译误替换为 Base 文本。

处理警告基线与常见误判

把所有输出一律视为失败,短期内看似严格,实际会让团队习惯忽略红灯。更可执行的规则是:errors 立即失败;warnings 与仓库中的已审查基线比较,只阻止新增项;notices 保存到构建产物中,定期清理。

常见排查顺序如下:

  1. 确认 DEVELOPER_DIR 与完整构建一致。
  2. 核对 IPHONEOS_DEPLOYMENT_TARGET 是否来自项目真实设置。
  3. 检查文件路径大小写,特别是重命名后仅改变字母大小写的资源。
  4. 搜索失效的 outlet、action 和自定义类名。
  5. 确认临时目录按作业隔离,避免并行编译覆盖输出。
  6. 用同一 Scheme 再执行一次不签名的构建验证模块上下文。

警告基线应保存稳定标识,避免记录临时绝对路径。升级 Xcode 时先在单独分支运行预检,审查新增诊断,再更新基线;不要在升级提交中直接过滤所有新警告。

建立可回滚的落地顺序

第一次接入时,可以只收集一周日志,不阻断合并。确认误报来源后,将确定性的 errors 设为失败,再逐步启用新增 warning 门禁。脚本本身应固定在仓库中,由本地和云端 Mac 共用,避免 CI 配置里藏着另一份实现。

最终检查项包括:工具链已固定;扫描目录有明确排除项;每个任务使用独立临时目录;部署目标来自项目配置;日志作为失败附件保留;本地化键只比较不覆盖;完整 Scheme 构建仍然执行。这样,界面资源问题会在几十秒级的预检阶段暴露,而不是等到归档末尾才留下一个难以复现的错误。

常见问题

ibtool 预检可以替代完整的 Xcode 构建吗?

不能。它适合快速检查界面资源的语法、对象连接和编译兼容性,最终仍需用项目实际 Scheme 完成构建与测试。

为什么本地能打开的 Storyboard 在 CI 中仍会失败?

常见原因是 CI 选择了不同的 Xcode、部署目标不一致、文件引用大小写错误,或自定义类所在模块未进入当前构建上下文。

是否应该把 ibtool 的 notices 全部作为失败处理?

不建议直接阻断。先将 errors 设为失败条件,warnings 纳入基线比较,notices 保存为日志,再逐步收紧规则。

VMMini M4

在独享物理节点上运行下一项任务

选择节点与计费周期,使用固定的 M4、16GB RAM 和 256GB SSD 配置开始部署。实际可用性以控制台实时返回为准。

立即租用云端 Mac