PEP 517 – ソースツリーのビルドシステムに依存しないフォーマット
- Author:
- Nathaniel J. Smith <njs at pobox.com>, Thomas Kluyver <thomas at kluyver.me.uk>
- BDFL-Delegate:
- Alyssa Coghlan <ncoghlan at gmail.com>
- Discussions-To:
- Distutils-SIG list
- Status:
- Final
- Type:
- Standards Track
- Topic:
- Packaging
- Created:
- 30-Sep-2015
- Post-History:
- 01-Oct-2015, 25-Oct-2015, 19-May-2017, 11-Sep-2017
- Resolution:
- Distutils-SIG message
Table of Contents
概要
distutils / setuptools は私たちを長い道のりに連れて行ってくれましたが、3つの重大な問題を抱えています。 (a) 使いやすいビルド時の依存関係の宣言、自動構成、さらには DRY 準拠のバージョン番号管理などの基本的なエルゴノミクスの欠如などの重要な機能が欠けていること、 (b) 拡張が難しいため、上記の問題に対するさまざまな解決策が存在するものの、それらはしばしば風変わりで壊れやすく、維持するのに高価であること、そして (c) distutils/setuptools がユーザーや pip のようなインストールツールによって期待される標準インターフェースを提供しているため、他のものを使用することが非常に難しいことです。
以前の取り組み(例:distutils2 や setuptools 自体)は、問題 (a) および/または (b) を解決しようとしました。この提案は (c) を解決することを目指しています。
この PEP の目標は、distutils-sig を Python ビルドシステムのゲートキーパーの役割から解放することです。distutils を使用したい場合はそれで良いですし、他のものを使用したい場合は、標準化された方法を使用して簡単に行えるようにする必要があります。distutils と連携することの難しさから、現在はそのようなシステムはあまり存在しませんが、私たちが考えていることを理解するために、flit や bento を参照してください。幸いなことに、ホイールはここでの多くの難しい問題をすでに解決しており、たとえば、ビルドシステムがすべての可能なインストール構成についても知っている必要がなくなったため、ビルドシステムに必要なのは、標準に準拠したホイールと sdists を出力する方法を持つことだけです。
したがって、パッケージソースツリーおよびソースディストリビューションと対話するための新しい、比較的最小限のインターフェースをインストールツール(例:pip)に提案します。
用語と目標
ソースツリー とは、VCS チェックアウトのようなものです。この形式からインストールするための標準インターフェースが必要です。たとえば、pip install some-directory/ のような使用法をサポートするためです。
ソースディストリビューション とは、特定のソースコードのリリースを表す静的なスナップショットであり、たとえば lxml-3.4.4.tar.gz のようなものです。ソースディストリビューションは多くの目的を果たします。リリースのアーカイブ記録を提供し、大規模なコードコーパスを取り込んで処理するための事実上の標準を提供し、さまざまな言語で書かれたコード(例:コード検索)を提供し、Debian/Fedora/Conda/… などの下流のパッケージングシステムの入力として機能します。Python エコシステムでは、ソースディストリビューションは特に重要な役割を果たします。たとえば、foo.whl というディストリビューションが bar に依存していると宣言している場合、pip install bar や pip install foo が自動的に bar の sdist を見つけてダウンロードし、ビルドして結果のパッケージをインストールするケースをサポートする必要があります。
ソースディストリビューションは短縮して sdists とも呼ばれます。
ビルドフロントエンド とは、ユーザーが実行するツールであり、任意のソースツリーまたはソースディストリビューションを受け取り、それらからホイールをビルドします。実際のビルドは各ソースツリーの ビルドバックエンド によって行われます。たとえば、pip wheel some-directory/ というコマンドでは、pip がビルドフロントエンドとして機能しています。
統合フロントエンド とは、パッケージの要件セット(例:requirements.txt ファイル)を受け取り、それらの要件を満たすように作業環境を更新しようとするツールです。これには、ホイールと sdists の組み合わせを見つけてビルドし、インストールすることが含まれる場合があります。たとえば、pip install lxml==2.4.0 というコマンドでは、pip が統合フロントエンドとして機能しています。
ソースツリー
setup.py を含む既存のレガシーソースツリーフォーマットがあります。これをさらに指定することは試みません。その事実上の仕様は、distutils、setuptools、pip、およびその他のツールのソースコードとドキュメントにエンコードされています。これを setup.py スタイルと呼びます。
ここでは、PEP 518 で定義された pyproject.toml ファイルを中心とした新しいスタイルのソースツリーを定義し、そのファイルの [build-system] テーブルに build-backend という追加のキーを拡張します。以下はその例です:
[build-system]
# PEP 518 で定義されています:
requires = ["flit"]
# この PEP で定義されています:
build-backend = "flit.api:main"
build-backend は、ビルドを実行するために使用される Python オブジェクトを名前で指定する文字列です(詳細は後述します)。これは、setuptools エントリポイントと同じ module:object 構文に従ってフォーマットされています。たとえば、文字列が上記の例のように "flit.api:main" である場合、このオブジェクトは次のように実行して検索されます:
import flit.api
backend = flit.api.main
:object 部分を省略することも合法です。たとえば:
build-backend = "flit.api"
これは次のように動作します:
import flit.api
backend = flit.api
正式には、文字列は次の文法に従う必要があります:
identifier = (letter | '_') (letter | '_' | digit)*
module_path = identifier ('.' identifier)*
object_path = identifier ('.' identifier)*
entry_point = module_path (':' object_path)?
そして、module_path をインポートし、次に
module_path.object_path を検索します(または
object_path が欠落している場合は module_path のみ)。
モジュールパスをインポートする際には、ソースツリーを含むディレクトリを sys.path に含めない限り、検索しません(たとえば、PYTHONPATH で指定されている場合など)。Python は特定の状況で作業ディレクトリを自動的に sys.path に追加しますが、バックエンドを解決するコードはこれに影響されるべきではありません。
pyproject.toml ファイルが存在しない場合、または build-backend キーが欠落している場合、ソースツリーはこの仕様を使用しておらず、ツールは setup.py を実行するレガシー動作に戻るべきです(直接実行するか、暗黙的に setuptools.build_meta:__legacy__ バックエンドを呼び出すかのいずれかです)。
build-backend キーが存在する場合、これが優先され、ソースツリーは指定されたバックエンドの形式と慣例に従います(したがって、バックエンドが必要としない限り setup.py は不要です)。プロジェクトは、ツールがこの仕様を使用しない場合に互換性を保つために setup.py を含めることを検討するかもしれません。
この PEP は、pyproject.toml で使用する backend-path キーも定義しています。詳細は「インツリービルドバックエンド」セクションを参照してください。このキーは次のように使用されます:
[build-system]
# PEP 518 で定義されています:
requires = ["flit"]
# この PEP で定義されています:
build-backend = "local_backend"
backend-path = ["backend"]
ビルド要件
この PEP は、pyproject.toml の「ビルド要件」セクションにいくつかの追加要件を課します。これらは、プロジェクトがビルド要件で満たすことができない条件を作成しないようにすることを目的としています。
- プロジェクトのビルド要件は、依存関係の有向グラフを定義します(プロジェクト A は B をビルドするために必要であり、B は C と D を必要とするなど)。このグラフにはサイクルが含まれていてはなりません。 (プロジェクト間の調整の欠如などにより)サイクルが存在する場合、フロントエンドはプロジェクトのビルドを拒否する場合があります。
- ビルド要件がホイールとして利用可能な場合、フロントエンドはこれを実用的な場合に使用して、深くネストされたビルドを回避する必要があります。ただし、フロントエンドにはビルド要件を見つける際にホイールを考慮しないモードがある場合があるため、プロジェクトはホイールを公開するだけで依存関係のサイクルが解消されると仮定してはなりません。
- フロントエンドは、依存関係のサイクルを明示的にチェックし、サイクルが見つかった場合は情報メッセージを表示してビルドを終了する必要があります。
特に、依存関係のサイクルがないという要件により、セルフホスティングを希望するバックエンド(つまり、バックエンドのホイールをビルドする際にそのバックエンドを使用するバックエンド)は、サイクルを引き起こさないように特別な配慮をする必要があります。通常、これは自分自身をインツリーバックエンドとして指定し、外部ビルド依存関係を回避すること(通常はそれらをベンダリングすること)によって行われます。
ビルドバックエンドインターフェース
ビルドバックエンドオブジェクトは、次のフックの一部またはすべてを提供する属性を持つことが期待されます。共通の config_settings 引数については、個々のフックの後に説明します。
必須フック
build_wheel
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
...
ホイールファイル(.whl ファイル)をビルドし、指定された wheel_directory に配置する必要があります。作成した .whl ファイルのベース名(フルパスではなく)をユニコード文字列として返す必要があります。
ビルドフロントエンドが以前に prepare_metadata_for_build_wheel を呼び出し、この呼び出しの結果として得られるホイールのメタデータに依存している場合は、作成された .dist-info ディレクトリへのパスを metadata_directory 引数として提供する必要があります。この引数が提供された場合、build_wheel は同一のメタデータを持つホイールを生成する必要があります。ビルドフロントエンドが提供するディレクトリは、prepare_metadata_for_build_wheel によって作成されたディレクトリと同一でなければなりません。認識されないファイルも含めてです。
prepare_metadata_for_build_wheel フックを提供しないバックエンドは、build_wheel の metadata_directory パラメータを無視するか、None 以外に設定されている場合は例外をスローすることができます。
異なるソースからのホイールが同じ方法でビルドされることを保証するために、フロントエンドは最初に build_sdist を呼び出し、次にアンパックされた sdist で build_wheel を呼び出す場合があります。ただし、バックエンドが sdist の作成に必要な要件が不足していることを示している場合、フロントエンドはソースディレクトリで build_wheel を直接呼び出すことに戻ります。
ソースディレクトリは読み取り専用である場合があります。したがって、バックエンドはソースディレクトリ内のファイルを作成または変更せずにビルドする準備ができている必要がありますが、このケースを処理しないことを選択することもできます。この場合、失敗はユーザーに表示されます。フロントエンドは読み取り専用のソースディレクトリを特別に処理する責任を負いません。
バックエンドは、中間成果物をキャッシュ場所や一時ディレクトリに保存することができます。キャッシュの有無はビルドの最終結果に実質的な違いをもたらしてはなりません。
build_sdist
def build_sdist(sdist_directory, config_settings=None):
...
.tar.gz ソースディストリビューションをビルドし、指定された sdist_directory に配置する必要があります。作成した .tar.gz ファイルのベース名(フルパスではなく)をユニコード文字列として返す必要があります。
.tar.gz ソースディストリビューション(sdist)には、パッケージのソースファイルを含む {name}-{version}``(例:``foo-1.0)という単一のトップレベルディレクトリが含まれています。このディレクトリには、ビルドディレクトリからの pyproject.toml と、PEP 345 で説明されている形式のメタデータを含む PKG-INFO ファイルも含まれている必要があります。歴史的に zip ファイルも sdists として使用されてきましたが、このフックは gzipped tarball を生成する必要があります。これはすでに sdists のより一般的な形式であり、一貫した形式を持つことでツールが簡単になります。
生成された tarball は、UTF-8 ベースのファイル名を指定する最新の POSIX.1-2001 pax tar 形式を使用する必要があります。これはまだ Python 3.6 に付属の tarfile モジュールのデフォルトではないため、tarfile モジュールを使用するバックエンドは format=tarfile.PAX_FORMAT を明示的に渡す必要があります。
一部のバックエンドには、バージョン管理ツールなど、sdists の作成に追加の要件がある場合があります。ただし、一部のフロントエンドは、一貫性を確保するためにホイールを作成する際に中間 sdists を作成することを好む場合があります。
バックエンドが依存関係が不足しているため、または他のよく理解された理由で sdist を作成できない場合、バックエンドは UnsupportedOperation という特定のタイプの例外をスローする必要があります。この例外はバックエンドオブジェクトで利用可能です。
フロントエンドがホイールの中間として sdist をビルドする際にこの例外を受け取った場合、ホイールを直接ビルドすることに戻る必要があります。
バックエンドは、この例外タイプを定義する必要はありません。
オプションのフック
get_requires_for_build_wheel
def get_requires_for_build_wheel(config_settings=None):
...
このフックは、pyproject.toml ファイルで指定されたものに加えて、build_wheel または prepare_metadata_for_build_wheel フックを呼び出す際にインストールする必要がある PEP 508 依存関係仕様を含む追加の文字列リストを返す必要があります。
例:
def get_requires_for_build_wheel(config_settings):
return ["wheel >= 0.25", "setuptools"]
定義されていない場合、デフォルトの実装は return [] と同等です。
prepare_metadata_for_build_wheel
def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
...
指定された metadata_directory 内にホイールメタデータを含む .dist-info ディレクトリを作成する必要があります(つまり、{metadata_directory}/{package}-{version}.dist-info/ のようなディレクトリを作成します)。このディレクトリは、ホイール仕様で定義されている有効な .dist-info ディレクトリでなければなりませんが、RECORD や署名を含む必要はありません。このフックは、このディレクトリ内に他のファイルを作成することもでき、ビルドフロントエンドはこれらのファイルを保持する必要がありますが、それ以外は無視する必要があります。
ここでの意図は、メタデータがビルド時の決定に依存する場合、ビルドバックエンドがこれらの決定を実際のホイールビルドステップで再利用するための便利な形式で記録する必要がある場合があるということです。
これにより、作成した .dist-info ディレクトリのベース名(フルパスではなく)をユニコード文字列として返す必要があります。
ビルドフロントエンドがこの情報を必要とし、メソッドが定義されていない場合、build_wheel を呼び出して結果のメタデータを直接確認する必要があります。
get_requires_for_build_sdist
def get_requires_for_build_sdist(config_settings=None):
...
このフックは、pyproject.toml ファイルで指定されたものに加えて、追加の PEP 508 依存関係仕様を含む文字列リストを返す必要があります。これらの依存関係は build_sdist フックを呼び出す際にインストールされます。
定義されていない場合、デフォルトの実装は return [] と同等です。
Note
編集可能なインストール
この PEP は元々、pip install -e のように編集可能なインストールを行うための install_editable フックを指定していましたが、このトピックの複雑さのために削除されましたが、後の PEP で指定される可能性があります。
簡単に言えば、回答すべき質問には、既存の「編集可能なインストール」を実装する合理的な方法が何か、編集可能なインストールを行う方法をバックエンドまたはフロントエンドが選択するべきか、そしてフロントエンドが選択する場合、編集可能なインストールを行うためにバックエンドから何が必要かが含まれます。
Config settings
config_settings
この引数は、すべてのフックに渡され、ユーザーが個々のパッケージビルドにアドホックな構成を渡すための「エスケープハッチ」として提供される任意の辞書です。ビルドバックエンドは、この辞書に任意のセマンティクスを割り当てることができます。ビルドフロントエンドは、ユーザーが任意の文字列キー/文字列値ペアをこの辞書に配置するためのメカニズムを提供する必要があります。たとえば、--package-config CC=gcc のような構文をサポートするかもしれません。
ユーザーが重複する文字列キーを提供する場合、ビルドフロントエンドは対応する文字列値を文字列のリストに結合する必要があります。
ビルドフロントエンドは、ユーザーがこの辞書にエントリを配置するための任意の他のメカニズムも提供することができます。たとえば、pip は次のようなコマンドライン引数の組み合わせをマップすることができます:
pip install \
--package-config CC=gcc \
--global-option="--some-global-option" \
--build-option="--build-option1" \
--build-option="--build-option2"
を次のような config_settings 辞書にマップします:
{
"CC": "gcc",
"--global-option": ["--some-global-option"],
"--build-option": ["--build-option1", "--build-option2"],
}
もちろん、特定のビルドバックエンドおよびパッケージに対して意味のあるオプションを渡すことをユーザーが確認する必要があります。
フックは位置引数またはキーワード引数で呼び出される場合があるため、これらの引数の順序と名前が上記の順序と一致するようにフックを実装する必要があります。
すべてのフックは、作業ディレクトリがソースツリーのルートに設定された状態で実行され、標準出力および標準エラーに任意の情報テキストを出力することができます。 標準入力から読み取ることはできず、ビルドフロントエンドはフックを呼び出す前に標準入力を閉じる場合があります。
ビルドフロントエンドは、バックエンドからの標準出力および/または標準エラーをキャプチャする場合があります。バックエンドが出力ストリームが端末/コンソールでないことを検出した場合(例:not sys.stdout.isatty())、そのストリームに書き込む出力が UTF-8 エンコードされていることを確認する必要があります。ビルドフロントエンドは、キャプチャされた出力が有効な UTF-8 でない場合に失敗してはなりませんが、その場合はすべての情報を保持しない場合があります(例:Python の replace エラーハンドラを使用してデコードする場合があります)。出力ストリームが端末である場合、バックエンドは端末で実行される任意のプログラムと同様に、出力を正確に表示する責任があります。
フックが例外をスローするか、プロセスの終了を引き起こす場合、これはエラーを示します。
ビルド環境
ビルドフロントエンドの責任の1つは、ビルドバックエンドが実行される Python 環境を設定することです。
特定の「仮想環境」メカニズムを使用する必要はありません。ビルドフロントエンドは virtualenv、venv、または特別なメカニズムを使用しない場合があります。ただし、使用されるメカニズムは次の基準を満たす必要があります。
- プロジェクトのビルド要件で指定されたすべての要件が Python からインポート可能である必要があります。特に:
get_requires_for_build_wheelおよびget_requires_for_build_sdistフックは、pyproject.tomlファイルで指定されたブートストラップ要件を含む環境で実行されます。prepare_metadata_for_build_wheelおよびbuild_wheelフックは、pyproject.tomlファイルで指定されたブートストラップ要件およびget_requires_for_build_wheelフックで指定された要件を含む環境で実行されます。build_sdistフックは、pyproject.tomlファイルで指定されたブートストラップ要件およびget_requires_for_build_sdistフックで指定された要件を含む環境で実行されます。
- これは、新しい Python サブプロセスがビルド環境によって生成される場合でも真でなければなりません。例:import sys, subprocess subprocess.check_call([sys.executable, …])
は、プロジェクトのすべてのビルド要件にアクセスできる Python プロセスを生成する必要があります。これは、ビルドバックエンドがレガシー
setup.pyスクリプトをサブプロセスで実行する場合に必要です。 - ビルド要件パッケージによって提供されるすべてのコマンドラインスクリプトは、ビルド環境の PATH に存在する必要があります。たとえば、プロジェクトが flit にビルド要件を宣言している場合、次のように flit コマンドラインツールを実行するメカニズムが機能する必要があります。import subprocess import shutil subprocess.check_call([shutil.which(“flit”), …])
ビルドバックエンドは、上記の基準を満たす環境で機能する準備ができている必要があります。特に、標準ライブラリに存在するパッケージや、ビルド要件として明示的に宣言されたパッケージにアクセスできると仮定してはなりません。
フロントエンドは、各フックを新しいサブプロセスで呼び出す必要があります。これにより、バックエンドはプロセスのグローバル状態(環境変数や作業ディレクトリなど)を自由に変更できます。フロントエンドがフックをこの方法で簡単に呼び出すための Python ライブラリが提供されます。
ビルドフロントエンドへの推奨事項(規範的ではない)
ビルドフロントエンドは、上記の基準を満たすビルド環境を設定するための任意のメカニズムを使用できます。たとえば、すべてのビルド要件をグローバル環境にインストールするだけで、任意の準拠パッケージをビルドするのに十分です。ただし、これは多くの理由で最適ではありません。このセクションには、フロントエンド実装者への非規範的なアドバイスが含まれています。
ビルドフロントエンドは、デフォルトで、標準ライブラリと明示的に要求されたビルド依存関係のみを含む、各ビルドのための分離された環境を作成する必要があります。これには2つの利点があります。
- これにより、単一のインストール実行で矛盾するビルド要件を持つ複数のパッケージをビルドできます。たとえば、パッケージ1が pbr==1.8.1 をビルド要件として指定し、パッケージ2が pbr==1.7.2 をビルド要件として指定している場合、これらはグローバル環境に同時にインストールすることはできません。これは、ユーザーが
pip install package1 package2を要求する場合の問題です。または、ユーザーがすでにグローバル環境に pbr==1.8.1 をインストールしており、パッケージが pbr==1.7.2 をビルド要件として指定している場合、ユーザーのバージョンをダウングレードするのは非常に失礼です。 - これは、パッケージ作成者が実際に正確なビルド依存関係を宣言することを最大化するための公衆衛生措置として機能します。パッケージ作成者に対して強い言葉で忠告することはできますが、ビルドフロントエンドがデフォルトで分離を強制しない場合、PyPI にビルドが作成者のマシンでは正常に動作するが他の場所では動作しないパッケージが多数存在することになり、誰もが必要としない頭痛の種になります。
ただし、ビルド要件がさまざまな方法で問題を引き起こす状況もあります。たとえば、パッケージ作成者が重要な要件を誤って省略する場合や、パッケージが foo >= 1.0 をビルド要件として宣言しているが、1.0 が最新バージョンであるときには問題なく動作していたが、現在は 1.1 がリリースされており、重大なバグがある場合、またはユーザーがパッケージの推奨する numpy==1.8 を無視して numpy==1.7 に対してパッケージをビルドすることを決定する場合(これにより、結果のビルドが古いバージョンの numpy と C ABI レベルで互換性があることを保証するため)、ビルドフロントエンドはユーザーがこれらのデフォルトを上書きするためのメカニズムを提供する必要があります。たとえば、ビルドフロントエンドは、ビルド環境を作成する際に virtualenv または同等のオプションに --system-site-packages オプションを渡す --build-with-system-site-packages オプションや、プロジェクトの通常のビルド要件を上書きする --build-requirements-override=my-requirements.txt オプションを提供することができます。
ここでの一般的な原則は、パッケージ作成者に対して衛生を強制しながら、必要に応じてエンドユーザーがフードを開けてダクトテープを適用できるようにすることです。
インツリービルドバックエンド
特定の状況では、プロジェクトはビルドバックエンドのソースコードをソースツリーに直接含めることを希望する場合があります。これが予想される具体的な状況は次の2つです。
- バックエンド自体が、自身の機能を使用して自分自身をビルドすることを希望する場合(「セルフホスティングバックエンド」)
- 標準のバックエンドをカスタムラッパーでラップするプロジェクト固有のバックエンドで、ラッパーがプロジェクト固有すぎて独立して配布する価値がない場合(「インツリーバックエンド」)
プロジェクトは、backend-path キーを pyproject.toml に含めることで、バックエンドコードがインツリーにホストされていることを指定できます。このキーにはディレクトリのリストが含まれており、フロントエンドはバックエンドをロードし、バックエンドフックを実行する際に sys.path の先頭にこれらのディレクトリを追加します。
backend-path キーの内容には次の2つの制限があります。
backend-pathのディレクトリはプロジェクトルートに対して相対的に解釈され、ソースツリー内の場所を参照する必要があります(相対パスとシンボリックリンクが解決された後)。- バックエンドコードは
backend-pathで指定されたディレクトリの1つからロードされる必要があります(つまり、backend-pathを指定してインツリーバックエンドコードを持たないことは許可されていません)。
最初の制限は、ソースツリーが自己完結型であり、ソースツリー外の場所を参照できないことを保証するためです。フロントエンドはこの条件をチェックする必要があります(通常、絶対パスに解決し、シンボリックリンクを解決し、プロジェクトルートに対してチェックすることによって)し、違反があった場合はエラーメッセージを表示して失敗する必要があります。
backend-path 機能は、インツリーバックエンドの実装をサポートすることを目的としており、既存のバックエンドの構成を許可することを目的としていません。上記の2番目の制限は、これが機能の使用方法であることを保証するためのものです。フロントエンドはこのチェックを強制する場合がありますが、必須ではありません。通常、これはバックエンドの __file__ 属性を backend-path の場所と照らし合わせてチェックすることによって行われます。
ソースディストリビューション
レガシー sdist フォーマットを引き続き使用し、新しい制限を追加します。
この形式はほとんど定義されていませんが、基本的には次のようになります。 {NAME}-{VERSION}.{EXT} という名前のファイルで、{NAME}-{VERSION}/ というビルド可能なソースツリーに展開されます。従来、これらには常に setup.py スタイルのソースツリーが含まれていましたが、pyproject.toml スタイルのソースツリーも含めることができます。
統合フロントエンドは、{NAME}-{VERSION}.{EXT} という名前の sdist が {NAME}-{VERSION}-{COMPAT-INFO}.whl という名前のホイールを生成することを要求します。
PEP 517 バックエンドによってビルドされた sdists の新しい制限は次のとおりです。
- それらは
.tar.gz拡張子を持つ gzipped tar アーカイブになります。現在は zip アーカイブや他の圧縮形式の tarball は許可されていません。 - Tar アーカイブは、UTF-8 ベースのファイル名を使用する最新の POSIX.1-2001 pax tar 形式で作成する必要があります。
- sdist に含まれるソースツリーには
pyproject.tomlファイルが含まれていることが期待されます。
進化的なメモ
ここでの目標の1つは、古いスタイルの sdists を新しいスタイルの sdists に変換することをできるだけ簡単にすることです。 (例:これは動的ビルド要件をサポートする1つの動機です)。理想的には、任意の「バージョン0」VCS チェックアウトにドロップして新しいものに変換できる単一の静的な pyproject.toml が存在することです。これはおそらく100%可能ではありませんが、近づくことができ、どれだけ近づいているかを追跡することが重要です…したがって、このセクションがあります。
大まかな計画は次のようになります。フック言語を理解し、setup.py への呼び出しに変換するビルドシステムパッケージ(setuptools_pypackage など)を作成します。これは、おそらく setup_requires= 引数を抽出する方法を提供するために setuptools にフックまたはモンキーパッチを提供し、sdist コマンドの新しいバージョンを提供して新しいスタイルの形式を生成する必要があります。これはすべて実行可能であり、多くのパッケージにとって十分です(ただし、ここで何かを最終決定する前に、そのようなシステムをプロトタイプ化することをお勧めします)。 (あるいは、これらの変更を setuptools 自体に加えることもできます)。
しかし、プロジェクトを新しい形式に自動的にアップグレードできない2つの障害が残っています。
- 現在、setup.py が実行される前に特定のパッケージが環境に存在することを主張するパッケージが存在します。これにより、ビルドスクリプトを分離された virtualenv のような環境で実行することを決定した場合、プロジェクトはこれをチェックし、新しいシステムにアップグレードする際にこれらの依存関係を明示的に宣言し始める必要があります(
setup_requires=またはpyproject.tomlでの静的宣言を通じて)。 - 現在、一貫したメタデータを宣言しないパッケージが存在します(例:
egg_infoとbdist_wheelが異なるinstall_requires=を取得する場合があります)。新しいシステムにアップグレードする際に、プロジェクトはこれが適用されるかどうかを評価し、適用される場合はそれを停止する必要があります。
却下されたオプション
- ホイールおよび sdist フックがそれぞれのアーカイブと同じ内容を含むアンパックされたディレクトリをビルドするというアイデアを議論しました。場合によっては、アーカイブをパックおよびアンパックする必要がなくなる可能性がありますが、これは時期尚早の最適化のように思えます。ツールがアーカイブを標準的な交換形式として扱うのは有利です(特にホイールの場合、アーカイブ形式はすでに標準化されています)。アーカイブの作成を厳密に制御することは、再現可能なビルドにとって重要です。そして、アンパックされたディストリビューションを必要とするタスクがアーカイブを必要とするタスクよりも一般的であるとは限りません。
build_wheelにビルドディレクトリを渡す追加のフックを検討しました。既存のビルドシステムを調べた結果、ファイルをビルドディレクトリに事前にコピーするよりも、build_wheelにビルドディレクトリを渡す方が多くのツールにとって理にかなっていることがわかりました。- 次に、
build_wheelにビルドディレクトリを渡すというアイデアも不要な複雑化と見なされました。ビルドツールは一時ディレクトリやキャッシュディレクトリを使用してビルド中に中間ファイルを保存できます。フロントエンドが制御するキャッシュディレクトリが必要な場合は、将来的に追加することができます。 - 予想される理由で失敗を示すために
build_sdistが使用するフックについて、さまざまなオプションが長時間議論されました。たとえば、NotImplementedErrorをスローする、NotImplementedまたはNoneを返すなどです。この議論を再開しようとしないでください。非常に良い理由がない限り、私たちはこの議論に非常に疲れています。 - バックエンドをソースツリー内のファイルからインポートすることを許可することは、Python のインポートが通常どのように機能するかとより一貫性があります。ただし、これを許可しないことで、モジュール名の競合による混乱を防ぐことができます。この PEP の初期バージョンでは、ソースツリー内のファイルからバックエンドをインポートする手段を提供していませんでしたが、次のリビジョンで
backend-pathキーが追加され、プロジェクトが必要に応じてこの動作をオプトインできるようになりました。
PEP 517 への変更の概要
この PEP に対して、最初のリファレンス実装が pip 19.0 でリリースされた後に行われた変更は次のとおりです。
- ビルド要件のサイクルが明示的に禁止されました。
[build-system]テーブルにbackend-pathキーを追加することで、インツリーバックエンドおよびバックエンドのセルフホスティングのサポートが追加されました。build-backendを明示的に指定しないソースツリーに対して、setup.pyを直接呼び出す代わりにsetuptools.build_meta:__legacy__PEP 517 バックエンドを使用することが許容されることが明確にされました。
付録 A: PEP 516 との比較
PEP 516 は、ビルドシステムインターフェースを指定するための競合する提案であり、現在はこの PEP に対して却下されています。主な違いは、ビルドバックエンドがコマンドラインベースのインターフェースではなく、Python フックベースのインターフェースを介して定義されていることです。
この付録では、この PEP が PEP 516 に対して提案された理由を文書化しています。
Python フックではなくコマンドラインインターフェースを指定することが、バックエンドへの呼び出しの複雑さを自体で減らすとは期待していません。なぜなら、ビルドフロントエンドはフックを子プロセス内で実行することを望むからです。これは、ビルドフロントエンド自体をバックエンドコードから分離し、ビルドバックエンドの実行環境をよりよく制御するために重要です。したがって、両方の提案では、pip にサブプロセスを生成し、何らかのコマンドライン/IPC インターフェースと通信するコードが必要であり、サブプロセス内にはこれらのコマンドライン引数を解析し、実際のビルドバックエンド実装を呼び出す方法を知っているコードが必要です。したがって、この図はすべての提案に等しく適用されます:
+-----------+ +---------------+ +----------------+
| frontend | -spawn-> | child cmdline | -Python-> | backend |
| (pip) | | interface | | implementation |
+-----------+ +---------------+ +----------------+
2つのアプローチの主な違いは、これらのインターフェース境界がプロジェクト構造にどのようにマッピングされるかです:
.-= この PEP =-.
+-----------+ +---------------+ | +----------------+
| frontend | -spawn-> | child cmdline | -Python-> | backend |
| (pip) | | interface | | | implementation |
+-----------+ +---------------+ | +----------------+
|
|______________________________________| |
Owned by pip, updated in lockstep |
|
|
PEP-defined interface boundary
Changes here require distutils-sig
.-= 代替案 =-.
+-----------+ | +---------------+ +----------------+
| frontend | -spawn-> | child cmdline | -Python-> | backend |
| (pip) | | | interface | | implementation |
+-----------+ | +---------------+ +----------------+
|
| |____________________________________________|
| Owned by build backend, updated in lockstep
|
PEP-defined interface boundary
Changes here require distutils-sig
PEP 定義のインターフェース境界を Python コードに移動することで、3つの重要な利点が得られます。
第一に、ビルドフロントエンドが少数(pip など)であるのに対し、カスタムビルドバックエンドの長い尾が存在する可能性が高いため(これらは各パッケージが特定のビルド要件に合わせて選択するため)、実際の図は次のようになります:
.-= この PEP =-.
+-----------+ +---------------+ +----------------+
| frontend | -spawn-> | child cmdline | -Python+> | backend |
| (pip) | | interface | | | implementation |
+-----------+ +---------------+ | +----------------+
|
| +----------------+
+> | backend |
| | implementation |
| +----------------+
:
:
.-= 代替案 =-.
+-----------+ +---------------+ +----------------+
| frontend | -spawn+> | child cmdline | -Python-> | backend |
| (pip) | | | interface | | implementation |
+-----------+ | +---------------+ +----------------+
|
| +---------------+ +----------------+
+> | child cmdline | -Python-> | backend |
| | interface | | implementation |
| +---------------+ +----------------+
:
:
つまり、この PEP は全体のエコシステム内のコードの量を減らします。そして特に、新しいビルドシステムを作成するための障壁を低くします。たとえば、これは完全な動作するビルドバックエンドです:
# mypackage_custom_build_backend.py
import os.path
import pathlib
import shutil
import tarfile
SDIST_NAME = "mypackage-0.1"
SDIST_FILENAME = SDIST_NAME + ".tar.gz"
WHEEL_FILENAME = "mypackage-0.1-py2.py3-none-any.whl"
#################
# sdist creation
#################
def _exclude_hidden_and_special_files(archive_entry):
"""Tarfile filter to exclude hidden and special files from the archive"""
if archive_entry.isfile() or archive_entry.isdir():
if not os.path.basename(archive_entry.name).startswith("."):
return archive_entry
def _make_sdist(sdist_dir):
"""Make an sdist and return both the Python object and its filename"""
sdist_path = pathlib.Path(sdist_dir) / SDIST_FILENAME
sdist = tarfile.open(sdist_path, "w:gz", format=tarfile.PAX_FORMAT)
# Tar up the whole directory, minus hidden and special files
sdist.add(os.getcwd(), arcname=SDIST_NAME,
filter=_exclude_hidden_and_special_files)
return sdist, SDIST_FILENAME
def build_sdist(sdist_dir, config_settings):
"""PEP 517 sdist creation hook"""
sdist, sdist_filename = _make_sdist(sdist_dir)
return sdist_filename
#################
# wheel creation
#################
def get_requires_for_build_wheel(config_settings):
"""PEP 517 wheel building dependency definition hook"""
# As a simple static requirement, this could also just be
# listed in the project's build system dependencies instead
return ["wheel"]
def build_wheel(wheel_directory,
metadata_directory=None, config_settings=None):
"""PEP 517 wheel creation hook"""
from wheel.archive import archive_wheelfile
path = os.path.join(wheel_directory, WHEEL_FILENAME)
archive_wheelfile(path, "src/")
return WHEEL_FILENAME
もちろん、これは ひどい ビルドバックエンドです。ユーザーが手動で src/mypackage-0.1.dist-info/ にホイールメタデータを設定する必要があります。バージョン番号が変更されると、複数の場所で手動で更新する必要があります…しかし、これは機能し、より多くの機能を段階的に追加することができます。多くの経験から、大規模な成功したプロジェクトはしばしば迅速なハックとして始まることが示されています(例:Linux – 「ただの趣味、大きくてプロフェッショナルにはならない」; IPython/Jupyter – 大学院生の $PYTHONSTARTUP ファイル)、したがって、私たちの目標が優れたビルドツールの活気に満ちたエコシステムの成長を奨励することである場合、新しいビルドシステムの作成の障壁を最小限に抑えることが重要です。
第二に、Python はインターフェースを記述するためのよりシンプルで豊富な構造を提供するため、仕様から不要な複雑さを取り除きます。仕様は最悪の場所であり、仕様の変更には多くの利害関係者間での痛みを伴う合意形成が必要です。コマンドラインインターフェースアプローチでは、複数の異なる種類の入力を単一の線形コマンドラインにマッピングするためのアドホックな方法を考え出す必要があります(例:ユーザーが指定した構成引数と PEP 定義の引数の衝突をどのように回避するか?オプションの引数をどのように指定するか?Python インターフェースを使用する場合、これらの質問にはシンプルで明確な回答があります)。サブプロセスを生成および管理する際には、多くの細かい詳細を正しく処理する必要があり、微妙なクロスプラットフォームの違いがあり、最も明白なアプローチのいくつか(例:build_requires 操作のデータを標準出力で返す)は予期しない落とし穴を作成する可能性があります(例:ビルド要件を計算するために子プロセスを生成する必要があり、これらの子プロセスが時折エラーメッセージを標準出力に出力する場合はどうなりますか?慎重なビルドバックエンド作成者はこの問題を回避できますが、Python インターフェースを定義する最も明白な方法は、この可能性を完全に排除します。なぜなら、フックの戻り値は明確に区別されているからです)。
一般に、ビルドバックエンドを独自のプロセスに分離する必要があるため、IPC の複雑さを完全に排除することはできませんが、IPC インターフェースの両側を単一のプロジェクトの制御下に置くことで、IPC インターフェースのバグを修正するコストを大幅に削減できます。
第三に、そして最も重要なことに、Python フックアプローチは、将来的にこの仕様を進化させるためのより強力なオプションを提供します。
具体的な例として、来年、新しい build_sdist_from_vcs フックを追加し、フロントエンドがバージョン管理追跡メタデータをバックエンドに渡す責任を負う代替の build_sdist フックを提供することを想像してください(ディスク上のすべてのファイルが追跡されていることを示すことを含む)。これにより、個々のバックエンドがそれらの情報をクエリする必要がなくなります。この移行を管理するために、ビルドフロントエンドが build_sdist_from_vcs を利用可能な場合に透過的に使用し、そうでない場合は build_sdist にフォールバックすることができるようにし、ビルドバックエンドが両方のメソッドを定義して、古いビルドフロントエンドと新しいビルドフロントエンドの両方と互換性を持つことができるようにする必要があります。
さらに、私たちのメカニズムは次の2つの目標も達成する必要があります。(a)たとえば、pip と flit の新しいバージョンが両方とも新しいインターフェースをサポートするように更新された場合、これを使用するために十分である必要があります。特に、flit を使用するすべてのプロジェクトが個々の pyproject.toml ファイルを更新する必要はありません。(b)交渉を行うために追加のプロセスを生成する必要はありません。プロセスの生成は、一部のプラットフォームで大規模なマルチパッケージスタックをデプロイする際にボトルネックになる可能性があるためです(Windows)。
ここで説明するインターフェースでは、これらの目標をすべて簡単に達成できます。なぜなら、pip が子プロセス内で実行されるコードを制御できるため、次のように簡単に書くことができるからです:
command, backend, args = parse_command_line_args(...)
if command == "build_sdist":
if hasattr(backend, "build_sdist_from_vcs"):
backend.build_sdist_from_vcs(...)
elif hasattr(backend, "build_sdist"):
backend.build_sdist(...)
else:
# error handling
代替案では、公開インターフェースの境界がサブプロセス呼び出しに配置されるため、これは不可能です。インターフェースがサポートされているかどうかをクエリするために追加のプロセスを生成する必要があります(PEP 516 の以前のドラフトに含まれていた代替案の1つ)、または自動交渉を完全に放棄する必要があります(現在のバージョンの PEP では)、インターフェースの変更がライブになる前に N 個の個々のパッケージが pyproject.toml ファイルを更新する必要があり、変更は新しいリリースに制限されます。
この PEP では、prepare_metadata_for_build_wheel コマンドをオプションにすることができます。私たちの設計では、ビルドフロントエンドが次のようなコードをサブプロセスランナーに配置することで簡単に処理できます:
def dump_wheel_metadata(backend, working_dir):
"""Dumps wheel metadata to working directory.
Returns absolute path to resulting metadata directory
"""
if hasattr(backend, "prepare_metadata_for_build_wheel"):
subdir = backend.prepare_metadata_for_build_wheel(working_dir)
else:
wheel_fname = backend.build_wheel(working_dir)
already_built = os.path.join(working_dir, "ALREADY_BUILT_WHEEL")
with open(already_built, "w") as f:
f.write(wheel_fname)
subdir = unzip_metadata(os.path.join(working_dir, wheel_fname))
return os.path.join(working_dir, subdir)
def ensure_wheel_is_built(backend, output_dir, working_dir, metadata_dir):
"""Ensures built wheel is available in output directory
Returns absolute path to resulting wheel file
"""
already_built = os.path.join(working_dir, "ALREADY_BUILT_WHEEL")
if os.path.exists(already_built):
with open(already_built, "r") as f:
wheel_fname = f.read().strip()
working_path = os.path.join(working_dir, wheel_fname)
final_path = os.path.join(output_dir, wheel_fname)
os.rename(working_path, final_path)
os.remove(already_built)
else:
wheel_fname = backend.build_wheel(output_dir, metadata_dir=metadata_dir)
return os.path.join(output_dir, wheel_fname)
したがって、フロントエンドの残りの部分に対して完全に一貫したインターフェースを公開できます。追加のサブプロセス呼び出しや重複したビルドなどはありません。しかし、これはプロジェクト内のプライベートなインターフェースの一部としてのみ書きたい種類のコードです(例:与えられた例では、作業ディレクトリが2つの呼び出し間で共有される必要がありますが、他のホイールビルドとは共有されません。メタデータヘルパー関数からの戻り値がホイールビルド関数に渡されることを前提としています)。
(そしてもちろん、メタデータコマンドをオプションにすることは、新しいバックエンドの開発の障壁を低くするための1つの部分です。上記で説明したように。)
その他の違い
上記の主要なコマンドライン対 Python フックの違いに加えて、この提案にはいくつかの他の違いがあります。
- メタデータコマンドはオプションです(上記で説明したように)。
- メタデータを単一の METADATA ファイルとしてではなく、ディレクトリとして返します。これは、実際にホイールメタデータが複数のファイルに分散している方法とより一致し、将来的により多くのオプションを提供します(たとえば、PEP 426 の提案に従って METADATA の形式を JSON に変更する代わりに、既存の METADATA を後方互換性のためにそのままにし、新しい拡張を JSON の「サイドカーファイル」として同じディレクトリ内に追加することができます。あるいはそうしないかもしれません。ポイントは、オプションをよりオープンに保つことです)。
- メタデータステップとホイールビルドステップの間で情報を渡すためのメカニズムを提供します。おそらく、これは良いアイデアだと誰もが同意するでしょうか?
- メタデータステップとホイールビルドステップの間で情報を渡すためのメカニズムを提供します。おそらく、これは良いアイデアだと誰もが同意するでしょうか?
- ビルド環境に関する詳細な推奨事項を提供しますが、これらは規範的ではありません。
著作権
このドキュメントはパブリックドメインに置かれています。
Source: https://github.com/python/peps/blob/main/peps/pep-0517.rst
Last modified: 2024-10-22 05:20:14 GMT