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

uv Offline Cache 實戰:離線安裝、Frozen Sync 與 Air-Gapped 部署

·8 分鐘· loading · loading · ·
Python Uv Offline Cache Air-Gapped Deployment Reproducible-Builds
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 128: 本文

一. 前言:Lockfile 在手,為什麼斷網還是裝不起來?
#

你把 pyproject.toml 和 uv.lock 都 commit 了。 到了沒有網路的機房,信心滿滿地執行:

uv sync --frozen

結果 uv 還是想抓 wheel、source distribution,甚至 Python 本體。 原因很簡單:lockfile 記錄要裝什麼,不等於檔案已經在本機。 真正能離線重建的交付物,至少要同時具備:

  1. 固定的 dependency graph;
  2. 對應平台與 Python 的安裝 artifact;
  3. 可用的 Python interpreter;
  4. 一次真的禁止網路的驗證。 這篇拍拍君不重講 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 的內部儲存與效能機制,結構可能隨版本演進。 因此交付時應該:

  1. 記錄產生 cache 的 uv 版本;
  2. 在同版本或經驗證版本上安裝;
  3. 保留原始 lockfile;
  4. 真的在目標平台做 offline smoke test;
  5. 不要手動修改 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,不是可重現部署。

延伸閱讀
#

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

相關文章

uv + Docker 實戰:Locked Sync、Layer Cache 與可重現 Image
·9 分鐘· loading · loading
Python Uv Docker Lockfile BuildKit Deployment Reproducible-Builds
uv pip-compile migration:從 requirements.txt 到可重現部署流程
·7 分鐘· loading · loading
Python Uv Requirements.txt Pip-Compile Dependency-Management Deployment
uv + GitHub Actions 實戰:Lockfile、Cache 與可重現 CI
·5 分鐘· loading · loading
Python Uv GitHub Actions Ci Lockfile Cache Developer-Tools
uv 管理 Python 版本:Install、Find、Pin、Upgrade 與直譯器選擇
·9 分鐘· loading · loading
Python Uv Python Versions Interpreter Virtualenv Developer-Tools
Python uv build/publish 實戰:從 wheel 到 private package workflow
·11 分鐘· loading · loading
Python Uv Packaging Wheel PyPI Private-Package Developer-Tools
Python uv scripts 實戰:PEP 723、inline dependencies 與單檔工具
·6 分鐘· loading · loading
Python Uv PEP 723 Script Developer-Tools Automation