Xcode 的 project.pbxproj 檔案採用文字格式儲存專案結構,但 Xcode 在新增檔案或修改設定時,不保證項目的插入順序一致。多人協作時,即使修改不同的檔案或 target,也可能因為項目順序差異而產生 merge conflict。這些衝突往往與實際變更無關,純粹是格式問題。
我的解決方案
將 project.pbxproj 中的各個區塊按固定規則排序,確保相同內容產生相同的檔案結構。配合 git pre-commit hook,每次提交前自動排序,團隊成員的專案檔就能維持一致的順序,大幅降低無意義的衝突。
我開發了一個腳本工具來執行這個任務,也已經在多個專案上跑了好幾年,GitHub repo 放在這裡:https://github.com/chiahsien/sort-Xcode-project-file
雖然目前推崇使用 Swift Package Manager 進行模組化,Xcode 16 也引入了 buildable folders 功能來減少專案檔變更,甚至也有 Tuist 或 Xcode Gen 這類的工具來生成專案檔,但這些新技術主要針對新專案或願意大幅重構的專案。對於已經開發多年、結構複雜的舊專案,貿然改用 SPM 模組化或轉換成 folder references 風險過高。此時,這個排序工具仍是最務實的選擇,能以最小成本解決 merge conflict 問題。
排序範圍
此工具排序以下 array 結構:
children— group 內的檔案與 subgroup(目錄排在檔案前面)files— build phase 的檔案列表buildConfigurations— build configuration 列表targets— 專案 target 列表packageProductDependencies— Swift Package product dependencypackageReferences— Swift Package reference
安全性
這些 array 是宣告性內容,Xcode 透過 24 字元的十六進位 ID 參照物件,而非依賴位置。排序不影響建置行為或專案結構,僅改變檔案內的呈現順序。
不排序的區塊:PBXFrameworksBuildPhase section 的 framework 連結順序會影響符號解析,工具會偵測到這個區塊並完整保留原始順序。其餘不在上述排序範圍內的 array(例如 buildPhases)則不會被處理,同樣維持原始順序。
寫入方式採用原子操作(透過暫存檔 + os.replace()),即使過程中發生錯誤,也不會留下損壞的 .pbxproj 檔案。
警告:
雖然這個工具已經在多個不同專案執行很長一段時間了,我還是強烈建議在修改之前先做好備份,才不會出現難以挽回的錯誤!
與原版的差異
本版本是 WebKit 專案的 fork,以 Python 3 重寫並新增以下功能:
- Natural sorting:數字部分按數值比較,
file2.m排在file10.m前面,符合人類直覺 - Case-insensitive 選項:提供
--case-insensitive參數支援不分大小寫排序,預設仍為 case-sensitive 以保持原始行為 - 目錄優先排序:
childrenarray 中目錄排在檔案前面,符合檔案系統慣例 - 自動去除重複:移除重複的項目 reference
- 擴充排序範圍:包含所有
childrenarray、filesarray、targets列表、packageProductDependencies與packageReferences - CI 檢查模式:
--check參數可檢查檔案是否已排序,不修改檔案,適合整合到 CI pipeline - 遞迴搜尋:
-r參數可遞迴搜尋目錄下所有project.pbxproj並排序,適合 monorepo - Stdin/stdout 支援:使用
-參數可從 stdin 讀取、寫到 stdout,方便管線操作 - 原子寫入:透過暫存檔 +
os.replace()確保寫入過程不會損壞原始檔案
使用方法
基本呼叫
python3 sort-Xcode-project-file.py path/to/Project.xcodeproj
腳本會自動找到 project.pbxproj 並就地排序。也可以直接指定 project.pbxproj 檔案路徑。
選項
# 使用 case-insensitive sorting
python3 sort-Xcode-project-file.py --case-insensitive Project.xcodeproj
# CI 檢查模式:exit 0 = 已排序,exit 1 = 未排序(不修改檔案)
python3 sort-Xcode-project-file.py --check Project.xcodeproj
# 遞迴搜尋目錄下所有 project.pbxproj 並排序
python3 sort-Xcode-project-file.py -r .
# 從 stdin 讀取,寫到 stdout
cat project.pbxproj | python3 sort-Xcode-project-file.py - > sorted.pbxproj
# 抑制 warning 訊息
python3 sort-Xcode-project-file.py -w Project.xcodeproj
# 顯示版本號
python3 sort-Xcode-project-file.py --version
# 顯示說明
python3 sort-Xcode-project-file.py --help
Git Pre-commit Hook 整合
在專案根目錄建立 Scripts 目錄,將 sort-Xcode-project-file.py 放進去,然後建立 .git/hooks/pre-commit:
#!/bin/sh
echo 'Sorting Xcode project files'
GIT_ROOT=$(git rev-parse --show-toplevel)
sorter="$GIT_ROOT/Scripts/sort-Xcode-project-file.py"
git diff --name-only --cached | grep "project.pbxproj" | while IFS= read -r filePath; do
fullFilePath="$GIT_ROOT/$filePath"
python3 "$sorter" "$fullFilePath"
git add "$fullFilePath"
done
echo 'Done sorting Xcode project files'
記得設定執行權限:
chmod +x .git/hooks/pre-commit
另外可以在 .gitattributes 加上以下設定,進一步減少合併衝突:
*.pbxproj merge=union
注意:
merge=union會讓 Git 自動保留衝突的雙方內容。搭配排序工具使用效果很好,但如果專案檔沒有經過排序,可能會產生無效的結果。請確保團隊成員都有使用這個排序工具。