古董级程序员,大厂出来后一直在创业公司,现在还在一线写 AI 相关的后端。曾经是CSDN论坛版主、MVP
文中出现的具体服务名、包名、仓库名等均为脱敏后的示例,与线上真实命名无对应关系,仅便于说明流程。
内部代码多了以后,总会遇到「同一份工具库被三四个服务引用」的情况。最省事的做法是 git submodule,但两年下来,submodule 的副作用就暴露出来了:构建镜像要 git、CI 要多 checkout 一层、本地 clone 还要记得 --recursive、换分支时还容易让 submodule 停在一个奇怪的 SHA 上。
这篇把我们把公共库从 submodule 迁到私有 PyPI 的过程拆开写一遍,包括:
pypiserver怎么装、怎么配认证;- PEP 440 的版本号写法,选哪种才不会把
uv/pip的解析器搞糊涂; - 发布端:
setuptools-scm+ 嵌入_version.py,解决 Docker 里没git也能编辑安装的问题; - 消费端:
uv接私服的正确姿势,以及日常用得上的几个进阶命令; - Docker BuildKit secret 的两种写法、CI 把凭证喂进 build 时常见的失败模式。
下面这张图就是整条链路的缩影——发布端把包推上去,中间是一台带认证的 pypiserver,消费端按 uv.lock 拉版本:

为什么不再用 submodule
先把两种做法放在同一标准下比较。
如果算的是「公共库发了一版新的,下游各自要在自己的仓库里跟上」,无论 submodule 还是私服,都是 N 个下游、N 次提交,私服不会把 N 变成 1。lock 该更新还是要更新,PR 该开还是要开。不把这层说透,容易让人误以为迁到私服就不用再动依赖;实际上要动还是要动,差别在于改动落在哪、和谁绑定。
版本最好只跟 lock 走,不要跟「谁今天有没有拉公共库」走。 Submodule 也是钉到一个 SHA,但它和日常 pull、切分支、submodule update 缠在一起;人多习惯杂,两个环境指着不同 SHA,有时是故意保留旧版,有时是忘了同步,事后很难一眼分清。私服这边,索引里是已经打好的 wheel/sdist;运行时到底装哪一版,只看当前分支的 uv.lock,以及 CI 有没有用 --frozen 严格按 lock 安装。没有发布新版本、没有改 lock,环境不会因为上游多推了几个 commit 就自己变掉。
排查问题时,版本号加 lock 的 diff 比两行 submodule 指针好读。 两个服务行为不一致,翻 PR:shared-lib 从 1.0.1.post… 升到 1.0.5.post0 一眼能看清。换成两个 submodule 的 hash 差一截,中间夹了哪些变更,还要各自进子仓库查 git log。
本地、CI、镜像,尽量装同一份发布产物。 Submodule 是「目录即依赖」;镜像里少 copy 一层、或者 editable 安装和实际 wheel 内容不一致,本地和线上可能早就不是同一套文件。私服是一条线:构建产物进索引,uv sync 拉的就是那份包,少一层「装的究竟是源码树还是打好的包」的猜测。
上游误推了不兼容改动,不该让还没准备好的下游自动跟上。 公共库默认分支上先合并了破坏性变更,submodule 只要有人更新指针就可能立刻用上。私服路径下,旧环境仍按旧 lock 解析;要用新版,得有人发布新版本,再有人执行 uv lock --upgrade-package 并把变更合进主干——门槛在可审查的 PR 里,而不是跟着别人的推送节奏走。
所以「下游自己决定何时升级」,核心不是省几次操作,而是发布(产物进入索引)与采纳(改 lock、合并代码)分开了:升级次数不会少,但每次升级在 diff 里有据可查,也少几种「到底是谁的 submodule 停在哪个 SHA」扯不清的情况。
先把 pypiserver 跑起来
我们用 pypiserver 作为私服。它的定位很克制:一个 WSGI 应用,托管本地目录里的 .whl / .tar.gz,支持 htpasswd 基础认证,够团队内部用。想要更花哨的(细粒度权限、审计、前端 UI)可以上 devpi 或 Nexus,但代价是运维复杂度也跟着涨。
一份够用的 docker-compose
在一台内网机器上建目录:
mkdir -p ~/pypi/{auth,packages}
cd ~/pypi
用 htpasswd 生成一个账号(系统没这命令就 apt install apache2-utils / brew install httpd):
htpasswd -sc auth/.htpasswd uploader
# 输入并确认密码
-s 走 SHA1(passlib 能读),-c 是「创建」——第二次加用户别带 -c,否则会把文件覆盖。
然后写 docker-compose.yml:
services:
pypiserver:
image: pypiserver/pypiserver:latest
restart: always
ports:
- "8080:8080"
volumes:
- ./auth:/data/auth:ro
- ./packages:/data/packages
# 上传/下载/列表 三类操作都要认证;想让 pull 对内网匿名就去掉 download,list
command


4369

被折叠的 条评论
为什么被折叠?



