同一份仓库在开发机上正常,在远端构建任务里却报 Module not found,或者打包后少了一份资源。先不要反复清缓存。macOS 常见的默认文件系统不区分文件名大小写,Config.json 与 config.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 环境。