Monorepo 是应对跨子包频繁修改、版本对齐成本高、CI 耗时爆炸等痛点的止损方案;需统一依赖管理、确保本地与 CI 环境一致,并严守子包独立测试边界。
Monorepo 不是“更先进”,而是对特定规模和协作模式的项目更可控——当 Python 项目中出现跨子包频繁修改、版本对齐成本高、CI 耗时爆炸或本地开发反复 pip install -e 失败时,Monorepo 就不是可选项,而是止损点。
跨包依赖变更必须原子提交
比如你改了 mycore-utils 里的一个序列化函数签名,同时要同步更新 myapi-service 和 mydata-pipeline 的调用逻辑。在 Multirepo 下,你得发三个 PR、等三次 CI、手动对齐版本号、祈祷 PyPI 发布不卡住;而在 Monorepo 中,一次 git commit 就包含全部改动,CI 验证的是“整体是否还能跑通”,不是“某个包单独装得上吗”。
常见错误现象:
- Multirepo 中下游服务还在用
mycore-utils==1.2.0,但上游已发布1.3.0并删了旧接口,导致运行时报AttributeError - CI 流水线因某子仓库未触发构建,漏测兼容性问题
实操建议:
- 在根目录
pyproject.toml中启用工作区支持,例如 Poetry 使用[tool.poetry.workspace],Rye 使用[tool.rye.workspace] - 所有子包路径必须声明为工作区成员,否则
rye add或poetry add会误装 PyPI 版本而非本地路径 - 禁止在子包
pyproject.toml中写死requires-python或build-system,交由根目录统一接管
本地开发时依赖解析必须指向源码而非 PyPI
很多人以为只要 pip install -e ./pkg-a 就万事大吉,但实际会踩坑:如果 pkg-b 的 pyproject.toml 里写了 mycore-utils = "^2.0",pip 仍可能从 PyPI 拉取二进制包,而不是链接到你正在改的 ./libs/mycore-utils。
关键原因在于 pip 解析顺序优先级:PyPI > git+ssh > local path,除非显式声明。
实操建议:
- 用 Rye 时,确保
rye sync后检查.venv/pyvenv.cfg中home和include-system-site-packages设置是否合理 - 用 Poetry 时,在根目录运行
poetry install,而非进入子目录执行poetry install—— 后者会忽略 workspace 配置 - 验证是否生效:在 Python REPL 中执行
import mycore_utils; mycore_utils.__file__,路径必须指向项目内源码,不能是site-packages/mycore_utils
CI 构建必须复现本地环境一致性
Monorepo 的最大陷阱是:本地能过,CI 报 ModuleNotFoundError。根本原因通常是 CI 没有正确识别工作区结构,或者缓存了旧的 poetry.lock 导致依赖树错乱。
性能影响很直接:一个没清理干净的 poetry.lock 可能让 CI 多花 4 分钟重解依赖,而 Rye 默认不生成 lock 文件,又可能让不同节点装出不同 minor 版本。
实操建议:
- CI 中强制使用
rye sync --all(Rye)或poetry install --no-root(Poetry),跳过根包安装,只装 workspace 成员 - 把
poetry.lock提交进 Git,且每次poetry update后人工确认变更范围,避免间接依赖被静默升级 - 禁用 CI 缓存
~/.cache/pypoetry或~/.rye,改用 workspace-aware 的缓存策略,例如只缓存.venv目录但每次rye sync前校验pyproject.toml时间戳
真正难的不是配置工作区,而是守住“所有子包必须能独立测试、不隐式依赖其他子包的私有模块”这条线——一旦开始在 pkg-a 里 from pkg-b.internal import hack,Monorepo 就从工具退化成债务加速器。
就爱读