🐍

uv tool install でインストールしたツールは独立したvenvで実行される

2024/12/28に公開

便利さと実装方法に感動したので書く。

uvとは

https://docs.astral.sh/uv/
Pythonのバージョン+依存ライブラリ管理ツール。もはや説明不要かもしれない。Rustで書かれているので「Pythonの管理ツールをPythonで動かす場合、そのツールを動かすPythonのバージョンはいくつになるの?」みたいなニワトリ卵問題は発生しない。

最強なので使ったことの無い人は本当に使ってみてほしい。pyenvpipenvpoetryに苦しめられた経験のある人は特に。

uv tool installとは

https://docs.astral.sh/uv/concepts/tools/
Pythonで書かれたツールをインストールするためのコマンド。公式ドキュメントに

in which case their dependencies are installed in a temporary virtual environment isolated from the current project.

とあるように、ツールごとに独立したvenvが作られて管理される。

自作ツールを作った時の記事でも書いたんですが、私はpipでCLIツールを入れてシステムのPythonで依存ライブラリ地獄が始まるのが好きではないので、この機能は非常に助かります。

試してみる

pipのテストといえばお馴染みの牛を呼んでみる。

> uv tool install pycowsay
Resolved 1 package in 346ms
Installed 1 package in 14ms
 + pycowsay==0.0.0.2
Installed 1 executable: pycowsay
> source ~/.zshrc
> pycowsay "Hello World"

  -----------
< Hello World >
  -----------
   \   ^__^
    \  (oo)\_______
       (__)\       )\/\
           ||----w |
           ||     ||

ちなみに

> pip3 list
Package    Version
---------- -------
altgraph   0.17.2
crcmod     1.7
future     0.18.2
macholib   1.15.2
pip        24.3.1
setuptools 58.0.4
six        1.15.0
wheel      0.37.0
> uv run pip list
Package    Version
---------- -------
pip        24.3.1
setuptools 75.6.0

と表示されるので、確かに他のPython環境にはpycowsayはインストールされていないことがわかる。すごい。

どうやって実現しているのか

uv tool run pycowsayみたいな感じで実行するのであれば独立した環境で実行されるのもわかるんですが、PATHが通って直接実行した場合も別環境で実行されるのが不思議でならなかったので、仕組みを調べてみることに。

> cat $(which pycowsay)
#!/Users/hakusai/.local/share/uv/tools/pycowsay/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from pycowsay.main import main
if __name__ == "__main__":
    sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
    sys.exit(main())

注目すべきはshebang

#!/Users/hakusai/.local/share/uv/tools/pycowsay/bin/python

なるほど、ツールごとにvenvを作った上で、そのPythonのパスをshebangで指定することによって、個別の環境で実行されるようにしているらしい。

uvのソースでいうとこのへん。
https://github.com/astral-sh/uv/blob/3733008e6cf1fbfa35faa007ec9f386d6b657ae5/crates/uv-install-wheel/src/wheel.rs#L101-L136

https://github.com/astral-sh/uv/blob/3733008e6cf1fbfa35faa007ec9f386d6b657ae5/crates/uv-install-wheel/src/wheel.rs#L26-L47

ツールごとに使うPythonのパスを判定して、shebangとして書き込んでいることがわかる。

ちなみに

ソースのコメントにもある通り、この実行ファイルの書き込み処理はpipを元にしているらしい。これってpipが書いてくれてたんだ。知らなかった。
https://github.com/pypa/pip/blob/7f8a6844037fb7255cfd0d34ff8e8cf44f2598d4/src/pip/_vendor/distlib/scripts.py#L41-L48

Discussion