Chapter 04

锁文件 uv.lock 深入

"在我机器上能跑" 的时代结束了——用锁文件把 CI、队友、生产锁进同一个依赖图

为什么需要锁文件?

考虑这个日常场景:你的 pyproject.tomlrequests>=2.31。今天你装的是 2.31.0,两周后同事 clone 代码跑 uv sync,新出了 2.32.1,他装的就是 2.32.1。两个人的代码在两个不同版本下运行——测试通过不代表生产能跑。

锁文件解决这个问题:第一次解析完依赖后,把精确到小版本和哈希的结果写到一个文件里,提交到 git。后面所有人、所有 CI、所有部署都从这个文件读取,永远拿到完全一样的依赖图。

锁文件 ≠ requirements.txt

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]] 块,记录:

跨平台通用锁(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 的特点:

# 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.

解决冲突的常见策略

策略 1:放宽约束
pkg==2.0.1 改成 pkg>=2.0,让 uv 有更大解空间。最常见也最有效。
策略 2:override-dependencies
[tool.uv] 里强制某个包的版本,覆盖所有间接约束。风险:可能装出运行时 crash 的组合,但在"上游还没发布新版"时有用。
策略 3:fork(环境分叉)
uv 支持在一个 lock 文件里同时存在多份互斥的解(如 Linux GPU 版 + Mac CPU 版的 PyTorch)。用 marker 表达即可,见第 9 章。
策略 4:降级/升级 Python
某些包只支持特定 Python 版本。把 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 一致,否则拒绝安装。这防御了两类攻击:

千万不要手动编辑 uv.lock

虽然它是 TOML 文件肉眼可读,但哈希、依赖图、version 都是 uv 计算出的。手动改一个字段会导致下次 uv sync 抱怨 lock 坏了。需要改的话:改 pyproject → 运行 uv lock

锁文件的 Git 最佳实践

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 版本管理这个老难题。