遠端雲端 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 宣告為輸出。把產生的檔案放在衍生目錄中,比直接修改儲存庫檔案更容易確定歸屬,也能降低平行工作彼此覆寫輸出的風險。
以最小變更完成驗收
每次只修改一項相依性、一個階段或一條輸出路徑,然後重新儲存日誌。一次刪除多個參照雖然可能讓建置通過,卻很難確認真正的根本原因,也可能漏掉封存設定中的同類問題。
修正後依照以下順序驗收:
- 執行一次乾淨建置,確認完整的圖可以從零開始建立。
- 不進行清理,連續建置兩次,確認指令碼能正確略過未變更的輸入。
- 分別檢查 Debug 與 Release;發布用專案還應執行一次 Archive。
- 比較兩次日誌,確認沒有新的重複輸出或無條件執行指令碼的警告。
- 在程式碼審查說明中記錄 Target 相依性調整、階段輸入與輸出,以及變更原因。
清理 DerivedData 可以作為最後的隔離測試,但不能當作修正結論。真正穩定的 Xcode 專案,應在不同平行度、乾淨建置與增量建置下得到相同的相依順序;如此一來,雲端 Mac 上的互動式開發與無人值守工作才不會因排程變化而隨機失敗。
常見問題
刪除 DerivedData 能修復 Xcode 建置循環嗎?
通常不能。刪除 DerivedData 只會移除舊的中間產物,Target 相依與重複輸出造成的循環仍會在下一次建置出現。
Run Script 階段為何容易造成建置圖問題?
腳本若讀取其他階段的產物卻未宣告輸入,或與 Copy Files 寫入相同路徑,Xcode 就無法建立穩定的執行順序。
修復後應如何驗收?
執行一次乾淨建置與兩次未清理的連續建置,確認循環、重複輸出及腳本每次無條件執行的警告均已消失。
將下一項遠端 Mac 任務部署到專用節點執行
依任務規模選擇兩種在售配置、五個節點,以及日租、週租、月租和季租週期。下單前即可完整核對配置與 USD 金額。