HireVM 工程紀錄

雲端 Mac 診斷 Xcode 建置圖循環與重複產物

雲端 Mac 診斷 Xcode 建置圖循環與重複產物

遠端雲端 Mac 上的 Xcode 建置突然出現 Cycle inside ...Multiple commands produce ... 時,最容易做錯的第一步就是立刻刪除 DerivedData。這樣做或許會暫時改變錯誤出現的順序,卻也會同時清除定位問題所需的線索。更可靠的做法是固定程式碼、Scheme、設定與 Xcode 路徑,保留完整日誌,再判斷問題屬於 Target 相依循環,還是多個 Build Phase 爭用同一個輸出路徑。

先穩定重現故障

開始排查前,先暫停自動拉取程式碼、相依套件升級與平行建置,避免工作區在兩次測試之間發生變化。記錄目前的提交、Xcode 版本,以及實際選用的開發者目錄:

set -o pipefail
git rev-parse HEAD
xcodebuild -version
xcode-select -p

mkdir -p .diagnostics
xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -configuration Debug \
  -destination 'generic/platform=iOS Simulator' \
  -showBuildTimingSummary \
  build 2>&1 | tee .diagnostics/build.log

如果專案使用 Workspace,請將 -project App.xcodeproj 替換為 -workspace App.xcworkspace,不要同時傳入兩者。接著擷取關鍵位置,但仍應保留原始日誌,以便核對上下文:

grep -nE \
  'Cycle inside|cycle in dependencies|Multiple commands produce|will be run during every build' \
  .diagnostics/build.log

日誌中的最後一條命令通常只是發現衝突的位置,不一定是造成循環的起點。應沿著 Xcode 輸出的相依鏈往上追查,找出第一個重複出現的 Target、指令碼或產物路徑。

區分兩類看似相近的錯誤

相依循環表示 A 必須等待 B,而 B 又直接或間接地等待 A。重複產物則表示兩個命令都宣告自己負責同一個檔案。兩者可能同時出現,但修正方式並不相同。

日誌特徵 優先檢查 常見根因
Cycle inside Target Dependencies、隱含相依性 App 與 Framework 互相依賴
Multiple commands produce Copy Files、Compile Sources、Run Script 同一個檔案被複製或產生兩次
指令碼每次都執行 Run Script 輸入與輸出 未宣告相依性,或停用相依性分析
只有 Archive 失敗 Embed 階段、封存指令碼 Debug 與 Release 的階段設定不一致

先用命令確認這次實際建置的 Target 與設定,不要根據 Xcode 介面中目前顯示的選項猜測:

xcodebuild -list -json -project App.xcodeproj \
  > .diagnostics/project-list.json

xcodebuild -showBuildSettings \
  -project App.xcodeproj \
  -scheme App \
  -configuration Debug \
  > .diagnostics/build-settings.txt

沿著 Target 與 Build Phase 追查相依鏈

在 Xcode 的 Target Dependencies 中,從錯誤鏈首尾涉及的 Target 開始檢查。App 可以依賴 Framework,但 Framework 不應為了存取 App 中的型別而反向依賴 App。共用程式碼應下移至獨立模組,或透過協定與注入解除反向參照。

接著逐一查看 Build Phases,並優先核對下列關係:

檢查重複歸屬

同一個原始碼檔案不應同時出現在兩個會產生相同目的檔的 Compile Sources 項目中。同一個 Framework 也不應既由系統產生的嵌入階段處理,又被自訂的 Copy Files 再複製一次。對於程式碼產生器,應明確指定產生目錄,避免將輸出寫回已由其他 Target 編譯的原始碼目錄。

檢查隱含相依性

如果指令碼透過固定路徑讀取另一個 Target 的建置產物,卻未明確宣告相依性,建置順序就可能隨平行度而改變。不要以停用平行建置來掩蓋問題;這只會讓錯誤更難觸發,並未修正圖結構。

明確宣告 Run Script 的輸入與輸出

Run Script 是相依循環的高發區域。當指令碼讀取設定檔、產生 Swift 檔案或複製資源時,應在該階段填寫 Input Files 與 Output Files,或使用檔案清單。指令碼本身也必須確保每項輸出只由單一階段負責。

例如,產生版本檔案的指令碼可以先驗證必要變數,再使用暫存檔案搭配不可分割替換:

set -euo pipefail

input="${SRCROOT}/Config/version.txt"
output="${DERIVED_FILE_DIR}/GeneratedVersion.swift"
tmp="${output}.tmp"

test -f "$input"
mkdir -p "$(dirname "$output")"

version="$(tr -d '
' < "$input")"
printf 'enum GeneratedVersion { static let value = "%s" }
' \
  "$version" > "$tmp"

if test -f "$output" && cmp -s "$tmp" "$output"; then
  rm "$tmp"
else
  mv "$tmp" "$output"
fi

對應階段應將 $(SRCROOT)/Config/version.txt 宣告為輸入,並將 $(DERIVED_FILE_DIR)/GeneratedVersion.swift 宣告為輸出。把產生的檔案放在衍生目錄中,比直接修改儲存庫檔案更容易確定歸屬,也能降低平行工作彼此覆寫輸出的風險。

以最小變更完成驗收

每次只修改一項相依性、一個階段或一條輸出路徑,然後重新儲存日誌。一次刪除多個參照雖然可能讓建置通過,卻很難確認真正的根本原因,也可能漏掉封存設定中的同類問題。

修正後依照以下順序驗收:

  1. 執行一次乾淨建置,確認完整的圖可以從零開始建立。
  2. 不進行清理,連續建置兩次,確認指令碼能正確略過未變更的輸入。
  3. 分別檢查 Debug 與 Release;發布用專案還應執行一次 Archive。
  4. 比較兩次日誌,確認沒有新的重複輸出或無條件執行指令碼的警告。
  5. 在程式碼審查說明中記錄 Target 相依性調整、階段輸入與輸出,以及變更原因。

清理 DerivedData 可以作為最後的隔離測試,但不能當作修正結論。真正穩定的 Xcode 專案,應在不同平行度、乾淨建置與增量建置下得到相同的相依順序;如此一來,雲端 Mac 上的互動式開發與無人值守工作才不會因排程變化而隨機失敗。

常見問題

刪除 DerivedData 能修復 Xcode 建置循環嗎?

通常不能。刪除 DerivedData 只會移除舊的中間產物,Target 相依與重複輸出造成的循環仍會在下一次建置出現。

Run Script 階段為何容易造成建置圖問題?

腳本若讀取其他階段的產物卻未宣告輸入,或與 Copy Files 寫入相同路徑,Xcode 就無法建立穩定的執行順序。

修復後應如何驗收?

執行一次乾淨建置與兩次未清理的連續建置,確認循環、重複輸出及腳本每次無條件執行的警告均已消失。

專用 Apple Silicon 實體節點

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

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

選擇配置並下單