nikkie-ftnextの日記

イベントレポートや読書メモを発信

pipのない仮想環境にもかかわらず、Ryeがpip-syncできる"魔法"を理解する

本記事は、PyCon Kyushu 2024 KAGOSHIMA 発表の補足記事です

目次

Ryeとpip-tools

作者のmitsuhiko氏からAstral社1に引き継がれたRye。
Rust製のPythonパッケージ管理ツール「uv」を使ってみよう | gihyo.jp

執筆時点の最新版は0.34.0。
https://github.com/astral-sh/rye/releases/tag/0.34.0
pip-toolsよりもuv2が優先して使われるようになったようです。
https://github.com/astral-sh/rye/blob/0.34.0/docs/guide/sync.md#syncing-and-locking

It currently defaults to uv as it offers significantly better performance, but will offer you the option to use pip-tools instead.

数ヶ月前(v0.16.0)にpip-toolsを使ったRyeのLockやSyncの実装を見ていました。
その中で魔法のようなことをやっていることが分かってきました(ちょうぜつ...!)

  • rye syncでプロジェクトにできる仮想環境にはpipはない(開発者には触らせないらしい)
  • その仮想環境にRyeはrequirements.txtに沿ってインストールできる

この魔法の仕組みについて理解したことを綴ります。
なお、uvが優先されていますが、rye config --set-bool behavior.use-uv=false3でpip-toolsを使うように設定できます。

関連エントリ

この2本の内容を前提にします。伏線だったのですよ...

pipのない仮想環境にpip-syncする仕組み

ソースコードを読んで、方法を理解するのを目的とします。

短いrequirements.txtを用意しました。

kojo-fan-art==0.1.1

pipのない仮想環境(プロジェクト)

Ryeはvirtualenvに--no-seedを指定して、プロジェクトにpipのない仮想環境を作ります。
https://github.com/astral-sh/rye/blob/0.34.0/rye/src/sync.rs#L369

理解するための実験としては、2つの仮想環境を用意します。

% python -V
Python 3.11.8
% python -m venv .venv/no_pip --without-pip
% python -m venv .venv/piptools --upgrade-deps
.
└── .venv/
    ├── piptools/  # pipのある仮想環境。この後pip-toolsも入れる
    └── no_pip/  # pipのない仮想環境
% .venv/piptools/bin/python -m pip list
<省略しますができます>
% .venv/no_pip/bin/python -m pip list
/.../.venv/no_pip/bin/python: No module named pip

プロジェクト外にあるRyeの仮想環境

いくつかありますが、今回は2つ紹介します
macOSrye pin 3.12しています)

  • ~/.rye/self/lib/python3.12/site-packages/
    • Rye自身の仮想環境のようです
    • virtualenvはここにありました
    • Ruffなども入っています
    • pip-toolsはありません
  • ~/.rye/pip-tools/cpython@3.12/lib/python3.12/site-packages/
    • pip-toolsはこちらでした
  • (ちなみにuvは ~/.rye/uv/0.1.44/uv に置かれているようです)

Ryeに合わせて、pip-toolsの入った仮想環境を作ります

% .venv/piptools/bin/python -m pip install pip-tools

シンボリックリンク+PYTHONPATHでpip-syncがpipを見つけられるようにしている

以前の記事で確認したように、PYTHONPATH環境変数を使えばpipのない仮想環境にもパッケージをインストールできます。
pipのない仮想環境でpip-syncできる一要素はPYTHONPATHです。
ですが、他にも要素があります。

pip-toolsの仮想環境から一時ディレクトリにpipだけシンボリックリンクしています。
https://github.com/astral-sh/rye/blob/0.34.0/rye/src/sync.rs#L270-L274

これを再現すると

% mkdir temp  # 一時ディレクトリの代わりに作成
% ln -s $PWD/.venv/piptools/lib/python3.11/site-packages/pip $PWD/temp/pip
% ls temp
pip

このとき

% PYTHONPATH=$PWD/temp .venv/no_pip/bin/python -m pip list

何も表示されません。
ですが、これで正常動作なんです

% echo $?
0
% ls .venv/no_pip/lib/python3.11/site-packages  # 空ディレクトリ

ここからpip-syncできちゃうんですね!

% .venv/piptools/bin/pip-sync --python-executable .venv/no_pip/bin/python
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'pip'

subprocess.CalledProcessError: Command '('.venv/no_pip/bin/python', '-c', 'import pip;print(pip.__version__)')' returned non-zero exit status 1.

% PYTHONPATH=$PWD/temp .venv/piptools/bin/pip-sync --python-executable .venv/no_pip/bin/python

PYTHONPATHでpipの実体のソースコードが見つかるので、「No module named 'pip'」とはならずpip-syncできたと理解しました。

% PYTHONPATH=$PWD/temp .venv/no_pip/bin/python -m pip list
Package      Version
------------ -------
kojo-fan-art 0.1.1
% ls .venv/no_pip/lib/python3.11/site-packages
kojo_fan_art-0.1.1.dist-info        the_solitary_castle_in_the_mirror

実際のコマンドは、pip-sync--pip-args--no-depsを渡しています。
https://github.com/astral-sh/rye/blob/0.34.0/rye/src/sync.rs#L277-L284

  --pip-args PIP_ARGS   Arbitrary pip arguments to pass directly to pip
                        install/upgrade commands

pip install --no-deps
https://pip.pypa.io/en/stable/cli/pip_install/#cmdoption-no-deps

site-packages下のpipだけシンボリックリンクを作る意図がピンとこなかったのですが、
PYTHONPATH=$PWD/temp .venv/no_pip/bin/python -m pip list
の出力が空だったのを見て、「pipのない仮想環境で最小限でpip-syncを動かせている」と分からされました(技術力で殴られた)。
lock (pip-compile)側は見られていませんが、余計な依存をlockファイルに書かないようにするための工夫とも言えそうです。

比較のため.venv/piptoolsのsite-packagesを渡すと、めちゃめちゃ依存が表示されます(pip-syncの前にやりました)

% PYTHONPATH=$PWD/.venv/piptools/lib/python3.11/site-packages .venv/no_pip/bin/python -m pip list
Package         Version
--------------- -------
build           1.2.1
click           8.1.7
packaging       24.0
pip             24.0
pip-tools       7.4.1
pyproject_hooks 1.1.0
setuptools      70.0.0
wheel           0.43.0

終わりに

ソースコードを見て理解したRyeの魔法「pipのない仮想環境にpip-sync」です

  • プロジェクトの仮想環境にはpipはない(開発者は触れない)
  • プロジェクトの仮想環境とは別に、pip-toolsの仮想環境がある
  • pip-toolsの仮想環境のうちpipのコードだけを参照できるようにして、pipのない仮想環境でのpip-syncを実現している

uvがデフォルトとなり、この魔法は今後あまり日の目を見ないのかもしれませんが、このアイデア・技術力はめちゃめちゃすごいと思いました。


  1. Python Monthly Topics中にもありますが、Ruffの開発元でもあります
  2. Python Monthly Topicsをぜひどうぞ。私が触った記録
  3. https://rye.astral.sh/guide/config/#manipulating-config