云端 Mac 上验收 App Thinning 变体与瘦身资源

云端 Mac 上验收 App Thinning 变体与瘦身资源

同一份 Xcode 归档交给分发流程后,最终落到不同设备上的内容并不完全相同。App Thinning 会按设备能力选择可执行切片与资源变体。日常构建只验证归档成功,很容易漏掉资源标签写错、无关架构混入、内嵌框架漂移,或者体积报告根本没有生成。更稳妥的做法,是在云端 Mac 上把“归档”和“设备变体验收”拆成两个独立阶段。

本文使用现有工程与可分发的归档配置,不改签名体系,也不要求每次都重新编译。目标是从一份 .xcarchive 导出 App Thinning 结果,生成结构清单,并让 CI 对明确异常给出非零退出码。

先固定验收输入

验收开始前,先记录 Xcode 路径、工程版本、构建号与归档位置。不要让任务依赖交互式 Shell 中的默认工具链。云端 Mac 可能并存多个 Xcode,CI 应显式选择当前流水线批准的版本。

set -euo pipefail

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
ARCHIVE_PATH="$PWD/output/App.xcarchive"
EXPORT_DIR="$PWD/output/thinning"
REPORT_DIR="$PWD/output/evidence"

xcodebuild -version
test -d "$ARCHIVE_PATH"
rm -rf "$EXPORT_DIR" "$REPORT_DIR"
mkdir -p "$EXPORT_DIR" "$REPORT_DIR"

随后读取归档中的应用信息,确认 CI 没有拿错上一个任务留下的产物。

INFO="$ARCHIVE_PATH/Info.plist"

/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleIdentifier" "$INFO"
/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleShortVersionString" "$INFO"
/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleVersion" "$INFO"

建议把工作目录绑定到任务编号,并在任务开始时创建空目录。不要使用全局固定的 output 目录让多个并发任务共享归档。

App Thinning 验收的输入必须是已经通过常规归档检查的单一 .xcarchive。如果输入本身不确定,后面对设备变体做再细的比较也没有意义。

导出所有兼容设备变体

准备独立的 ExportOptions.plist。method 应与团队当前使用的 Xcode 和既有分发流程保持一致;下面的关键点是把 thinning 设置为导出全部兼容变体。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>method</key>
  <string>app-store-connect</string>
  <key>thinning</key>
  <string>&lt;thin-for-all-variants&gt;</string>
  <key>stripSwiftSymbols</key>
  <true/>
</dict>
</plist>

执行导出时,把结果与日志都放进当前任务目录:

xcodebuild \
  -exportArchive \
  -archivePath "$ARCHIVE_PATH" \
  -exportPath "$EXPORT_DIR" \
  -exportOptionsPlist "$PWD/ExportOptions.plist" \
  2>&1 | tee "$REPORT_DIR/export.log"

find "$EXPORT_DIR" -maxdepth 3 -type f -print \
  | LC_ALL=C sort \
  > "$REPORT_DIR/export-files.txt"

如果现有流水线使用不同的导出方法,不要为了照抄示例而改变它。保留原有 method,只在隔离分支验证 thinning 配置,并将所用 plist 一并归档。

核对架构与内嵌内容

导出成功不等于结果正确。先找到安装包并逐个解压,再检查主可执行文件的架构。脚本不应假定应用目录名称固定,可以从 Payload 中发现 .app。

mkdir -p "$REPORT_DIR/unpacked"

while IFS= read -r ipa; do
  name="$(basename "$ipa" .ipa)"
  dest="$REPORT_DIR/unpacked/$name"
  mkdir -p "$dest"
  ditto -x -k "$ipa" "$dest"

  app="$(find "$dest/Payload" -maxdepth 1 -name '*.app' -type d | head -n 1)"
  test -n "$app"

  executable="$(/usr/libexec/PlistBuddy \
    -c 'Print :CFBundleExecutable' "$app/Info.plist")"

  printf '%s: ' "$name"
  lipo -archs "$app/$executable"

  find "$app/Frameworks" -maxdepth 1 -type d -name '*.framework' 2>/dev/null \
    | sed "s#^$app/##" \
    | LC_ALL=C sort \
    > "$REPORT_DIR/$name-frameworks.txt"
done < <(find "$EXPORT_DIR" -type f -name '*.ipa' | LC_ALL=C sort)

对当前 iOS 设备分发结果,主应用出现模拟器架构应直接失败。框架清单则适合与仓库中的批准基线比较。新增框架不一定错误,但必须经过代码审查,避免调试组件或临时依赖进入交付包。

不要比较整个安装包哈希

不同设备变体本来就可能拥有不同的资源与可执行切片,签名数据也会造成二进制差异。直接要求所有 IPA 的 SHA-256 相同,会把正常瘦身误判为失败。

更有效的比较对象包括:

  • 主可执行文件的允许架构;
  • Frameworks 与 PlugIns 的相对路径清单;
  • 必须存在的本地化、字体和数据文件;
  • 不应进入成品的调试配置、测试资源与源码文件;
  • 每个变体的解压体积及相对历史基线的变化。

把资源与体积变成证据

先确认导出流程确实产生了体积报告,再生成每个变体的文件清单。文件大小会受资源裁剪影响,因此基线应按变体分别保存。

SIZE_REPORT="$(find "$EXPORT_DIR" -type f \
  -name 'App Thinning Size Report.txt' | head -n 1)"

test -n "$SIZE_REPORT"
cp "$SIZE_REPORT" "$REPORT_DIR/"

for app in "$REPORT_DIR"/unpacked/*/Payload/*.app; do
  variant="$(basename "$(dirname "$(dirname "$app")")")"
  (
    cd "$app"
    find . -type f -print0 \
      | xargs -0 stat -f '%z %N' \
      | LC_ALL=C sort
  ) > "$REPORT_DIR/$variant-files-and-sizes.txt"
done

体积门禁不要只设置一个永不变化的绝对数字。建议同时保留两种判断:团队批准的硬上限,以及相对最近稳定基线的增长幅度。硬上限阻止明显失控,相对基线负责捕捉一次提交带来的异常增长。

对资源目录,还应检查业务关键文件是否存在。例如离线模板、首屏字体或特定语言资源不能只靠“应用能启动”来证明完整。把这些路径维护成仓库内的文本清单,逐项执行 test -e,比在脚本里散落大量条件更容易审查。

在 CI 中设置明确失败条件

最终脚本至少应阻断以下情况:没有导出任何 IPA、缺少体积报告、主应用包含未允许架构、关键资源缺失、框架集合无审批变化,以及变体体积超过团队上限。

同时保存这些证据:

  1. Xcode 版本与 DEVELOPER_DIR;
  2. 归档的包标识、版本和构建号;
  3. 实际使用的 ExportOptions.plist;
  4. 完整导出日志;
  5. 每个变体的架构、框架和文件清单;
  6. App Thinning 体积报告。

失败时不要立即删除导出目录。先把证据打包上传,再由任务退出钩子清理工作区。若在 PingMini 的云端 Mac 上并发执行,应让每个任务使用独立目录,并在控制台确认当前可选配置;验收逻辑本身不应依赖某个固定节点。

App Thinning 的价值不只在减小下载体积,更在于它改变了最终交付物。把设备变体当成正式构建产物检查,才能在资源标签、架构或依赖发生变化时,于发布前得到可复现、可定位的失败结果。

常见问题

App Thinning 验收应该检查归档还是导出的安装包?

两者都要检查。归档用于确认输入应用、版本和嵌入内容正确,导出结果用于验证设备变体、架构、资源裁剪和体积报告是否符合预期。

两个设备变体的文件哈希不同就代表构建异常吗?

不代表。设备资源、可执行切片和签名内容可能不同,应比较文件清单、允许的架构、关键资源和异常新增项,而不是要求整个安装包哈希一致。

CI 中最适合作为阻断条件的 App Thinning 异常有哪些?

缺少体积报告、导出目录为空、出现未允许架构、关键资源缺失、嵌入框架集合漂移以及单个变体体积超过团队设定上限,都适合直接阻断任务。

为下一次构建准备独享物理 Mac

从三档固定配置中选择适合开发、持续集成或本地模型测试的云端 Mac,并按任务周期完成配置。

配置云端 Mac