为什么需要锁文件?
考虑这个日常场景:你的 pyproject.toml 写 requests>=2.31。今天你装的是 2.31.0,两周后同事 clone 代码跑 uv sync,新出了 2.32.1,他装的就是 2.32.1。两个人的代码在两个不同版本下运行——测试通过不代表生产能跑。
锁文件解决这个问题:第一次解析完依赖后,把精确到小版本和哈希的结果写到一个文件里,提交到 git。后面所有人、所有 CI、所有部署都从这个文件读取,永远拿到完全一样的依赖图。
pip freeze > requirements.txt 可以凑出锁文件的效果,但它有三个致命缺陷:① 不含哈希(被投毒包替换你察觉不到);② 不区分"直接依赖"和"间接依赖";③ 只记录当前平台装了什么,换个平台就错了。真正的锁文件必须是跨平台可重现的依赖图。
uv.lock 结构解读
version = 1
requires-python = ">=3.12"
[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://.../requests-2.32.3.tar.gz",
hash = "sha256:55365...", size = 131218 }
wheels = [
{ url = "https://.../requests-2.32.3-py3-none-any.whl",
hash = "sha256:70761c...", size = 64928 },
]
每个包一个 [[package]] 块,记录:
- name / version:精确到补丁号
- source:来自哪个索引(PyPI、私有、git、path)
- dependencies:它自己依赖谁(构成完整图)
- sdist / wheels:源码包和预编译包的 URL、SHA-256 哈希、大小(用于完整性校验和 CDN 选择)
跨平台通用锁(Universal Lock)
uv.lock 是 uv 相对 pip-tools、Poetry、PDM 的一个关键优势——一份锁文件包含所有平台。解析时 uv 考虑所有可能的 Python 版本和操作系统组合,在 lock 里用 markers 表达"哪个 wheel 在哪种情况下用"。
[[package]]
name = "numpy"
version = "2.0.0"
wheels = [
{ url = "...linux_x86_64.whl", hash = "..." },
{ url = "...macosx_arm64.whl", hash = "..." },
{ url = "...win_amd64.whl", hash = "..." },
# ... 所有平台的 wheel 都在
]
Mac 上的开发者和 Linux 的 CI 看同一个 lock 文件,各自装各自平台的 wheel,但版本号和哈希是验证过的。这是 Poetry 等工具做不到的(Poetry 的 lock 是当前平台解析结果,换平台要重 lock)。
--frozen vs --locked 的关键区别
| 参数 | 行为 | 典型场景 |
|---|---|---|
uv sync(无参数) |
如果 lock 与 pyproject 不同步,自动重新生成 lock 再安装 | 本地开发 |
--frozen |
完全不看 pyproject 是否一致,盲信 lock 装。最快,但不安全 | 生产容器启动、已知 lock 正确的场景 |
--locked |
验证 lock 是否与 pyproject 一致,不一致则报错退出,不会偷偷修改 | CI 流水线(防止有人改了 pyproject 没更新 lock) |
# CI 推荐
uv sync --locked --no-dev
# Docker 启动阶段推荐
uv sync --frozen --no-dev
# 日常开发,让 uv 自动维护
uv sync
PubGrub — uv 的依赖解析算法
依赖解析的本质是一个 SAT 求解问题:给定一堆约束("包 A 需要 B>=1.0"、"包 B 1.2 和 C 冲突"),找出一组能满足所有约束的版本组合。pip 用的是递归回溯(慢且冲突信息糟糕),uv 用的是 PubGrub(Dart 包管理器 pub 发明的算法)。
PubGrub 的特点:
- 单位传播:类似 SAT 求解器的 CDCL,能快速剪枝不可能的分支。
- 冲突学习:遇到冲突会学习原因并避免重复走到相同的死胡同。
- 人话报错:冲突时输出的解释是"因为 A 要求 B<2,而 C 要求 B>=2,所以无解",而不是 pip 那种吐一堆回溯信息。
# uv 冲突报错示例(可读性拉满)
× No solution found when resolving dependencies:
╰─▶ Because your project depends on old-pkg==1.0
which requires urllib3<2,
and your project depends on httpx>=0.27
which requires urllib3>=2,
we can conclude that these requirements are incompatible.
hint: Try `uv add old-pkg==2.0` if available, or relax the
httpx constraint.
解决冲突的常见策略
pkg==2.0.1 改成 pkg>=2.0,让 uv 有更大解空间。最常见也最有效。[tool.uv] 里强制某个包的版本,覆盖所有间接约束。风险:可能装出运行时 crash 的组合,但在"上游还没发布新版"时有用。requires-python 从 >=3.13 降到 >=3.12,解空间立刻变大。uv lock 命令
# 根据 pyproject 重新生成 lock(不动 venv)
uv lock
# 强制所有依赖升级到最新允许版本
uv lock --upgrade
# 只升级一个
uv lock --upgrade-package django
# 校验 lock 是否与 pyproject 一致,不一致退出 1(CI 用)
uv lock --check
# 导出为 requirements.txt(给不支持 uv.lock 的系统用)
uv export --format requirements-txt > requirements.txt
uv export --no-dev --format requirements-txt > requirements-prod.txt
哈希校验与供应链安全
uv.lock 里每个 wheel 都带 SHA-256 哈希。安装时 uv 会校验:下载的文件哈希必须与 lock 一致,否则拒绝安装。这防御了两类攻击:
- 仓库被投毒:即使攻击者能改 PyPI 上的文件,你的 lock 哈希不匹配,安装立即失败。
- 中间人篡改:下载时代理被劫持注入恶意代码,哈希校验识破。
虽然它是 TOML 文件肉眼可读,但哈希、依赖图、version 都是 uv 计算出的。手动改一个字段会导致下次 uv sync 抱怨 lock 坏了。需要改的话:改 pyproject → 运行 uv lock。
锁文件的 Git 最佳实践
- 必须提交 uv.lock。它是"可重现构建"的唯一来源。
- 团队 PR 审阅 lock 变更是标准流程——看清楚升了哪个包、引入了哪些新间接依赖。
- 重大升级(major 版本号变化)前手动
uv tree看依赖图变化。 uv lock --upgrade这种"全面升级"要单独开 PR,不要和业务改动混在一起。
PEP 751 — 未来的官方锁文件标准
截至 2026 年,Python 生态终于有了官方的跨工具锁文件标准 PEP 751(文件名 pylock.toml)。uv 已支持 uv export --format pylock.toml 导出。未来趋势:uv.lock 保持 uv 内部使用的扩展特性,pylock.toml 作为跨工具交换格式。
uv.lock = 精确版本 + 跨平台 wheel 列表 + SHA-256 哈希,是团队和生产环境可重现的唯一保证。CI 用 --locked,Docker 启动用 --frozen,本地开发让它自动维护。依赖冲突时优先放宽约束,必要时用 override 或 fork。下一章处理 Python 版本管理这个老难题。