雲端 Mac 建置順利完成,產物下載到交付機後卻出現指令碼無法執行、符號連結變成一般檔案,或應用程式套件驗證失敗。這類問題通常不在編譯階段,而是發生於最後一次壓縮與解壓縮。macOS 檔案除了內容與路徑,還可能帶有延伸屬性、存取控制清單、資源分支及符號連結關係。若交付流程只確認「壓縮檔可以開啟」,這些變化就會一路進入發布階段。
先定義需要完整保留的產物
不要直接壓縮整個工作區。應先建立只包含交付內容的暫存目錄,並明確界定哪些中繼資料屬於產物契約的一部分。
常見產物包括應用程式套件、框架、動態函式庫、命令列工具、設定範本及符號檔。快取目錄、測試暫存檔、版本控制目錄與本機日誌都不應混入。使用暫存目錄也能避免封裝期間,來源目錄持續被建置工作修改。
set -euo pipefail
ROOT="$PWD"
SOURCE="$ROOT/build/Release"
STAGE="$ROOT/artifacts/stage"
ARCHIVE="$ROOT/artifacts/release.zip"
rm -rf "$STAGE"
mkdir -p "$STAGE"
ditto "$SOURCE/MyProduct.app" "$STAGE/MyProduct.app"
ditto "$SOURCE/MyFramework.framework" "$STAGE/MyFramework.framework"
第一輪檢查應記錄一般檔案、目錄與符號連結的數量。符號連結在不同工具之間傳遞時尤其容易被展開,進而造成封存檔大小改變,或使相對路徑失效。
find "$STAGE" -type f | wc -l
find "$STAGE" -type d | wc -l
find "$STAGE" -type l -print
打包前盤點 macOS 中繼資料
使用 ls -le@ 檢視權限、ACL 與延伸屬性,再用 xattr 展開關鍵物件的屬性內容。不要把所有不熟悉的屬性都視為無用資料。
ls -le@ "$STAGE"
xattr -lr "$STAGE" > "$ROOT/artifacts/xattr-before.txt"
find "$STAGE" -type l -exec sh -c '
for item do
printf "%s -> %s\n" "$item" "$(readlink "$item")"
done
' sh {} + > "$ROOT/artifacts/symlinks-before.txt"
不要把
xattr -cr當成通用修復方式。遞迴清除雖然可能暫時排除某些表面錯誤,卻也會一併刪除鑑識資訊。正確順序是先盤點並判斷來源,再針對已確認不需要的個別屬性進行最小範圍處理。
還需要檢查可執行檔的權限位元。指令碼在儲存庫中具備執行權限,不代表複製到暫存目錄後仍會保持不變。可使用 find "$STAGE" -type f -perm -111 建立基準;若某個指令碼必須可執行,應將權限要求寫入驗收指令碼,而不是依賴人工記憶。
使用 ditto 建立可交付的壓縮檔
ditto 是 macOS 內建工具,適合處理應用程式套件與框架目錄。建立 ZIP 時啟用資源與延伸屬性保留,並保留暫存目錄這一層,可讓接收方取得穩定的頂層結構。
rm -f "$ARCHIVE"
ditto -c -k \
--sequesterRsrc \
--keepParent \
"$STAGE" \
"$ARCHIVE"
shasum -a 256 "$ARCHIVE" > "$ARCHIVE.sha256"
du -h "$ARCHIVE"
封存檔名稱應固定,內部頂層目錄也應固定。不要將時間戳記、機器名稱或暫存工作區的絕對路徑寫入目錄名稱,否則下游指令碼只能猜測進入點。若管線需要唯一識別碼,可將提交編號存放於外層的產物記錄中,而不要改變壓縮檔內部配置。
對來源不受控的檔案,還應在其進入暫存目錄前,檢查檔名是否含有換行字元、不可見字元或異常的 Unicode 組合。一般空格可以安全處理,但指令碼必須始終以雙引號包住變數,避免路徑被 Shell 拆分。
在獨立目錄解壓縮並比較
驗收時不可覆蓋解壓縮到原目錄,否則缺少的檔案可能被舊檔案掩蓋。每次都應使用全新目錄,並由 ditto 負責解壓縮。
VERIFY="$ROOT/artifacts/verify"
rm -rf "$VERIFY"
mkdir -p "$VERIFY"
ditto -x -k "$ARCHIVE" "$VERIFY"
test -d "$VERIFY/stage/MyProduct.app"
test -d "$VERIFY/stage/MyFramework.framework"
find "$VERIFY/stage" -type l -exec sh -c '
for item do
printf "%s -> %s\n" "$item" "$(readlink "$item")"
done
' sh {} + > "$ROOT/artifacts/symlinks-after.txt"
diff -u \
"$ROOT/artifacts/symlinks-before.txt" \
"$ROOT/artifacts/symlinks-after.txt"
檔案內容驗證應分別針對打包前與解壓縮後的目錄產生清單。路徑可能包含空格,因此不要使用未加引號的迴圈。以下 Python 程式片段會依相對路徑排序,並為一般檔案計算 SHA-256。
from hashlib import sha256
from pathlib import Path
import sys
root = Path(sys.argv[1])
for path in sorted(root.rglob("*"), key=lambda p: str(p.relative_to(root))):
if path.is_file() and not path.is_symlink():
digest = sha256(path.read_bytes()).hexdigest()
print(digest, path.relative_to(root))
分別對 stage 與 verify/stage 執行,再使用 diff 比較輸出。這樣可以同時發現內容變更、檔案遺漏及非預期新增。
將結構與簽章檢查設為發布門禁
如果產物包含已簽章的應用程式套件或框架,應在解壓縮後再次執行簽章驗證。只在壓縮前驗證一次並不足夠,因為問題可能由封存工具引入。
codesign --verify --deep --strict \
"$VERIFY/stage/MyProduct.app"
codesign -d --verbose=4 \
"$VERIFY/stage/MyProduct.app" 2>&1 |
sed -n '1,20p'
--deep 適合用於最終完整性檢查,但不能取代逐層定位。驗證失敗時,應先確認問題出在主應用程式、內嵌框架,還是輔助可執行檔,再針對具體路徑執行 codesign --verify --strict --verbose=4。同時比較打包前後的 xattr 輸出,以確認變化是否由封存步驟造成。
建議將驗收失敗條件固定為:頂層目錄不一致、檔案清單不同、符號連結目標改變、必要的執行權限位元遺失、關鍵延伸屬性異常,或簽章驗證失敗。只要出現任何一項,就應停止發布,而不是重新壓縮後碰運氣。
常見陷阱與交付清單
最常見的錯誤是混用多種工具:先由指令碼建立一般 ZIP,再由上傳程式自動解壓縮並重新壓縮。處理鏈越長,中繼資料語意就越難追蹤。應指定唯一的打包工具,並將原始壓縮檔直接交給產物儲存系統。
第二類錯誤是建置與打包同時進行。編譯器仍在寫入檔案時,清單與壓縮檔可能取自不同時間點。正確做法是等待建置程序成功結束,將結果複製到唯讀暫存目錄,再開始封存。
第三類錯誤是只在同一台機器上測試。至少應刪除舊的驗收目錄,重新解壓縮,並從壓縮檔內的路徑啟動檢查;條件允許時,再由獨立工作執行同一套指令碼。BerryVM 上的長期管線可以將盤點檔案、壓縮檔雜湊值與驗收日誌作為同一批次的交付記錄,但日誌不得包含憑證或未經遮蔽處理的環境變數。
最終清單應保持簡短:暫存目錄已凍結、符號連結已記錄、延伸屬性已盤點、壓縮檔雜湊值已產生、獨立解壓縮已完成、檔案清單一致、執行權限位元符合預期,且適用的簽章檢查已通過。完成這些步驟後,打包就不再是建置結束後隨手執行的動作,而是一道可重複執行、並能準確判定失敗的工程門禁。
常見問題
為什麼不能只用 zip -r 打包 macOS 建置產物?
一般 ZIP 流程可能忽略資源分支、延伸屬性、ACL 或符號連結語意。產物若包含應用程式套件、框架或工具目錄,應優先使用 ditto 並驗收解包結果。
打包前可以直接執行 xattr -cr 嗎?
不建議對整個目錄遞迴清除。應先列出屬性並確認用途,只針對已判定不需要的項目做最小範圍處理,以免刪除交付或診斷所需資訊。
只比對壓縮檔的 SHA-256 是否足夠?
不夠。雜湊只能證明壓縮檔未被改動,仍需在獨立目錄解包後檢查結構、符號連結、屬性,以及適用時的程式碼簽章。
將下一項開發任務放到雲端 Mac 上
選擇 Berry M4 或 Berry M4 Pro,確認五個節點、租用期間與附加選項後提交訂單。