HexVM 工程实践

云端 Mac 的 Xcode 构建设置漂移审计实战

云端 Mac 的 Xcode 构建设置漂移审计实战

同一提交在开发机正常归档,到了 HexVM 云端 Mac 却更换了部署目标,或突然少编译一个架构,问题往往不在源码,而在 Xcode 最终解析出的构建设置。工程文件、.xcconfig、环境变量和命令行参数会逐层覆盖,仅审查其中一个文件,很难确认目标真正采用了什么配置。

这类漂移不一定立即导致失败。它可能先改变产物,直到发布阶段才暴露。更稳妥的做法是保存一份经过清洗的有效设置基线,每次合并和正式归档前重新生成并比较。

先界定需要审计的配置面

不要直接保存 xcodebuild 的全部输出。绝对路径、临时目录和构建编号会制造大量无意义差异,应先把设置分成三类。

类型 典型键 处理方式
产物边界 PRODUCT_BUNDLE_IDENTIFIERSUPPORTED_PLATFORMSARCHS 变化即审查
编译行为 SWIFT_VERSIONSWIFT_OPTIMIZATION_LEVELGCC_PREPROCESSOR_DEFINITIONS 变化即审查
节点噪声 BUILD_DIRTEMP_DIRPROJECT_TEMP_DIR 删除或归一化

签名相关设置也要纳入基线,但不要把证书文件、私钥内容或临时凭据写入仓库。审计目标是配置名称、签名模式和权限边界,不是复制敏感材料。

基线不是“永远不变”的配置文件。它是一份需要代码审查的预期状态:允许变化,但变化必须可见、可解释、可回滚。

导出目标实际生效的设置

先固定工程、Scheme、构建配置和目标平台。工作区项目把 -project 换成 -workspace,其余参数保持一致。

mkdir -p .ci/build-settings

xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -configuration Release \
  -destination 'generic/platform=iOS' \
  -showBuildSettings \
  > .ci/build-settings/raw.txt

执行前用 xcodebuild -list -project App.xcodeproj 核对 Scheme 是否共享。若流水线使用额外参数,例如功能开关或自定义 SYMROOT,生成基线和执行构建时必须传入同一组参数,否则比较的是两个不同上下文。

多 Target 工程会输出多组同名键。此时不要粗暴去重,应保留 Target 标题,或分别对每个 Scheme 建立文件。应用、扩展和测试目标的部署范围本来就可能不同。

清洗路径噪声并生成稳定基线

下面的脚本提取 KEY = VALUE 行,删除临时目录键,并把工作目录与用户目录替换成稳定标记。脚本不解析密钥,也不会输出环境变量全集。

from pathlib import Path
import os

source = Path(".ci/build-settings/raw.txt")
target = Path(".ci/build-settings/current.txt")

ignored = {
    "BUILD_DIR",
    "BUILD_ROOT",
    "CONFIGURATION_BUILD_DIR",
    "DERIVED_FILES_DIR",
    "PROJECT_TEMP_DIR",
    "TARGET_TEMP_DIR",
    "TEMP_DIR"
}

root = str(Path.cwd())
home = str(Path.home())
rows = []

for line in source.read_text().splitlines():
    stripped = line.strip()
    if " = " not in stripped:
        continue
    key, value = stripped.split(" = ", 1)
    if key in ignored:
        continue
    value = value.replace(root, "<ROOT>").replace(home, "<HOME>")
    rows.append(f"{key}={value}")

target.write_text("
".join(sorted(rows)) + "
")

首次确认结果后,把 current.txt 复制为按用途命名的基线,例如 release-ios.txt,再提交到仓库。

python3 .ci/normalize_build_settings.py
cp .ci/build-settings/current.txt \
   .ci/build-settings/release-ios.txt

如果依赖路径仍频繁变化,先判断它是否影响编译输入。不要为了得到“零差异”而过滤 HEADER_SEARCH_PATHSFRAMEWORK_SEARCH_PATHSOTHER_SWIFT_FLAGS;这些键的变化往往正是需要发现的问题。

把差异变成流水线门禁

检查任务应在解析依赖之后、正式归档之前运行。这样既能获得完整搜索路径,又能在昂贵的归档步骤开始前停止错误配置。

set -euo pipefail

xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -configuration Release \
  -destination 'generic/platform=iOS' \
  -showBuildSettings \
  > .ci/build-settings/raw.txt

python3 .ci/normalize_build_settings.py

diff -u \
  .ci/build-settings/release-ios.txt \
  .ci/build-settings/current.txt

diff 返回非零状态时,流水线应停止并保留差异文本。不要自动用新文件覆盖基线,否则门禁会退化成记录器。

设置允许清单

团队可以为确定无害的键维护显式忽略清单,但清单必须短,并写明原因。建议对下列变化保持阻断:

  • IPHONEOS_DEPLOYMENT_TARGETSUPPORTED_PLATFORMS
  • ARCHSEXCLUDED_ARCHSONLY_ACTIVE_ARCH
  • SWIFT_VERSION 与优化级别
  • CODE_SIGN_STYLE、签名身份选择方式
  • PRODUCT_BUNDLE_IDENTIFIER 与权限文件路径
  • DEBUG_INFORMATION_FORMAT 与链接器参数

排查常见误报与真实漂移

若每次运行都出现大段路径差异,先检查根目录替换是否覆盖符号链接解析后的路径。可在任务开始时记录 pwd -P,并让脚本同时归一化逻辑路径和真实路径。

若本地没有差异、云端持续有差异,按顺序核对:是否使用同一 Scheme、配置名称是否一致、命令行是否追加设置、依赖解析是否完成、环境变量是否参与 .xcconfig 展开。不要先修改基线去迁就结果。

遇到真实变化时,先定位来源,再更新基线:

  1. xcodebuild -showBuildSettings 确认变化属于哪个 Target。
  2. 在工程文件、.xcconfig 和流水线参数中搜索对应键。
  3. 说明变化目的及对 Debug、Release 的影响。
  4. 完成一次干净构建和归档验证。
  5. 将配置修改与基线更新放在同一次审查中。

建立可持续的审计节奏

配置基线应按 Scheme、构建配置和平台拆分,不要用一份文件覆盖所有场景。日常提交可以检查主应用的 Release 基线;涉及扩展、测试包或发布流程时,再运行对应检查。

每次 Xcode 版本策略调整后,应重新生成设置并逐项审查,因为默认值可能变化。审查重点不是差异数量,而是哪些差异会改变编译输入、产物结构、签名边界和运行目标。保留这条证据链后,构建失败不再需要从整台机器猜起,配置变化也能和源码变化一样被追踪。

常见问题

为什么不只比较工程文件或 xcconfig?

工程文件和 xcconfig 只代表输入。xcodebuild 导出的结果还包含继承、条件设置和命令行覆盖,因此更接近目标实际使用的配置。

哪些构建设置变化应该直接阻止流水线?

部署目标、支持架构、Swift 语言版本、优化级别、代码签名方式和产品标识变化通常应阻止流水线;临时目录与机器路径应先归一化。

独享 Apple Silicon

为下一次构建选择独享云端 Mac

按工作负载选择 Mac mini 机型、租期与区域。每笔订单对应独立物理节点,实际可用性以控制台实时返回为准。

选择云端 Mac 方案