本节目标:让依赖能从企业私有源或国内镜像拉取,并在完全断网的环境下用缓存与 wheelhouse 完成可校验的安装。
适用版本:Python 3.12+(实测 3.14.6);uv 0.12.23
2.2 私有源、镜像与离线安装
2.1 生成了 uv.lock,它精确记录了每个包来自哪个索引。但「从哪装」在真实环境里常常不自由:企业内网封了 PyPI,CI runner 走的是代理,生产部署机干脆没有外网。本节把索引、镜像、缓存与离线安装这条链路走通。
2.2.1 为什么需要私有源
三类场景会逼着你配置自定义索引:
| 场景 | 诉求 | 典型做法 |
|---|---|---|
| 企业内网 | 不能直连公网 | 私有 PyPI(devpi、Nexus、Artifactory) |
| 国内加速 | 拉取慢、易超时 | 清华、阿里等镜像源 |
| 内部包 | 分发私有 wheel | 私有源 + 凭证 |
镜像与私有源在配置层面几乎一样,差别只在「是否含私有包」和「是否需要凭证」。下面先看镜像。若想从更底层的「包如何被发现、安装、导入」理解私有仓库,可延伸阅读 Python 模块与包管理 。
2.2.2 配置镜像:[[tool.uv.index]]
uv 支持在 pyproject.toml 里声明索引。把清华镜像设为默认源:
[project]
name = "index-demo"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["httpx>=0.27,<1"]
[[tool.uv.index]]
name = "tsinghua"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
default = true
本机实测 uv lock,从调试日志可见请求确实打到了镜像:
DEBUG No cache entry for: https://pypi.tuna.tsinghua.edu.cn/simple/httpx/
DEBUG Sending fresh GET request for: https://pypi.tuna.tsinghua.edu.cn/simple/httpx/
DEBUG Sending fresh HEAD request for: https://pypi.tuna.tsinghua.edu.cn/packages/.../httpx-0.28.1-py3-none-any.whl
default = true 表示用它替代 PyPI。若只想追加一个源(比如私有包在私服、其余仍走 PyPI),去掉 default,uv 会按声明顺序依次尝试。多索引时还能用 explicit = true 限定某索引只服务于指定包,避免「同名包被恶意源抢先」的依赖混淆攻击:
[[tool.uv.index]]
name = "internal"
url = "https://pypi.internal.example.com/simple"
explicit = true
[tool.uv.sources]
internal-lib = { index = "internal" }
这里 explicit = true 的 internal 索引只在解析 internal-lib 时被查询,其余包仍走默认源。没有这道隔离,一个名为 httpx 的私服包就可能顶替公网的 httpx——这正是依赖混淆(dependency confusion)的经典入口。
2.2.3 配置放在哪一层
除了 pyproject.toml,uv 还认 uv.toml(项目根)与用户级配置(~/.config/uv/uv.toml,本机实测该路径默认不存在,按需创建)。优先级从低到高:
| 层级 | 位置 | 适合 |
|---|---|---|
| 用户级 | ~/.config/uv/uv.toml | 个人全局镜像 |
| 项目级 | pyproject.toml / uv.toml | 团队统一、进版本库 |
| 环境变量 | UV_* / PIP_* | CI 临时注入 |
| 命令行 | --index-url 等 | 一次性调试 |
一条经验:能进版本库的尽量进版本库(项目级 pyproject.toml),环境变量只留给凭证与临时覆盖。这样别人 clone 下来 uv sync 就能复现你的源配置。
2.2.4 环境变量:UV_INDEX_URL 与 PIP_INDEX_URL
不适合改项目文件时(比如临时调试、CI 注入),用环境变量。uv 有自己的变量,也兼容 pip 的变量。实测两者都能生效:
UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple uv pip compile requirements.in
PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple uv pip compile requirements.in
Resolved 7 packages in 6ms
httpx==0.28.1
也可以用命令行参数,优先级最高:
uv pip compile requirements.in --index-url https://pypi.tuna.tsinghua.edu.cn/simple
优先级从低到高依次是:PIP_* 环境变量 < UV_* 环境变量 < 命令行参数 < pyproject.toml 中的 [[tool.uv.index]]。团队协作时把索引写进 pyproject.toml(进版本库、可复现);个人临时覆盖用环境变量。
2.2.5 私有源与凭证
私有源几乎都需要认证。三种常见方式:
| 方式 | 配置 | 适用 |
|---|---|---|
| URL 内嵌 | https://user:pass@host/simple | 快速验证,别提交 |
| 环境变量 | UV_INDEX_<NAME>_USERNAME / _PASSWORD | CI 注入密钥 |
netrc | ~/.netrc 存 host 凭证 | 本机长期使用 |
用环境变量时,索引名会参与变量名拼装——[[tool.uv.index]] 里 name = "internal",就对应 UV_INDEX_INTERNAL_USERNAME 与 UV_INDEX_INTERNAL_PASSWORD。凭证永远不要写进 pyproject.toml 或 uv.lock:锁文件会记录索引 URL,但不应记录密码。CI 里用仓库 Secret 注入这些变量即可。
2.2.6 uv 的缓存机制
uv 会把下载过的包与元数据缓存起来,这是离线能力的基础。查看缓存位置与大小:
uv cache dir
uv cache size
/Users/leting.yan/.cache/uv
4558848
uv cache size 输出的是字节数(本机实测该命令仍标为 experimental)。缓存内部按用途分目录:wheels-v6 存 wheel、sdists-v9 存源码包、simple-v25 存索引元数据、archive-v0 存解包产物。清理用:
uv cache clean # 清空整个缓存
uv cache prune # 只清「不再被引用」的条目(CI 里常用)
缓存是共享的:uv cache dir 默认在用户目录,多个项目复用。这意味着 CI 只要把缓存目录做持久化,第二次构建就能省下绝大部分下载时间——2.3 会专门讲缓存键设计。
2.2.7 离线安装:从 lock 到 wheelhouse
离线安装有两条路。第一条是依赖缓存:先在联网环境把包拉进缓存,之后断网也能装。本机实测了完整过程。
先在一个只有 uv.lock、缓存里还没有 wheel 的环境执行离线同步:
uv sync --offline
error: Failed to download `pygments==2.21.0`
cause: Network connectivity is disabled, but the requested data wasn't found in the cache: https://files.pythonhosted.org/.../pygments-2.21.0-py3-none-any.whl
注意:uv lock 只解析元数据、不下载 wheel,所以「有锁文件」不等于「能离线装」。联网执行一次 uv sync 把包真正拉进缓存后,再离线:
uv sync # 联网,下载并缓存全部 wheel
uv sync --offline # 断网
Resolved 14 packages in 4ms
Checked 12 packages in 51ms
这次离线同步成功——缓存就是离线环境的「预取包」。
第二条路是 wheelhouse:把全部 wheel 提前下载到一个目录,随制品一起交付。标准做法是用 pip download 预取:
pip download --dest wheelhouse --only-binary=:all: "httpx==0.28.1"
Saved ./wheelhouse/httpx-0.28.1-py3-none-any.whl
Saved ./wheelhouse/httpcore-1.0.9-py3-none-any.whl
Saved ./wheelhouse/h11-0.16.0-py3-none-any.whl
Saved ./wheelhouse/anyio-4.15.1-py3-none-any.whl
Saved ./wheelhouse/idna-3.20-py3-none-any.whl
Saved ./wheelhouse/typing_extensions-4.16.0-py3-none-any.whl
Saved ./wheelhouse/certifi-2026.7.22-py3-none-any.whl
Successfully downloaded httpx httpcore h11 anyio idna typing_extensions certifi
--only-binary=:all: 强制只要 wheel、不要 sdist,避免目标机还得现场编译。之后在离线机上:
pip install --no-index --find-links wheelhouse --require-hashes -r requirements.txt
--no-index 禁用一切网络索引,--find-links 只从本地目录找包,--require-hashes 逐条校验内容——三者叠加,就是一次完全离线、内容可校验的安装。uv 侧等价物是 uv sync --offline --find-links wheelhouse。
本机实测了从 wheelhouse 的离线安装(装到隔离目录以免污染环境):
uv pip install --no-index --find-links wheelhouse --target ./target "httpx==0.28.1"
Resolved 7 packages in 82ms
Prepared 7 packages in 309ms
Installed 7 packages in 106ms
+ anyio==4.15.1
+ certifi==2026.7.22
+ h11==0.16.0
+ httpcore==1.0.9
+ httpx==0.28.1
+ idna==3.20
+ typing-extensions==4.16.0
全程 --no-index,没有一次网络请求,7 个包全部来自本地 wheelhouse/。这就是气隙环境里安装依赖的样子。
2.2.8 内网与 CI 的策略取舍
两条路各有适用面:
| 方案 | 优点 | 缺点 | 适合 |
|---|---|---|---|
| 私有源 / 镜像 | 始终能拿最新版本 | 需维护服务与凭证 | 日常开发、CI |
| 依赖缓存 | 零额外服务 | 缓存易被清理 | 本地重复构建 |
| wheelhouse | 完全自包含、可审计 | 制品体积大、需随版本更新 | 生产部署、气隙环境 |
务实组合:开发与 CI 走镜像 + 缓存,生产与气隙环境走 wheelhouse + --require-hashes。wheelhouse 要跟 uv.lock 一起做版本管理——锁文件变了,wheelhouse 必须重新生成,否则装出来的就是旧版本。
2.2.9 气隙环境的完整流程
把上面各节串成一条可执行清单。联网侧(有外网,负责生产制品):
uv lock # 1. 解析并生成 uv.lock
uv export --format requirements-txt \
--generate-hashes -o requirements.txt # 2. 导出带哈希的 requirements
pip download --dest wheelhouse \
--only-binary=:all: -r requirements.txt # 3. 预取全部 wheel
# 4. 把 requirements.txt + wheelhouse/ 打包,随版本一起归档
离线侧(气隙机,负责安装):
pip install --no-index --find-links wheelhouse \
--require-hashes -r requirements.txt
两个必须一起交付、一起做版本管理的东西:带哈希的 requirements 与 wheelhouse。只给 wheelhouse 不给哈希,就无法校验内容;只给哈希不给 wheel,就得现场编译。还有一条容易踩的坑:若某依赖只有 sdist、没有 wheel,离线机就得具备编译工具链(gcc、头文件),pip download --only-binary=:all: 会直接失败——这类包要么在上游找预编译 wheel,要么在联网侧就编译好再放进 wheelhouse。
小结
- 自定义索引在
pyproject.toml用[[tool.uv.index]]声明,default = true替代 PyPI,explicit = true限定某源只服务指定包以防御依赖混淆。 - 环境变量
UV_INDEX_URL与PIP_INDEX_URL本机实测均生效,优先级为PIP_*<UV_*< 命令行 <pyproject.toml。 - 私有源凭证用环境变量或
netrc注入,绝不写进pyproject.toml或uv.lock。 - uv 缓存在用户目录且多项目共享;
uv cache prune只清未引用条目,适合 CI。 uv lock只解析不下载,因此「有锁文件」不等于「能离线装」;联网uv sync填充缓存后,uv sync --offline才能成功。- wheelhouse 用
pip download --only-binary=:all:预取,配合--no-index --find-links --require-hashes实现完全离线、可校验的安装。
到这里,依赖既能从任意源拉取,也能在断网环境落地。但一个项目往往要同时支持多个 Python 版本,还要让 CI 又快又不重复下载。下一节把「多版本矩阵」与「CI 缓存」拼成最后一块基建。
阅读导航:上一节:2.1 依赖解析与锁文件 · 下一节:2.3 多版本 Python 矩阵与 CI 缓存 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。