在云端 Mac 上构建与分发 XCFramework:二进制缓存加速多仓库集成

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

在云端 Mac 上构建与分发 XCFramework:二进制缓存加速多仓库集成

上周三个仓库同时提交,CI 里同一个内部音频处理框架被重新编译了三遍,每次都要跑将近八分钟的 archive。等到第三次构建失败提示签名证书过期,值班的同事在群里问了一句:「这玩意儿不是昨天刚编过吗,为什么每个仓库都要自己再来一遍?」这句话戳中了很多团队的痛点——共享二进制框架本该只编一次,却因为没有缓存机制,变成了每条流水线各跑各的重复劳动。

在 HireVPS 的独享云端 Mac 上,我们把这套流程重新搭了一遍:统一编译多切片 XCFramework,签名、算校验和,再落到一个轻量的本地二进制缓存里,多个仓库直接拉取产物,不用各自重复构建。下面把具体做法整理出来。

为什么要自己缓存 XCFramework

Swift Package Manager 支持 binaryTarget 直接引用预编译的 XCFramework,这本身不是新知识,但真正让它发挥作用的前提是「产物可信、可追溯、能被多方共享」。如果每个仓库的 CI 各自跑一遍 archive,你得到的其实是好几份「理论上一样但实际没验证过一致」的二进制,一旦某次编译环境有细微差异(比如 Xcode 小版本更新),排查起来会很麻烦。集中编译、统一分发,才能保证所有下游拿到的是完全同一份产物。

经验之谈:凡是被 3 个以上仓库引用的内部框架,都值得单独拿出来做二进制缓存;引用少于 3 个的,重复构建的成本可能还不如维护缓存高。

云端 Mac 的资源优势

这套流程对机器有两个硬性要求:一是需要独享的 macOS 图形与命令行环境来跑完整的 xcodebuild archive,二是编译多个架构切片时磁盘 I/O 压力不小,共享资源的机器很容易互相拖慢。在 HireVPS 按天租用一台 M4 或 M4 Pro 机型,选好节点后拿到独立 SSH 凭据,root 权限直接可用,不需要和别人抢 CPU 时间片,连续跑几个小时的多架构编译也不会被邻居任务打断。具体开哪个机型、哪个节点当前可选,在控制台确认当前可选配置即可。

构建多切片 XCFramework

一个能同时给真机和模拟器用的 XCFramework,至少要包含两个切片:ios-arm64(真机)和 ios-arm64-simulator(Apple Silicon 模拟器)。如果团队里还有 Intel Mac 跑模拟器,记得加上 ios-x86_64-simulator,否则那台机器一跑测试就会报架构不匹配。

分别归档两个切片

xcodebuild archive \
  -scheme AudioCore \
  -destination "generic/platform=iOS" \
  -archivePath build/AudioCore-iOS.xcarchive \
  SKIP_INSTALL=NO \
  BUILD_LIBRARY_FOR_DISTRIBUTION=YES

xcodebuild archive \
  -scheme AudioCore \
  -destination "generic/platform=iOS Simulator" \
  -archivePath build/AudioCore-iOSSim.xcarchive \
  SKIP_INSTALL=NO \
  BUILD_LIBRARY_FOR_DISTRIBUTION=YES

合并成 XCFramework

xcodebuild -create-xcframework \
  -framework build/AudioCore-iOS.xcarchive/Products/Library/Frameworks/AudioCore.framework \
  -framework build/AudioCore-iOSSim.xcarchive/Products/Library/Frameworks/AudioCore.framework \
  -output build/AudioCore.xcframework

编译完成后打个 zip,这份文件就是要分发的最终产物,后面不要再手动改动它。

签名与校验和

内部框架也建议签名,方便下游校验来源:

codesign --sign "Apple Distribution: Your Team" \
  --timestamp \
  build/AudioCore.xcframework

zip -r AudioCore-1.4.0.xcframework.zip build/AudioCore.xcframework

签完打包后,用 SwiftPM 自带的命令算一次校验和,这个值要写进下游仓库的 Package.swift:

swift package compute-checksum AudioCore-1.4.0.xcframework.zip

校验和验证失败最常见的原因是打包后又改了 zip 内容,或者换了压缩工具导致文件内部顺序不同。原则很简单:算完校验和之后,这个 zip 文件就冻结了,不再动它。

搭建本地二进制缓存

不需要多复杂的服务,一个能提供静态文件下载的目录结构就够用:

层级 内容 示例路径
框架名 每个内部框架一个目录 /cache/AudioCore/
版本号 每个版本一个子目录 /cache/AudioCore/1.4.0/
产物 zip 包 + 校验和文本 AudioCore-1.4.0.xcframework.zip, checksum.txt

下游仓库的 Package.swift 里这样引用:

.binaryTarget(
    name: "AudioCore",
    url: "https://cache.internal.example/AudioCore/1.4.0/AudioCore-1.4.0.xcframework.zip",
    checksum: "上一步算出的校验和"
)

存储规划上,中等团队保留 3-4 个核心框架各最近 10 个版本足够,单份压缩包通常在 20-80MB,总量控制在几个 GB 内,超过 90 天没被拉取的旧版本定期清理即可。

接入多仓库 CI 流水线

各下游仓库的 CI 不再执行 xcodebuild archive,只需要 swift package resolve 拉取指定版本,时间从几分钟的编译直接降到几秒的下载。发新版本时,在缓存目录里新建版本号目录、更新对应仓库的 checksum 引用即可,不需要动缓存服务器代码。

踩坑提醒:切勿让多个仓库同时引用「latest」这种可变标签,校验和机制要求 URL 和内容一一对应,一旦允许覆盖同一版本号下的文件,SwiftPM 的完整性校验就失去了意义。

检查清单

常见问题

XCFramework 必须包含哪些切片才能同时支持真机和模拟器?

至少要有 ios-arm64(真机)和 ios-arm64-simulator(Apple Silicon 模拟器)两个切片;如果团队里还有 Intel Mac 跑模拟器,需额外加 ios-x86_64-simulator,否则那台机器上跑测试会直接报架构不匹配。

binaryTarget 的 checksum 校验失败一般是什么原因?

最常见的是打包后又手动改动了 zip 包内容,或者用了不同压缩工具导致文件顺序不同;正确做法是用 swift package compute-checksum 直接对最终要分发的 zip 文件算一次,算完不再改动这个文件。

本地二进制缓存服务器需要多大存储规划?

以中等团队为例,3-4 个核心框架各保留最近 10 个版本,单份压缩后通常在 20-80MB 之间,总量控制在几个 GB 内即可,建议在云端 Mac 的 SSD 上单独分区存放并定期清理超过 90 天未被拉取的版本。

HireVPS

今天就试试独享云 Mac mini

按天起租,2 分钟拿到 SSH/VNC 凭据,配置随时可升级。

立即租用 Mac mini