HireVM 工程紀錄

雲端 Mac 的 SwiftPM 建置工具外掛沙盒排查實戰

雲端 Mac 的 SwiftPM 建置工具外掛沙盒排查實戰

同一個 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 節點上,建議將以下檢查納入流水線,而不是依賴人工觀察:

  1. 固定 Xcode 的選擇結果,並在日誌開頭記錄 xcodebuild -versionswift --version
  2. 為每個工作區設定獨立的 DerivedData 與套件簽出目錄,避免平行工作彼此交叉寫入。
  3. 第一次建置從空目錄開始,驗證外掛沒有依賴家目錄中的歷史檔案。
  4. 保留外掛失敗前後的詳細建置日誌,但不要將權杖、簽署資料或完整環境變數寫入日誌。
  5. 對關鍵產生檔案執行摘要比較,確認同一個提交能夠重複產生相同結果。
  6. 修改單一輸入後重新執行,確認增量建置只更新預期輸出。
  7. 在控制台確認目前可選的設定,並依據同時建置的數量選擇資源;外掛正確性檢查不應依賴某一台機器的偶然狀態。

最終目標不是讓某次建置「碰巧通過」,而是讓 SwiftPM 完整理解產生步驟:輸入變更時準確重建,輸入未變時保持安靜,切換到新的雲端 Mac 工作區後仍能得到相同產物。做到這一點後,沙盒不再是阻礙,而是用來驗證建置邊界是否真實且完整的一道自動檢查。

常見問題

SwiftPM 建置工具外掛可以直接修改原始碼目錄嗎?

不建議如此設計。產生檔應寫入外掛工作目錄並列入 outputFiles;需要修改儲存庫內容的操作,應拆成由開發者主動執行的命令外掛。

為什麼外掛在本機成功,在雲端 Mac CI 卻遭沙盒拒絕?

本機殘留檔案可能掩蓋未宣告的輸出,產生器也可能依賴家目錄、目前目錄或絕對路徑。以乾淨檢出和固定建置目錄執行,才能顯示真正依賴。

關閉沙盒能否作為修正方式?

不應作為長期方案。應完整宣告輸入輸出、僅寫入授權目錄,並確保相同輸入每次產生位元組一致的結果。

專用 Apple Silicon 實體節點

將下一項遠端 Mac 任務部署到專用節點執行

依任務規模選擇兩種在售配置、五個節點,以及日租、週租、月租和季租週期。下單前即可完整核對配置與 USD 金額。

選擇配置並下單