uv.lockをコミットしているのに、ローカルとCIでインストールされるパッケージが違うことがあります。
原因はロックの失敗とは限りません。uvのロックファイルは、複数のPythonバージョンやOSに対応する依存関係を保持できます。
固定されるのは依存関係の解決結果であり、すべての環境に同じパッケージを入れることではありません。
小さなプロジェクトで差を再現し、CIで固定すべき条件を確認します。
同じuv.lockでも依存パッケージが変わる
次のような構成を考えます。
- ローカル:Python 3.12、Linux
- CI:Python 3.14、Windows
- 両方で同じ
uv.lockを使用 - 両方で
uv sync --lockedを実行
この条件で、インストール済みパッケージの一覧が異なることは正常です。
再現用に、PythonのバージョンとOSによって依存先が変わるプロジェクトを作ります。
必要なものはuvとPython 3.12、3.14です。外部のPythonパッケージを追加する必要はありません。
pyproject.tomlを次のようにします。
[project]
name = "uv-marker-demo"
version = "0.1.0"
requires-python = ">=3.12,<3.15"
dependencies = [
"tomli==2.2.1; python_version < '3.13'",
"colorama==0.4.6; sys_platform == 'win32'",
]
[dependency-groups]
dev = [
"pytest==8.3.5",
]
[tool.uv]
package = false
ここでは、条件による違いを見やすくするため、依存パッケージのバージョンを固定しています。
tomliはPython 3.12でだけ必要になり、coloramaはWindowsでだけ必要になります。
Python 3.13以降には標準ライブラリのtomllibがありますが、今回は環境マーカーの動作を観察するため、あえてtomliを条件付き依存として指定しています。
package = falseは、このプロジェクト自身をインストール対象にしないための設定です。
まずロックファイルを生成します。
uv lock
続いてPython 3.12の環境を作ります。
uv sync --python 3.12 --locked
同じロックファイルを使ってPython 3.14に切り替えます。
uv sync --python 3.14 --locked
Linuxで実行した場合、前者にはtomliが含まれますが、後者には含まれません。
Windowsでは、どちらの環境にもcoloramaが含まれます。
つまり、バージョンを完全に固定した依存指定でも、インストール対象の集合は変わります。
uv.lockは単一環境のインストール記録ではない
uvの公式ドキュメントでは、プロジェクトのロックファイルをユニバーサルロックファイルとして説明しています。
これは、ロックを生成したマシンだけで使う依存関係一覧ではありません。
requires-pythonで宣言したPythonの対応範囲や、依存パッケージの環境マーカーを考慮して解決されます。
今回の例では、次の条件が別々に働きます。
| 条件 | 影響する対象 |
|---|---|
requires-python |
プロジェクトが対応するPythonの範囲 |
python_version |
Pythonバージョン別の依存関係 |
sys_platform |
OS別の依存関係 |
dependency-groups |
同期時に含める依存グループ |
特に混同しやすいのが、ロックファイルに記録されたパッケージと、実際にインストールされるパッケージの違いです。
ロックファイルには、現在の環境では使用しないパッケージが記録されることがあります。
uvは同期時に、対象環境に適用される依存関係を選択します。
そのため、uv.lockをテキスト検索してパッケージ名が見つかったとしても、そのパッケージが現在の環境にインストールされるとは限りません。
Pythonから環境マーカーを確認する
Python標準ライブラリだけで、今回の条件を確認できます。
# inspect_env.py
import json
import platform
import sys
def selected_dependencies():
result = []
if sys.version_info[:2] < (3, 13):
result.append("tomli")
if sys.platform == "win32":
result.append("colorama")
return result
print(json.dumps({
"python": platform.python_version(),
"platform": sys.platform,
"conditional_dependencies": selected_dependencies(),
}, indent=2))
実行します。
uv run --locked python inspect_env.py
Python 3.12のLinuxなら、conditional_dependenciesは次の内容になります。
{
"conditional_dependencies": [
"tomli"
]
}
これは出力の一部です。実際にはPythonバージョンとプラットフォームも表示されます。
このコードは環境マーカーの判定を単純化したものです。uvの依存関係解決器そのものを再実装しているわけではありません。
実際のパッケージ選択には、パッケージ側の依存条件や配布ファイルの互換性も関係します。
重要なのは、ロックファイルが同じでも、環境条件が異なれば選択結果が変わるという点です。
CIではPythonと依存グループを明示する
環境差を減らすには、ロックファイルだけでなく、同期時の条件も固定します。
まず、ローカルとCIで同じPythonバージョンを指定します。
uv sync --python 3.14 --locked
ただし、これだけではOSの違いは解消しません。
LinuxとWindowsで同じPython 3.14を使っても、今回のcoloramaのような条件付き依存は変わります。
CIで何を保証したいかによって、設定を分ける必要があります。
テスト用の依存グループを揃える
uvは通常、devグループをデフォルトの同期対象に含めます。
一方、CIで次のように実行すると、開発用依存関係は除外されます。
uv sync --locked --no-dev
同じロックファイルでも、同期対象グループが違えばインストール結果は変わります。
テスト用の依存関係を明示するなら、次の形にします。
uv sync --python 3.14 --locked --group dev
ここで--group devはdevグループを含める指定です。
devグループがデフォルトで有効な構成では、明示しても追加の効果はありません。
本番用の最小構成を確認したいなら、逆に--no-devなどで不要なグループを除外します。
グループを明示する目的は、ローカルとCIで意図せず異なる同期コマンドを使わないことです。
インストール結果をPythonで検証する
同期後の環境に何が入ったかは、Python標準ライブラリのimportlib.metadataで確認できます。
# verify_deps.py
import importlib.metadata as metadata
import json
import platform
import sys
PACKAGES = ("tomli", "colorama", "pytest")
def installed_version(name):
try:
return metadata.version(name)
except metadata.PackageNotFoundError:
return None
result = {
"python": platform.python_version(),
"platform": sys.platform,
"packages": {
name: installed_version(name)
for name in PACKAGES
},
}
print(json.dumps(result, indent=2))
同期した環境で実行します。
uv run --locked python verify_deps.py
この方法なら、ロックファイルの内容を推測するのではなく、現在のPython環境にインストールされたディストリビューションを確認できます。
nullは、その環境に対象パッケージがインストールされていないことを示します。
CIで検証するなら、この結果をジョブのログに残すだけでも、差分を調査しやすくなります。
ただし、検証コードが見ているのはインストール済みパッケージのバージョンです。wheelのファイルハッシュやネイティブライブラリの実行時動作まで保証するものではありません。
--lockedと--frozenを混同しない
もう一つ確認しておきたいのが、ロックファイルを扱うオプションです。
uv sync --lockedは、既存のロックファイルがプロジェクトの依存関係定義に対して最新であることを要求します。
ロックファイルの更新が必要な状態なら、更新せずにエラーにします。
一方、--frozenはロックファイルが最新かどうかの確認を省略して、既存のロックファイルを使用します。
したがって、CIでpyproject.tomlとuv.lockの整合性を検証したい場合は、--lockedを選びます。
uv lock --check
uv sync --python 3.14 --locked --group dev
uv lock --checkはロックファイルが最新かどうかを検証します。続くuv sync --lockedでも整合性が確認されるため、両方の実行は必須ではありません。
ロックファイルの更新を禁止したいのか、整合性確認を省略したいのかで、オプションの意味は変わります。
また、--lockedを指定しても、PythonのバージョンやOSが自動的に統一されるわけではありません。
ここを混同すると、ロックファイルに問題がないのに、依存関係の解決が不安定だと判断してしまいます。
CIで揃えるべきなのは、ロックファイルに加えて、Pythonバージョン、対象OS、同期グループです。
さらに、ネイティブ拡張を含むパッケージでは、同じバージョンでもプラットフォームごとに異なるwheelが使われることがあります。
今回の検証は依存パッケージの選択条件に限定しています。バイナリの完全一致まで求める場合は、配布アーティファクトや実行基盤も別途確認する必要があります。
複数OSへの対応そのものが目的なら、インストール結果を無理に一致させる必要はありません。各OSで期待される依存関係が選択され、テストが通ることを検証する方が適切です。
参考
- uv公式ドキュメント:Project structure and files
- uv公式ドキュメント:Resolution
- uv公式ドキュメント:CLI Reference
- uv公式ドキュメント:Dependencies
- Python公式ドキュメント:importlib.metadata
- Python Packaging User Guide:Dependency specifiers
ロックファイルは依存関係の解決結果を固定する仕組みであり、実行環境全体のスナップショットではありません。OSごとのwheel、システムライブラリ、CPUアーキテクチャまで一致させたい場合は、コンテナイメージなど別の固定手段も必要です。まずはCIとローカルのPythonバージョン、環境マーカー、同期グループを記録し、どの差分が意図されたものなのかを確認するのがよいと考えています。

