2026年 07月 17日 的归档
代码库放 iCloud 文件夹会怎样?
之前我有个习惯,会把代码(整个repo)放到 iCloud 管理的 ~/Documents 下。一直觉得挺方便的,因为即使没有 commit+push,我到另一台电脑上也能接着干活,登了同一个iCloud账号的目录会自动同步。
但最近遇到一个特别邪门的问题。一个用 uv 管理的 Python 项目,editable 方式安装,前一个小时还好好的,突然就:
|
1 2 3 4 5 |
$ .venv/bin/mycli --help Traceback (most recent call last): File ".venv/bin/mycli", line 4, in <module> from mycli.cli import main ModuleNotFoundError: No module named 'mycli.cli' |
邪门在哪呢?同一个 venv、同一个解释器,pytest 跑起来一千六百多个用例全绿。测试说包在,入口脚本说包没了。去 site-packages 里看,editable 安装落下的 .pth 文件好好躺在那,内容就是一行指向源码目录的路径,权限 644,路径也真实存在。文件在、内容对、权限对,但它就是不生效。

排查:谁把我的 .pth 吞了
先补一句背景:editable 安装的机制,是往 site-packages 里放一个 .pth 文件,Python 启动时 site 模块会读它,把里面的路径追加进 sys.path。这个文件失效,包就从解释器眼里消失。
做了三个对照实验,结果非常有意思:
1. 自己写一个探针 .pth(内容随便指个存在的目录)放进同一个目录——生效;
2. 把出问题的 .pth 原样 cp 成另一个文件名——生效;
3. 出问题的那个文件本身——死活不生效。
同目录、同内容、同权限,副本能用原件不能用,那唯一的区别只剩下文件的元数据了。ls 加个 -O 把 BSD 文件标志打出来:
|
1 2 3 |
$ ls -lO .venv/lib/python3.12/site-packages/*.pth -rw-r--r--@ 1 me staff hidden 56 Jul 16 17:42 _editable_impl_mycli.pth -rw-r--r--@ 1 me staff hidden 18 Jul 16 17:45 _virtualenv.pth |
flags 一栏赫然写着 hidden——macOS 的 UF_HIDDEN 文件标志。cp 不会复制 BSD flags,所以副本是干净的,这就解释了实验 2。而 CPython 从 3.11 起,site.py 处理 .pth 时加了一条安全加固(防止恶意软件用隐藏 .pth 做隐蔽注入):
|
1 2 3 4 |
if ((getattr(st, 'st_flags', 0) & stat.UF_HIDDEN) or (getattr(st, 'st_file_attributes', 0) & stat.FILE_ATTRIBUTE_HIDDEN)): _trace(f"Skipping hidden .pth file: {fullname!r}") return |
带隐藏标志的 .pth,静默跳过,一个字的错都不报。两个各自都算合理的行为叠在一起,效果就是:文件在,import 没了。
那谁给文件打的 hidden?chflags nohidden 清掉,几秒钟之后再看,又变回 hidden 了——有个进程在实时跟我对抗。答案到这基本明了:这个仓库在 ~/Documents 底下,而这台 Mac 开着 iCloud 的“桌面与文稿”同步。把仓库挪出 Documents、重建 venv,flags 干干净净,问题再没复发。
iCloud Drive 到底在做什么
开了”桌面与文稿”同步之后,~/Documents 就不再是普通目录,而是由 fileproviderd 守护进程托管的同步空间。它主要干三类事:
1. 监听并上传每一次文件变更——它假设文件是”偶尔被人编辑的文档”;
2. 冲突消解——当它认为同一个文件出现两个竞争版本,不丢弃任何一边,而是生成”xxx 2″”xxx 3″这样的冲突副本;
3. 元数据管理——给托管文件打 xattr 和文件标志(上面那个 hidden 就是它干的),开了”优化 Mac 存储”还会把冷文件驱逐成无数据的占位符,读取时才按需下载。
对文档来说这些设计都挺好。但代码库不是文档。
代码库为什么全中
第一,写入模式冲突。开发工具链的写入是高频、批量、依赖原子性的:包管理器一次重写上万个小文件,Python 用”临时文件 + rename”做原子写,git 靠锁文件和 rename 更新引用。同步进程和这些写入异步竞争,竞争输了就落冲突副本。我这次在 site-packages 里就看到了 “_editable_impl_mycli 2.pth”、”3.pth”、”4.pth” 一窝副本,主 .pth 的内容更是被拼接成了四份路径首尾相连的乱码:
|
1 |
/Users/me/Documents/dev/mycli/src/Users/me/Documents/dev/mycli/src/Users/me/... |
第二,元数据篡改。就是前面 UF_HIDDEN 那一段,而且它是主动维护的,你清掉它还会打回来。
第三,驱逐。.git/objects 里的 packfile、venv 里的二进制,都可能被”优化存储”驱逐成占位符——在线时表现为构建随机卡顿,离线时表现为仓库”损坏”。
第四,git 自己的风险。.git 里的 index、refs 一旦出现冲突副本,仓库状态可能真损坏。git 本身就是分布式同步工具,外面再套一层文件级同步,等于双重同步,语义必然打架。
这次踩到的坑
1. chflags nohidden 修不好——几秒内被 fileproviderd 顶回来,别在这条路上浪费时间;
2. 症状自相矛盾极具误导性:pytest 全绿(它靠 rootdir 机制自己把源码目录塞进了路径),入口脚本却挂了,两个工具对”包是否存在”给出相反答案,直觉上会先怀疑一万个别的东西,最后才怀疑文件系统在说谎;
3. 损坏是异步、随机发生的:同一个 venv 一小时内坏了两次,中间检查全都正常——因为坏不坏取决于同步进程什么时候追上来;
4. 诊断口诀:文件明明在但 import 不到,先 ls -lO 看 flags 列。
结论
iCloud(以及 Dropbox、OneDrive 这些文件级同步盘,机制细节不同但三板斧一样)适合放终态文档:文稿、图片、表格。任何带衍生状态的目录——代码库、venv、node_modules、.git、构建缓存——都不该放进去。代码的跨机同步交给 git,这本来就是它的职责,而且语义正确。仓库放个非托管路径(比如 ~/dev)就好;实在有目录必须留在 iCloud 里又想排除,macOS 没有官方排除项,只有给目录名加 .nosync 后缀这个 hack。
一个按文档假设设计的同步器,遇上一堆违反它全部假设的文件,双方都没有 bug,组合在一起就是灾难。就此,完毕。