同一個 Swift 套件在開發者電腦上能正常產生程式碼,移到雲端 Mac 的乾淨工作區後卻出現 Operation not permitted,也可能建置雖然成功,實際編譯的仍是舊檔案。這通常不是機器效能問題,而是建置工具外掛依賴了未宣告的檔案、寫入了錯誤目錄,或問題被本機殘留的產物掩蓋。排查重點不在於反覆清除快取,而是逐項還原外掛的輸入、輸出與執行邊界。
先確認失敗發生在哪一層
SwiftPM 建置工具外掛涉及三層:SwiftPM 負責規劃命令,外掛的可執行檔負責產生內容,再由 Xcode 使用產生的檔案。先從儲存庫根目錄保留完整日誌:
set -o pipefail
rm -rf .ci
mkdir -p .ci
xcodebuild \
-scheme App \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath "$PWD/.ci/DerivedData" \
-clonedSourcePackagesDirPath "$PWD/.ci/SourcePackages" \
build 2>&1 | tee .ci/build.log
grep -E 'sandbox|deny|plugin|Operation not permitted|No such file' .ci/build.log
如果日誌顯示完全找不到外掛工具,請檢查它是否已宣告為套件 target 的可執行工具;如果工具啟動後才失敗,就繼續檢查讀寫路徑。若建置成功但產生的內容沒有更新,則應優先核對輸入與輸出宣告。
不要一開始就把關閉沙盒當成修正方式。沙盒拒絕通常能準確指出產生器正在讀取或改寫建置圖之外的內容;繞過限制只會把問題延後到下一次乾淨建置。
將輸入與輸出納入建置圖
穩健的外掛只會讀取明確宣告的輸入,並將結果寫入 pluginWorkDirectory。以下結構會讓 SwiftPM 同時追蹤 schema 與產生的 Swift 檔案:
import PackagePlugin
@main
struct CodegenPlugin: BuildToolPlugin {
func createBuildCommands(
context: PluginContext,
target: Target
) async throws -> [Command] {
let tool = try context.tool(named: "schema-gen")
let input = target.directory.appending("Schemas/api.json")
let output = context.pluginWorkDirectory
.appending("Generated/API.swift")
return [
.buildCommand(
displayName: "Generate API.swift",
executable: tool.path,
arguments: [input.string, output.string],
inputFiles: [input],
outputFiles: [output]
)
]
}
}
產生器啟動後應自行建立 Generated 父目錄,而且只寫入傳入的輸出路徑。不要預設寫入儲存庫的 Sources、桌面、使用者家目錄或固定的 /Users/...。建置工具外掛的用途,是產生會參與本次編譯的衍生檔案;如果工作必須修改儲存庫內容,應改用由開發者明確執行的命令外掛,而不是在每次建置時暗中改寫原始碼。
識別隱含依賴
常見的隱含輸入包括目前工作目錄內的設定檔、由環境變數指定的範本、家目錄中的快取,以及產生器自動掃描的整個資料夾。請逐項回答以下四個問題:
| 檢查項目 | 正確做法 | 風險訊號 |
|---|---|---|
| 輸入檔案 | 全部列入 inputFiles |
執行時遞迴掃描未宣告的目錄 |
| 輸出檔案 | 全部列入 outputFiles |
檔名隨時間或機器而變 |
| 工作路徑 | 使用透過參數傳入的絕對路徑 | 依賴 pwd 或家目錄 |
| 產生順序 | 排序後穩定輸出 | 依賴檔案系統的列舉順序 |
讓產生器本身維持確定性
即使權限設定正確,產生器仍可能導致增量建置持續失效。最常見的原因,是在檔案開頭寫入目前時間、暫存目錄或主機名稱。相同輸入每次都產生不同位元組時,SwiftPM 就無法判斷變更是否真實存在。
在雲端 Mac 上連續執行兩次乾淨產生,然後比較摘要:
rm -rf .ci/first .ci/second
mkdir -p .ci/first .ci/second
.build/debug/schema-gen \
Schemas/api.json .ci/first/API.swift
.build/debug/schema-gen \
Schemas/api.json .ci/second/API.swift
shasum -a 256 .ci/first/API.swift .ci/second/API.swift
cmp .ci/first/API.swift .ci/second/API.swift
兩個摘要必須一致。產生器也應對欄位、檔案與宣告進行排序,統一換行字元,並只在內容變更時取代目標檔案。可以先寫入暫存檔,比較後再以原子方式移動,避免建置中斷時留下不完整的 Swift 檔案。
使用乾淨工作區重現快取問題
本機成功不代表宣告完整。本機 DerivedData、舊的外掛輸出,或過去手動產生的原始碼,都可能讓遺漏的宣告暫時無法顯現。雲端建置應至少執行一次不含任何歷史狀態的驗收,再執行第二次增量建置。
兩輪驗收法
第一輪先刪除專用建置目錄再進行建置,確認所有產生檔案都由外掛建立。第二輪在不修改輸入的情況下再次建置,確認外掛不會無故重寫輸出。接著只修改一個 schema 欄位並執行第三輪,確認對應的產生檔案確實更新。
可以使用以下檢查快速找出產生檔案寫入了錯誤位置:
find "$PWD" -type f -name 'API.swift' -print
git status --short
正常情況下,衍生檔案會位於建置目錄,git status 不應顯示外掛自動改寫了受版本控制的原始碼。若第二輪仍會重複執行,請檢查輸出是否確實存在、工具是否強制更新時間戳記,以及是否有某個輸入目錄宣告得過於寬泛。
固化雲端 Mac 的驗收邊界
在 HireVM 的遠端建置環境或其他乾淨的 macOS 節點上,建議將以下檢查納入流水線,而不是依賴人工觀察:
- 固定 Xcode 的選擇結果,並在日誌開頭記錄
xcodebuild -version與swift --version。 - 為每個工作區設定獨立的 DerivedData 與套件簽出目錄,避免平行工作彼此交叉寫入。
- 第一次建置從空目錄開始,驗證外掛沒有依賴家目錄中的歷史檔案。
- 保留外掛失敗前後的詳細建置日誌,但不要將權杖、簽署資料或完整環境變數寫入日誌。
- 對關鍵產生檔案執行摘要比較,確認同一個提交能夠重複產生相同結果。
- 修改單一輸入後重新執行,確認增量建置只更新預期輸出。
- 在控制台確認目前可選的設定,並依據同時建置的數量選擇資源;外掛正確性檢查不應依賴某一台機器的偶然狀態。
最終目標不是讓某次建置「碰巧通過」,而是讓 SwiftPM 完整理解產生步驟:輸入變更時準確重建,輸入未變時保持安靜,切換到新的雲端 Mac 工作區後仍能得到相同產物。做到這一點後,沙盒不再是阻礙,而是用來驗證建置邊界是否真實且完整的一道自動檢查。
常見問題
SwiftPM 建置工具外掛可以直接修改原始碼目錄嗎?
不建議如此設計。產生檔應寫入外掛工作目錄並列入 outputFiles;需要修改儲存庫內容的操作,應拆成由開發者主動執行的命令外掛。
為什麼外掛在本機成功,在雲端 Mac CI 卻遭沙盒拒絕?
本機殘留檔案可能掩蓋未宣告的輸出,產生器也可能依賴家目錄、目前目錄或絕對路徑。以乾淨檢出和固定建置目錄執行,才能顯示真正依賴。
關閉沙盒能否作為修正方式?
不應作為長期方案。應完整宣告輸入輸出、僅寫入授權目錄,並確保相同輸入每次產生位元組一致的結果。
將下一項遠端 Mac 任務部署到專用節點執行
依任務規模選擇兩種在售配置、五個節點,以及日租、週租、月租和季租週期。下單前即可完整核對配置與 USD 金額。