CI 工作經常需要掛載 DMG,例如檢查安裝套件配置、讀取測試樣本、驗證簽署後的交付目錄,或比較兩個磁碟映像的內容。單次手動執行通常不會出現問題,真正的故障往往發生在工作並行、指令碼異常結束,或前一次作業留下已掛載卷宗時。此時,固定讀取 /Volumes/Product 的指令碼可能不會立即報錯,反而悄悄讀取另一項工作的內容。
為何預設掛載方式會在並行時失控
hdiutil attach App.dmg 會依照映像內的卷宗標籤選擇掛載名稱。如果已存在同名卷宗,macOS 可能建立附帶編號的新路徑。當指令碼仍然存取原本的固定目錄時,測試結果便不再可信。
另一個常見問題是只在成功路徑上執行 detach。只要驗證指令失敗、工作遭到終止,或後續步驟提早返回,清理指令就不會執行。長期重複使用的雲端 Mac 節點會逐漸累積殘留卷宗,進而發生卷宗標籤衝突、檔案遭占用或工作目錄判斷錯誤。
掛載點應視為該項工作專屬的暫存資源,而不是機器上的共用路徑。建立、使用、卸載與診斷都應由同一項工作負責。
開始調整前,先記錄目前狀態:
/usr/bin/hdiutil info
/bin/df -h
/bin/ls -la /Volumes
hdiutil info 用於確認映像、裝置與掛載路徑之間的對應關係;df 只能顯示檔案系統使用情況,無法取代映像關聯檢查。
為每項工作配置唯一掛載點
掛載目錄應包含穩定且唯一的工作識別碼。不要只使用儲存庫名稱,因為同一個儲存庫可能同時執行多個分支或重試工作。識別碼也必須過濾斜線、空格等路徑字元。
以下指令碼會先驗證映像,再以唯讀方式掛載,並確保在正常結束、指令失敗或收到終止訊號時,都會嘗試卸載:
#!/bin/bash
set -euo pipefail
IMAGE="${1:?usage: mount-image.sh path/to/file.dmg}"
RAW_JOB_ID="${CI_JOB_ID:-local-$$}"
JOB_ID="${RAW_JOB_ID//[^a-zA-Z0-9._-]/_}"
MOUNT="${TMPDIR%/}/sdkmac-image-${JOB_ID}"
ATTACHED=0
cleanup() {
local status=$?
trap - EXIT INT TERM
if (( ATTACHED == 1 )); then
if ! /usr/bin/hdiutil detach "$MOUNT"; then
printf 'unable to detach %s\n' "$MOUNT" >&2
fi
fi
/bin/rmdir "$MOUNT" 2>/dev/null || true
exit "$status"
}
trap cleanup EXIT INT TERM
/usr/bin/hdiutil verify "$IMAGE"
/bin/mkdir -p "$MOUNT"
/usr/bin/hdiutil attach \
-nobrowse \
-readonly \
-mountpoint "$MOUNT" \
"$IMAGE"
ATTACHED=1
test -r "$MOUNT"
/usr/bin/find "$MOUNT" -maxdepth 2 -type f -print
-nobrowse 可避免已掛載卷宗進入圖形介面的常規瀏覽流程,-readonly 則能防止檢查步驟意外改寫樣本。指令碼使用 rmdir 而非 rm -rf:如果卸載失敗,前者不會跨入仍處於掛載狀態的目錄刪除內容。
唯讀並非適用於所有情境
需要測試寫入行為時,不要直接將共用的基準映像改為可寫。應先為目前工作複製映像,確認副本位於該工作專屬的暫存目錄後,再掛載副本。如此一來,失敗的工作就不會污染下一次執行所使用的基準輸入。
| 使用目的 | 建議模式 | 驗收重點 |
|---|---|---|
| 檢查安裝套件或樣本 | 唯讀掛載 | 檔案存在、權限與摘要 |
| 驗證寫入流程 | 工作專屬的可寫副本 | 寫入結果與卸載完整性 |
| 比較兩份映像 | 兩個獨立的唯讀掛載點 | 路徑不重名、比較順序固定 |
讓卸載失敗留下可供診斷的證據
卸載失敗通常不是 hdiutil 本身隨機出錯,而是某個程序仍將目前目錄、開啟中的檔案或工作路徑留在卷宗內。不要立即使用強制卸載掩蓋原因,應先找出占用者:
/usr/sbin/lsof +D "$MOUNT" 2>/dev/null || true
/usr/bin/hdiutil info
/bin/ps -axo pid,ppid,command
lsof +D 在大型目錄上可能較慢,因此只應在失敗分支中執行。應優先檢查測試程序、日誌收集器、壓縮工具,以及透過 cd 進入掛載目錄後未離開的 shell。通常可透過等待子程序結束、關閉檔案控制代碼,並在卸載前明確切回工作目錄來解決問題。
清理函式也應保留原始結束碼。如果卸載失敗覆蓋了真正的測試錯誤,流水線只會顯示清理異常,最初的失敗原因反而會遺失。更穩妥的做法是同時保留測試狀態與清理狀態,並將兩者分別寫入工作摘要。
建立殘留卷宗巡檢,而非盲目刪除
節點開始新工作前,可以檢查由該流水線命名的掛載目錄,但不能只因名稱相似就直接刪除。首先要確認該目錄確實是掛載點,再核對對應工作是否仍在執行,以及映像是否屬於目前的工作空間。
建議將巡檢結果分為三類:
- 目前工作擁有的卷宗:由結束陷阱正常卸載。
- 其他作用中工作擁有的卷宗:記錄後略過,禁止跨工作清理。
- 找不到作用中工作的疑似殘留卷宗:收集
hdiutil info、占用程序與建立情境,確認後再卸載。
強制卸載只能作為人工確認後的最後手段。如果自動化指令碼僅依照目錄存在時間執行操作,可能中斷耗時較長但仍正常運作的工作。目錄時間也不等同於掛載時間;複製檔案或讀取中繼資料都可能改變判斷依據。
將映像處理納入流水線驗收
一套可重複使用的磁碟映像處理步驟,至少應檢查以下項目:
- 輸入檔案存在,並在掛載前完成
hdiutil verify。 - 每項工作都使用獨立目錄,不依賴映像的卷宗標籤產生路徑。
- 預設採用唯讀模式;需要寫入時,建立工作專屬副本。
- 所有結束路徑都設有清理陷阱,子程序也能收到終止訊號。
- 卸載前先離開掛載目錄,並等待正在讀取該卷宗的程序結束。
- 卸載失敗時保留裝置、路徑與程序證據,不執行遞迴刪除。
- 工作結束後確認掛載點已消失,且未誤清理其他並行工作。
在 SDKMac 的獨享實體 Mac 節點上,這套方法同樣適用於互動式疑難排解與無人值守流水線。關鍵不在於增加更多清理指令,而是為每個已掛載卷宗建立明確的擁有者、生命週期與失敗證據。做到這一點後,DMG 檢查才不會因為機器重複使用或並行執行而產生難以重現的結果。
常見問題
為什麼不能依賴 DMG 預設的 /Volumes 掛載名稱?
並行工作可能掛載同名卷宗,系統會自動附加編號;若腳本仍讀取固定路徑,就可能取得另一個工作的內容。
hdiutil detach 失敗後可以直接刪除掛載目錄嗎?
不可以。應先找出占用程序並正常卸載;卷宗仍掛載時遞迴刪除,可能改動可寫映像中的資料並破壞失敗現場。
CI 工作應該預設使用哪種掛載方式?
只需讀取安裝包、測試樣本或基準檔案時應使用唯讀掛載。需要寫入時,則為每個工作建立獨立的可寫副本。
為持續整合選擇一台雲端 Mac
比較 SDKMac M4 與 SDKMac M4 Pro 的配置、地區和四種計費週期,然後建立訂單。