↓快轉到主要內容
  1. 教學文章/

uv Resolver 除錯:版本衝突、Markers、Constraints 與 Lockfile 診斷

·7 分鐘· loading · loading · ·
Python Uv Dependency Resolver Lockfile Packaging Debugging
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 133: 本文

featured
依賴解析失敗時,最沒幫助的一句話通常是:

那就把版本 pin 死吧。 Pin 對了,問題可能暫時消失;pin 錯了,只是把真正的衝突藏進下一次升級。 今天拍拍君要把 uv resolver 當成一個可以觀測、縮小、驗證的系統來除錯。

一. 前言:Resolver 不是在跟你作對
#

假設 API 專案想同時安裝:

[project]
name = "resolver-lab"
version = "0.1.0"
requires-python = ">=3.11,<3.13"
dependencies = [
  "numpy>=2",
  "numba==0.59.0",
]

執行:

uv lock

uv 會拒絕產生解答,因為 numba==0.59.0 需要 numpy>=1.22,<1.27, 但專案又要求 numpy>=2。 這不是 resolver 壞掉,而是集合交集為空:

專案:numpy >= 2
Numba:numpy >= 1.22, < 1.27
交集:∅

真正的任務不是「讓 uv 閉嘴」,而是回答四個問題:

  1. 哪兩條需求形成矛盾?
  2. 衝突只發生在某個 Python、平台或 extra 嗎?
  3. 應該修改直接依賴、上游 metadata,還是暫時加政策?
  4. 修正後,lockfile 到底改了什麼? 如果你要先熟悉一般 uv init、uv add 與 uv sync, 可以先看 uv 完全教學; 若要整理 dev、test、docs 分組,則看 uv Dependency Groups。

二. 建立最小重現:先把雜訊拿掉
#

不要直接在五年歷史的大型 repo 裡亂改二十個版本。 先建一個乾淨目錄:

mkdir resolver-lab
cd resolver-lab
uv init --bare --name resolver-lab --python ">=3.11,<3.13"

逐步加入疑似衝突的套件:

uv add "numpy>=2"
uv add "numba==0.59.0"

第二步應該失敗,而且錯誤鏈會比原專案短很多。 最小重現至少要固定這些輸入:

uv --version
uv python find
python -VV
uname -a

若問題跟 private index 有關,也要記錄「index 名稱與優先順序」, 但不要把 token、帳號或完整含憑證 URL 貼進 issue。

先移除不參與解析的東西
#

建立重現時,先拿掉:

  • application source code
  • 測試資料與 notebook
  • 不相關的 dependency groups
  • 已知不影響問題的 extras
  • 本機 .venv 保留:
  • pyproject.toml
  • 必要的 workspace members
  • index 設定的最小安全版本
  • 能重現問題的指令
  • 完整錯誤輸出 這樣才能分辨是 dependency metadata、平台選擇,還是環境殘留。

三. 讀錯誤訊息:從結論往回走
#

uv 的 resolution error 常用「Because A depends on B」串出推論。 閱讀順序可以反過來:

  1. 先找最後一句 requirements are unsatisfiable
  2. 找出造成空集合的 package
  3. 往上追兩條互斥的版本範圍
  4. 回到 pyproject.toml 找需求來源 不要只截最上面三行。 真正有用的是完整因果鏈,例如:
Because numba==0.59.0 depends on numpy>=1.22,<1.27
and your project depends on numpy>=2,
the project's requirements are unsatisfiable.

遇到資訊不足時,加上 verbose:

uv lock -v

如果還需要 resolver 細節:

RUST_LOG=uv=debug uv lock

Debug log 可能很長,也可能含 index URL。 分享前先搜尋並遮掉 credentials。

四. Dependency Chain:誰把它帶進來的?
#

專案已經有可用 lockfile 時,先看 tree:

uv tree

只追某個 package:

uv tree --package numpy

反向查「誰依賴它」:

uv tree --invert --package numpy

如果同一個 package 從多條路徑出現:

uv tree --invert --package numpy --no-dedupe

注意:新解析已經失敗時,tree 不能憑空展示不存在的解答。 此時可先:

  1. 用舊的 uv.lock 查既有 dependency chain
  2. 回退剛修改的單一需求
  3. 再用 uv tree --invert 找上游
  4. 最後回到最小重現重做失敗 Tree 是「已解析結果」的視角;錯誤鏈則是「為何無解」的視角。 兩者要一起看。

五. Markers:衝突可能只存在某個環境
#

Python 依賴不只看版本,也會看 environment markers。 例如不同 Python 版本選不同的 NumPy:

[project]
requires-python = ">=3.10,<3.14"
dependencies = [
  "numpy>=1.26,<2; python_version < '3.12'",
  "numpy>=2; python_version >= '3.12'",
]

Marker 的引號、邊界與覆蓋範圍都要檢查。 最常見的錯誤包括:

  • python_version 邊界留了一個洞
  • 兩條 marker 在同一環境同時成立
  • 只測目前電腦,忘了 lockfile 是 universal resolution
  • 把 sys_platform 和 platform_system 混用
  • 上游 wheel 根本沒有支援目標平台 可以對不同目標看 tree:
uv tree --python-version 3.11
uv tree --python-version 3.12
uv tree --python-platform linux
uv tree --python-platform macos

想看 lockfile 包含的全部分支:

uv tree --universal

Marker 要描述事實,不要掩護猜測
#

好的 marker 是:

"uvloop>=0.21; sys_platform != 'win32'"

因為 uvloop 的平台限制是真實契約。 危險的 marker 是:

"some-package<2; python_version < '3.13'"

如果沒確認上游 metadata,這可能只是把今天的失敗藏起來。

六. Constraints:限制被選中的版本
#

uv project 可以在 tool.uv.constraint-dependencies 放 constraint:

[tool.uv]
constraint-dependencies = [
  "urllib3<2.3",
]

Constraint 的語意是:

如果 dependency graph 裡需要 urllib3,就只能從 <2.3 選。 它不會主動把 urllib3 加進 dependencies。 適合用在:

  • 暫避已知 regression
  • 統一 transitive dependency 的安全上限
  • 等待上游 release 修正 metadata
  • 讓多個 workspace members 遵守同一政策 不適合用在:
  • 隱藏真正應該升級的 direct dependency
  • 永久取代上游正確的版本宣告
  • 看不懂錯誤時隨便試一個版本 每個臨時 constraint 最好附上理由:
[tool.uv]
constraint-dependencies = [
  # Remove after upstream issue #123 is released.
  "urllib3<2.3",
]

七. Overrides:有意識地改寫需求
#

override-dependencies 比 constraint 更強。

[tool.uv]
override-dependencies = [
  "urllib3==2.2.3",
]

它會用你的規格取代 dependency graph 中該 package 的宣告。 這代表即使上游寫了不相容範圍,你也要求 resolver 忽略原限制。 所以 override 應該視為「帶風險的例外」,不是普通 pin:

  1. 先確認衝突來自錯誤或過度保守的 metadata
  2. 用測試證明實際相容
  3. 記錄上游 issue 或 release 條件
  4. 設定移除期限
  5. 在 CI 覆蓋受影響路徑

Constraint 與 Override 的差別
#

工具 保留上游需求嗎 會新增套件嗎 風險
direct dependency 是 是 低到中
constraint 是 否 中
override 否,會改寫 視設定而定 高
如果 constraint 已經導致空集合,override 也許能「解開」解析; 但能解析不等於能執行。

八. Resolution Forks:一份 Lockfile 可以有多條路
#

uv 會為不同 Python 版本、平台或 marker 建立 resolution forks。 例如:

[project]
requires-python = ">=3.10,<3.14"
dependencies = [
  "importlib-metadata>=8; python_version < '3.12'",
]

Python 3.10/3.11 需要 backport;較新的 Python 可能不需要。 這不是「一份 lockfile 不夠乾淨」,而是 lockfile 在記錄多環境契約。 可用下列指令觀察:

uv tree --universal
uv lock --dry-run

如果支援環境太寬,resolver 工作量與 lockfile 複雜度都會增加。 先問產品真正支援什麼:

[project]
requires-python = ">=3.11,<3.13"

不要為了「看起來相容」宣告一個從未測過的巨大區間。 uv 也提供 fork strategy:

uv lock --fork-strategy requires-python
uv lock --fork-strategy fewest
  • requires-python:偏好每個 Python 範圍中較新的相容版本
  • fewest:偏好用較少版本覆蓋更多環境 這是選擇策略,不是修復無解衝突的魔法。

九. Lockfile Diff:把修正當成可審查變更
#

不要在調整版本後直接 git add uv.lock。 先做 dry run:

uv lock --dry-run

只升級目標 package:

uv lock --upgrade-package httpx

然後檢查:

git diff -- pyproject.toml uv.lock
uv lock --check
uv sync --locked

Review lockfile 時,至少看:

  • 哪些 package 新增或移除
  • direct dependency 是否只改了預期範圍
  • transitive dependency 有沒有大範圍洗牌
  • marker 與 resolution fork 是否增加
  • source/index 是否意外改變
  • sdist/wheel artifacts 是否符合目標平台

用小工具摘要版本變化
#

uv.lock 是 TOML,可以用標準庫讀:

from pathlib import Path
import tomllib


def versions(path: str) -> dict[str, str]:
    data = tomllib.loads(Path(path).read_text())
    return {
        package["name"]: package["version"]
        for package in data.get("package", [])
        if "version" in package
    }


before = versions("uv.lock.before")
after = versions("uv.lock")

for name in sorted(before.keys() | after.keys()):
    if before.get(name) != after.get(name):
        print(f"{name}: {before.get(name)} -> {after.get(name)}")

同名 package 在 universal lockfile 可能有多個版本; 大型專案可把值改成 set[str],避免摘要遺漏 fork。

十. CI Evidence Bundle:讓別人能重現
#

Resolver 問題最怕只有一張紅字截圖。 拍拍君建議 CI 失敗時保留:

set -eu

uv --version
python -VV
uv lock --check
uv tree --universal > uv-tree.txt

若 uv lock --check 失敗,後面的 tree 可能不會執行。 可以分開收集:

uv lock --check 2>&1 | tee uv-lock-check.txt
status=${PIPESTATUS[0]}

uv tree --frozen --universal > uv-tree.txt 2>&1 || true
exit "$status"

Evidence bundle 可以包含:

  • uv --version
  • Python 版本與平台
  • 最小化 pyproject.toml
  • 已遮密的 index 設定
  • 完整 resolver error
  • 舊 lockfile 的 reverse tree
  • pyproject.toml 與 uv.lock diff 這比「本機可以、CI 不行」有用太多。 完整的 GitHub Actions cache 與 matrix 寫法, 請搭配 uv + GitHub Actions 實戰。

十一. 一套不容易迷路的診斷順序
#

遇到 resolution failure 時,照這個順序:

  1. 保存完整錯誤:不要只截最後一行
  2. 固定工具與環境:uv、Python、OS、architecture
  3. 縮成最小重現:只留衝突的 direct dependencies
  4. 畫出兩條需求鏈:誰要求下限,誰要求上限
  5. 檢查 markers:Python、平台、extras 是否重疊
  6. 查舊 tree:用 --invert 找 transitive source
  7. 先改 direct dependency:優先升級真正的上游
  8. 必要時加 constraint:保留上游契約
  9. 最後才用 override:附測試、issue 與期限
  10. 審查 lockfile diff:確認沒有意外洗牌
  11. 跨環境驗證:測支援的 Python 與平台
  12. 留下 evidence bundle:讓下一個人不用重猜

結語
#

Resolver error 其實是一份「為什麼條件無法同時成立」的證明。 把它拆成版本集合、dependency chain、markers 與 resolution forks, 問題就會從一團紅字,變成幾個可以逐一驗證的假設。 拍拍君最推薦記住三個原則:

  1. 先縮小問題,再修改版本
  2. Constraint 是政策,override 是帶風險的例外
  3. 每次修正都要用 lockfile diff 與跨環境測試驗證 下次 uv 說「No solution found」,先別急著刪 lockfile。 它多半已經把答案藏在 dependency chain 裡了。🧩

延伸閱讀
#

Python 學習 - 本文屬於一個選集。
§ 133: 本文

相關文章

uv + GitHub Actions 實戰:Lockfile、Cache 與可重現 CI
·5 分鐘· loading · loading
Python Uv GitHub Actions Ci Lockfile Cache Developer-Tools
Python uv build/publish 實戰:從 wheel 到 private package workflow
·11 分鐘· loading · loading
Python Uv Packaging Wheel PyPI Private-Package Developer-Tools
Python uv 進階:workspace、lockfile、script 與專案管理完全攻略
·8 分鐘· loading · loading
Python Uv Workspace Lockfile Packaging Dev Tools
uv + Docker 實戰:Locked Sync、Layer Cache 與可重現 Image
·9 分鐘· loading · loading
Python Uv Docker Lockfile BuildKit Deployment Reproducible-Builds
uv Offline Cache 實戰:離線安裝、Frozen Sync 與 Air-Gapped 部署
·8 分鐘· loading · loading
Python Uv Offline Cache Air-Gapped Deployment Reproducible-Builds
uv pip-compile migration:從 requirements.txt 到可重現部署流程
·7 分鐘· loading · loading
Python Uv Requirements.txt Pip-Compile Dependency-Management Deployment