2026-04-15:租賃雲端 Mac CI 上的 Xcode 衍生資料、TMPDIR 與 xcresult 隔離
行動釋出團隊在香港、東京、首爾、新加坡與美國東岸租賃Mac mini M4建置機時,常把「Xcode 很慢」怪到編譯器,而真正的瓶頸多半是檔案系統爭用:兩條流水線寫入同一段 DerivedData、某個 UI 測試把/tmp塞滿,或.xcresult在沒有配額時膨脹到6 GB以上,讓並行封存搶不到 NVMe 寫入頻寬。本篇以2026-04-15為時間戳,先界定誰需要依作業隔離,整理無頭 SSH 主機上可重現的失敗樣態,用表格比較目錄策略,提供帶數字的八步 Shell 手冊,並串連SwiftPM 快取與註冊表解析與自動與手動簽章兩篇姊妹文,讓下一次跳轉仍可預測、可稽核。
在進入細節前,先釐清:Apple Silicon裸機能壓低 CPU 抖動,但 NVMe 寫入吞吐與 inode 壓力仍是硬上限。若所有作業仍共用~/Library/Developer/Xcode/DerivedData,「偶發紅燈建置」在統計上幾乎不可避免。把DERIVED_DATA_PATH、TMPDIR、-resultBundlePath與 SwiftPM 的-clonedSourcePackagesDirPath綁到同一作業前綴,才能把容量規劃從猜測變成可量測的 IO 預算,並在比較不同區域節點時給出可信依據。
誰該在雲端 Mac 上依作業拆分 DerivedData
當你的佇列在滾動24 小時視窗內,可能出現三條以上並行的xcodebuild程序碰觸同一工作區家族(單體儲存庫、多 scheme、或共享二進位產物)時,隔離就值得投入。主幹式 iOS 團隊與白牌工廠尤其典型:夜間常疊加 UI 測試、封存與靜態分析。若所有作業沿用預設路徑,你會看到SwiftEmitModule隨機失敗、模組圖陳舊、XCTest 附件遺失。把暫存 IO 收斂到可預測的每作業樹,不只讓trap清理變簡單,也能讓平台團隊在評估是否增租第二台 M4 時,先看磁碟曲線而不是靠直覺。
無頭 SSH 建置機上我們反覆看到的故障
- 邊建邊索引——作業 A 的背景索引寫入與作業 B 正在讀取的索引片衝突,表面症狀是「找不到型別」直到徹底清理。
- 共享 TMPDIR——SwiftPM 與 clang 會留下大量小檔;另一條流水線的清理腳本若依前綴刪除,可能刪到兄弟程序仍開啟的路徑。
- xcresult 無配額——UI 測試預設錄影與截圖;三套套件疊加後,單一包體可輕易超過8 GB。
- 封存與測試交錯——夜間先在共享 DerivedData 上封存,又立刻跑整合測試;封存若半途中止,索引與模組快取更容易半寫入。
DERIVED_DATA_PATH、TMPDIR、結果包路徑,以及 SwiftPM 的-clonedSourcePackagesDirPath視為同一作業前綴;在入隊時建立,在trap EXIT裡銷毀(長駐代理需先上傳再刪)。
隔離策略比較
| 方案 | 優點 | 缺點 | 適用情境 |
|---|---|---|---|
在/Volumes/builds下為每作業設定DERIVED_DATA_PATH |
清理確定;可按流水線配額 | 無暖快取時首編較慢 | 共享 Mac mini M4 的並行 CI |
| 唯讀暖快取 + 疊加複製 | 冷啟動編譯較快 | rsync 複雜;純 SSH 主機較難維運 | Xcode 小版本一致的釋出列車 |
| 拆分 macOS 使用者帳號 | 簽章身分硬隔離 | 維運與授權成本較高 | 受規管的多租戶混跑 |
NVMe 配額:可直接貼進手冊的數字
| 產物 | 典型穩態 | UI 測試尖峰 |
|---|---|---|
| DerivedData(中型 App,Debug) | 6–14 GB | 額外約 3 GB暫存寫入 |
| SwiftPM 檢出 + 建置 | 1–4 GB | 解析新標籤時再約 2 GB |
| xcresult 套件 | 單元測試400–900 MB | 含影片時3–10 GB |
租賃雲端 Mac 的八步執行手冊
- 打戳——匯出
JOB_ID=$(date +%s)-$RANDOM與ROOT=/Volumes/builds/$JOB_ID;建立"$ROOT/DerivedData" "$ROOT/tmp" "$ROOT/results"。 - 固定環境——
export DERIVED_DATA_PATH="$ROOT/DerivedData"、export TMPDIR="$ROOT/tmp",並同步TEMP與TMP。 - trap 清理——確認無其他作業重用同一
JOB_ID後註冊trap 'rm -rf "$ROOT"' EXIT;長駐代理需先歸檔結果。 - xcodebuild——測試加
-resultBundlePath "$ROOT/results/Test-$JOB_ID.xcresult";封存仍把 IPA 推到物件儲存,細節見IPA 匯出與上傳。 - SwiftPM——傳入
-clonedSourcePackagesDirPath "$ROOT/spm",並與上文 SwiftPM 文章的註冊表鑑權一致。 - 並行上限——在24 GB記憶體主機上,重型 UI 測試建議≤2路並行;腳本無法約束時由佇列互斥。
- 上傳——以
ditto -c -k --sequesterRsrc --keepParent壓縮 xcresult,校驗雜湊後再刪本地 zip。 - 遙測——前後各記一次
df -h;若牆鐘超過42 分鐘且磁碟占用高於 90%,多半是附件失控。
export JOB_ROOT=/Volumes/builds/ci-$CI_PIPELINE_ID
mkdir -p "$JOB_ROOT"/{dd,tmp,res,spm}
export DERIVED_DATA_PATH="$JOB_ROOT/dd" TMPDIR="$JOB_ROOT/tmp" TEMP="$JOB_ROOT/tmp" TMP="$JOB_ROOT/tmp"
平台團隊一個下午就能加上的指標掛鉤
若磁碟隔離沒人觀測,就等於沒做。建議在每個作業包裝器輸出三個量:(1)編譯結束後du -sk "$ROOT";(2)xcodebuild archive牆鐘秒數;(3)/Volumes/builds所在卷的剩餘 GB。推送到 Prometheus、CloudWatch,或寫入 syslog 由 Vector 擷取皆可。若 DerivedData 中位數體積週環比上升超過18%,通常代表有人關閉增量編譯或新增重型程式碼產生,而不是立刻需要加機器。
同時把 Xcode 建置號(xcodebuild -version)與JOB_ID打到同一行日誌。同一台主機在16.2與16.3之間切換時,索引碎片不相容會放大尾部延遲;把升級與指標尖峰對齊,可避免把問題誤判為「雲端供應商不穩」。若你夜間執行xcrun simctl delete unavailable,請避開高峰編譯視窗,以免與作業佇列爭用同一條 NVMe 佇列。
銜接 SwiftPM 與簽章紀律
拆分 DerivedData 不能取代描述檔與憑證治理。若團隊每週輪換憑證,仍應依自動與手動簽章流程操作,避免codesign在無介面環境等待人工。若透過 SwiftPM 引入二進位相依性,請把Package.resolved納入版本控制,並在每作業目錄下鏡像檢出快取——全域SourcePackages與依作業 DerivedData 混用仍可能在鎖檔上競態。
常見問題:租賃 Apple Silicon 上的 DerivedData
| 問題 | 實務答案 |
|---|---|
| 是否應把 DerivedData 軟連結到 NFS? | 除非往返延遲穩定低於2 ms且能接受較慢的索引;更穩妥是本地 NVMe + 產物上傳。 |
| Xcode Server 舊版面是否仍相關? | 現代 CI 可忽略/Library/Developer/XcodeServer;務必在腳本中顯式傳路徑。 |
| Rosetta 情境要注意什麼? | 避免 x86_64 模擬器與 arm64 前綴混用;依架構拆分目錄,降低胖二進位重建風暴。 |
為何區域化的 Mac mini M4 仍然重要
目錄隔離解決的是軟體層競態,但網路物理距離仍在:東京工程師從美國東岸拉 Git LFS 或符號表,RTT 會以分鐘級等待呈現。把建置機放在 SCM、測試人員與稽核日誌同一大洲,才能把/Volumes/builds的價值吃滿。MacXCode 在香港/日本/韓國/新加坡/美國的佈局,正是為了讓磁碟與網路同時贴近你的合規邊界。請先閱讀說明中心的 SSH 基線,再在連續14 個夜間視窗磁碟指標都健康後,才放大並行。
結論:依作業的DERIVED_DATA_PATH + TMPDIR + 明確xcresult路徑,把「隨機紅燈建置」變成可量測的 IO 預算;隨後你才能用定價頁的資料決定下季度該租幾台 Mac,而不是靠直覺拍板。