雲端 Mac 上的建置已經通過,壓縮檔也能正常解開,但接收方仍可能看到 ._Config.json、.DS_Store,或在上傳階段遇到來源不明的延伸屬性。問題通常不在編譯器,而是 Finder 操作、跨檔案系統複製,以及封存工具處理 macOS 元資料的方式不同。解決方法不是粗暴地清理整個工作區,而是將交付流程拆分為可稽核的暫存、掃描、封存與複驗四個步驟。
先定義哪些內容可以進入交付包
macOS 檔案除了名稱和內容,還可能帶有延伸屬性、資源分支與 Finder 元資料。這些資訊在本機檔案系統中未必會造成問題,但進入 ZIP、共享磁碟或物件儲存空間後,可能轉換成 AppleDouble 伴隨檔案。
應先定義產物規則,而不是一邊打包一邊刪除:
| 物件 | 預設策略 | 原因 |
|---|---|---|
.DS_Store |
拒絕 | 只用於描述 Finder 目錄檢視方式 |
._* |
拒絕 | 通常由資源分支在跨檔案系統轉換時產生 |
com.apple.quarantine |
檢查並定向處理 | 可能來自下載工具或瀏覽器 |
com.apple.ResourceFork |
依產物類型判斷 | 一般設定包通常不需要,但特定檔案可能依賴 |
| 已簽署的應用程式套件 | 保留結構並複驗 | 不加區分地清理會增加簽章異常的風險 |
清理對象應是獨立的暫存目錄,而不是原始碼工作區。如此一來,即使策略設定有誤,也不會修改快取、相依套件目錄或下次建置需要的輸入。
如果交付內容包含已簽署的應用程式,應先驗證原始產物的簽章。一般文件、設定與日誌可以採用更嚴格的元資料拒絕策略;這兩類物件不應共用同一條遞迴刪除命令。
建立一次性暫存目錄
暫存目錄必須由目前的工作獨占,並在結束時清除。不要重複使用固定的 /tmp/release,因為平行工作可能互相覆寫。
#!/bin/bash
set -euo pipefail
SOURCE="${1:?source path required}"
OUTPUT="${2:?output path required}"
STAGE="$(mktemp -d "${TMPDIR:-/tmp}/bamini-artifact.XXXXXX")"
UNPACK="$(mktemp -d "${TMPDIR:-/tmp}/bamini-unpack.XXXXXX")"
cleanup() {
rm -rf "$STAGE" "$UNPACK"
}
trap cleanup EXIT
/usr/bin/ditto --norsrc "$SOURCE" "$STAGE/payload"
find "$STAGE/payload" -type f -name '.DS_Store' -delete
find "$STAGE/payload" -type f -name '._*' -delete
mktemp 會讓每次工作取得不同的目錄,trap 則可涵蓋成功、失敗與中斷等結束路徑。此處使用 ditto --norsrc 複製內容,避免主動攜帶資源分支,但後續掃描仍不可省略,因為輸入目錄中可能原本就存在實體化的 ._ 檔案。
如果工作流程需要交付完整的應用程式套件,應先確認複製策略能保留該應用程式所需的內容。更穩妥的方式是將應用程式套件與一般附件分開暫存,並分別套用對應策略。
掃描延伸屬性,而不是全部清除
xattr -cr 很方便,但涵蓋範圍也過於寬泛。它不僅會刪除已知的隔離屬性,也會刪除尚未納入檢查規則的其他屬性。結果是流水線看似「轉綠」,根本原因卻被掩蓋。
可以先輸出屬性名稱,再讓明確禁止的項目中止工作:
ATTR_REPORT="$STAGE/xattr.txt"
if /usr/bin/xattr -lr "$STAGE/payload" >"$ATTR_REPORT" 2>&1; then
if grep -E 'com\.apple\.(quarantine|ResourceFork)' "$ATTR_REPORT"; then
echo "disallowed macOS metadata found" >&2
exit 41
fi
fi
if find "$STAGE/payload" -type f \( -name '.DS_Store' -o -name '._*' \) -print -quit |
grep -q .; then
echo "hidden metadata files found" >&2
exit 42
fi
這道關卡採用「一經發現即失敗」的策略,適合用來先定位元資料是由哪個步驟引入。確認來源後,再針對下載、複製或產生階段進行修正。若業務需求確實允許某個延伸屬性,應將檔案範圍與屬性名稱寫入允許清單,而不是直接放行整個目錄。
記錄來源步驟
在原始碼簽出後、編譯後與暫存後各掃描一次。首次出現異常的階段,就是應開始排查的邊界。常見來源包括使用圖形介面瀏覽目錄、從非原生檔案系統複製,以及工具先下載再解包。若只在最終封存前掃描,雖然能攔截污染內容,卻無法指出是哪個步驟造成問題。
封存後必須重新開啟檢查
暫存目錄乾淨,不代表封存工具不會重新寫入元資料。產生 ZIP 後,應先檢查成員名稱,再解包到新目錄中複驗。
rm -f "$OUTPUT"
/usr/bin/ditto -c -k --norsrc --keepParent \
"$STAGE/payload" "$OUTPUT"
/usr/bin/unzip -Z1 "$OUTPUT" >"$STAGE/members.txt"
if grep -E '(^|/)\.DS_Store$|(^|/)\._[^/]+$' "$STAGE/members.txt"; then
echo "archive contains forbidden metadata files" >&2
exit 43
fi
/usr/bin/ditto -x -k "$OUTPUT" "$UNPACK"
if find "$UNPACK" -type f \( -name '.DS_Store' -o -name '._*' \) -print -quit |
grep -q .; then
echo "extracted artifact failed metadata check" >&2
exit 44
fi
檢查成員清單速度較快,適合作為第一道關卡;解包複驗則可涵蓋路徑轉換與解壓縮行為。兩者都應執行,因為上傳系統實際接收的是封存檔,而使用者最終操作的是解包後的內容。
如果套件中包含已簽署的應用程式,應找到解包後的 .app,再執行嚴格的簽章驗證。不要只驗證打包前的副本,否則就無法發現封存參數造成的變更。
將關卡接入可重複執行的交付流程
建議將指令碼固定為「輸入目錄、輸出檔案」兩個參數,並讓每次工作產生以下證據:
- 暫存前的產物路徑與類型。
- 延伸屬性掃描結果。
- ZIP 成員清單。
- 解包後的隱藏檔案檢查結果。
- 存在應用程式套件時的二次簽章驗證結果。
- 最終封存檔的 SHA-256 摘要。
摘要可使用 shasum -a 256 artifact.zip 產生。它無法證明兩個 ZIP 的內容在語意上完全一致,卻能確認上傳、下載與交接過程中沒有發生位元組變化。
在 BAMini 的雲端 Mac 上執行這類工作時,應先在主控台確認目前可選的設定,再將指令碼放入儲存庫並交由 CI 呼叫,不要依賴某位成員記得手動清理。關卡失敗後,保留成員清單與屬性報告,並刪除包含專案內容的暫存目錄;下一次工作必須建立新的暫存目錄,避免前一次殘留內容影響判斷。
最終目標不是讓所有延伸屬性消失,而是確保交付包只包含團隊明確核准的內容。暫存隔離負責控制修改範圍,分階段掃描負責定位來源,解包複驗則負責驗證實際交付結果。三者缺一不可,才能防止隱藏元資料在不同複製路徑中反覆出現。
常見問題
可以直接對整個工作區執行 xattr -cr 嗎?
不建議。它會遞迴移除全部延伸屬性,可能破壞需要保留的資源分支,也會掩蓋上游問題。只應處理獨立暫存目錄,並依允許清單移除屬性。
刪除 .DS_Store 後為何壓縮包仍有 ._ 檔案?
._ 檔案是 AppleDouble 旁車檔,不等同於 .DS_Store。封裝時必須停用資源分支複製,並在封裝後檢查成員清單。
已簽章的應用程式應先清理還是先驗證?
先驗證原始應用程式,再複製到暫存目錄;不要對應用程式套件無差別清除屬性。封裝後解包到新目錄並再次嚴格驗證簽章。
將下一次建置放到雲端 Mac mini
選擇配置、計費週期與節點區域,使用獨享實體機執行開發、建置與自動化工作。