一个 iOS 工程在开发分支上可以正常编译,Archive 里却可能悄悄多出动态 Framework、指向开发机目录的绝对链接,或暴露原本只打算内部使用的全局符号。工程文件审查很难覆盖这些变化,因为真正交付的是链接完成后的 Mach-O。更稳妥的做法,是让云端 Mac CI 直接检查归档产物,并把依赖与符号变化变成需要审核的差异。
先定义要拦截的变化
这类门禁不应该判断某个库“好或坏”,而应回答三个可验证的问题:
- 主程序和内嵌 Framework 实际依赖了什么?
@rpath指向的自带库是否真的存在于 App 包内?- 新增的全局符号是否属于预期接口?
基线不是永久白名单,而是一次经过人工确认的产物快照。工具链升级、依赖更新或接口调整后可以更新基线,但必须和代码变更一起审查。
建议把检查放在 Release Archive 之后。Debug 构建可能包含诊断库和不同的优化结果,不能代表最终交付边界。首次落地时只生成报告,不立即阻断;等团队确认基线内容后,再把差异改为失败状态。
从 Archive 提取检查对象
先生成独立归档,避免复用开发者本机的历史产物:
rm -rf out/App.xcarchive
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$PWD/out/App.xcarchive" \
clean archive
如果签名由后续独立任务处理,可按工程实际情况设置 CODE_SIGNING_ALLOWED=NO。不要把这个覆盖项机械加入所有项目;包含特定能力或自定义构建阶段的工程,应先验证无签名归档是否完整。
归档中的主程序通常位于 Products/Applications。检查范围至少包括 App 主可执行文件和 Frameworks 目录内每个 Framework 的可执行文件。扩展目标也应独立纳入,因为它们拥有自己的依赖边界。
| 对象 | 主要检查 | 常见异常 |
|---|---|---|
| App 主程序 | 系统库、自带 Framework、全局符号 | 非系统绝对路径 |
| 动态 Framework | 二级依赖、安装名、公开符号 | 依赖未随包交付 |
| App Extension | 独立依赖与符号基线 | 误复用主程序假设 |
生成依赖与符号清单
下面的脚本读取主程序和内嵌 Framework,分别输出稳定排序的依赖与全局符号。排序可以避免工具输出顺序变化制造无意义差异。
set -euo pipefail
archive="${1:?archive path required}"
app=$(find "$archive/Products/Applications" -maxdepth 1 -name '*.app' -print -quit)
test -n "$app"
mkdir -p audit/current
main=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$app/Info.plist")
printf '%s
' "$app/$main" > audit/current/binaries.txt
if test -d "$app/Frameworks"; then
while IFS= read -r framework; do
executable=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$framework/Info.plist")
printf '%s
' "$framework/$executable"
done < <(find "$app/Frameworks" -maxdepth 1 -name '*.framework' -type d | sort)
fi >> audit/current/binaries.txt
: > audit/current/dependencies.txt
: > audit/current/symbols.txt
while IFS= read -r binary; do
name=${binary#"$app/"}
otool -L "$binary" |
sed '1d' |
awk -v n="$name" '{$1=$1; print n " " $1}' \
>> audit/current/dependencies.txt
xcrun nm -gU "$binary" |
awk -v n="$name" 'NF {print n " " $NF}' \
>> audit/current/symbols.txt
done < audit/current/binaries.txt
LC_ALL=C sort -u -o audit/current/dependencies.txt audit/current/dependencies.txt
LC_ALL=C sort -u -o audit/current/symbols.txt audit/current/symbols.txt
提交基线时只保留相对对象名,不要记录云端 Mac 的工作目录。流水线随后执行:
diff -u audit/baseline/dependencies.txt audit/current/dependencies.txt
diff -u audit/baseline/symbols.txt audit/current/symbols.txt
把报告变成可靠门禁
仅做全文差异会比较敏感。更实用的策略是先执行硬规则,再检查基线变化。依赖以 /System/Library/Frameworks/、/usr/lib/ 或受控的 @rpath/ 开头通常合理;其他绝对路径应直接失败。对于 @rpath/Example.framework/Example,脚本还应确认包内存在对应 Framework,不能只因为前缀正确就放行。
符号变化则适合人工审核。Swift 名称经过修饰后可能很长,不要凭字符串长度判断风险。重点查看突然出现的大批全局符号、调试辅助接口、测试入口,以及本应隐藏却跨模块可见的 C 或 Objective-C 符号。
降低误报
固定 Xcode 版本与 Release 配置,并确保每次比较的是同一 scheme。若项目包含多个扩展,为每个二进制保存独立基线,而不是把所有结果混成一个集合。工具链升级造成系统符号变化时,应在升级合并请求中更新基线,避免日后无法解释差异来源。
失败后按产物反查
出现新依赖时,先用 otool -L 确认由哪个二进制引入,再检查目标的链接设置、条件编译和依赖嵌入阶段。若 @rpath 目标缺失,重点核对 Framework 是否只参与链接却没有复制进包。出现意外符号时,可用 nm -gU 定位对象,再检查可见性声明、桥接代码和链接器导出配置。
不要在失败后直接覆盖基线。正确顺序是确认变更来源、判断是否影响交付边界、修复或批准变化,最后才重新生成基线。这样建立的门禁不依赖某台云端 Mac 的临时状态,也能为每次发布留下可复核的二进制边界记录。
常见问题
为什么不能只检查工程里的依赖声明?
工程声明不等于最终产物。条件编译、链接参数和依赖脚本都可能改变 Archive,发布门禁应直接检查最终生成的 Mach-O 文件。
导出符号发生变化就一定要阻止发布吗?
不一定。门禁的作用是要求人工确认变化;升级工具链或主动调整公共接口时,可以审核差异后更新并提交基线。
符号清单可以用来发现写进二进制的密钥吗?
不能单独依赖符号清单。它适合发现接口和链接边界变化,密钥检测还应覆盖源码、配置文件、资源文件与构建日志。
独享物理节点
把开发与构建任务放到独享云端 Mac
按任务选择机型、节点区域和租用周期。每份实例对应独享物理机,是非虚拟机环境。