本文要点
- Archive ≠ Build:Archive 固定走 Release + Any iOS Device,并生成
.xcarchive;本地 Debug 编过不代表 Archive 能过。 - CI 卡点集中在四段:Scheme/配置、编译缓存、签名与钥匙串、exportArchive——日志里要先定位段,再动手。
xcodebuild archive与菜单 Archive 等价,但 CI 必须显式指定-scheme、-configuration Release、-destination 'generic/platform=iOS'。- 「卡住不动」多半是等钥匙串授权、等网络拉描述文件、或内存 swap——不是 xcodebuild 死了。
- 自托管云 Mac + 持久 DerivedData/专用钥匙串,往往比托管 Runner「每次冷 Archive」稳定一个数量级。
1. Archive 和 Build 到底差在哪
很多团队把 CI 失败概括成「xcodebuild 挂了」,其实 Cmd+B Build 与 Product → Archive 是两条路径:
| 维度 | Build(⌘B) | Archive |
|---|---|---|
| 典型 Configuration | Debug(本地开发) | Release(上架/TestFlight) |
| Destination | 模拟器或已连接真机 | Any iOS Device (arm64) |
| 产物 | .app 在 DerivedData |
.xcarchive + 可导出 IPA |
| 签名要求 | 开发签可编过部分目标 | 分发证书 + 描述文件必须齐全 |
| 优化级别 | 低,编得快 | 全量优化,编译时间显著更长 |
因此「本地 3 分钟能编过」与「CI Archive 20 分钟还失败」并不矛盾:你本地可能在跑 Debug + 模拟器,CI 在跑 Release + 真机架构 + 完整签名链。先对齐对比对象,再谈优化。编译慢本身可参考 xcodebuild 在 CI 为什么比本地慢 2–3 倍。
2. 从菜单到命令行:Product → Archive 的等价操作
在 Xcode GUI 里点 Product → Archive,底层大致等价于:
- 选中 Shared Scheme,Configuration 为 Release;
- Destination 切到 Any iOS Device(不能是模拟器);
- 执行
xcodebuild archive,把产物写入ARCHIVE_PATH; - (可选)在 Organizer 里 Distribute App → 对应
xcodebuild -exportArchive。
CI 里常见最小 Archive 命令如下(路径按项目替换):
xcodebuild archive \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
CODE_SIGN_STYLE=Manual \
DEVELOPMENT_TEAM=XXXXXXXXXX \
| tee archive.log
三个容易漏的参数:
-destination 'generic/platform=iOS':不写时 xcodebuild 可能默认模拟器,Archive 直接失败或行为诡异。-archivePath:必须可写;托管 Runner 上临时目录权限问题会导致「编完落盘失败」。- Scheme 必须 Shared 且提交到 Git:CI checkout 后本地未共享的 Scheme 不存在,报
scheme not found。
官方参考:Apple — Building your app、Distributing your app。
3. 四阶段流水线(一图读懂)
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 1. Resolve │ → │ 2. Archive │ → │ 3. Sign │ → │ 4. Export │
│ SPM/Pods │ │ Release 全量编 │ │ 证书/描述文件 │ │ IPA / Upload │
│ DerivedData │ │ → .xcarchive │ │ 钥匙串访问 │ │ TestFlight │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
pod install xcodebuild codesign exportArchive
常占 5–15 min archive 8–25 min 静默挂起高发 plist 配错高发
CI「卡在这里」时,先问:日志最后一行停在哪个阶段? 停在 CompileSwift 是编译;停在 CodeSign 是签名;停在 exportArchive 是导出配置。混谈只会反复清缓存。
4. 阶段 1:Resolve & Compile(为什么 CI 更慢)
Archive 之前的依赖解析与编译,本地往往「热」、CI 往往「冷」:
- CocoaPods / SPM:CI 每次
pod install重装,日志一片Installing …,还没进 Archive 就吃掉 10 分钟。见 Flutter iOS CI 缓存传播链。 - DerivedData 冷启动:Release 全量编 + 无缓存时,所有 Pod 原生代码重编。自托管 Runner 把
DERIVED_DATA_PATH固定在 SSD 上,第二次 Archive 常能砍半时间。 - 内存压力:Archive 峰值内存高于 Debug;16GB 机器并行多 Job 会 swap,表现为「CPU 不高但一直不动」。见 Runner 内存与 swap 治理。
判断法:若日志在大量 CompileC / SwiftCompile,且针对你没改过的第三方库,优先修缓存,别先动签名。
5. 阶段 2:Archive(生成 .xcarchive)
xcodebuild archive 成功后,应在 -archivePath 看到目录结构:
MyApp.xcarchive/
Info.plist
Products/Applications/MyApp.app
dSYMs/...
常见失败模式:
- Scheme 的 Archive 动作未勾选目标:本地用别的 Scheme 能编,CI 指定的 Scheme Archive 为空。
- 多 Target / Extension 漏配:主 App 过了,Notification Service Extension 签名失败,整包 Archive 失败。
- Build number / 版本冲突:CI 未递增
CFBundleVersion,上传阶段才爆,但有人误以为是 Archive 慢。
验收:Archive 结束后立刻 ls -la "$ARCHIVE_PATH" 并 plutil -p "$ARCHIVE_PATH/Info.plist",确认 ApplicationProperties 存在——别等 export 失败再回头查 Archive 是否完整。
6. 阶段 3:Code Sign(CI 第一大坑)
本地 Archive 时,Xcode 可能弹出钥匙串授权;你点「始终允许」,流水线就通了。无人值守 CI 没有这一步,于是:
errSecInternalComponent、User interaction is not allowed:私钥在登录钥匙串,CI 用户无 GUI 授权。- 描述文件过期 / Bundle ID 不匹配:日志停在
CodeSign,提示Provisioning profile … doesn't match。 - 误开
-allowProvisioningUpdates:需要 Apple ID 交互登录,Headless 环境会一直等。 - 钥匙串未解锁:Job 开头缺
security unlock-keychain/set-key-partition-list。
云 Mac / 自托管 Runner 上的推荐做法:为 CI 单独建钥匙串文件(非登录钥匙串),导入分发证书 + 私钥,Job 开头解锁并设为默认。完整链见 云 Mac iOS CI:codesign 与钥匙串边界。
# Job 开头示意(口令走 CI Secret)
KEYCHAIN=$RUNNER_TEMP/ci-signing.keychain-db
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security import dist.p12 -k "$KEYCHAIN" -P "$P12_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security list-keychains -s "$KEYCHAIN" login.keychain-db
若用 fastlane match 或 Xcode Cloud 管理凭据,原则不变:CI 进程必须能无交互访问私钥,而不是「证书在机器上 somewhere」。match 文档见 fastlane match。
7. 阶段 4:Export IPA(exportArchive)
Archive 只生成 .xcarchive;上架 TestFlight 还需要 Export。菜单里的 Distribute App 对应:
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
-exportPath "$RUNNER_TEMP/export" \
-exportOptionsPlist ExportOptions.plist
ExportOptions.plist 里 method 决定导出类型(app-store、ad-hoc、development 等)。CI 高发问题:
- method 与描述文件类型不一致:用 development 描述文件却填
app-store。 - 漏配
teamID/signingCertificate:Manual 签名下 export 阶段才报错,误以为 Archive 慢。 - 上传与导出混在一个 step:
altool/ Transporter 网络慢,被算进「Archive 超时」。
建议把 archive、export、upload 拆成三个 step 并分别记时;上传可用 xcrun altool --upload-app 或 xcrun notarytool(macOS 分发),与 Archive 解耦。
8. CI 卡住的七类根因对照表
| 症状 / 日志关键词 | 阶段 | 首选动作 |
|---|---|---|
| 长时间无新日志,CPU 低 | Sign | 查钥匙串是否解锁;禁用需 GUI 的 provisioning 自动更新 |
scheme 'Foo' not found |
前置 | Scheme 勾选 Shared 并提交;CI 用 -list 核对 |
No profiles for … were found |
Sign | 核对 Bundle ID、描述文件是否入仓或 match 拉取成功 |
大量 CompileC 针对未改 Pod |
Compile | 持久化 DerivedData;避免 Job 内 clean |
pod install > 8 分钟 |
Resolve | 缓存 ios/Pods;pod install --deployment |
| Job 被平台 kill(无错误栈) | 全局 | 调高 timeout;查 swap / 磁盘满(磁盘治理) |
exportArchive failed + plist 相关 |
Export | 单独跑 export;核对 ExportOptions.plist 的 method |
值班时把日志从后往前找第一个 error:,再对照上表——比全盘 flutter clean 省一小时。
9. 排障 Runbook:先分段打点,再对症修
- 本地复现 Release Archive:Product → Scheme → Edit Scheme → Archive 用 Release;Destination 选 Any iOS Device。本地不过,CI 不必跑。
- CI 加四段计时:
pod install→archive→exportArchive→upload,每段写入 step summary。 - 保留完整日志:慎用过度安静的 log 美化器;签名失败需要原始
CodeSign行。 - 固定 Xcode 版本:用
xcode-select或sudo xcode-select -s锁路径;与.xcode-version或 workflowmacos-15对齐。 - 48 小时验收:同一 commit 连续 Archive 两次,第二次应明显变快(缓存命中);仍慢则查内存与磁盘。
发版冲刺、临时加构建机场景,可配合 远程 Mac 发版冲刺短租,避免在托管 Runner 上硬扛 Archive 超时。
10. 可复制的 CI 命令模板(GitHub Actions 示意)
jobs:
archive-ios:
runs-on: [self-hosted, macOS, ios] # 或 macos-14 托管机
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- name: Install pods
run: pod install --deployment
working-directory: ios
- name: Archive
run: |
set -o pipefail
xcodebuild archive \
-workspace ios/MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
| tee archive.log
env:
DEVELOPER_DIR: /Applications/Xcode_16.4.app/Contents/Developer
- name: Export IPA
run: |
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
-exportPath "$RUNNER_TEMP/export" \
-exportOptionsPlist ios/ExportOptions.plist
自托管 Runner 标签路由、launchd 保活与 CI 用户隔离,见 Mac mini 自托管 Runner 搭建 与 Flutter + GitHub Actions 架构总览。
11. 结论与下一步
Product → Archive 不是「多编一会儿」,而是 Release 全量构建 + 归档 + 分发级签名 + 可选导出 的完整链条。CI 卡在这里, rarely 是 Xcode 坏了,更多是:把 Debug 经验套在 Release 上、把签名当成本地 GUI 专属、把 export 和 archive 混成一个黑盒 step。
可执行的下一步:本地先 Archive 过关 → CI 四段打点 → 专用钥匙串 + 持久缓存 → 再考虑加机器或升配。对 Apple 平台团队,云 Mac 自托管 Runner 往往比反复调托管机 cache 更省总时间——因为 Archive 要的是稳定的热路径,不是一次性最快的芯片。
用云 Mac 跑通 Archive 全链路
独占 M4 裸金属,适合 xcodebuild archive、专用签名钥匙串与持久 DerivedData。日租验证 Archive 耗时与稳定性,满意再升月租常驻 Runner。
配置租 Mac 方案 · 查看 M4 规格 · 开通验收清单