Oakmini Engineering Notes

用區分大小寫的 APFS 卷宗檢查建置路徑問題

CI/CD 實踐 ·約 7 分鐘閱讀

用區分大小寫的 APFS 卷宗檢查建置路徑問題

同一份儲存庫在開發機上運作正常,到了遠端建置工作卻出現 Module not found,或是封裝後少了一份資源。這時先別反覆清除快取。macOS 常見的預設檔案系統不區分檔名大小寫,因此 Config.jsonconfig.json 可能會被視為同一路徑;一旦指令碼、相依套件或部署目標嚴格比對大小寫,原本被掩蓋的問題才會浮現。最穩妥的排查方式不是修改系統碟,而是在雲端 Mac 上掛載一個隔離、區分大小寫的 APFS 卷宗。

先確認問題是否與路徑大小寫有關

先從失敗記錄中找出相關路徑,不要只看最後一行的結束代碼。遇到下列現象時,應優先檢查:

現象 可能原因 驗證方式
本機可以匯入,建置工作卻找不到模組 import 路徑與實際檔名不同 使用 git ls-files 核對完整路徑
提交兩份資源後只剩一份 名稱只有大小寫不同 在區分大小寫的卷宗中重新複製儲存庫
清理後第一次建置失敗 舊產物掩蓋了錯誤引用 刪除輸出目錄後執行完整建置
封裝階段覆寫檔案 指令碼統一轉換了名稱 檢查複製與重新命名步驟

同時執行 diskutil info /,記錄目前系統卷宗的 File System Personality。這個步驟只用來確認環境,不要直接轉換正在執行中的系統卷宗。

路徑問題必須在乾淨的工作區中重現。若把舊快取複製到測試卷宗,原始問題可能會再次被掩蓋。

建立隔離的 APFS 測試卷宗

稀疏映像檔會隨實際寫入量成長,很適合用於臨時檢查。以下會建立容量上限為 80GB 的測試卷宗;若專案包含大型相依套件,應根據儲存庫、相依套件與建置產物的用量峰值預留空間。

mkdir -p "$HOME/apfs-lab"
hdiutil create \
  -size 80g \
  -type SPARSEBUNDLE \
  -fs "Case-sensitive APFS" \
  -volname BuildCase \
  "$HOME/apfs-lab/BuildCase.sparsebundle"

hdiutil attach "$HOME/apfs-lab/BuildCase.sparsebundle"
diskutil info "/Volumes/BuildCase"

確認輸出內容明確顯示區分大小寫,並確認掛載點為 /Volumes/BuildCase。不要將存取金鑰、簽章資料或需要長期保存的資料寫入這個臨時卷宗。

在新卷宗中重新取得儲存庫

不要直接從原工作區拖曳複製,因為發生衝突的檔案可能早已遺失。請從受控儲存庫重新複製,並建立獨立的建置目錄:

mkdir -p /Volumes/BuildCase/work
cd /Volumes/BuildCase/work
git clone "$REPOSITORY_URL" project
cd project
git status --short

此時 git status 應該沒有任何輸出。如果在複製階段就出現目標路徑已存在的錯誤,通常表示儲存庫樹狀結構中存在忽略大小寫後名稱重複的路徑。

稽核 Git 檔名與指令碼引用

以下指令碼只會讀取 Git 追蹤的路徑,將其轉換為 Unicode 正規形式,再依不區分大小寫的結果分組。它不會修改任何檔案:

from collections import defaultdict
import subprocess
import unicodedata

raw = subprocess.check_output(
    ["git", "ls-files", "-z"],
    text=True
)
groups = defaultdict(list)

for path in raw.split(""):
    if not path:
        continue
    key = unicodedata.normalize("NFC", path).casefold()
    groups[key].append(path)

found = False
for paths in groups.values():
    if len(paths) > 1:
        found = True
        print("COLLISION")
        for path in paths:
            print(f"  {path}")

raise SystemExit(1 if found else 0)

將檔案儲存為 tools/check_path_case.py,再執行 python3 tools/check_path_case.py。結束代碼為 1 時,應統一命名並透過 git mv 提交變更。若只修改字母大小寫,可以先改成過渡名稱,再改為目標名稱,避免原工作區忽略這項變更:

git mv Sources/config.json Sources/config.tmp
git mv Sources/config.tmp Sources/Config.json

檔名沒有衝突,不代表引用一定正確。接著搜尋建置指令碼、資源清單、專案設定與測試固定資料中的舊路徑,尤其要留意自動產生檔案與手動編寫設定之間的差異。

將檢查納入可重現建置流程

測試工作應先驗證掛載點,再執行路徑稽核,最後從空目錄開始建置。這樣可以避免測試卷宗未掛載時,檔案意外寫回系統碟。

set -euo pipefail

test -d /Volumes/BuildCase
cd /Volumes/BuildCase/work/project
python3 tools/check_path_case.py

rm -rf .build-output
mkdir .build-output
./scripts/build.sh "$PWD/.build-output"

如果專案沒有統一的建置進入點,請先將現有命令封裝到 scripts/build.sh,並明確傳入輸出目錄。相依套件快取可以另外掛載,但第一次驗證時應停用舊快取;通過後再逐項恢復,藉此判斷是哪一層引入了錯誤路徑。

常見陷阱與收尾檢查

完成排查前,請逐項確認:

  • 儲存庫是在測試卷宗中重新複製,而不是從舊目錄拷貝。
  • 檔名修正使用 git mv,而且提交記錄中能清楚看到重新命名。
  • 指令碼沒有強制將路徑全部轉成小寫或大寫。
  • 資源複製命令遇到同名目標時會失敗,而不是無聲覆寫。
  • 建置產物、相依套件目錄與記錄檔都位於預期的掛載點。
  • 乾淨建置與增量建置各執行一次,而且結果一致。

驗證完成後,先匯出必要的記錄,再卸載測試卷宗:

hdiutil detach "/Volumes/BuildCase"

確認資料不需保留後,才能刪除 BuildCase.sparsebundle。若要長期將這項檢查作為品質閘門,應保留建立卷宗的指令碼,而不是保留測試資料;每次工作開始時,也應檢查卷宗格式、剩餘容量與掛載路徑。

常見問題

需要把雲端 Mac 的系統碟改成區分大小寫嗎?

不需要。較安全的方式是建立 APFS 稀疏映像並掛載成獨立測試卷宗,只放入儲存庫與建置目錄,完成後直接卸載。

為什麼 Git 沒有直接回報檔名大小寫衝突?

Git 會保留路徑寫法,但不區分大小寫的工作目錄可能把兩個路徑映射到同一檔案。應在測試卷宗重新複製並比較折疊後的路徑。

需要獨享的 Mac mini 實體節點

查看可用的設定、節點與固定計費週期,將文章中的步驟實際應用於可長期使用的雲端 Mac 環境。

立即租用 Mac mini