限时优惠

Xcode Product → Archive 全流程解析:为什么你的 CI 总是卡在这里

CI Xcode Archive · iOS 发版
2026-06-15 约 14 分钟阅读

本地点一下 Product → Archive,Organizer 里就能导出 IPA;CI 里跑 xcodebuild archive 却常在同一阶段挂 20 分钟、超时、或静默失败。差距不在「会不会用 Xcode」,而在你是否把 Archive 当成一条可观测的流水线,而不是一条黑盒命令。

本文按阶段拆解:Build → Archive → Sign → Export 各自做什么、xcodebuild 如何等价复现菜单操作,以及 CI 上七类高频卡点的症状—动作对照表。

本文要点

  1. Archive ≠ Build:Archive 固定走 Release + Any iOS Device,并生成 .xcarchive;本地 Debug 编过不代表 Archive 能过。
  2. CI 卡点集中在四段:Scheme/配置编译缓存签名与钥匙串exportArchive——日志里要先定位段,再动手。
  3. xcodebuild archive 与菜单 Archive 等价,但 CI 必须显式指定 -scheme-configuration Release-destination 'generic/platform=iOS'
  4. 「卡住不动」多半是等钥匙串授权等网络拉描述文件、或内存 swap——不是 xcodebuild 死了。
  5. 自托管云 Mac + 持久 DerivedData/专用钥匙串,往往比托管 Runner「每次冷 Archive」稳定一个数量级。
Xcode Archive 与 CI 流水线示意
本地 Archive 有 GUI 反馈;CI 里同一条链必须靠日志分段,否则只会看到「Job 跑了 40 分钟」。

1. Archive 和 Build 到底差在哪

很多团队把 CI 失败概括成「xcodebuild 挂了」,其实 Cmd+B BuildProduct → 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,底层大致等价于:

  1. 选中 Shared Scheme,Configuration 为 Release
  2. Destination 切到 Any iOS Device(不能是模拟器);
  3. 执行 xcodebuild archive,把产物写入 ARCHIVE_PATH
  4. (可选)在 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 appDistributing 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 没有这一步,于是:

  • errSecInternalComponentUser 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.plistmethod 决定导出类型(app-storead-hocdevelopment 等)。CI 高发问题:

  • method 与描述文件类型不一致:用 development 描述文件却填 app-store
  • 漏配 teamID / signingCertificate:Manual 签名下 export 阶段才报错,误以为 Archive 慢。
  • 上传与导出混在一个 stepaltool / Transporter 网络慢,被算进「Archive 超时」。

建议把 archiveexportupload 拆成三个 step 并分别记时;上传可用 xcrun altool --upload-appxcrun 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/Podspod install --deployment
Job 被平台 kill(无错误栈) 全局 调高 timeout;查 swap / 磁盘满(磁盘治理
exportArchive failed + plist 相关 Export 单独跑 export;核对 ExportOptions.plist 的 method

值班时把日志从后往前找第一个 error:,再对照上表——比全盘 flutter clean 省一小时。

9. 排障 Runbook:先分段打点,再对症修

  1. 本地复现 Release Archive:Product → Scheme → Edit Scheme → Archive 用 Release;Destination 选 Any iOS Device。本地不过,CI 不必跑。
  2. CI 加四段计时pod installarchiveexportArchiveupload,每段写入 step summary。
  3. 保留完整日志:慎用过度安静的 log 美化器;签名失败需要原始 CodeSign 行。
  4. 固定 Xcode 版本:用 xcode-selectsudo xcode-select -s 锁路径;与 .xcode-version 或 workflow macos-15 对齐。
  5. 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 规格 · 开通验收清单