Read OSS

python-build の内側:pyenv が Python をソースからコンパイルする仕組み

上級

前提知識

  • 第1回:アーキテクチャとディレクトリ構成
  • 第2回:shim システムとバージョン解決
  • 第3回:シェル統合と環境管理

python-build の内側:pyenv が Python をソースからコンパイルする仕組み

pyenv install 3.13.2 を実行すると、pyenv は CPython のソース tarball をダウンロードしてチェックサムを検証します。次にプラットフォームのパッケージマネージャーを検出し、OpenSSL・readline・zlib の依存パスを解決します。その後 ./configure && make && make install を実行し、pip をセットアップしてすべての shim を再生成します。この一連の処理を取り仕切るのが、プラグインとしてバンドルされた python-build です。

この記事では、インストールを要求した瞬間から最終的な rehash までのビルドパイプライン全体を追います。バージョン定義が実行可能なビルドスクリプトとして機能する仕組み、Homebrew/MacPorts のエコシステムの違いを依存関係検出がどう吸収するか、そして何百ものバージョン定義ファイルをどのように管理しているかを見ていきましょう。

プラグインアーキテクチャ:python-build が pyenv に統合される仕組み

第1回で見たとおり、ディスパッチャーは plugins/*/bin を PATH に追加します。これにより、plugins/python-build/bin/pyenv-install が何も登録することなく pyenv install として使えるようになります。このプラグインは pyenv-uninstall とスタンドアロンの python-build コマンドも提供しています。

統合のポイントは plugins/python-build/bin/pyenv-install にあり、すべてのプラグインからバージョン定義のパスを集約しています。

plugins/python-build/bin/pyenv-install#L30-L37

このループは、インストールされているすべてのプラグインから share/python-build/ ディレクトリを収集し、コロン区切りの検索パスを構築します。サードパーティのプラグインは、このディレクトリにファイルを置くだけで独自のバージョン定義を追加できます。

flowchart TD
    A["pyenv install 3.13.2"] --> B["pyenv-install<br/>(plugin wrapper)"]
    B --> C["Aggregate PYTHON_BUILD_DEFINITIONS<br/>from all plugins"]
    B --> D["Resolve prefix via pyenv-latest"]
    B --> E["Source install hooks"]
    B --> F["Detect bootstrap Python"]
    F --> G["python-build 3.13.2<br/>$PYENV_ROOT/versions/3.13.2"]
    G --> H["Source definition file"]
    H --> I["Download + verify + build"]
    I --> J["pyenv-rehash"]
    
    style G fill:#f96,stroke:#333

pyenv-install ラッパーは、python-build が関知しなくてよいいくつかの処理を担います。具体的には、install フック(129〜152行目)、bootstrap Python の検出(201〜243行目)、force/skip-existing のロジック、そしてインストール後の rehash(284〜286行目)です。

plugins/python-build/bin/pyenv-install#L284-L286

実行可能スクリプトとしてのバージョン定義

バージョン定義はデータファイルではなく、python-build の内部で実行される Bash スクリプトです。CPython 3.13.2 の定義ファイルの全体を見てみましょう。

plugins/python-build/share/python-build/3.13.2

prefer_openssl3
export PYTHON_BUILD_CONFIGURE_WITH_OPENSSL=1
export PYTHON_BUILD_TCLTK_USE_PKGCONFIG=1
install_package "openssl-3.4.0" "https://...#sha256" mac_openssl --if has_broken_mac_openssl
install_package "readline-8.2" "https://...#sha256" mac_readline --if has_broken_mac_readline
if has_tar_xz_support; then
    install_package "Python-3.13.2" "https://...#sha256" standard verify_py313 copy_python_gdb ensurepip
else
    install_package "Python-3.13.2" "https://...#sha256" standard verify_py313 copy_python_gdb ensurepip
fi

わずか10行の中に、いくつもの処理が詰まっています。

  1. prefer_openssl3 は OpenSSL 1.1 より 3.x を優先するよう設定します。
  2. OpenSSL と readline のパッケージは条件付きでインストールされます。--if has_broken_mac_openssl という節により、プラットフォームのバンドル版が壊れている場合にのみダウンロードが実行されます(OpenSSL の代わりに LibreSSL が同梱されている macOS ではよくある状況です)。
  3. Python 本体のビルドには standard ビルド関数が使われ、続いて verify_py313(バージョン固有の検証)、copy_python_gdb(GDB ヘルパーのインストール)、ensurepip(pip のブートストラップ)が実行されます。
  4. if has_tar_xz_support の分岐では、xz が利用可能な場合に容量の小さい .tar.xz アーカイブを優先します。

「定義をスクリプトとして扱う」このパターンは強力です。任意のビルドロジックを記述できるため、PyPy の定義ではプリコンパイル済みバイナリをダウンロードし、Miniconda の定義ではインストーラースクリプトを実行し、Stackless Python の定義ではパッチを適用する、といったことが自在にできます。

ビルドパイプライン:configure、make、install

ビルドのコアロジックは、plugins/python-build/bin/python-build の853行目にある build_package_standard_build() に実装されています。

plugins/python-build/bin/python-build#L853-L933

Python 専用の処理(874行目の if [ "$package_var_name" = "PYTHON" ])では、広範な依存関係の検出が行われます。macOS では、パッケージマネージャーを次の順序でカスケード検索します。

sequenceDiagram
    participant Build as build_package_standard_build
    participant HB as can_use_homebrew()
    participant MP as can_use_macports()
    participant Lock as lock_in()
    
    Build->>HB: Check Homebrew availability
    alt Homebrew found
        HB->>Lock: lock_in("homebrew")
        HB-->>Build: Use Homebrew dependencies
        Build->>Build: use_homebrew_tcltk()
        Build->>Build: use_homebrew_readline()
        Build->>Build: use_homebrew_ncurses()
        Build->>Build: use_homebrew_zlib()
    else MacPorts found
        Build->>MP: Check MacPorts availability
        MP->>Lock: lock_in("macports")
        MP-->>Build: Use MacPorts dependencies
    else Neither
        Build->>Build: use_xcode_sdk_zlib() on macOS ≥10.14
    end
    Build->>Build: ./configure --prefix=...
    Build->>Build: make -j$(nproc)

エコシステムのロックイン機構(128〜178行目)は、設計上の重要な決断です。

plugins/python-build/bin/python-build#L128-L178

lock_in()locked_in() は、Homebrew と MacPorts の排他的使用を強制します。最初の依存関係がどちらか一方のエコシステムで解決されると、以降の依存関係はすべて同じソースから取得しなければなりません。Homebrew の OpenSSL と MacPorts の readline を混在させると、バイナリの非互換という深刻な問題が生じるからです。_PYTHON_BUILD_ECOSYSTEM_LOCKED_IN 変数はラッチとして機能し、一度セットされると変更できません。

最終的な configure/make のシーケンス(927〜932行目)はシンプルですが、高度にパラメーター化されています。PYTHON_CONFIGURE_OPTSPYTHON_CFLAGSPYTHON_MAKE_OPTS といった環境変数を使えば、カスタムビルドのための調整も可能です。

plugins/python-build/bin/python-build#L927-L932

948〜951行目の build_package_standard() は、ビルドとインストールのフェーズをまとめて実行します。

plugins/python-build/bin/python-build#L948-L951

ヒント: pyenv が自分の環境でどのような configure フラグを使っているか確認したい場合は、pyenv install 3.13.2 -v を実行しましょう。verbose フラグを指定すると、ビルドログがリアルタイムで流れ、すべての configure 引数と make の呼び出し内容を確認できます。

ダウンロード、検証、展開

ビルドの前に、python-build はソース tarball を取得する必要があります。412〜422行目の HTTP クライアント検出チェーンは、3つのクライアントを順番に試します。

plugins/python-build/bin/python-build#L412-L422

優先順序は aria2c → curl → wget の順です。並列接続に対応した aria2c でダウンロード速度を優先しつつ、どこでも使えるツールへのフォールバックも確保しています。

280〜305行目の install_package_using() は、フェッチとビルドをつなぐ中心的な関数です。

plugins/python-build/bin/python-build#L280-L305

--if による条件分岐(291〜296行目)はエレガントな設計です。--if 以降の引数は述語関数として扱われ、述語が false を返すと install_package の呼び出し全体が何もせずに 0(成功)を返します。定義ファイルの --if has_broken_mac_openssl もこの仕組みで動いており、Linux や OpenSSL が正常な macOS では OpenSSL のビルドステップがまるごとスキップされます。

flowchart TD
    A["install_package 'openssl-3.4.0' URL mac_openssl --if has_broken_mac_openssl"] --> B{"--if predicate:<br/>has_broken_mac_openssl()"}
    B -->|"Returns true<br/>(macOS with LibreSSL)"| C["fetch_tarball: download + checksum verify"]
    B -->|"Returns false<br/>(Linux or good OpenSSL)"| D["return 0<br/>(skip entirely)"]
    C --> E["make_package: extract + build"]
    E --> F["mac_openssl: platform patches"]
    F --> G["Installed openssl to PREFIX"]

チェックサムの検証は URL のフォーマットに組み込まれています。URL の末尾にある #sha256hash フラグメントが解析され、ダウンロードしたファイルと照合されます。別途チェックサムファイルを用意することなく、サプライチェーンの整合性を担保できます。

ブートストラップ問題とインストールフック

CPython のビルドには、既存の Python インタープリターが必要です(一部のビルドステップや ensurepip のために)。これは鶏と卵の問題を生み出しますが、pyenv-install は既存の互換バージョンを検出することでこれを解決しています。

plugins/python-build/bin/pyenv-install#L201-L243

CPython の場合(202〜216行目)、インストール対象のメジャー.マイナー・メジャー・フルバージョンに一致する Python をインストール済みのものから探します。Anaconda/Miniconda バージョンは明示的に除外されます。これはそれらがバンドルする curl がすべてのプラットフォームで動作しないためです(211行目)。見つかったバージョンは PYENV_BOOTSTRAP_VERSION に設定されます。

sequenceDiagram
    participant Install as pyenv-install
    participant Whence as pyenv-whence
    participant Build as python-build
    
    Install->>Install: VERSION_NAME = "3.13.2"
    Install->>Whence: pyenv-whence python3.13
    alt Found compatible version
        Whence-->>Install: "3.12.8"
        Install->>Install: PYENV_BOOTSTRAP_VERSION=3.12.8
    else No compatible version
        Install->>Whence: pyenv-whence python3
        Whence-->>Install: "3.11.7"
        Install->>Install: PYENV_BOOTSTRAP_VERSION=3.11.7
    end
    Install->>Install: export PYENV_VERSION=$PYENV_BOOTSTRAP_VERSION
    Install->>Build: python-build 3.13.2 $PREFIX

ソースからビルドする PyPy の場合(218〜241行目)、ブートストラップの要件はより厳格です。Python 2.7 と pycparser モジュールが必要です。

インストールフックを使うと、プラグインがビルドの前後に処理を差し込めます。

plugins/python-build/bin/pyenv-install#L129-L152

組み込みの pyenv.d/install/latest.bash フックは、ビルド開始前にバージョンプレフィックスを解決します。これにより、pyenv install 3.12 と実行するだけで最新の 3.12.x リリースが自動的にインストールされます。

何百ものバージョン定義を管理する

share/python-build/ ディレクトリには、CPython・PyPy・Miniconda・Anaconda・Miniforge・Jython・Stackless・MicroPython など、700 を超えるバージョン定義ファイルが収められています。これを手作業で管理し続けるのは到底持続できません。

plugins/python-build/scripts/add_cpython.py スクリプトは、CPython の定義生成を自動化しています。

plugins/python-build/scripts/add_cpython.py#L1-L41

このスクリプトは CPython のダウンロードアーカイブをスクレイピングし、ローカルに定義ファイルがない新バージョンを見つけ出し、tarball をダウンロードして SHA-256 チェックサムを計算し、同じマイナーバージョンの直近の定義を元に新しい定義ファイルを生成します。「直前のファイルから派生させる」このアプローチにより、configure フラグや依存関係の扱いが段階的に進化していきます。

スクリプトの実行には requestsrequests_htmlpackaging など複数の Python パッケージが必要です。Python のインストールを管理するツールが Python のパッケージに依存するというのは少し皮肉ですが、開発・メンテナンス用のツールであってユーザー向けではないため、これは妥当なトレードオフと言えます。

ヒント: 新しい CPython リリースがアナウンスされたのに pyenv がまだ対応していない場合、同じマイナーバージョンの最新定義をコピーし、バージョン番号とチェックサムを書き換えることで手動で定義を作れます。add_cpython.py スクリプトはこれを自動化したものですが、パターンを理解しておけば、スクリプトを待たずに素早く対応できます。

実行の骨格

python-build のメイン実行は2719行目から始まり、ビルド環境のセットアップが行われます。

plugins/python-build/bin/python-build#L2719-L2748

2745行目の source "$DEFINITION_PATH" が決定的な瞬間です。ここでバージョン定義ファイルが実行されます。それ以前はすべてビルドパス・ログファイル・コンパイラフラグなど環境のセットアップに費やされており、それ以降はクリーンアップ処理です。実際のダウンロードとビルドを指揮するのは定義ファイル自身であり、ここまで見てきた関数群を呼び出す形で処理が進みます。

2743行目の trap build_failed ERR により、ビルドが失敗した際にはログファイルへのパスを示す分かりやすいエラーメッセージが表示されます。

次回に向けて

これでライフサイクル全体を追い終えました。シェル統合 → shim による横取り → バージョン解決 → バイナリのコンパイル、という一連の流れです。しかし pyenv の真の強みは拡張性にあります。次回は、フックシステムを詳しく見ていきます。パッケージをインストールするたびに自動で shim を再生成する pip-rehash フック、そして Anaconda のバンドル済みシステムツールが shim ディレクトリを汚染しないように防ぐ conda 除外フックなど、巧みな仕組みを解説します。