工程實務

雲端 Mac 上排查延伸屬性造成的 iOS 簽署失敗

雲端 Mac 上排查延伸屬性造成的 iOS 簽署失敗

在本機可以順利完成封存,換到雲端 Mac 後卻在簽署階段出現 resource fork, Finder information, or similar detritus not allowed。這類故障通常不是憑證或描述檔失效,而是某張圖片、腳本產物或複製的目錄帶有 macOS 延伸屬性。這些屬性會隨資源一起進入 App 套件,直到簽署工具執行嚴格檢查時才會暴露。

先確認失敗發生在哪一層

不要一看到「簽署失敗」就立刻重建憑證。應先從完整建置記錄中找出第一筆 codesign 錯誤,並記錄失敗物件是主 App、Framework、延伸功能還是資源套件。若記錄中明確出現 FinderInfoResourceForkdetritus,排查重點就應轉向檔案中繼資料。

延伸屬性常見於以下路徑:

  • 使用圖形化檔案管理器複製進儲存庫的圖片、字型或壓縮檔;
  • 從共用目錄解壓縮後直接提交的資源;
  • 建置腳本透過 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 或資源目錄 檔案匯入方式、解壓縮工具 修復來源檔案
腳本輸出目錄 cpditto 參數 重建暫存目錄
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 的資源目錄,不必掃描整個個人目錄。命中時應輸出相對路徑與屬性名稱,然後以非零狀態結束。這樣可以在耗時的封存開始前就讓工作失敗,也更容易定位最近的變更。

檢查腳本應納入版本控制,並固定其執行環境:

  1. 使用腳本本身的位置解析儲存庫根目錄;
  2. 對含有空格的路徑使用空字元分隔;
  3. 將報告寫入本次工作專用的產物目錄;
  4. 掃描階段不得修改檔案;
  5. 目標目錄不存在時直接報錯。

封存後驗證簽署

延伸屬性掃描通過後,再使用系統簽署工具驗證封存:

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,並保存日誌與封存校驗值。

NowMini M4

在獨享實體節點上執行下一項任務

使用 M4、16GB RAM 與 256GB SSD 的雲端 Mac,依任務週期選擇新加坡、東京、首爾或香港節點。

立即租用雲端 Mac