一個 Swift Package 在本機預覽時運作正常,接入雲端 Mac 的封存任務後卻出現空白圖示、預設文案或 JSON 讀取失敗。最棘手的通常不是編譯錯誤,而是流水線依然顯示成功。SwiftPM 會處理 Bundle.module,但不會替團隊判斷資源是否完整宣告、呼叫名稱是否一致,也無法證明資源已經進入最終的應用程式套件。解決方法是將資源視為建置產物的一部分來驗收,而不是等到測試人員開啟頁面後才發現缺漏。
先辨識資源遺失的三個位置
開始排查前,先確認問題發生在哪一層。第一層是原始碼目錄中有檔案,但 Package.swift 的 target 沒有宣告對應資源;第二層是資源已經產生 .bundle,呼叫程式碼卻使用了主應用程式的 bundle;第三層是檔案已進入應用程式套件,但檔名大小寫、子目錄或本地化目錄與執行階段的查詢不一致。
建議先保留一個最小重現入口,讓它分別讀取一張圖片、一份 JSON 與一個本地化字串。若三種類型同時失敗,應優先檢查 bundle 的選擇;若只有單一檔案失敗,則優先檢查資源規則與路徑。
「編譯通過」只能證明編譯器接受了目前的輸入,無法證明執行階段所需的檔案已經進入交付成品。
在 Package.swift 中,應明確選擇 .process 或 .copy。圖片、資源目錄與本地化內容通常使用 .process;只有必須保留原始目錄結構或原始位元組的檔案才使用 .copy。不要一次將整個儲存庫目錄複製進資源套件,否則測試樣本、暫存輸出,甚至不應交付的設定都可能被一併帶入。
用清單固定資源契約
資源驗收不能只比較檔案數量。新增一項資源、刪除一項舊資源或重新命名時,檔案總數可能完全不變。更穩妥的做法是在儲存庫中維護 ci/expected-resources.txt,每一行記錄相對於資源 bundle 根目錄的路徑:
Assets.car
Defaults/config.json
en.lproj/Localizable.strings
zh-Hans.lproj/Localizable.strings
清單只應包含執行階段必須存在的檔案。按需下載的內容、測試夾具與開發預覽資源都不應混入。進行程式碼審查時,資源重新命名必須與清單變更同時出現,讓審查者能清楚看見交付契約發生了哪些變化。
資源目錄本身也要進行反向檢查:原始碼中存在但未宣告的檔案應產生警告;清單要求但建置結果中不存在的檔案則應直接讓任務失敗。前者有助於清理儲存庫,後者則負責守住交付結果。
建置後直接檢查應用程式套件
每個任務都應使用獨立的 DerivedData,避免其他平行任務留下的舊 bundle 讓檢查誤判為通過。以下指令碼以模擬器建置為例;實際流水線只需替換 scheme,並將 bundle 名稱的比對規則改為專案的真實模組名稱。
set -euo pipefail
DERIVED="${RUNNER_TEMP:-/tmp}/resource-check-${BUILD_ID:-local}"
rm -rf "$DERIVED"
xcodebuild \
-scheme ExampleApp \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath "$DERIVED" \
build
APP=$(find "$DERIVED/Build/Products" -type d -name 'ExampleApp.app' -print -quit)
test -n "$APP"
COUNT=$(find "$APP" -type d -name '*FeatureKit*.bundle' | wc -l | tr -d ' ')
test "$COUNT" = "1"
BUNDLE=$(find "$APP" -type d -name '*FeatureKit*.bundle' -print -quit)
while IFS= read -r item; do
test -z "$item" && continue
test -e "$BUNDLE/$item" || {
printf 'missing resource: %s\n' "$item" >&2
exit 1
}
done < ci/expected-resources.txt
這裡刻意先斷言 bundle 數量為一。若直接取得第一個相符結果,舊產物、測試 bundle 或同名模組都可能掩蓋問題。檢查對象也必須是 .app 內部的 bundle,而不是 DerivedData 的中間目錄,因為只有前者能代表最終的嵌入結果。
將高頻誤區轉成門禁檢查
不同資源類型需要採用不同的驗收方式,不能全部用 test -e 草率帶過。
| 資源類型 | 最低檢查要求 | 常見失敗 |
|---|---|---|
| JSON | 檔案存在且能成功解析 | 複製了空白檔案或格式有誤 |
| 本地化字串 | 目標語言目錄存在且可讀取指定鍵 | 語言目錄命名錯誤、回退至預設文案 |
| 圖片與色彩 | 資源編譯產物存在,冒煙測試頁面可載入 | 名稱大小寫不一致或誤用主 bundle |
| 範本檔案 | 驗證雜湊值或關鍵欄位 | 產生步驟覆寫了儲存庫中的版本 |
macOS 常見的工作磁碟區不區分大小寫,因此 IconDark 與 icondark 之間的錯誤可能在開發機上被掩蓋。不要依賴檔案系統碰巧容錯,應從清單擷取路徑,並與實際路徑進行區分大小寫的逐字比較。JSON 檔案還要交由解析器檢查,屬性列表應使用 plutil -lint,字串目錄則至少要檢查必要語言與關鍵鍵值。
若 Package 使用產生器外掛程式,應先確認產生步驟發生在資源驗收之前,並將產生目錄限制在該任務專屬的暫存路徑中。當多個任務共用同一個產生目錄時,其中一個任務可能讀取到另一個分支的檔案,形成最難重現的假成功。
保存證據並確保任務可重跑
任務失敗時,不要上傳整個 DerivedData。它不僅體積龐大、雜訊繁多,還可能包含不適合長期保存的路徑資訊。更實用的證據包應包括:資源清單、實際 bundle 檔案樹、建置日誌末段、解析器錯誤,以及目前的提交識別碼。檔案樹可以用以下方式產生:
find "$BUNDLE" -print \
| sed "s#^$BUNDLE/##" \
| LC_ALL=C sort \
> resource-bundle-tree.txt
封存日誌與檔案樹之前,應移除存取權杖、使用者目錄與暫時憑證。任務結束後,刪除獨立的 DerivedData 與暫存產生目錄,確保下一次執行一定從空目錄開始。DPLYMAC 上的平行執行器也應遵守相同原則:任務可以重複使用下載快取,但不能重複使用未經識別的建置輸出。
最終門禁應回答三個問題:宣告中要求哪些資源、最終應用程式套件中實際包含哪些資源,以及執行階段能否依照真實名稱讀取它們。只要這三層各自留下可檢查的結果,SwiftPM 資源缺失就能從偶發的介面故障,轉變為在提交階段即可定位的一般建置錯誤。
常見問題
為什麼 SwiftPM 套件能成功編譯,執行時卻仍找不到資源?
編譯成功不代表執行時名稱、路徑大小寫與最終套件位置正確。應直接檢查產生的 .app 以及其中的資源 .bundle。
資源驗收應檢查原始碼目錄還是最終成品?
兩者都要檢查。原始碼檢查可找出未宣告檔案,成品檢查則確認資源已正確處理並嵌入實際交付內容。
將經驗驗證的工作流程部署至獨享實體節點
依工作選擇 M4 或 M4 Pro、目標節點與計費週期,下單前確認設定與附加選項。