在本機可以順利完成封存,換到雲端 Mac 後卻在簽署階段出現 resource fork, Finder information, or similar detritus not allowed。這類故障通常不是憑證或描述檔失效,而是某張圖片、腳本產物或複製的目錄帶有 macOS 延伸屬性。這些屬性會隨資源一起進入 App 套件,直到簽署工具執行嚴格檢查時才會暴露。
先確認失敗發生在哪一層
不要一看到「簽署失敗」就立刻重建憑證。應先從完整建置記錄中找出第一筆 codesign 錯誤,並記錄失敗物件是主 App、Framework、延伸功能還是資源套件。若記錄中明確出現 FinderInfo、ResourceFork 或 detritus,排查重點就應轉向檔案中繼資料。
延伸屬性常見於以下路徑:
- 使用圖形化檔案管理器複製進儲存庫的圖片、字型或壓縮檔;
- 從共用目錄解壓縮後直接提交的資源;
- 建置腳本透過
cp -R搬移的預先產生目錄; - 未納入版本控制、但長期留在工作目錄中的本機產物;
- 還原快取時連同檔案中繼資料一起寫回的相依套件。
刪除 DerivedData 只能清除建置輸出。如果污染位於原始碼或外部輸入中,下一次建置仍會穩定重現。
清理前應先保留失敗命令、目標路徑與記錄。否則即使建置偶然恢復,也無法判斷真正的污染來源。
建立唯讀掃描基準
檢查儲存庫輸入
xattr -lr 會遞迴列出路徑及其屬性。先將結果寫入記錄,不要一邊掃描一邊刪除:
set -euo pipefail
ROOT="${SRCROOT:-$PWD}"
REPORT="${TMPDIR:-/tmp}/ios-xattr-source.log"
xattr -lr "$ROOT" > "$REPORT" 2>&1 || true
grep -E \
'com\.apple\.(FinderInfo|ResourceFork)' \
"$REPORT" || true
儲存庫較大時,可以排除 .git、DerivedData 與相依套件快取,再逐一檢查實際參與封裝的目錄。重點不在命中數量,而是回答三個問題:屬性附著在哪個檔案、該檔案由誰產生,以及它透過哪個 Build Phase 進入套件。
| 命中位置 | 優先檢查 | 處理原則 |
|---|---|---|
| Assets 或資源目錄 | 檔案匯入方式、解壓縮工具 | 修復來源檔案 |
| 腳本輸出目錄 | cp、ditto 參數 |
重建暫存目錄 |
| DerivedData | 快取還原、舊產物 | 清除快取後重現 |
| Framework 內部 | 下載與解壓縮步驟 | 重新取得並驗收 |
檢查封存套件
如果封存已經產生,可以直接掃描 .app,確認污染是否已進入交付物:
ARCHIVE_PATH="$PWD/build/App.xcarchive"
APP_PATH="$ARCHIVE_PATH/Products/Applications/App.app"
REPORT="$PWD/build/xattr-app.log"
test -d "$APP_PATH"
xattr -lr "$APP_PATH" > "$REPORT" 2>&1 || true
if grep -Eq 'com\.apple\.(FinderInfo|ResourceFork)' "$REPORT"; then
echo "forbidden extended attributes found"
exit 1
fi
此處使用固定的範例路徑。實際管線應從封存目錄中解析出唯一的 .app,若找不到候選項或找到多個候選項,就應直接失敗,避免檢查到錯誤的套件。
清理時保留可追溯性
不建議一開始就對整個工作區執行 xattr -cr。這會移除所有延伸屬性,不但擴大修改範圍,也可能掩蓋下載、解壓縮或快取還原步驟中的問題。更穩妥的做法是,只刪除已確認會破壞簽署的兩類屬性:
TARGET="$PWD/StagingPayload"
find "$TARGET" -print0 |
while IFS= read -r -d '' item; do
xattr -d com.apple.FinderInfo "$item" 2>/dev/null || true
xattr -d com.apple.ResourceFork "$item" 2>/dev/null || true
done
TARGET 最好指向一次性的暫存副本,而不是開發者的原始工作區。完成清理後應重新掃描;如果仍有命中,就停止建置,不要在最終驗收條件後加上 || true。
若屬性來自壓縮檔,應修正解壓縮後的驗收步驟;若屬性來自建置腳本,則應讓腳本每次都建立空白的暫存目錄,再依照明確的檔案清單進行複製。只在流程末尾補上一條遞迴清理命令,會讓污染持續存在,並在其他工作中再次發生。
將檢查接入封存流程
建置前檢查輸入
建置前門禁只需掃描會進入 App 的資源目錄,不必掃描整個個人目錄。命中時應輸出相對路徑與屬性名稱,然後以非零狀態結束。這樣可以在耗時的封存開始前就讓工作失敗,也更容易定位最近的變更。
檢查腳本應納入版本控制,並固定其執行環境:
- 使用腳本本身的位置解析儲存庫根目錄;
- 對含有空格的路徑使用空字元分隔;
- 將報告寫入本次工作專用的產物目錄;
- 掃描階段不得修改檔案;
- 目標目錄不存在時直接報錯。
封存後驗證簽署
延伸屬性掃描通過後,再使用系統簽署工具驗證封存:
APP_PATH="$PWD/build/App.xcarchive/Products/Applications/App.app"
codesign --verify \
--strict \
--verbose=2 \
"$APP_PATH"
如果 App 內含獨立的 Framework 或延伸功能,還應逐一遍歷並驗證這些巢狀程式碼物件。不要只檢查主套件,就假設內部元件全部安全。驗證記錄、封存路徑與原始碼提交編號應一併保存,以便區分「原始碼相同,但輸入快取不同」的情況。
以驗收清單結束排查
一次可靠的修復應同時符合以下條件:
- 原始碼與外部輸入的掃描結果已經留存;
- 已確定污染檔案的產生或複製路徑;
- 清理只作用於明確的屬性或一次性暫存副本;
- 清除 DerivedData 與相關快取後,仍可重新完成封存;
- 最終 App 套件內沒有 FinderInfo 與 ResourceFork;
- 主 App 與巢狀程式碼物件都通過嚴格簽署驗證;
- 同一提交可在全新的工作目錄中重複通過。
延伸屬性故障之所以難以排查,不是因為命令複雜,而是因為問題橫跨原始碼、檔案系統中繼資料、快取與簽署四個層面。將「先記錄、再定位、後清理、終驗收」固化為門禁後,這類問題就不再依賴人工登入節點臨時處理,也不會因為更換工作目錄而失去線索。
常見問題
為什麼刪除 DerivedData 後仍會出現簽署錯誤?
若 FinderInfo 或 ResourceFork 已附著在原始資源、下載檔案或腳本複製的目錄,重新建置仍會把它帶入 App 套件。應先掃描輸入來源。
可以直接對整個儲存庫執行 xattr -cr 嗎?
不建議。先記錄命中路徑,再只移除 FinderInfo 與 ResourceFork。全量遞迴清理應限定在可丟棄的暫存副本中。
清理後如何確認封存可交付?
重新封存後掃描 App 套件,確認目標延伸屬性為零,再執行 codesign --verify --strict,並保存日誌與封存校驗值。
在獨享實體節點上執行下一項任務
使用 M4、16GB RAM 與 256GB SSD 的雲端 Mac,依任務週期選擇新加坡、東京、首爾或香港節點。