同一份 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><thin-for-all-variants></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、缺少体积报告、主应用包含未允许架构、关键资源缺失、框架集合无审批变化,以及变体体积超过团队上限。
同时保存这些证据:
- Xcode 版本与
DEVELOPER_DIR; - 归档的包标识、版本和构建号;
- 实际使用的
ExportOptions.plist; - 完整导出日志;
- 每个变体的架构、框架和文件清单;
- App Thinning 体积报告。
失败时不要立即删除导出目录。先把证据打包上传,再由任务退出钩子清理工作区。若在 PingMini 的云端 Mac 上并发执行,应让每个任务使用独立目录,并在控制台确认当前可选配置;验收逻辑本身不应依赖某个固定节点。
App Thinning 的价值不只在减小下载体积,更在于它改变了最终交付物。把设备变体当成正式构建产物检查,才能在资源标签、架构或依赖发生变化时,于发布前得到可复现、可定位的失败结果。
常见问题
App Thinning 验收应该检查归档还是导出的安装包?
两者都要检查。归档用于确认输入应用、版本和嵌入内容正确,导出结果用于验证设备变体、架构、资源裁剪和体积报告是否符合预期。
两个设备变体的文件哈希不同就代表构建异常吗?
不代表。设备资源、可执行切片和签名内容可能不同,应比较文件清单、允许的架构、关键资源和异常新增项,而不是要求整个安装包哈希一致。
CI 中最适合作为阻断条件的 App Thinning 异常有哪些?
缺少体积报告、导出目录为空、出现未允许架构、关键资源缺失、嵌入框架集合漂移以及单个变体体积超过团队设定上限,都适合直接阻断任务。
为下一次构建准备独享物理 Mac
从三档固定配置中选择适合开发、持续集成或本地模型测试的云端 Mac,并按任务周期完成配置。