MiniDebug 工程筆記

雲端 Mac 上稽核 Xcode Run Script:輸入輸出與沙箱驗收

雲端 Mac 上稽核 Xcode Run Script:輸入輸出與沙箱驗收

雲端 Mac 上的專案從本機建置遷移到無人值守工作後,最容易被忽略的往往不是編譯器參數,而是 Build Phases 裡的 Run Script。腳本可能讀取儲存庫外的設定、寫入原始碼目錄,或因未宣告輸出而在每次建置時執行。單次執行通常看不出問題,但並行工作可能互相覆寫檔案,增量建置也會逐漸失去意義。處理這類故障時,應先明確定義每個腳本的檔案邊界,再啟用沙箱進行驗證,而不是先增加重試次數。

先建立可重現的稽核基準

先固定專案、Scheme、建置設定與 Derived Data 目錄。稽核期間不要重複使用日常建置目錄,否則舊產物可能讓缺漏的產生步驟看起來仍能正常運作。

set -euo pipefail

ROOT="$PWD"
DERIVED="$ROOT/.audit-derived"

rm -rf "$DERIVED"

xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -configuration Debug \
  -derivedDataPath "$DERIVED" \
  clean build | tee "$ROOT/audit-clean.log"

如果專案使用 Workspace,請將 -project 換成 -workspace。第一次建置用來確認完整流程,之後保持原始碼不變,再執行一次不含 clean 的建置。記錄 Run Script 的執行次數、總耗時與輸出檔案的修改時間。這兩份日誌分別代表乾淨建置與增量建置的基準。

稽核的目標不是讓所有腳本都略過執行,而是確保每個腳本只會在輸入發生變化、輸出缺失,或執行條件明確要求時執行。

盤點所有 Run Script 階段

先從專案檔案找出腳本,再回到 Xcode 確認它屬於哪個 Target、位於編譯前或編譯後,以及是否已啟用相依性分析。文字搜尋適合用來進行初步篩選:

grep -nE "PBXShellScriptBuildPhase|shellScript =|inputPaths =|outputPaths =" \
  App.xcodeproj/project.pbxproj

為每個階段建立表格,不要只記錄腳本名稱。腳本名稱經常只是「Run Script」,無法用於故障排查。

檢查項目 需要回答的問題
執行條件 每次都執行,還是只在相依項目變更時執行
輸入 會讀取哪些原始碼、設定、工具與檔案清單
輸出 產生的檔案、報告或完成標記會寫入何處
副作用 是否會修改原始碼、全域設定或共用快取
並行性 兩個工作同時執行時,是否會寫入同一路徑
失敗策略 子命令失敗後,是否會立即回傳非零狀態

建議在腳本開頭使用 set -euo pipefail。同時也要檢查透過管線串接的命令,因為若未啟用 pipefail,前一個命令的失敗可能會被最後成功執行的 tee 掩蓋。

使用 xcfilelist 宣告檔案邊界

路徑不多時,可以直接填入 Input Files 與 Output Files;檔案較多時,使用 .xcfilelist 會更容易審查。路徑應盡量以 $(SRCROOT)$(DERIVED_FILE_DIR) 等建置變數為基礎,避免寫死使用者目錄。

例如,一個根據 YAML 設定產生摘要的階段,可以使用以下輸入清單:

$(SRCROOT)/Config/app.yml
$(SRCROOT)/Scripts/generate-config.sh

輸出清單只應宣告腳本實際產生的檔案:

$(DERIVED_FILE_DIR)/Generated/config.sha256

對應的腳本應先將暫存檔案寫入目標目錄,再以原子方式取代最終檔案,避免並行讀取到尚未完成的產物:

set -euo pipefail

SOURCE="$SRCROOT/Config/app.yml"
OUTPUT="$DERIVED_FILE_DIR/Generated/config.sha256"
TEMP="$OUTPUT.tmp.$$"

mkdir -p "$(dirname "$OUTPUT")"
shasum -a 256 "$SOURCE" > "$TEMP"
mv "$TEMP" "$OUTPUT"

不要將整個儲存庫目錄籠統地宣告為輸入,也不要把原始碼根目錄當作輸出。過寬的範圍雖然可能消除錯誤,卻會讓任何檔案變更都觸發腳本,並掩蓋真正的相依關係。產生的檔案應優先寫入 Derived Data;若程式碼產生步驟確實需要寫回儲存庫,應獨立執行,並透過版本控制檢查差異。

啟用沙箱並分析拒絕日誌

完成第一輪宣告後,可以透過命令列覆寫建置設定進行驗收,無須一開始就修改所有設定:

xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -configuration Debug \
  -derivedDataPath "$PWD/.audit-derived" \
  ENABLE_USER_SCRIPT_SANDBOXING=YES \
  build | tee "$PWD/audit-sandbox.log"

若出現 sandbox deny,先找出遭拒絕的路徑、操作類型與對應階段。若腳本讀取了設定卻未宣告,應將其加入輸入;若建立或修改了未宣告的檔案,則應將其加入輸出。工具本身讀取系統執行階段程式庫時,通常不需要把整個系統目錄加入清單;重點是檢查腳本明確存取的專案檔案、設定檔與自建工具路徑。

常見的錯誤做法是直接關閉沙箱,或將使用者主目錄加入輸入範圍。前者會讓隱性相依繼續存在,後者則會造成不可控制的重建。若腳本依賴儲存庫外的檔案,應先將該檔案複製到工作的工作目錄,完成驗證後再將其作為明確輸入。

將增量與並行驗收納入 CI

修正後至少執行四組測試:使用空白 Derived Data 的乾淨建置、原始碼不變時的第二次建置、修改單一已宣告輸入後的建置,以及使用兩個獨立 Derived Data 目錄的並行建置。並行工作應共用原始碼的唯讀副本,但不得共用輸出目錄。

驗收時依照以下順序檢查:

  1. 乾淨建置可以從零開始產生所有必要產物。
  2. 第二次建置不會無條件執行已有穩定輸出的腳本。
  3. 修改已宣告的輸入後,對應階段會重新執行。
  4. 修改無關檔案不會觸發該階段。
  5. 兩個並行工作不會覆寫同一個暫存檔案或報告。
  6. 腳本失敗時,xcodebuild 會回傳非零狀態。
  7. 日誌不會輸出權杖、私鑰內容或完整的環境變數。

最後,將沙箱設定寫入團隊實際使用的建置設定,並保留一項定期執行的乾淨建置工作。增量建置負責速度,乾淨建置負責找出遺漏的相依關係;只有兩者都能通過,Run Script 才算具備可重複執行的明確邊界。

常見問題

為什麼 Run Script 每次建置都會執行?

通常是沒有宣告輸出檔案,或關閉了依賴分析執行條件。補上穩定的輸入與輸出路徑後,Xcode 才能判斷是否需要重新執行。

啟用腳本沙箱後出現 deny 紀錄該怎麼辦?

先確認被拒絕的路徑與讀寫方向,再把必要路徑加入 Input Files、Output Files 或對應的 File List,不應以關閉沙箱取代修正。

如何驗證增量建置仍然正確?

連續執行兩次相同建置,第二次應略過腳本;修改一個已宣告輸入後,腳本應重新執行並更新輸出,其他情況則保持穩定。

獨享實體 Mac

把下一次 iOS 建置放到 MiniDebug M4 上。

固定提供 M4、16GB RAM 與 256GB SSD,可按日、週、月或季租用,並可從新加坡、日本東京、韓國首爾、香港、美国東部五個節點中選擇。實際可用狀態以控制台即時回傳為準。

選擇節點並訂購