雲端 Mac CI 的磁碟映像掛載、鎖定與殘留卷宗治理

雲端 Mac CI 的磁碟映像掛載、鎖定與殘留卷宗治理

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。通常可透過等待子程序結束、關閉檔案控制代碼,並在卸載前明確切回工作目錄來解決問題。

清理函式也應保留原始結束碼。如果卸載失敗覆蓋了真正的測試錯誤,流水線只會顯示清理異常,最初的失敗原因反而會遺失。更穩妥的做法是同時保留測試狀態與清理狀態,並將兩者分別寫入工作摘要。

建立殘留卷宗巡檢,而非盲目刪除

節點開始新工作前,可以檢查由該流水線命名的掛載目錄,但不能只因名稱相似就直接刪除。首先要確認該目錄確實是掛載點,再核對對應工作是否仍在執行,以及映像是否屬於目前的工作空間。

建議將巡檢結果分為三類:

  1. 目前工作擁有的卷宗:由結束陷阱正常卸載。
  2. 其他作用中工作擁有的卷宗:記錄後略過,禁止跨工作清理。
  3. 找不到作用中工作的疑似殘留卷宗:收集 hdiutil info、占用程序與建立情境,確認後再卸載。

強制卸載只能作為人工確認後的最後手段。如果自動化指令碼僅依照目錄存在時間執行操作,可能中斷耗時較長但仍正常運作的工作。目錄時間也不等同於掛載時間;複製檔案或讀取中繼資料都可能改變判斷依據。

將映像處理納入流水線驗收

一套可重複使用的磁碟映像處理步驟,至少應檢查以下項目:

在 SDKMac 的獨享實體 Mac 節點上,這套方法同樣適用於互動式疑難排解與無人值守流水線。關鍵不在於增加更多清理指令,而是為每個已掛載卷宗建立明確的擁有者、生命週期與失敗證據。做到這一點後,DMG 檢查才不會因為機器重複使用或並行執行而產生難以重現的結果。

常見問題

為什麼不能依賴 DMG 預設的 /Volumes 掛載名稱?

並行工作可能掛載同名卷宗,系統會自動附加編號;若腳本仍讀取固定路徑,就可能取得另一個工作的內容。

hdiutil detach 失敗後可以直接刪除掛載目錄嗎?

不可以。應先找出占用程序並正常卸載;卷宗仍掛載時遞迴刪除,可能改動可寫映像中的資料並破壞失敗現場。

CI 工作應該預設使用哪種掛載方式?

只需讀取安裝包、測試樣本或基準檔案時應使用唯讀掛載。需要寫入時,則為每個工作建立獨立的可寫副本。

獨享實體節點

為持續整合選擇一台雲端 Mac

比較 SDKMac M4 與 SDKMac M4 Pro 的配置、地區和四種計費週期,然後建立訂單。

選擇租用方案