本文要點
- 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 卡在這裡,很少是 Xcode 壞了,更多是:把 Debug 經驗套在 Release 上、把簽章當成本機 GUI 專屬、把 export 和 archive 混成一個黑盒 step。
可執行的下一步:本機先 Archive 過關 → CI 四段打點 → 專用鑰匙圈 + 持久快取 → 再考慮加機器或升規。對 Apple 平台團隊,雲 Mac 自託管 Runner 往往比反覆調託管機 cache 更省總時間——因為 Archive 要的是穩定的熱路徑,不是一次性最快的晶片。
用雲 Mac 跑通 Archive 全鏈路
獨占 M4 裸金屬,適合 xcodebuild archive、專用簽章鑰匙圈與持久 DerivedData。日租驗證 Archive 耗時與穩定性,滿意再升月租常駐 Runner。
配置租 Mac 方案 · 查看 M4 規格 · 開通驗收清單