云端 Mac CI 中如何可靠解析系统与 Xcode 输出

CI/CD 实践 ·约 9 分钟阅读

云端 Mac CI 中如何可靠解析系统与 Xcode 输出

同一条 CI 脚本在交互式终端运行正常,换到无人值守的云端 Mac 后却选错 Scheme,常见原因不是 Xcode 本身,而是脚本把“给人看的文字”当成了接口。系统语言、工具版本、警告插入位置甚至一个额外空格,都可能让 grep、cut 和固定行号失效。更稳妥的做法是优先读取 JSON 或 plist,把退出码、原始输出和解析结果分别保存。

先找出不稳定的文本解析

先搜索流水线中的高风险组合:

grep -RInE 'xcodebuild|simctl|system_profiler|sw_vers' ci scripts \
  | grep -E 'grep|awk|sed|cut|tail|head'

并非所有管道都错误。危险的是脚本依赖自然语言,例如查找“Build Settings”、截取冒号后的第二列,或假设目标设备总在第三行。LC_ALL=C 可以减少语言差异,却不能固定字段顺序、警告内容和未来版本格式。

如果命令输出主要服务于人类阅读,就不要默认它也是稳定的机器接口。

审计时为每个读取动作写清三个问题:需要哪个字段,命令是否提供结构化格式,字段缺失时应当失败还是使用明确默认值。不要让空字符串继续流入后续归档步骤。

把构建设置转换为 JSON 契约

xcodebuild -showBuildSettings -json 可以直接返回目标构建设置。应先检查命令退出码,再解析文件,不能把标准错误混入 JSON。

set -euo pipefail

mkdir -p artifacts/environment

if ! xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -showBuildSettings \
  -json \
  > artifacts/environment/build-settings.json \
  2> artifacts/environment/build-settings.stderr; then
  cat artifacts/environment/build-settings.stderr >&2
  exit 1
fi

python3 - <<'PY'
import json
from pathlib import Path

path = Path("artifacts/environment/build-settings.json")
if not path.exists() or path.stat().st_size == 0:
    raise SystemExit("build settings output is empty")

items = json.loads(path.read_text(encoding="utf-8"))
if len(items) != 1:
    raise SystemExit(f"expected one target, got {len(items)}")

settings = items[0].get("buildSettings", {})
required = ["PRODUCT_BUNDLE_IDENTIFIER", "SDKROOT", "CONFIGURATION_BUILD_DIR"]
missing = [key for key in required if not settings.get(key)]
if missing:
    raise SystemExit("missing build settings: " + ", ".join(missing))

print(settings["PRODUCT_BUNDLE_IDENTIFIER"])
PY

若工作区包含多个目标,不要强行要求数组长度为一。应按 target 字段选择目标,并在零匹配或多匹配时终止。这样新增扩展目标后,脚本不会悄悄读取错误配置。

用 simctl JSON 固定设备选择

模拟器列表尤其不适合按行解析。设备名称可能包含空格,同名设备也可能分布在不同运行时。先保存完整结果,再按运行时、名称和可用状态筛选:

xcrun simctl list -j devices available \
  > artifacts/environment/simulators.json

python3 - <<'PY'
import json
from pathlib import Path

data = json.loads(
    Path("artifacts/environment/simulators.json").read_text(encoding="utf-8")
)
matches = []
for runtime, devices in data.get("devices", {}).items():
    if not runtime.startswith("com.apple.CoreSimulator.SimRuntime.iOS-"):
        continue
    for device in devices:
        if device.get("isAvailable") and device.get("name") == "iPhone 16":
            matches.append((runtime, device["udid"]))

if len(matches) != 1:
    raise SystemExit(f"expected one matching simulator, got {len(matches)}")

print(matches[0][1])
PY

生产脚本还应把期望的运行时版本写进仓库配置,而不是自动选择“最新”。自动取最新会让系统组件更新直接改变测试基线。最终执行测试时传入 UDID,比只写设备名称更少歧义。

不要忽略空集合

“没有匹配设备”应当是明确失败。临时创建模拟器之前,先确认所需运行时确实存在;任务结束后只删除本任务创建的设备,不要清理共享设备集。并行任务最好分别设置 -derivedDataPath 和独立的 CoreSimulator 设备集。

从 plist 读取主机事实

macOS 版本信息可以直接从系统 plist 读取,避免解析 sw_vers 的标签文本:

SYSTEM_PLIST=/System/Library/CoreServices/SystemVersion.plist

product_version=$(
  /usr/libexec/PlistBuddy -c 'Print :ProductVersion' "$SYSTEM_PLIST"
)
build_version=$(
  /usr/libexec/PlistBuddy -c 'Print :ProductBuildVersion' "$SYSTEM_PLIST"
)

printf '%s\n' "$product_version" > artifacts/environment/macos-version.txt
printf '%s\n' "$build_version" > artifacts/environment/macos-build.txt
xcodebuild -version > artifacts/environment/xcode-version.txt

读取项目 plist 时优先使用 plutil -extract KEY raw -o - FILE。字段可能不存在时,先用 plutil -type KEY FILE 检查,并由脚本决定缺失是否允许。不要用正则处理 XML;plist 可能是二进制格式,字符串也可能经过转义。

建立可诊断的数据采集边界

结构化格式并不意味着命令必然成功。一个可靠采集步骤至少应保留四类证据:命令退出码、标准错误、未修改的原始文件,以及解析后的精简结果。原始文件用于复盘工具行为,精简结果供后续任务消费。

建议让每个采集器只负责一个对象,并把输出写入临时文件。语法与必填字段全部通过后,再用 mv 原子替换正式文件。这样命令中断时不会留下半截 JSON。

提交前检查项

合并流水线改动前,至少验证以下事项:

  • 脚本没有按固定行号或自然语言标签提取字段。
  • 标准输出与标准错误写入不同文件。
  • JSON 与 plist 在读取前经过语法检查。
  • 数组筛选对零匹配和多匹配都有明确处理。
  • Xcode、macOS 构建号和选择出的设备 UDID能够随任务归档。
  • 日志只记录环境事实,不输出令牌、访问资料或完整环境变量。
  • 更新工具版本后先运行采集验收,再开放正式构建任务。

最终目标不是把所有命令都改成 JSON,而是建立清晰边界:机器消费结构化数据,人类阅读诊断日志。确实没有结构化接口时,也应固定命令版本、隔离语言环境、检查列数,并把解析失败当成失败,而不是继续使用一个看似合理的空值。

常见问题

设置 LC_ALL=C 后还需要结构化输出吗?

需要。固定语言只能减少部分文本差异,不能保证字段顺序、空格、提示信息和未来版本保持不变;JSON 或 plist 才适合作为自动化接口。

结构化命令失败时应该保留哪些文件?

至少保留退出码、标准错误、原始 JSON 或 plist、Xcode 版本和 macOS 构建号,并在解析前检查文件非空且语法有效。

独享物理 Mac

把下一项开发任务放到云端 Mac 上

选择 Berry M4 或 Berry M4 Pro,核对五个节点、租用周期和附加选项后提交订单。

选择配置并订购