一. 前言:Lockfile 在手,為什麼斷網還是裝不起來? #
你把 pyproject.toml 和 uv.lock 都 commit 了。 到了沒有網路的機房,信心滿滿地執行:
uv sync --frozen
結果 uv 還是想抓 wheel、source distribution,甚至 Python 本體。 原因很簡單:lockfile 記錄要裝什麼,不等於檔案已經在本機。 真正能離線重建的交付物,至少要同時具備:
- 固定的 dependency graph;
- 對應平台與 Python 的安裝 artifact;
- 可用的 Python interpreter;
- 一次真的禁止網路的驗證。 這篇拍拍君不重講 uv 入門,也不重講 CI 或 Docker cache。 我們只處理一件事:把「我覺得應該能離線」變成「我已證明可以離線」。
二. 先分清楚四個容易混在一起的開關 #
--locked:Lockfile 不准偷偷變
#
uv sync --locked
它會檢查 uv.lock 是否仍和專案 metadata 一致。 如果需要更新 lockfile,指令就失敗。 但它沒有禁止網路,缺少 artifact 時仍可下載。
--frozen:直接相信現有 Lockfile
#
uv sync --frozen
它不檢查 lockfile 是否需要更新,也不重新鎖定。 適合已經確認輸入正確的部署流程。 但它同樣不代表離線。
--offline:禁止網路存取
#
uv sync --offline --frozen
這才是離線驗證的核心。 uv 只會使用本機 cache 與本機可取得的檔案。 缺一個 wheel、index metadata 或 Git checkout,就應該明確失敗。
--no-cache:反而不是今天要的東西
#
uv sync --no-cache
--no-cache 會避開持久 cache,改用這次執行的暫存目錄。 它適合某些乾淨測試,不適合準備離線交付。
| 開關 | Lockfile 行為 | 網路 | 持久 Cache |
|---|---|---|---|
--locked |
驗證、不更新 | 可用 | 使用 |
--frozen |
不驗證、不更新 | 可用 | 使用 |
--offline |
不直接決定 | 禁止 | 只讀本機可用內容 |
--no-cache |
不直接決定 | 可用 | 不使用既有持久 cache |
拍拍君的部署組合通常是:
uv sync --offline --frozen --no-python-downloads
其中 --no-python-downloads 是另一層保險:不要讓 uv 嘗試自動取得 Python。
三. 先定義「同一個目標平台」 #
Cache 不是神奇的跨平台壓縮包。 準備機與離線機至少要對齊:
- 作業系統;
- CPU 架構,例如
x86_64或aarch64; - Python implementation 與 minor version;
- glibc / musl 等平台標籤;
- 需要的 dependency groups 與 extras。 先在兩端記錄:
uname -s
uname -m
python3 -VV
python3 -c 'import sysconfig; print(sysconfig.get_platform())'
uv --version
macOS arm64 暖好的 wheel,不會自動變成 Linux x86_64 wheel。 純 Python 套件可能碰巧可用;NumPy、Cryptography、PyArrow 這類含原生元件的套件通常不行。 最穩的原則是:在哪種 runner 上部署,就在哪種 runner 上準備與驗證。
四. 建立一個專用 Cache,而不是打包整個日常 Cache #
先從乾淨、可界定的目錄開始:
export UV_CACHE_DIR="$PWD/.uv-offline-cache"
uv cache dir
uv cache size
不要直接把日常使用多年的全域 cache 當交付物。 那裡可能混有:
- 其他 Python 版本的 wheel;
- 已不在 lockfile 的套件;
- 私有專案留下的 artifact;
- 不同平台或不同 index 的資料;
- 與今天部署無關的 Git checkout。 專用 cache 比較大嗎?不一定。 但它更容易回答「這批檔案是怎麼產生的」。
五. 在線準備階段:先驗證 Lock,再暖 Cache #
在有網路的準備機執行:
export UV_CACHE_DIR="$PWD/.uv-offline-cache"
uv lock --check
uv sync --locked --no-python-downloads
第一行保證 lockfile 沒有 drift。 第二行實際下載並安裝目前平台需要的 artifact。 如果 production 不需要 dev dependencies,準備時就要使用相同選擇:
uv sync --locked --no-dev --no-python-downloads
有 extras 或 groups,也要明確寫出來:
uv sync --locked \
--no-default-groups \
--group runtime \
--extra postgres \
--no-python-downloads
不能在線上暖「全部」,到了離線端才臨時決定另一組依賴。 Cache 完整性永遠相對於一個明確的安裝選擇。
六. 不要被現成 .venv 騙過
#
第一次 uv sync 成功,只能證明環境已經建立。 它不能證明 cache 足以重建環境。 安全做法是把原本環境移開,再從零測一次:
mv .venv .venv-online
uv sync \
--offline \
--frozen \
--no-python-downloads
成功後再做 runtime smoke test:
uv run --offline --frozen --no-python-downloads \
python -c 'import httpx; print(httpx.__version__)'
測完若要清理,先確認哪一個環境要保留,不要直接對模糊路徑做遞迴刪除。 你也可以在 disposable CI runner 或暫存工作目錄完成這段驗證。 重點是:測試必須從沒有 .venv 的狀態開始。
七. --offline 測的是行為,不是心情
#
「我當時 Wi-Fi 好像沒連」不是可靠測試。 請把禁止網路寫進指令:
UV_OFFLINE=1 \
UV_PYTHON_DOWNLOADS=never \
UV_CACHE_DIR="$PWD/.uv-offline-cache" \
uv sync --frozen
這樣 log、CI 與操作手冊都能看見測試條件。 建議再開 verbose 記錄來源判斷:
uv sync --offline --frozen --no-python-downloads -v
Verbose log 可能包含內部路徑,不要未檢查就公開貼到 issue。
八. Cache 快照怎麼交付? #
驗證完成後,交付內容至少包含:
offline-bundle/
├── project/
│ ├── pyproject.toml
│ ├── uv.lock
│ └── src/
├── uv-cache/
├── MANIFEST.sha256
└── README-offline.md
將剛才的專用 cache 複製成 bundle 裡的 uv-cache/。 離線端指定同一個位置:
export UV_CACHE_DIR="$PWD/../uv-cache"
uv sync --offline --frozen --no-python-downloads
如果 cache 與 .venv 位於不同 filesystem,uv 可能無法 hardlink。 此時可明確使用 copy:
uv sync \
--offline \
--frozen \
--no-python-downloads \
--link-mode copy
這會多用一些磁碟空間,但通常比依賴跨 filesystem link 更穩定。
九. 重要限制:uv Cache 不是正式套件 Repository #
複製 cache 很實用,但不要把它描述成永遠可攜的標準 artifact。 它是 uv 的內部儲存與效能機制,結構可能隨版本演進。 因此交付時應該:
- 記錄產生 cache 的 uv 版本;
- 在同版本或經驗證版本上安裝;
- 保留原始 lockfile;
- 真的在目標平台做 offline smoke test;
- 不要手動修改 cache 內部檔名或目錄。 若交付物要長期保存、跨團隊使用,wheelhouse 或內部 package index 通常更適合。
十. 更可稽核的方案:建立 Wheelhouse #
uv 目前沒有把 project lock 直接「下載成 wheelhouse」的通用命令。 可以先用 uv 匯出鎖定結果:
uv export \
--locked \
--format requirements.txt \
--no-emit-project \
--output-file requirements.locked.txt
再使用能下載 distribution 的工具,在目標平台相容環境準備 wheelhouse:
python3 -m pip download \
--only-binary=:all: \
--dest wheelhouse \
--requirement requirements.locked.txt
--only-binary=:all: 會讓缺少 wheel 的套件提早失敗。 這比到了 air-gapped 主機才發現需要 compiler、header 與 build backend 好得多。 離線端可以使用 uv 的 pip-compatible installer:
uv venv --python 3.12 --no-python-downloads
uv pip sync requirements.locked.txt \
--no-index \
--find-links wheelhouse \
--python .venv/bin/python
Windows 請把 interpreter path 改成 .venv\\Scripts\\python.exe。 Wheelhouse 是清楚的 distribution 集合,較容易做掃描、簽章與保存。 但它也有取捨:project editable install、workspace member、Git dependency 與平台矩陣要另外處理。
十一. Air-Gapped 環境最常漏掉的不是套件 #
Python Interpreter #
如果離線機沒有相容 Python,dependency cache 再完整也沒用。 請預先安裝系統 Python,或用組織批准的方式交付 uv-managed Python。 驗證時固定加上:
--no-python-downloads
Git Dependencies #
git+https://... 依賴需要完整、可用的 Git checkout 或替代 wheel。 僅有 commit hash 不代表 repository 已存在離線端。 長期 air-gapped 部署最好先建 wheel,放進受控 artifact repository。
Local Path Dependencies #
Lockfile 可能引用 workspace member 或本機路徑。 如果 bundle 沒有一起帶過去,cache 不會替你補出 source tree。
Private Index #
Cache 命中不等於 private index 身分資訊已安全交付。 不要把 token 寫進 pyproject.toml、lockfile 或 README。 離線部署應依賴已審核 artifact,而不是把 production credential 帶進隔離區。
十二. 缺件時,怎麼讀錯誤? #
Failed to fetch
#
通常代表 cache 中缺少需要的 artifact 或 metadata。 先確認:
- 準備時是否使用相同 group / extra;
- Python minor version 是否一致;
- OS 與 architecture 是否一致;
- dependency 是否只有 sdist;
- 是否有 direct URL 或 Git dependency。
No solution found
#
這不一定是 lockfile 壞掉。 如果你沒有使用 --frozen,uv 可能正在嘗試重新解析,而離線 metadata 不完整。 部署流程應先確認 lockfile,再使用 frozen sync。
想要下載 Python #
代表目標 interpreter 不存在或不符合 request。 執行:
uv python find 3.12 --no-python-downloads
先把 interpreter 問題與 package 問題拆開。
Cache 在,但仍然不能安裝 #
可能是 artifact 不適用於目標 platform tag,也可能準備階段只命中了既有 .venv。 回到乾淨環境重跑 offline sync,別靠目錄大小猜答案。
十三. Cache 維護:不要在交付前手滑清掉 #
常見 cache 指令:
uv cache dir
uv cache size
uv cache clean ruff
uv cache prune
clean 與 prune 是維護工具,不是「發佈前一定要做」的儀式。 一旦你已驗證專用 cache,可以先封存與產生 manifest,再處理其他副本。 對 air-gapped bundle 而言,少幾百 MB 通常不如可重建性重要。
十四. 為交付物建立完整性 Manifest #
可以用系統工具記錄檔案雜湊:
find project uv-cache -type f -print0 \
| sort -z \
| xargs -0 shasum -a 256 \
> MANIFEST.sha256
離線端驗證:
shasum -a 256 -c MANIFEST.sha256
注意:checksum 能發現檔案改變,不能證明發布者身分。 高風險環境還需要組織的簽章、artifact provenance、惡意程式掃描與批准流程。
十五. 一個可重跑的準備腳本 #
下面把重要條件集中在一起:
#!/usr/bin/env bash
set -euo pipefail
export UV_CACHE_DIR="$PWD/.uv-offline-cache"
export UV_PYTHON_DOWNLOADS=never
uv --version
python3 -VV
uv lock --check
uv sync --locked --no-dev
mv .venv .venv-online-check
uv sync --offline --frozen --no-dev
uv run --offline --frozen --no-dev \
python -c 'import importlib.metadata as m; print(m.version("httpx"))'
uv cache size
這個腳本仍需依你的專案調整 package 名稱、groups、extras 與 smoke test。 不要只測 import。 API 服務可以跑一個 health check;資料工具可以處理一小份 fixture;CLI 可以執行 --help 加一條真實子命令。
十六. 建議的兩階段驗證 #
階段 A:準備機 #
uv lock --check通過;- 從空
.venv執行 offline frozen sync; - 核心 smoke test 通過;
- 記錄 uv、Python、OS、architecture;
- 產生 checksum manifest。
階段 B:目標隔離環境 #
- 驗證 manifest;
- 確認 interpreter;
- 指定 bundle 內 cache;
- 再跑一次 offline frozen sync;
- 執行同一份 smoke test;
- 保存部署 log 與 artifact ID。 只有階段 A 成功,仍不能代表傳輸、權限與目標 filesystem 沒問題。
十七. 什麼時候該改用內部 Index? #
如果只有一台臨時離線筆電,專用 cache bundle 很方便。 如果有多個服務、平台與團隊,建議升級成:
- 內部 PyPI mirror;
- 經批准的 wheel repository;
- 固定的 artifact promotion 流程;
- SBOM、掃描與簽章;
- 按平台建置的 bundle。 Cache 解決「不要重複下載」。 Repository 解決「哪些 artifact 被組織允許使用」。 兩者不是同一個治理層次。
十八. 發佈前檢查清單 #
-
pyproject.toml與uv.lock已 commit -
uv lock --check通過 - 準備機與目標機平台契約一致
- groups、extras 與 production 選擇已固定
- Python interpreter 已預先準備
- 使用專用
UV_CACHE_DIR - 從空
.venv完成--offline --frozen - 核心 runtime smoke test 通過
- Git、URL、local path dependencies 已處理
- Cache bundle 或 wheelhouse 有 checksum manifest
- 離線目標機再次驗證成功
- 版本、平台與操作紀錄已保存
結語 #
離線部署最危險的字是「應該」。
uv.lock 解決版本圖,cache 或 wheelhouse 解決 artifact,Python 安裝解決 interpreter,而 --offline 才負責揭穿所有遺漏。 拍拍君建議把流程縮成一句話:
在線準備,空環境重建,明確離線,再到目標機重驗。
只要少了最後兩步,你得到的可能只是一次幸運的 cache hit,不是可重現部署。