自建私有 PyPI:pypiserver + uv 实战笔记

古董级程序员,大厂出来后一直在创业公司,现在还在一线写 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值