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

uv Tool 生命週期:uvx、持久安裝、版本固定與 PATH 診斷

·8 分鐘· loading · loading · ·
Python Uv Uvx Cli PATH Developer-Tools
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 148: 本文

一. 前言:裝好了,為什麼還是舊版本?
#

拍拍君曾經看過一種很迷惑的除錯現場: 工具管理器說「安裝成功」,終端機卻繼續印舊版本。 有人於是再裝一次、再加一次 --force,最後連自己用了哪一份都說不清。

問題往往不在安裝,而在兩條不同的選擇路徑: uv 決定工具環境,shell 決定裸指令會跑哪個檔案。 它們不是同一個問題,也不一定得到同一個答案。

這篇不重講 uv 入門 的專案建立流程。 我們只追蹤一個 CLI:從臨時執行、持久安裝,到升級、回退與移除。 例子使用 Ruff,但不討論 lint 規則;想學規則請看 Ruff 教學。

二. 先把三個入口分開
#

入口 主要用途 環境歸誰管理
uvx ruff 執行獨立工具 uv 工具介面,可能重用已安裝工具
uv tool install ruff 後執行 ruff 提供持續可用的命令 uv 持久工具環境;shell 找入口
uv run ruff 使用專案約定的檢查器 專案環境與依賴契約

uvx 是 uv tool run 的別名,不是另一套安裝器。 「臨時」也不等於每次重新下載,更不等於執行完立即清空磁碟。 未安裝的工具可以使用 uv cache 裡的可丟棄環境;持久安裝則放在工具目錄。

工具不會因此變成專案依賴。 但環境隔離不是安全沙箱:工具仍可讀寫你有權存取的檔案。 跑 formatter 時,它依然可能改動目前工作目錄中的原始碼。 我們的實驗只用 --version,不碰真實專案。

這些定位可對照 uv 工具概念文件。 下面則用實際命令驗證,不靠「應該是這樣」猜測。

三. 建立不會弄亂日常工具的實驗室
#

以下以 macOS / Linux 的 Bash 執行,整篇程式區塊依序放在同一個 shell。 需要預先安裝 uv、Python 3.12 與 3.11;後者用來證明環境分離。 若缺少 Python,先完成 uv Python 管理 的準備。 本文實測 uv 0.10.2、CPython 3.12.12 / 3.11.14;不是宣稱它們是最新版本。 Ruff 0.14.0 與 0.14.1 是刻意選的歷史測試版本,不是更新推薦。

set -euo pipefail
LAB="$(mktemp -d /tmp/pypy-tool-lab.XXXXXX)"
UV_CMD="$(command -v uv)"
export UV_TOOL_DIR="$LAB/tools"
export UV_TOOL_BIN_DIR="$LAB/bin"
export UV_CACHE_DIR="$LAB/cache"
export UV_PYTHON="$("$UV_CMD" python find --system 3.12)"
export UV_PYTHON_DOWNLOADS=never
export UV_NO_CONFIG=1
BASE_PATH="$PATH"
mkdir -p "$UV_TOOL_BIN_DIR"
export PATH="$UV_TOOL_BIN_DIR:$BASE_PATH"
cd "$LAB"
"$UV_CMD" --version
"$UV_CMD" tool dir
"$UV_CMD" tool dir --bin

三個專用目錄各有責任:

  • UV_TOOL_DIR:持久工具環境與管理資訊。
  • UV_TOOL_BIN_DIR:讓 shell 搜尋的 executable 入口。
  • UV_CACHE_DIR:這次操作自己的下載及可丟棄環境快取。

UV_NO_CONFIG=1 避免讀進日常專案或使用者 uv 設定。 UV_PYTHON_DOWNLOADS=never 則讓缺少直譯器時明確失敗,不偷偷下載補齊。 這不是完整環境封鎖;套件仍會從設定的套件來源取得,並會連網。 有企業索引政策的環境,請先確認來源設定,而不是直接照抄到正式機。

四. 臨時執行:不要把 Python 版本當工具版本
#

第一步先執行指定的工具版本:

"$UV_CMD" tool run --from 'ruff==0.14.0' ruff --version
"$UV_CMD" tool list

第一行應印 ruff 0.14.0;第二行此時不應列出持久安裝的 Ruff。 它有下載、有環境,卻沒有變成「已安裝工具」。 之後同規格的執行可以利用 cache;不要把 cache 存在誤判為正式安裝。

這裡刻意使用完整的 uv tool run 寫法,方便固定 uv executable 來源。 平常也可以寫 uvx ruff@0.14.0 --version。 --from 明確分開「提供套件」和「要執行的命令」;兩者名稱不一定相同。 例如 HTTPie 套件提供的常用入口叫 http,不叫 httpie。

還有一個常見誤會:uvx --python 3.12 ruff 固定的是工具環境的 Python, 不是 Ruff 套件版本。要控制 Ruff,仍須指定 ruff==... 或 ruff@...。 把兩種版本分開記錄,才能談得上重現。

五. 持久安裝:入口和環境要一起盤點
#

現在把同一版本安裝成長期可呼叫的工具:

"$UV_CMD" tool install 'ruff==0.14.0'
"$UV_CMD" tool list
command -v ruff
ruff --version
"$UV_CMD" tool run ruff --version
python3 - <<'PY'
import os
from pathlib import Path
entry = Path(os.environ["UV_TOOL_BIN_DIR"]) / "ruff"
target = entry.resolve(strict=True)
print("入口:", entry)
print("實體:", target)
assert target.is_relative_to(Path(os.environ["UV_TOOL_DIR"]).resolve())
PY

此時 ruff 和 uv tool run ruff 都應得到 0.14.0。 在 Unix 上,入口通常是連往工具環境的 symlink;Windows 的入口處理不同。 本例只驗證 Unix,不把路徑及 shell 指令當成跨平台保證。

裝過工具之後,未指定版本的 uvx 預設會重用已安裝版本。 因此「我用 uvx,所以一定拿到最新版」並不成立。 沒安裝時,未固定版本的執行也可能重用先前的 cache。

需要探索最新版可用 uvx ruff@latest --version, 但那不是固定結果的團隊契約。本篇不執行它,避免隨文章閱讀日期漂移。 臨時換版,則明確提出規格:

"$UV_CMD" tool run --from 'ruff==0.14.1' ruff --version
ruff --version

兩行應依序印出 0.14.1、0.14.0。 這正是我們要的邊界:試跑另一版本,不改寫持久安裝。 --isolated 可以忽略已安裝工具,但它本身不是「固定最新版」或版本鎖。

六. PATH 遮蔽:做一次可控的故障
#

現在故意放一個同名入口在前面。 它只是印一行字,不會執行真正的 Ruff。

mkdir -p "$LAB/shadow"
cat > "$LAB/shadow/ruff" <<'SH'
#!/bin/sh
printf '%s\n' 'shadow-entry-not-ruff'
SH
chmod +x "$LAB/shadow/ruff"
export PATH="$LAB/shadow:$UV_TOOL_BIN_DIR:$BASE_PATH"
hash -r
type -a ruff
ruff --version
"$UV_TOOL_BIN_DIR/ruff" --version
"$UV_CMD" tool run ruff --version
export PATH="$UV_TOOL_BIN_DIR:$BASE_PATH"
hash -r

三次版本查詢的意義不同:

查詢 本例結果 在問誰
ruff --version shadow-entry-not-ruff shell 找到的第一個入口
"$UV_TOOL_BIN_DIR/ruff" --version ruff 0.14.0 明確指定的持久入口
uv tool run ruff --version ruff 0.14.0 uv 選定的工具環境

這時重新安裝並不能修正前面那個假的入口。 --force 也不會把別的 PATH 目錄、shell alias 或 function 全部清乾淨。 先看 type -a;若有 alias 或 function,要另外檢查其定義。 hash -r 在 Bash 清除命令位置快取;zsh 可用 rehash。

真實機器上若工具目錄根本不在 PATH,先看 uv tool dir --bin。 確認要加入的目錄後,才使用 uv tool update-shell 並重新開 shell。 它可能修改 shell 設定,所以本實驗只改當前程序的 PATH,不執行它。

七. 專案工具與個人工具,不要共用一個猜測
#

建立一個小型專案環境:Python 3.11、Ruff 0.14.1。 個人工具仍維持 Python 3.12、Ruff 0.14.0。

mkdir -p "$LAB/project"
"$UV_CMD" venv --python 3.11 "$LAB/project/.venv"
"$UV_CMD" pip install --python "$LAB/project/.venv/bin/python" 'ruff==0.14.1'
(
  source "$LAB/project/.venv/bin/activate"
  hash -r
  command -v ruff
  ruff --version
  "$UV_CMD" tool run ruff --version
)
"$LAB/project/.venv/bin/python" -c 'import sys; print(sys.version_info[:2]); print(sys.prefix)'
"$UV_TOOL_DIR/ruff/bin/python" -c 'import sys; print(sys.version_info[:2]); print(sys.prefix)'

在子 shell 啟用專案環境後,裸 ruff 應跑 0.14.1; uv tool run ruff 仍跑個人工具的 0.14.0。 兩份 Python 回報則應分別是 (3, 11) 與 (3, 12),prefix 也不同。 本例已用 UV_PYTHON 明確要求工具直譯器,不依賴自動探索的偶然結果。

正式專案若已用 uv 管理,請把檢查器列進專案依賴並以 uv run 執行。 像 pytest 需要 import 專案套件,更不能把獨立 uvx 環境當成專案環境。 Ruff 雖多半直接讀檔,不需要 import 專案,規則和版本仍應由團隊約定。

另外,工具環境隔離不代表設定檔隔離。 在專案目錄執行 Ruff,仍可能讀取該目錄的 Ruff 設定; 「用哪份 Python/套件」和「讀哪份設定/資料」要分別追蹤。

八. 升級前先問:當初裝的是什麼規格?
#

先對精確固定的 0.14.0 執行一般升級:

"$UV_CMD" tool upgrade ruff
test "$("$UV_TOOL_BIN_DIR/ruff" --version)" = 'ruff 0.14.0'

==0.14.0 仍是需求,升級不應偷偷跨越它。 這不是升級失敗,而是遵守先前契約。 若當初安裝的是允許一段版本的需求,一般升級可以在範圍內挑選較新版本。 不過主套件固定,也不代表所有間接依賴都已鎖定;本例 Ruff 恰好很單純。

接著用 install 變更為只容許兩個版本的範圍。 為了讓升級可觀察,先以最低直接依賴策略選到 0.14.0,再改回最高版本策略:

"$UV_CMD" tool install 'ruff>=0.14.0,<0.14.2' --resolution lowest-direct
test "$("$UV_TOOL_BIN_DIR/ruff" --version)" = 'ruff 0.14.0'
"$UV_CMD" tool upgrade ruff --resolution highest
"$UV_CMD" tool list
"$UV_TOOL_BIN_DIR/ruff" --version
test "$("$UV_TOOL_BIN_DIR/ruff" --version)" = 'ruff 0.14.1'

lowest-direct 只是本次重現升級的實驗安排,不是日常更新建議。 要直接切換到指定版本,可用 uv tool install 'ruff==0.14.1'。 別假設 upgrade 'ruff==0.14.1' 能解除舊的 ==0.14.0; 本次實測這樣會產生互斥需求錯誤,應由 install 明確改變原需求。 不要為了升級一個工具直接 uv tool upgrade --all,把故障範圍放大。

真實驗收不只看版本:還要對固定 fixture 跑檢查,記錄退出碼與輸出差異。 這裡只驗證管理生命週期,不宣稱兩版 Ruff 的所有 lint 行為等價。 官方 Using tools 指南 另有版本選擇與升級說明。

九. 回退不是「撤銷剛才所有事情」
#

若升版後不符合預期,明確重新指定舊版本:

"$UV_CMD" tool install 'ruff==0.14.0'
test "$("$UV_TOOL_BIN_DIR/ruff" --version)" = 'ruff 0.14.0'
"$UV_CMD" tool run ruff --version

這是工具套件版本的回退,不是檔案內容的回復。 若新 formatter 已改寫原始碼,降版不會把內容改回來;要用 Git 審核及還原。 若工具還有外部資料格式、plugin 或額外依賴,也要另做相容性檢查。

本例沒有備份 uv 私有管理檔,也沒有手改工具環境中的 site-packages。 把 pip install 塞進工具 venv,會讓實際環境與管理資訊逐漸不一致。 日常應交給 uv tool install / upgrade / uninstall 管理。

十. 移除:入口消失,和「所有痕跡消失」不同
#

最後移除這次持久安裝,但保留專案環境作為對照:

"$UV_CMD" tool uninstall ruff
"$UV_CMD" tool list
test ! -e "$UV_TOOL_BIN_DIR/ruff"
test ! -L "$UV_TOOL_BIN_DIR/ruff"
test ! -d "$UV_TOOL_DIR/ruff"
"$LAB/project/.venv/bin/ruff" --version
"$UV_CMD" tool run --from 'ruff==0.14.0' ruff --version
"$UV_CMD" tool list

應看到:持久入口與工具環境消失,專案 Ruff 仍是 0.14.1; 之後明確規格的臨時執行仍可跑 0.14.0,但不重新出現在持久工具清單。 移除不是 cache 清除,也不是卸載其他工具管理器或專案裡的同名工具。

實驗結束後可先 cd 回原目錄,關閉這個 shell,還原本次環境變數。 $LAB 留著供稽核,確認路徑與內容後再用自己的垃圾桶工具清理。 不要在日常環境為了這個實驗執行全域 cache 清除或工具全部移除。

十一. 這次實驗實際驗證了什麼?
#

本文十個 Bash 區塊已依序在新的隔離目錄執行完成,並核對以下觀察:

邊界 實際結果
未安裝時指定版本執行 可跑 0.14.0,持久清單仍為空
臨時指定另一版 跑 0.14.1,持久入口仍為 0.14.0
同名 PATH 入口遮蔽 裸命令跑假入口,絕對入口與 uv 工具仍正確
啟用專案環境 專案 3.11 / 0.14.1,工具 3.12 / 0.14.0
精確 pin 後一般升級 維持 0.14.0
受限範圍升級與回退 0.14.0 → 0.14.1 → 0.14.0
移除持久工具 入口消失,專案與臨時執行不受影響

這些是指定版本、指定平台的行為驗收,不是速度測試。 未驗證 Windows shell、所有 plugin、所有 uv 版本或外部工具的完整相容性。

十二. 一份能交接的工具紀錄
#

拍拍君會為常駐 CLI 保存這幾項,而不是只截圖「安裝成功」:

  1. uv 版本、作業系統及架構。
  2. 工具套件的需求規格,以及觀察到的實際版本。
  3. 工具 Python 版本、入口位置與最終 executable 位置。
  4. 附加依賴、套件來源政策與必要設定。
  5. 升級前後 fixture、退出碼及明確的回退命令。

這不是完整依賴 lock,也沒有把公開套件名稱視為來源真實性的證明。 若要完整重建有複雜 plugin 的工具,應另外保存解析結果、artifact 與驗收資料。 CLI 版本固定只是起點,不是可重現性的全部。

結語
#

uvx 解決「怎麼方便地執行」,uv tool install 解決「怎麼持續提供入口」。 shell 的 PATH 則決定你直接輸入工具名稱時,到底會碰到哪個入口。 把這三件事分開,很多「明明裝了卻沒更新」就不神祕了。

下次升級前,先記錄規格和來源;升級後,驗收同一份 fixture。 有明確的回退入口,比把 --force 當幸運符可靠多了。

延伸閱讀
#

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

相關文章

uv 管理 Python 版本:Install、Find、Pin、Upgrade 與直譯器選擇
·9 分鐘· loading · loading
Python Uv Python Versions Interpreter Virtualenv Developer-Tools
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
Rich Logging Dashboard 實戰:進度、表格與 Log Console 整合
·7 分鐘· loading · loading
Python Rich Logging Cli Dashboard Developer-Tools
Python uv scripts 實戰:PEP 723、inline dependencies 與單檔工具
·6 分鐘· loading · loading
Python Uv PEP 723 Script Developer-Tools Automation
Python difflib 實戰:文字差異比對、相似度比較與 patch 輸出完全攻略
·10 分鐘· loading · loading
Python Difflib Text-Processing Developer-Tools Cli