下流のパッケージングをサポートする¶
- ページステイタス:
ドラフト
- 最終査読日:
2025-?
PyPI および pip のような Python のパッケージングツールは、Python パッケージを配布する主要な手段ですが、その一方で、しばしば他のパッケージングエコシステムの一部として利用可能にされています。これらの再パッケージングの努力は、ダウンストリーム パッケージングと総称され (対して元の方は アップストリーム パッケージングと総称されます)、 Linux ディストロ・Conda・Homebrew・MacPorts といったプロジェクトが (訳註:ダウンストリームパッケージングに) 該当します。これらは、一般的には、 Python パッケージングツール単独では取り扱えないようなユースケースをよりよくサポートすること、例えば、特定のオペレーティングシステムとの間でネイティブな統合をすることや、非 Python ソフトウェアの特定のバージョンとの互換性を保証することを目指しています。
この議論では、ダウンストリームパッケージングが通常どのように行われるのか、および、ダウンストリームパッケージングが直面する追加的なチャレンジがどのようなものであるかを説明することを試みます。 (アップストリームプロジェクトを保守する上で何らかの大きな追加作業を招来しない範囲で) ダウンストリームパッケージングを 劇的に 簡単なものにするような、プロジェクトの保守者が追随することを選択してもよいいくつかの選択可能なガイドラインを提供することを目指しています。これが全部採用するかまったく採用しないかという提案ではないことを銘記してください -- アップストリーム側の保守者が為し得ることは、たとえそれが小さな部分だけだったとしても、すべて役に立ちます。ダウンストリーム側の保守者もこれらの問題を解決するためのパッチを提供することに意欲的です。他のダウンストリーム群がパッチを作成してリベースし続ける必要を取り除くことや、同じ問題に対して一貫性のない解決策を適用するリスクを取り除くことができるので、このようなパッチをマージすることは非常に助けになります。
ソフトウェアの保守者達とダウンストリームのパッケージ作業者の間の良い関係を確立することは、相互に利益をもたらします。ダウンストリーム側は、しばしば、パッケージを改良するために彼らの経験・時間・ハードウェアを共有したいと望み、そうしなければ獲得するのに多大な労力を必要とする他パッケージとの関係について情報を提供したいと望んでいます。パッケージ作業者は、しばしば、プロダクションレベルになった後でユーザがそのバグを踏むよりも前に、高品質のバグレポートを提供し、可能な時ならいつでもパッチを供給します。例えば、彼らは、新しい Python のバージョンがリリースされた時に起きる互換性問題のいずれについても、彼らが再配布するパッケージがアップデートされていることを確実にすることに恒常的に活発です。
ダウンストリームのビルドには、バイナリ形式での再配布だけでなく、 (例えば、Gentoo Linux のようなソースコードありきのディストロで) ユーザ側のシステムで行われるソースビルドも含まれることを銘記してください。
完全な形のソースコード配布物を提供する¶
なぜ?¶
ダウンストリームのパッケージ作業者の大多数は、アップストリーム側が提供するバイナリのパッケージを使ってやるよりも、ソースコードからパッケージをビルドすることを好みます。いくつかの場合には、パッケージを配布物に同梱してもらうためにソースコードを使うことが要求されていることもあります。これは、また、汎用 <universal> の wheel を提供する純 Python のパッケージにも当てはまります。ソースコード配布物を用いる理由には次のようなものがあるでしょう:
すべてのパッケージのソースコードを監査することができるようにするため。
試験一式を動作させることができるようにし、また、説明文書をビルドすることができるようにするため。
プロジェクトのリポジトリからのバックポートされたコミットを含めてパッチの適用が容易にできるようにするため、また、逆にプロジェクトへパッチを送ることができるようにするため。
アップストリームのビルドではカバーされていない特定のプラットフォームでのビルドができるようにするため。
システムのライブラリの特定のバージョンに対してビルドができるようにするため。
すべての Python パッケージを通じて守備一貫したビルドプロセスを保持するため。
通常は、 Git リポジトリからパッケージをビルドすることができますが、代わりに静的なアーカイブファイルを提供する重要ないくつかの理由があります:
単一のファイルをダウンロードする方が、より効率が良く、より信頼できて、例えば Git clone を使うよりも良くサポートされていることがしばしばです。これによって、貧弱なインターネット接続性の元にあるユーザを助けることができます。
ダウンストリーム側は、引き続くビルドに用いるためのソースファイルの真正性を検証するために、時を経てもビット単位で同一であることを要求するハッシュ値を使うことがしばしばあります。例えば、仮にサーバ上の gzip がアップグレードされれば、圧縮されたデータが変化するかもしれないので、自動的に作成される Git アーカイブはこのようなことを保証しません。
アーカイブファイルはミラーサイトにおくことも可能で、アップストリームとダウンストリームの両方の帯域幅を節約することができます。実際のビルドは、ローカルミラーまたは前もって再配布を受けたソースファイル群にしかアクセスが提供されないファイアウォールの内側もしくはオフラインの環境下で後から実行することが可能です。
明示的にアーカイブファイルを公開することで、そのソースコードアーカイブが作成された時点でバージョン管理システムのメタデータに関する依存関係がすべて解決されていたことを保証することができます。例えば、自動的に生成される Git アーカイブはコミットタグ情報をすべて削除していて、そのビルドに必要なバージョン情報の詳細が不正確になっている可能性があります。
どのように?¶
理想的には、 ** PyPI上で公開されたソースコード配布物のアーカイブには、そのパッケージの Git リポジトリからパッケージ自体をビルドし、テストスイートを走らせ、説明文書をインストールするために必要なファイルや、シェル補完やエディタ用のサポートファイルなどエンドユーザにとって役に立つかもしれない他のファイルを全て含めるべきです ** 。
この点は、パッケージそれ自体に属するファイル群にだけ適用します。Python のパッケージ管理機構に極めてよく似ていますが、ダウンストリームパッケージの処理では、必要な Python の依存関係や、そのパッケージとそのビルドスクリプトが必要とするシステムツールや外部のライブラリの依存関係を提供します。しかしながら、ダウンストリーム側が必要な依存関係を決定し、それらの変更をチェックするために、これらの依存関係を列挙するファイル (例えば requirements*.txt
ファイル) もまた同梱されるべきです。
プロジェクトによっては、PyPI からのソースコード配布物を使う Python パッケージ管理機構に関して心配する場合があります。彼らとしては、特定のプロジェクトをソースコードからビルドするという問題含みではっきりと機能しないことがわかっている代替策を有効にすることになるので、これらのツール類によって使われることのないファイル群で彼らのパッケージのサイズが増えることを望まなかったり、ソースコード配布物を公開することを全く望まなかったりするのです。
他方で、(例えば NumPy のような) いくつかのプロジェクトでは、そのインストールパッケージの中にテストスイートを同梱することに決めたものがあります。これによって、そのようなパッケージをインストールした後に、例えば依存先のパッケージをアップグレードした後にリグレッション (退行)がないかどうかの確認を行うために、ユーザがテストスイートを走らせることを許すという利点を追加することができます。さらに別のやり方としては、テストスイートやテストデータを別の Python パッケージに分割する方法があります。そのような方法は、巨大なテストベクトルを cryptography-vectors_package に分離する形で、 cryptography プロジェクトで採用されています。
リリースの際のワークフローでソースコード配布物を用いるのは良い考えです。例えば、 ビルド ツールは正にそれをやります -- まずソースコード配布物をビルドし、それを使って wheel をビルドするのです。これによって、ソースコード配布物が実際に動作することを確実にし、誤って公式の wheel よりも数の少ないファイル群をインストールしてしまうことがなくなります。
理想的には、テストスイートを走らせたり説明文書をビルドしたりといったことや、全ての必要なファイルが実際に含まれていることを確実にする特殊なテストを追加するのに、ソースコード配布物も使いましょう。当然のことですが、これはより多くの努力を必要としますので、こうしなくても問題はありません -- ダウンストリームのパッケージ作業者たちが素早く不足しているファイルを報告してくれるでしょう。
ビルドプロセスの最中にインターネットを使うことはやめましょう¶
なぜ?¶
ダウンストリームのビルドは、しばしば、インターネットにアクセスすることができないサンドボックス化された環境の中で行われます。パッケージのソースファイルがこの環境内に展開され、必要な全ての依存関係がインストールされます。
仮にこれが当てはまらない場合でも、適切にダウンロードを認証するために十分な注意を払っていたものと仮定して、インターネットを使うことはいくつもの理由から推奨されません:
インターネット接続は (例えば受信状態が悪いために) 不安定であるかもしれませんし、プロセスを失敗させたりハングさせたりしかねない一時的な問題の影響を受けるかもしれません。
リモートにあるリソース群は、もはやビルドができなくなるような形で、一時的または永続的に利用不可能になるかもしれません。これは、特に、誰かが古いバージョンのパッケージをビルドする必要に迫られた時に問題となります。
リモートのリソース群は、ビルドが再現可能にならない形に変化するかもしれません。
リモートのサーバにアクセスすることは、当該システムでそのパッケージをビルドしていることに関する情報を暴露するので、プライバシー問題と潜在的なセキュリティ問題をもたらします。
ユーザは、制御下にないインターネットアクセスが追加料金やその他の不都合に帰結してしまう、そのようなデータ量に制約のある通信サービスを使っているかもしれません。
どのように?¶
パッケージが、例えば売り物の依存関係を自動的にダウンロードしたり、Git のサブモジュールをフェッチしたりするような、そのようなインターネットを使用する何らかのカスタムビルド バックエンド 動作を実装しているなら、そのソースコード配布物は、これらのファイル群をすべて同梱するか、別途準備することができるようにするかのいずれかをするべきで、ファイル群がすでに存在する場合にはインターネットを使用しないようにしなければなりません。
この点は、パッケージのメタデータの中で指定された Python の依存関係には当てはまらないということ、また、それらは (ビルド や pip のような) フロントエンド によるビルドやインストールのプロセスの最中にフェッチされるということに注意してください。ダウンストリーム側では、Python の依存関係をローカルに準備するようなフロントエンドを使います。
理想的には、カスタムビルド用スクリプトは、明示的に要求された場合を除いて、インターネットへのアクセスを試みることさえ全く行うべきでありません。もし、何らかのリソースが不足していてフェッチする必要があるなら、まずユーザに許可を求めるべきです。それが実現不可能であれば、次善の策は、あらゆるインターネットアクセスをさせないというオプトアウトのスイッチを提供するべきです。これは、例えば NO_NETWORK
環境変数が空でない値に設定されているか否かを調べることで行うことができます。
ダウンストリームは、しばしばテストを走らせたり説明文書をビルドしたりしますので、理想的を言えば、上記のことはこれらのプロセスにも同様に拡大適用されるべきです。
もし、リモートのリソースをフェッチするのであれば、悪意ある第三者がファイルを置換しているという事態から保護するために、 (通常はハッシュ値を使って) 絶対に その真正性を検証 しなければならないことも覚えておいてください。
システムの依存関係に対してビルドすることをサポートする¶
なぜ?¶
Python プロジェクトの中には、 C や C++ で書かれたライブラリのような非 Python の依存関係を持つものがあります。アップストリームパッケージング内でこのような依存関係のシステムバージョンを使おうとする試行は、エンドユーザに多数の問題をもたらすかもしれません:
公開された wheel は、使っているライブラリのバイナリ互換のバージョンがユーザのシステム上に存在することを要求します。もし、ライブラリが不足していたり非互換のバージョンがインストールされていたりすると、 Python パッケージは、経験豊かとは言えないユーザにとっては明晰でないエラーを伴って失敗するかもしれませんし、動作時に誤動作することさえあるかもしれません。
ソースコード配布物からのビルドには、ソースコードレベルで互換性のあるバージョンの依存関係先が、システムによってはライブラリ本体とは別のパッケージとして用意されていることがある、開発用のヘッダーファイルやその他の補助ファイルと共に、存在していることが要求されます。
経験を積んだユーザにとってさえ、互換性のある依存関係先のバージョンをインストールすることは極めて難しかもしれません。例えば、使っている Linux ディストロが要求されているバージョンを提供していないこともあり得るし、他のパッケージが互換性のないバージョンのものを要求しているかもしれません。
Python パッケージとそのシステム依存関係の間の関連性は、パッケージングシステムによって記録されているということはありません。次回のシステム更新がライブラリを、 Python パッケージとのバイナリ互換性を破壊するような、より新しいバージョンへとアップグレードするかもしれず、ユーザが修復のために介入することが要求されるかもしれません。
これらの理由は、依存関係先を静的にリンクするか、インストールされるパッケージの中にローカルコピーを用意するかのいずれかに決める合理的な理由になります。ソースコード配布物の中にも依存関係先を含めることもあるかもしれません。時に、これらの依存関係先もまた PyPI でパッケージされていて、他のすべての Python パッケージのように、プロジェクトの依存関係先として宣言され得ることもあります。
しかしながら、これらの課題のどれひとつを取ってもそれはダウンストリームのパッケージングには当てはまらず、ダウンストリーム側ではシステムの依存関係に直接にリンクする方を好む十分な理由があります。とりわけ次のような場合:
多くのケースでは、部品間で動的な依存関係を信頼できる形で共有することが、ダウンストリーム側のパッケージングエコシステムの 目的 の大きな部分を占めています。それをサポートするのを補助することは、そのようなシステムのユーザが好ましいフォーマットでアップストリームのプロジェクトにアクセスすることをより容易にします。
静的なリンキングと提供は、その出自の検証が難しくなるので、外部への依存関係の使用をわかりにくくします。
動的なリンキングは、ダウンストリームのパッケージングエコシステム全体で使用されるライブラリを素早く体系的に置き換えることができるようにしますが、それは、セキュリティ上の脆弱性や致命的なバグを含んでいることが判明した時には特に重要になり得ます。
システムの依存関係を使うことで、ダウンストリーム保守者が様々なパッケージ内で提供される依存関係に統一的にパッチしなければならないということがない形で、特定のプラットフォーム上でユーザ経験を改善し得るところのダウンストリームでのカスタマイゼーションからの利益をパッケージにもたらします。これは、互換性の向上やセキュリティハードニングを含み得ます。
Static linking and vendoring can result in multiple different versions of the same library being loaded in the same process (for example, attempting to import two Python packages that link to different versions of the same library). This sometimes works without incident, but it can also lead to anything from library loading errors, to subtle runtime bugs, to catastrophic failures (like suddenly crashing and losing data).
Last but not least, static linking and vendoring results in duplication, and may increase the use of both disk space and memory.
どのように?¶
A good compromise between the needs of both parties is to provide a switch
between using vendored and system dependencies. Ideally, if the package has
multiple vendored dependencies, it should provide both individual switches
for each dependency, and a general switch to control the default for them,
e.g. via a USE_SYSTEM_DEPS
environment variable.
If the user requests using system dependencies, and a particular dependency is either missing or incompatible, the build should fail with an explanatory message rather than fall back to a vendored version. This gives the packager the opportunity to notice their mistake and a chance to consciously decide how to solve it.
It is reasonable for upstream projects to leave testing of building with system dependencies to their downstream repackagers. The goal of these guidelines is to facilitate more effective collaboration between upstream projects and downstream repackagers, not to suggest upstream projects take on tasks that downstream repackagers are better equipped to handle.
Support downstream testing¶
なぜ?¶
A variety of downstream projects run some degree of testing on the packaged Python projects. Depending on the particular case, this can range from minimal smoke testing to comprehensive runs of the complete test suite. There can be various reasons for doing this, for example:
Verifying that the downstream packaging did not introduce any bugs.
Testing on additional platforms that are not covered by upstream testing.
Finding subtle bugs that can only be reproduced with particular hardware, system package versions, and so on.
Testing the released package against newer (or older) dependency versions than the ones present during upstream release testing.
Testing the package in an environment closely resembling the production setup. This can detect issues caused by non-trivial interactions between different installed packages, including packages that are not dependencies of your package, but nevertheless can cause issues.
Testing the released package against newer Python versions (including newer point releases), or less tested Python implementations such as PyPy.
Admittedly, sometimes downstream testing may yield false positives or bug reports about scenarios the upstream project is not interested in supporting. However, perhaps even more often it does provide early notice of problems, or find non-trivial bugs that would otherwise cause issues for the upstream project's users. While mistakes do happen, the majority of downstream packagers are doing their best to double-check their results, and help upstream maintainers triage and fix the bugs that they reported.
どのように?¶
There are a number of things that upstream projects can do to help downstream repackagers test their packages efficiently and effectively, including some of the suggestions already mentioned above. These are typically improvements that make the test suite more reliable and easier to use for everyone, not just downstream packagers. Some specific suggestions are:
Include the test files and fixtures in the source distribution, or make it possible to easily download them separately.
Do not write to the package directories during testing. Downstream test setups sometimes run tests on top of the installed package, and modifications performed during testing and temporary test files may end up being part of the installed package!
Make the test suite work offline. Mock network interactions, using packages such as responses or vcrpy. If that is not possible, make it possible to easily disable the tests using Internet access, e.g. via a pytest marker. Use pytest-socket to verify that your tests work offline. This often makes your own test workflows faster and more reliable as well.
Make your tests work without a specialized setup, or perform the necessary setup as part of test fixtures. Do not ever assume that you can connect to system services such as databases — in an extreme case, you could crash a production service!
If your package has optional dependencies, make their tests optional as well. Either skip them if the needed packages are not installed, or add markers to make deselecting easy.
More generally, add markers to tests with special requirements. These can include e.g. significant space usage, significant memory usage, long runtime, incompatibility with parallel testing.
Do not assume that the test suite will be run with
-Werror
. Downstreams often need to disable that, as it causes false positives, e.g. due to newer dependency versions. Assert for warnings usingpytest.warns()
rather thanpytest.raises()
!Aim to make your test suite reliable and reproducible. Avoid flaky tests. Avoid depending on specific platform details, don't rely on exact results of floating-point computation, or timing of operations, and so on. Fuzzing has its advantages, but you want to have static test cases for completeness as well.
Split tests by their purpose, and make it easy to skip categories that are irrelevant or problematic. Since the primary purpose of downstream testing is to ensure that the package itself works, downstreams are not generally interested in tasks such as checking code coverage, code formatting, typechecking or running benchmarks. These tests can fail as dependencies are upgraded or the system is under load, without actually affecting the package itself.
If your test suite takes significant time to run, support testing in parallel. Downstreams often maintain a large number of packages, and testing them all takes a lot of time. Using pytest-xdist can help them avoid bottlenecks.
Ideally, support running your test suite via
pytest
. pytest has many command-line arguments that are truly helpful to downstreams, such as the ability to conveniently deselect tests, rerun flaky tests (via pytest-rerunfailures), add a timeout to prevent tests from hanging (via pytest-timeout) or run tests in parallel (via pytest-xdist). Note that test suites don't need to be written withpytest
to be executed withpytest
:pytest
is able to find and execute almost all test cases that are compatible with the standard library'sunittest
test discovery.
Aim for stable releases¶
なぜ?¶
Many downstreams provide stable release channels in addition to the main package streams. The goal of these channels is to provide more conservative upgrades to users with higher stability needs. These users often prefer to trade having the newest features available for lower risk of issues.
While the exact policies differ, an important criterion for including a new package version in a stable release channel is for it to be available in testing for some time already, and have no known major regressions. For example, in Gentoo Linux a package is usually marked stable after being available in testing for a month, and being tested against the versions of its dependencies that are marked stable at the time.
However, there are circumstances which demand more prompt action. For example, if a security vulnerability or a major bug is found in the version that is currently available in the stable channel, the downstream is facing a need to resolve it. In this case, they need to consider various options, such as:
putting a new version in the stable channel early,
adding patches to the version currently published,
or even downgrading the stable channel to an earlier release.
Each of these options involves certain risks and a certain amount of work, and packagers needs to weigh them to determine the course of action.
どのように?¶
There are some things that upstreams can do to tailor their workflow to stable release channels. These actions often are beneficial to the package's users as well. Some specific suggestions are:
Adjust the release frequency to the rate of code changes. Packages that are released rarely often bring significant changes with every release, and a higher risk of accidental regressions.
Avoid mixing bug fixes and new features, if possible. In particular, if there are known bug fixes merged already, consider making a new release before merging feature branches.
Consider making prereleases after major changes, to provide more testing opportunities for users and downstreams willing to opt-in.
If your project is subject to very intense development, consider splitting one or more branches that include a more conservative subset of commits, and are released separately. For example, Django currently maintains three release branches in addition to main.
Even if you don't wish to maintain additional branches permanently, consider making additional patch releases with minimal changes to the previous version, especially when a security vulnerability is discovered.
Split your changes into focused commits that address one problem at a time, to make it easier to cherry-pick changes to earlier releases when necessary.