Python のパッケージング

はじめに

アプリケーションのインストール方法には二つの流儀があります。一つ目の流儀は Windows と Mac OS X で一般的なもので、アプリケーションは必要なものを全て含むべきであり、インストールするときには何にも依存してはいけないという考え方です。この考え方ではアプリケーションの管理が単純になります: アプリケーションはスタンドアローンの“ツール”であり、インストールおよびアンインストールで OS の他の部分がめちゃくちゃになることもありません。もしアプリケーションが一般的でないライブラリを必要とするなら、そのライブラリはアプリケーションのディストリビューションに含まれます。

二つ目の流儀は Linux ベースのシステムで標準的なもので、ソフトウェアをパッケージと呼ばれる必要なものを全て含んだ小さな単位の集まりと扱います。いくつかのライブラリがまとめられてパッケージとなり、このライブラリパッケージが他のパッケージに依存することもあります。アプリケーションのインストールには他の多数のライブラリの特定のバージョンを検索・インストールする必要があり、依存ライブラリは通常数千個のライブラリを収めた中央レポジトリからフェッチされます。こういった考え方があるために、Linux のディストリビューションは dpkgRPM といったパッケージ管理システムを使って依存関係を追跡し、同じライブラリの互換性の無い二つのバージョンがインストールされるのを防いでいます。

両方の流儀に利点と欠点があります。全ての部分を個別に更新・置換できる高度にモジュール化されたシステムがあれば、各ライブラリが一つだけ存在するようになってパッケージの管理が簡単になります。さらにアプリケーションが利用するライブラリを個別に更新することも可能であり、例えばあるライブラリのセキュリティフィックスはそのライブラリを利用する全てのアプリケーションにすぐに伝わります。これに対してアプリケーションが独自にライブラリを持っていた場合、特に異なるアプリケーションが異なるバージョンのライブラリを使っていた場合には、セキュリティフィックスのデプロイが困難になります。

しかしこのモジュール性を欠点だと考える開発者もいます。開発者自身が依存関係を制御できないためです。システムのアップグレードで起こる“依存関係地獄”に影響されることのない安定なアプリケーション動作環境を保証するには、アプリケーションをスタンドアローンのソフトウェアツールとして提供した方が簡単です。

アプリケーションに必要なものを全て含めるようにすると、複数のオペレーティングシステムをサポートする場合にも開発者の仕事が簡単になります。プロジェクトの中には、独自のディレクトリ構造内で動作し、ログファイルでさえホストのシステムと一切対話しないで動作するポータブルなアプリケーションをリリースしているものもあります。

Python のパッケージングシステムは二つ目の流儀 ――インストールごとに複数の依存関係―― を、開発者、管理者、パッケージャー、ユーザーにとってできる限り使いやすくすることを目指しています。しかし残念ながらこのシステムには様々な問題があり、バージョンスキーマが直感的でない、上手く扱えないデータファイルがある、再パッケージングが難しいなど様々な問題が生じます。三年前、私は Pythoneer のグループと共にパッケージングシステムを再開発してこういった問題を解決することを決心しました。この章ではパッケージング同盟 (the Fellowship of the Packaging) と自称する私たちが修復しようとしている問題、およびそういった問題に対する私たちの解決法について説明します。

用語

Python においてパッケージという言葉は Python ファイルを含むディレクトリを指し、モジュールという言葉は一つの Python ファイルを指します。“パッケージ”という言葉をシステムが使った場合には通常のプロジェクトのリリースを指すこともあるので、この用語には曖昧さがあります。

Python 開発者もはっきりしないまま話をすることがあります。この曖昧さを取り除く一つの方法は、Python モジュールを含んだディレクトリを“Python パッケージ”と呼ぶことです。なお“リリース”はプロジェクトのあるバージョンを指し、“ディストリビューション”はソースまたはバイナリのリリースを tarball や zip ファイルにした配布できる形式を指します。

Python 開発者の苦しみ

多くの Python プログラマーは自身のプログラムが全ての環境で実行できることを望みながらも、Python の標準ライブラリとシステム固有のライブラリの両方を利用するのが普通です。しかしありとあらゆるパッケージングシステム用にアプリケーションのパッケージを個別に作成するのは不可能なので、Python 固有のリリースを作成しなければなりません。Python 固有のリリースとは、オペレーティングシステムに関わらず Python のインストールディレクトリの内部にインストールされるリリースのことです。その上で、次のことを祈るわけです:

これが不可能な場合もあります。例えば Plone (Python で書かれた高機能な CMS) は数百個の小さな Python ライブラリを使っており、全てのライブラリがパッケージシステムで利用可能なわけではありません。そのため Plone はポータブルアプリケーションの中に必要なライブラリを全て含める必要があります。これを行うために Plone は zc.buildout というプログラムを利用しており、このプログラムを使って全ての依存関係を調べ、任意のシステムで実行できるポータブルなアプリケーションを単一ディレクトリに作成します。C コードもここでコンパイルされるので、出来上がるディレクトリが事実上のバイナリリリースとなります。

これは開発者からすると非常に便利です: 依存関係を後で説明する Python の標準的な記法で記述すれば、あとは zc.buildout を使ってアプリケーションをリリースできます。しかし前述の通り、このようにリリースを行うとシステム内に同じ内容のファイルを含んだ“要塞”がいくつも構築されることになります。これは Linux システム管理者が大嫌いなものです。Windows の管理者は気にしないかもしれませんが、CentOS や Debian の管理者は気にします。なぜならこういったシステムでは、システム内の全てのファイルが登録され、分類され、管理者のツールに知られているという仮定の下でシステムの管理が行われるからです。

システムの管理者はあなたのアプリケーションをシステムの標準的な方法で再パッケージします。ここで答えなければならないのが、「Python のパッケージシステムを他のパッケージシステムに自動的に変換できるか?」という問題です。もしこれができるなら、アプリケーションやライブラリは任意のシステムへと追加のパッケージング作業無しにインストールできます。ここで“自動的に”というのは作業の全てがスクリプトに行われるというわけでは必ずしもありません。RPMdpkg などのパッケージャではそれは不可能であり、再パッケージするプロジェクトにちょっとした情報を埋め込まなければならないからです。また開発者がパッケージに関する基本的なルールを理解していないために、再パッケージに手間がかかってしまうこともよくあります。

既存の Python のパッケージシステムを使ってパッケージャを苦しめる例を一つ示しましょう: “MathUtils”という名前のライブラリを“Fumanchu”というバージョン名でリリースすることです。このライブラリを書いた聡明な数学者は、自身のプロジェクトのバージョン名に飼っているネコの名前を付けるのがイケていると思ったのでしょう。しかし“Fumanchu”が彼の二匹目のネコで、最初のネコの名前が“Phil”であり、“Fumanchu”は“Phil”の後に来なければならないことを、パッケージャはどうやって関知しろというのでしょうか?

この例は極端に思えるかもしれませんが、現在使われているツールと標準で起こる可能性のある問題です。最悪なのは、easy_installpip といったツールが自身の標準的でないレジストリを使ってインストールされたファイルを記録しており、“Fumanchu”や“Phil”といったバージョン名を辞書順にソートしてしまう場合です。

もう一つの問題が、データファイルをどう扱うかというものです。例えばアプリケーションが SQLite データベースを使っていたら? データベースをパッケージディレクトリに配置したとすると、システムがそのツリーへの書き込みを禁じている場合にはアプリケーションが落ちてしまうかもしれません。仮に落ちなかったとしても、アプリケーションのバックアップデータの保存場所 (/var) に関する Linux の慣習を破ることになります。

現在のパッケージングのアーキテクチャ

Distutils パッケージは Python の標準ライブラリに含まれており、これまでに説明したような問題に対処します。Distutils が Python における標準なので、人々は欠点と共にこのツールと付き合っています。より洗練されたツールもいくかあり、SetuptoolsDistutils の上に機能を追加したライブラリ、DistributeSetuptools のフォークです。また Setuptools に依存するさらに高機能なインストーラ Pip もあります。

しかしこういった新しいツールは全て Distutils の上に作られており、その問題も受け継いでいます。Distutils の修復も試みられましたが、上手く行きませんでした。他のツールが Distutils のコードをあまりにも深く使っているために、その内部でさえ少しでも変更してしまうと Python のパッケージングエコシステムが傷つけられてしまうのです。

そのため私たちは Distutils を凍結し、後方互換性をあまり気にしない Distutils2 の開発を同じコードベースで始めることになりました。何を変えたか、そしてなぜ変えたかを理解するために、まずは Distutils について詳しく見ていきます。

Distutils の基礎と設計の問題

Distutils のコマンドは run メソッドを持ったクラスとして表され、run はオプションと共に呼び出されます。Distutils はこの他にも全てのコマンドが参照できるグローバル変数を保持する Distribution クラスも提供します。

Distutils を使用するには、開発者はプロジェクトに Python モジュールを一つ追加します。このモジュールは慣習的に setup.py と呼ばれ、Distutils のメインのエントリーポイントである setup 関数の呼び出しを含みます。この関数に付いているたくさんのオプションは Distribution のインスタンスに渡され、コマンドから使用されます。次に示すのは、名前とバージョン、モジュールのリストという基本的なオプションを定義する例です:

from distutils.core import setup

setup(name='MyProject', version='1.0', py_modules=['mycode.py'])

そしてこのモジュールを Distutils のコマンドから使用します。例えば sdist コマンドを使うとソースディストリビューションをアーカイブで作成し、dist ディレクトリに配置できます。

$ python setup.py sdist

install コマンドを使えばプロジェクトをインストールを同じスクリプトを使って実行できます:

$ python setup.py install

Distutils には他にも次のようなコマンドがあります:

プロジェクトに関する情報はコマンドラインオプションを使って取得できます。

プロジェクトのインストールやプロジェクトの情報の取得は必ず Distutilssetup.py に対して起動することで行われます。例えばプロジェクトの名前を取得するには次のようにします:

$ python setup.py --name
MyProject

つまりプロジェクトをビルドするのであれ、公開するのであれ、インストールするのであれ、setup.py がプロジェクトととの対話口となります。開発者はプロジェクトの内容を関数に対するオプションに記述し、そのファイルをパッケージングに関する全てのタスクに使用します。このファイルはインストーラがターゲットのシステムにプロジェクトをインストールするときにも使われます。

Python プロジェクトのセットアップ
図 14.1
Python プロジェクトのセットアップ

パッケージング、リリース、インストールに単一の Python モジュールを使うことは、Distutils の大きな問題の一つです。例えば lxml プロジェクトの name を取得しようとすると、setup.py は単純な文字列を返す他にも様々な処理を行います:

$ python setup.py --name
Building lxml version 2.2.
NOTE: Trying to build without Cython, pre-generated 'src/lxml/lxml.etree.c'
needs to be available.
Using build configuration of libxslt 1.1.26
Building against libxml2/libxslt in the following directory: /usr/lib/lxml

ユーザーは setup.py をインストールにしか使用せず、Distutils の他の機能は開発中にしか使わないと開発者が思い込んでいるために、上述のコマンドが失敗してしまう場合さえあるかもしれません。setup.py に複数の役割があるために、こういった思い違いが起きがちです。

メタデータと PyPI

Distutils がディストリビューションをビルドすると、PEP 3141 で表される標準に沿った Metadata ファイルが作成されます。このファイルには静的なバージョンの一般的なメタデータ、例えばプロジェクトの名前やリリースのバージョンが含まれます。メタデータの主なフィールドを以下に示します:

これらのフィールドのほとんどは、他のパッケージングシステムにも同じような要素があります。

Python Package Index2 (PyPI) は CPAN のようなパッケージの中央レポジトリであり、プロジェクトの登録と公開は Distutilsregister コマンドと upload コマンドを使って行います。register コマンドは Metadata ファイルをビルドし、それを PyPI に送信します。送信されたデータはウェブページやウェブサービスを通してインストーラなどのツールおよび人間が閲覧できるようになります。

PyPI レポジトリの例
図 14.2
PyPI レポジトリの例

PyPI ではプロジェクトを Classifiers ごとに閲覧でき、著者の名前やプロジェクトの URL も取得できます。それ以外にも、Requires フィールドを使って Python モジュールの依存関係を定義することが可能です。プロジェクトに Requires メタデータを追加するには setup 関数の requires オプションを使います:

from distutils.core import setup

setup(name='foo', version='1.0', requires=['ldap'])

この例における ldap モジュールに対する依存関係の定義は完全に宣言的であり、ツールやインストーラがそのモジュールの存在を確認することはありません。このやり方は Perl のように require キーワードを通してモジュールレベルの依存関係を定義するようになっていれば問題ありません。もしそうならインストーラが PyPI の中で依存関係を探してそれをインストールすればよいだけであり、CPAN は基本的にこれを行っています。しかし Python では ldap というモジュールが任意の Python プロジェクトに表れる可能性があるために、こうすることはできません。つまり Distutils が複数のパッケージやモジュールを含むプロジェクトをリリースできるようにしているために、この Requires というメタデータフィールドの存在意義が全く無くなっているのです。

Metadata ファイルに関するもう一つの問題点は、このファイルが Python スクリプトによって作られるために、ファイルの内容が実行されるプラットフォーム固有になってしまう点です。例えば Windows 用の機能を提供するプロジェクトでは、setup.py を次のように書きます:

from distutils.core import setup

setup(name='foo', version='1.0', requires=['win32com'])

しかしこうすると、たとえポータブルな機能があったとしてもプロジェクト全体が Windows 上でのみ動作するようになってしまいます。この問題は一見すると、requires オプションを Windows のときだけ付けるようにすれば解決するように見えます:

from distutils.core import setup
import sys

if sys.platform == 'win32':
    setup(name='foo', version='1.0', requires=['win32com'])
else:
    setup(name='foo', version='1.0')

しかしこうすると問題はさらに悪くなります。ソースのアーカイブはこのスクリプトを使って作られ、その後 PyPI へと公開されることを思い出してください。つまり PyPI へと送られる静的な Metadata ファイルは、プロジェクトをコンパイルするプラットフォームに依存することになります。これを言い換えれば、あるモジュールがプラットフォーム固有なことをメタデータで静的に指定する方法は存在しないということです。

PyPI のアーキテクチャ

PyPI のワークフロー
図 14.3
PyPI のワークフロー

前述の通り PyPI は Python プロジェクトの中央インデックスであり、既存のプロジェクトをカテゴリごとに閲覧したり自身のプロジェクトを登録したりといったことを誰でも行うことができます。ソースまたはバイナリの形でディストリビューションをアップロードして既存のプロジェクトに追加したり、それをダウンロードしてインストールや研究のために使うことができます。PyPI では他にも、インストーラなどのツールが利用するためのウェブサービスも提供されています。

プロジェクトの登録とディストリビューションのアップロード

PyPI へのプロジェクトの登録は Distutilsregister コマンドで行われます。このコマンドはプロジェクトのメタデータやバージョン情報を含んだ POST リクエストを生成します。PyPI は登録されるプロジェクトを PyPI の登録ユーザーと結び付けるので、リクエストには Basic 認証用のヘッダーも付きます。認証情報は Distutils の設定としてローカルに保存されるか、register コマンドを起動したときにプロンプトから入力されます。次に使用例を示します:

$ python setup.py register
running register
Registering MPTools to http://pypi.python.org/pypi
Server response (200): OK

登録されたプロジェクトには HTML バージョンのメタデータを含んだウェブページが割り当てられます。パッケージャがディストリビューションを PyPI にアップロードするときには upload コマンドが使われます:

$ python setup.py sdist upload
running sdist
...
running upload
Submitting dist/mopytools-0.1.tar.gz to http://pypi.python.org/pypi
Server response (200): OK

ファイルを PyPI に直接アップロードする代わりに、メタデータの Download-URL フィールドを使ってユーザーに他の場所を伝えることも可能です。

PyPI へのクエリ

PyPI はウェブユーザーのための HTML ページの他にも、プロジェクトを閲覧するツール用のサービスを二つ提供しています: Simple Index プロトコルと XML-RPC API です。

Simple Index プロトコルのルートは http://pypi.python.org/simple/ であり、このページは登録されている全てのプロジェクトへの相対リンクを含んだプレーンな HTML ページです:

<html><head><title>Simple Index</title></head><body>
...    ...    ...
<a href='MontyLingua/'>MontyLingua</a><br/>
<a href='mootiro_web/'>mootiro_web</a><br/>
<a href='Mopidy/'>Mopidy</a><br/>
<a href='mopowg/'>mopowg</a><br/>
<a href='MOPPY/'>MOPPY</a><br/>
<a href='MPTools/'>MPTools</a><br/>
<a href='morbid/'>morbid</a><br/>
<a href='Morelia/'>Morelia</a><br/>
<a href='morse/'>morse</a><br/>
...    ...    ...
</body></html>

例えば MPTools プロジェクトには MPTools/ というリンクが付いており、このディレクトリにプロジェクトがあることを意味します。リンク先のページにはプロジェクトに関する情報が全て載っています:

例えば MPTools のページは次のようになっています:

<html><head><title>Links for MPTools</title></head>
<body><h1>Links for MPTools</h1>
<a href="../../packages/source/M/MPTools/MPTools-0.1.tar.gz">MPTools-0.1.tar.gz</a><br/>
<a href="http://bitbucket.org/tarek/mopytools" rel="homepage">0.1 home_page</a><br/>
</body></html>

したがってインストーラなどのツールがプロジェクトのディストリビューションを検索したい場合には、インデックスページを検索するか、あるいは http://pypi.python.org/simple/PROJECT_NAME/ が存在するかを調べることになります。

このプロトコルには主な欠点が二つあります。まず、現在の PyPI はシングルサーバーです。たいていのユーザーはディストリビューションのローカルコピーを作成しますが、それでも過去二年間に何度かあったダウンタイムには、ビルド時に依存プロジェクトを PyPI で検索するインストーラを定期的に使う必要がある開発者は作業が行えなくなりました。例えば Plone アプリケーションをビルドするときには PyPI へのクエリが全部で数百個生成されます。このような状況では、PyPI が単一障害点となる可能性があります。

次に、ディストリビューションが PyPI に含まれておらず Simple Index ページに Download-URL が設定されている場合、インストーラはそのリンクをたどりますが、そのリンクがまだ生きていること、および本当にリリースが提供されていることについては、そうであるようにと祈るしかありません。このたらい回しがある限り、Simple Index を使った処理の信頼性は落ちます。

Simple Index プロトコルの目標は、プロジェクトをインストールするときに使用するリンクのリストをインストーラに渡すことです。プロジェクトのメタデータはここで公開されず、登録されたプロジェクトの追加情報は XML-RPC メソッドを使って取得します:

>>> import xmlrpclib
>>> import pprint
>>> client = xmlrpclib.ServerProxy('http://pypi.python.org/pypi')
>>> client.package_releases('MPTools')
['0.1']
>>> pprint.pprint(client.release_urls('MPTools', '0.1'))
[{'comment_text': &rquot;,
'downloads': 28,
'filename': 'MPTools-0.1.tar.gz',
'has_sig': False,
'md5_digest': '6b06752d62c4bffe1fb65cd5c9b7111a',
'packagetype': 'sdist',
'python_version': 'source',
'size': 3684,
'upload_time': <DateTime '20110204T09:37:12' at f4da28>,
'url': 'http://pypi.python.org/packages/source/M/MPTools/MPTools-0.1.tar.gz'}]
>>> pprint.pprint(client.release_data('MPTools', '0.1'))
{'author': 'Tarek Ziade',
'author_email': 'tarek@mozilla.com',
'classifiers': [],
'description': 'UNKNOWN',
'download_url': 'UNKNOWN',
'home_page': 'http://bitbucket.org/tarek/mopytools',
'keywords': None,
'license': 'UNKNOWN',
'maintainer': None,
'maintainer_email': None,
'name': 'MPTools',
'package_url': 'http://pypi.python.org/pypi/MPTools',
'platform': 'UNKNOWN',
'release_url': 'http://pypi.python.org/pypi/MPTools/0.1',
'requires_python': None,
'stable_version': None,
'summary': 'Set of tools to build Mozilla Services apps',
'version': '0.1'}

このアプローチの問題点は、XML-RPC API を使って公開しているデータの中には Simple Index のページで公開できる静的なファイルも含まれる点です。そのようなファイルは Simple Index のページで公開した方がクライアントツールが単純になり、さらに PyPI がクエリを処理する必要もなくなります。

Python インストールのアーキテクチャ

Python プロジェクトを python setup.py install でインストールすると、標準ライブラリに含まれる Distutils がプロジェクトのファイルをユーザーのシステムにコピーします。

Python 2.5 以降ではモジュールとパッケージと共にメタデータファイルもコピーされ、project-version.egg-info という名前のファイルとなります。例えば virtualenv プロジェクトには virtualenv-1.4.9.egg-info というファイルがあるでしょう。このメタデータファイルをプロジェクトごとに見ていけばシステムにインストールされているプロジェクトのバージョン付きリストが手に入るので、このファイルはインストールされたプロジェクトに関するデータベースとなります。ただし Distutils はシステムにインストールするファイルのリストを記録しません。これを言い換えると、システムにコピーした全てのファイルを消去する方法は存在しません。install コマンドにはインストールするファイルをテキストで保存する --record オプションがあるのを考えれば、これは残念なことです。このオプションはデフォルトで無効であり、Distutils のドキュメントはほとんどこのオプションに触れていません。

Setuptools, Pip など

章の最初で触れた通り、Distutils が持つ問題点の修正を試みたプロジェクトがいくつかあります。成功の度合いは様々です。

依存関係の問題

PyPI では、複数のモジュールからなる Python パッケージをいくつかまとめたものを Python プロジェクトとして公開できます。しかし同時に、プロジェクトはモジュールレベルの依存関係を Require を使って定義できます。どちらのアイデアも悪くありませんが、両方を同時に使うというのは良くありません。

必要なのはプロジェクトレベルの依存関係を定義する仕組みであり、Distutils の上に機能を追加する Setuptools はちょうどこれを提供します。さらに依存するプロジェクトを PyPI からフェッチ・インストールするスクリプト easy_installSetuptools から提供されます。現在ユーザーが PyPI を使うときにはモジュールレベルの依存関係は全く使われず、最初から Setuptools の拡張機能を使います。しかしこの機能は Setuptools が独自に追加したオプションなので、Distutils と PyPI はこれを理解できません。Setuptools は事実上の標準を作成し、問題のある設計を覆うハックとなりました。

easy_install はプロジェクトのアーカイブをダウンロードし、プロジェクトの setup.py を実行して必要なメタデータを取得します。全ての依存プロジェクトについてこれを行い、依存グラフはダウンロードの度に少しずつ構成されます。

新しいメタデータは PyPI を通してオンラインで取得できますが、それでも easy_install は全てのアーカイブをダウンロードします。この理由は、前述の通り、メタデータがプラットフォームごとに異なる可能性があるにもかかわらず、PyPI で公開されるメタデータがアップロードするときに使われたプラットフォームに固有となっているためです。このようにプロジェクトとその依存プロジェクトをインストールできるのはとても便利な機能であり、90% の場合はこれで十分です。そのため Setuptools は広く使われるようになりました。しかしそれでも、問題が無いわけではありません:

アンインストールの問題

インストールされたファイルのリストを Setuptools のメタデータに持たせることは可能であったにもかかわらず、Setuptools はアンインストーラを提供しませんでした。これに対して PipSetuptools のメタデータを拡張してインストールされたファイルを記録するようにしているので、アンインストールが可能です。しかしこれは“またもう一つの”メタデータであり、結果として Python のインストール場所にインストールされたプロジェクトには最大で四種類のメタデータが含まれることになってしまいました:

データファイルはどうなる?

Distutils では、データファイルをシステム上の任意の位置にインストールできます。パッケージのデータファイルを次のように定義したとします:

setup(...,
  packages=['mypkg'],
  package_dir={'mypkg': 'src/mypkg'},
  package_data={'mypkg': ['data/*.dat']},
  )

すると mypkg プロジェクト内の拡張子が .dat であるファイルがディストリビューションに追加され、Python のインストール場所に Python モジュールと共にインストールされるようになります。

Python ディストリビューションの外側にインストールするデータファイルについては、アーカイブに保存したデータファイルを指定した場所に移動させる別のオプションがあります:

setup(...,
    data_files=[('bitmaps', ['bm/b1.gif', 'bm/b2.gif']),
                ('config', ['cfg/data.cfg']),
                ('/etc/init.d', ['init-script'])]
    )

しかし OS のパッケージャからすると、このやりかたは最悪です。なぜなら:

データファイルを含むプロジェクトをパッケージャが再パッケージしようとした場合には、プラットフォームに合うよう setup.py ファイルを編集する以外に選択肢がありません。つまり、コードを見ていってデータファイルを定義している全ての行を編集しなければなりません。ファイルの場所を決めるのが開発者だからです。SetuptoolsPip を使ってもこの問題は改善されません。

改良された標準

こんなわけで、パッケージング環境は複雑で分かりにくくなってしまいました。全てが単一の Python モジュールで行われ、メタデータは表現力不足であり、プロジェクトの内容を完全に記述する方法はありません。これを改善するために私たちが行っていることをこれから説明します。

メタデータ

最初のステップは Metadata の標準化です。新しいメタデータは PEP 345 で定義され、次の要素が含まれます:

バージョン

新しいメタデータの目標は、Python プロジェクトに対して操作を行う全てのツールがプロジェクトを同じ方法で識別できるようにすることです。バージョンの指定方法について言えば、これは全てのツールが“1.0”が“1.1”の前に来ることを知っていることを意味します。これだけなら簡単ですが、プロジェクトが特殊なバージョン化スキームを持っている場合、問題はずっと難しくなります。

一貫したバージョン付けを保証する唯一の方法は、プロジェクトが従うべき標準を公開することです。私たちは昔ながらのシーケンスベースのスキームを選択しました。PEP 386 で定義されるフォーマットは以下です:

N.N[.N]+[{a|b|c|rc}N[.N]+][.postN][.devN]

ここで:

プロジェクトのリリースプロセスに応じて、リリースの間の中間バージョンを dev や post のマーカーで表すことができます。多くのプロジェクトで dev マーカーが使われています。

PEP 386 はこのスキームの上に厳密な順序を定義しています:

順序の例を次に示します:

1.0a1 < 1.0a2.dev456 < 1.0a2 < 1.0a2.1.dev456
  < 1.0a2.1 < 1.0b1.dev456 < 1.0b2 < 1.0b2.post345
    < 1.0c1.dev456 < 1.0c1 < 1.0.dev456 < 1.0
      < 1.0.post456.dev34 < 1.0.post456

このスキームの狙いは、パッケージングシステムが Python プロジェクトのバージョンを自身のスキームに変換しやすいようにすることです。PyPI に送信したプロジェクトが PEP 345 準拠のメタデータを持っていているにもかかわらずバージョン番号が PEP 386 に従っていない場合、PyPI はそのプロジェクトを拒否します。

依存関係の問題

PEP 345 は PEP 314 の Requires, Provides, Obsoletes を置き換える新しいフィールドを三つ定義します。そのフィールドは Requires-Dist, Provides-Dist, Obsoletes-Dist であり、メタデータの中で何度も使うことができます。

Requires-Dist の各エントリーはディストリビューションが必要とする他の Distutils プロジェクトの名前を表す文字列です。そのフォーマットは Distutils のプロジェクト名 (Name フィールド) と同じで、バージョン番号をカッコで囲んで付けることもできます。Distutils プロジェクトの名前は PyPI で検索できる名前と対応し、バージョンの宣言は PEP 386 に従います。いくつか例を示します:

Requires-Dist: pkginfo
Requires-Dist: PasteDeploy
Requires-Dist: zope.interface (>3.5.0)

Provides-Dist はプロジェクトの別名を定義するのに使います。これはプロジェクトが他のプロジェクトをマージするときに便利です。例えば ZODB プロジェクトは transaction プロジェクトを含むので、メタデータに次のように記述します:

Provides-Dist: transaction

Obsoletes-Dist は他のプロジェクトを obsolete なバージョンとして印を付けるのに使います:

Obsoletes-Dist: OldName

環境マーカー

環境マーカーはフィールドの後に続くセミコロンから始まるマーカーで、実行環境に関する条件を追加します。いくつか例を示します:

Requires-Dist: pywin32 (>1.0); sys.platform == 'win32'
Obsoletes-Dist: pywin31; sys.platform == 'win32'
Requires-Dist: foo (1,!=1.3); platform.machine == 'i386'
Requires-Dist: bar; python_version == '2.4' or python_version == '2.5'
Requires-External: libxslt; 'linux' in sys.platform

環境マーカーのための簡易言語は、Python プログラマー以外にも理解しやすいよう意図的に簡単にしてあります。使えるのは文字列に関する ==in 演算子 (およびその否定) による比較とブール演算子です。PEP 345 においてこのマーカーが使えるのは次のフィールドです:

何がインストールされるのか?

全ての Python ツールに共通のインストールフォーマットは相互運用のために必須です。インストーラ A がプロジェクト Foo をインストールしたことを インストーラ B から検出できるようにするには、二つのインストーラはインストール済みのプロジェクトに関する同一のデータベースを共有・更新しなければなりません。

もちろん理想的なユーザーがシステム内で使用するインストーラは一つだけですが、それでも新しい機能があるインストーラに乗り換えてみることは考えられます。例えば出荷状態の Mac OS X には Setuptools がインストールされており、ユーザーは自動的に easy_install スクリプトを手にすることになります。しかしユーザーが新しいツールに乗り換えるときには、Setuptools との後方互換性が必要です。

RPM のようなパッケージングシステムを持つプラットフォームで Python インストーラを使ったとき生じる別の問題もあります。こういったシステムではプロジェクトがインストールされていることをシステムに伝える方法が存在しないのです。さらに悪いことに、Python インストーラから中央パッケージングシステムにどうにかインストールを通知できたとしても、Python メタデータからシステムデータへのマッピングが必要になります。例えばプロジェクトの名前がシステムごとに変えられる可能性があります。この改名が起きる理由はいくつかありますが、最も良くあるのが名前の衝突です。つまり、Python とは関係ない他のプロジェクトが同じ名前で RPM に登録されている場合です。あるいは python を接頭語に持つ名前がプラットフォームにおける命名規則に沿っていないこともあります。例えば foo-python という名前のプロジェクトは、Fedora RPM において python-foo と改名される可能性が高いです。

この問題を解決する一つの方法は、中央パッケージングシステムが管理するグローバルな Python のインストール場所はそのまま放置にして、その他の作業は独立した環境で行うというものです。Virtualenv のようなツールはこれを行います。

いずれにせよ、Python のインストールフォーマットを統一する必要があります。Python プロジェクトをインストールする際には他のパッケージングシステムとの相互運用性が重要だからです。サードパーティのパッケージングシステムが新しい Python プロジェクトを自身のシステムに登録したときには、Python のインストール場所に配置するメタデータを正しく生成しなければなりません。こうすることでそのプロジェクトは Python のインストール場所に問い合わせを行う任意の API から利用可能になります。

こうしておけばメタデータのマッピング問題が対処可能になります。RPM はラップしている Python プロジェクトを知っており、Python レベルのメタデータを生成できるからです。例えば、RPM は python26-webob が PyPI エコシステムにおいて WebOb と呼ばれていることを知っています。

標準の話に戻りましょう。PEP 376 はインストールされたパッケージに関する標準を定義しており、そのフォーマットは SetuptoolsPip で使われているものと似ています。それは名前の最後に dist-info が付いたディレクトリを作り、その中に次のファイルを作成するというものです:

このフォーマットが全てのツールに理解されれば、Python のプロジェクトを特定のインストーラや機能に頼ることなく管理できるようになります。加えて PEP 376 はメタデータをディレクトリとして定義しているので、新しいファイルを追加してメタデータを拡張するのも簡単です。実際、後述する新しいメタデータファイル RESOURCES が将来追加される可能性があるのですが、この場合にも PEP 376 の変更は必要になっていません。いずれこのファイルが全てのツールにとって便利であると判明すれば、PEP に追加されることになります。

データファイルのアーキテクチャ

前述の通り、パッケージャは開発者によって書かれたコードを改変することなくインストール時のデータファイルの移動場所を変更できる必要があります。そして同時に、開発者はデータファイルを移動先の場所を気にすることなく利用できなければなりません。私たちの解決法はそう特別なものではありません: 間接参照です。

データファイルの仕様

あなたの MPTools アプリケーションが設定ファイルを利用するとしましょう。開発者はそのファイルを Python パッケージに入れ、__file__ を使って読み込むかもしれません:

import os

here = os.path.dirname(__file__)
cfg = open(os.path.join(here, 'config', 'mopy.cfg'))

こうすると設定ファイルがコードと同じようにインストールされ、開発者は設定ファイルをコードと同じディレクトリに配置しなければならないことになります。つまり今の例では、config というサブディレクトリに配置しなければなりません。

私たちが設計した新しいデータファイルのアーキテクチャでは、プロジェクトツリーを全てのファイルのルートとして扱い、そのツリーの中の任意のファイルへのアクセスを許可しています。プロジェクトツリーが Python パッケージ内にあってもそうでない単純なディレクトリにあっても同様です。データファイル用のディレクトリを作成すれば、pkgutil.open を使ってアクセスできます:

import os
import pkgutil

#Open the file located in config/mopy.cfg in the MPTools project
cfg = pkgutil.open('MPTools', 'config/mopy.cfg')

pkgutil.open はまずプロジェクトのメタデータを読み、RESOURCES ファイルがあるかどうかを調べます。この中にはファイルの名前からシステム内のファイルへのマッピングが含まれます:

config/mopy.cfg {confdir}/{distribution.name}

ここで {confdir} はシステムの設定ディレクトリを指す変数であり、{distribution.name} にはメタデータで定義される Python プロジェクトの名前が代入されます。

ファイルの探索
図 14.4
ファイルの探索

このメタデータファイル RESOURCES がインストール時に作られる限り、API は mopy.cfg ファイルの場所を見つけて開発者に渡すことができます。config/mopy.cfg はプロジェクトツリーからの相対パスなので開発時にも使うことができ、その場合にはプロジェクトのメタデータがその場で生成された上で pkgutil の探索パスに追加されます。

データファイルの宣言

プロジェクトがデータファイルを利用するときには、ファイルの配置場所を setup.cfg ファイルに記述します。このとき記述するのは (glob スタイルのパターン, ターゲット) の組が並んだマッパーです。各パターンがプロジェクトツリーのいくつかのファイルを指し、ターゲットがインストールパスを指定します。このときターゲットには波括弧で囲った変数を使うこともできます。例えば MPToolssetup.cfg は次のように書けます:

[files]
resources =
        config/mopy.cfg {confdir}/{application.name}/
        images/*.jpg    {datadir}/{application.name}/

sysconfig モジュールは利用可能な変数のリストをドキュメントと共に提供し、各プラットフォームにおける変数のデフォルト値を設定します。例えば Linux における {confdir} の値は /etc です。そのためインストーラは上述のマッパーを sysconfig と共に使うことで、ファイルを配置場所をインストール時に計算できます。その後インストーラは RESOURCES ファイルを作成し、pkgutil によるファイル探索の準備が整います。

インストーラによる sysconfig の利用
図 14.5
インストーラによる sysconfig の利用

PyPI の改良

PyPI は事実上の単一障害点であると前に説明しました。PEP 380 はこの問題に対処するもので、PyPI がダウンした場合に代替サーバーへフォールバックするためのミラーリングプロトコルを定義します。ここでの目標は世界中のコミュニティメンバーがミラーを立てられるようにすることです。

ミラーリング
図 14.6
ミラーリング

ミラーのリストは X.pypi.python.org という形のホスト名のリストとなっており、Xa, b, c, ..., aa, ab,... です。a.pypi.python.org がマスターサーバーで、ミラーは b から始まります。last.pypi.python.org のCNAME レコードは最後のミラーのホスト名を指しているので、PyPI を使うクライアントは CNAME を見るだけでミラーのリストを取得できます。

例えば次の呼び出しは最後のミラーが h.pypi.python.org であることを示しているので、このときの PyPI が (b から h までの) 6 個のミラーを持っていることが分かります:

>>> import socket
>>> socket.gethostbyname_ex('last.pypi.python.org')[0]
'h.pypi.python.org'

このプロトコルを使うと、クライアントはミラーの IP の場所を調べてリクエストを一番近いミラーにリダイレクトできます。またマスターサーバーやミラーがダウンしているときに次のミラーにフォールバックすることも可能です。このミラーリングプロトコル自身は単純な rsync よりも複雑になります。ダウンロード数を正確に測定したり、最小限のセキュリティを提供したりするためです。

同期

ミラーは中央サーバーとやり取りするデータの量を最小限にしなけばなりません。そのためミラーは PyPI のXML-RPC 呼び出しを使って changelog を最初に必ず取得し、ミラーの最終変更日時から変更があったパッケージだけを再フェッチします。また各パッケージ P に対して、simple/Pserversig/P にあるドキュメントのコピーを必ず行います。

あるパッケージが中央サーバーで削除された場合には、そのパッケージと関連する全てのファイルを削除しなければなりません。パッケージファイルの変更の検出には、ファイルの ETag をキャッシュした上で If-None-Match ヘッダーを使ってリクエストをスキップする方法が使われます。同期が終わると、ミラーは /last-modified を現在時刻に設定します。

統計の伝播

いずれかのミラーからリリースをダウンロードすると、プロトコルはダウンロードが行われたという情報を最初にマスターの PyPI サーバーに伝え、それから他のミラーにも伝えます。こうすることで、人やツールに向けて PyPI で表示されるリリースのダウンロード数が全てのミラーの値を集計したものとなります。

統計は日ごとおよび週ごとに中央 PyPI サーバーの stats ディレクトリに CSV ファイルとして保存されます。各ミラーは自身の統計を local-stats ディレクトリに保存します。それぞれのファイルにはアーカイブのダウンロード数が保存され、それらがユーザーエージェントごとにグループ化されます。中央サーバーは統計を集計するために一日ごとにミラーを訪れ、ミラーの値をグローバルの stats ディレクトリに組み入れます。そのためミラーは少なくとも一日に一回は /local-stats を更新しなければなりません。

ミラーの認証

どんなミラーリングシステムでもそうですが、ミラーされたデータが本物であることをクライアントが検証したい場合があります。考えられる脅威としては次のようなものがあります:

最初の攻撃を検出するには、パッケージの作者がパッケージを PGP 鍵で署名し、ディストリビューションが信頼する作者からのものであるとユーザーが検証できるようにする必要があります。ミラーリングプロトコルによって対処できるのは二つ目の脅威だけですが、man-in-the-middle 攻撃を検出するための試みもいくつか行われています。

中央インデックスは /serverkey という URL で DSA 鍵を提供しています。これは openssl dsa -pubout3で生成したものです。この URL はミラーしてはならず、クライアントは公式の PyPI から serverkey を直接フェッチするか、PyPI のクライアントソフトウェアに付属するコピーを必ず利用します。ただしミラーも鍵のロールオーバーを検出するために鍵のダウンロードを行います。

各パッケージのミラーされたシグネチャは /serversig/package に保存されます。これは対応する URL /simple/package の DSA シグネチャを DER 形式で表したもので、SHA-1 with DSA が使われます4

ミラーを使うクライアントは次のようにしてパッケージを検証します:

  1. /simple ページをダウンロードし、その SHA-1 ハッシュを計算する。

  2. そのハッシュの DSA シグネチャを計算する。

  3. 対応する /serversig をダウンロードし、その値をステップ 2 で計算した値と一バイトずつ比較する。

  4. ミラーからダウンロードした全てのファイルについて、MD5 ハッシュを計算して (/simple ページと比較して) 検証する。

中央インデックスからダウンロードする場合は検証は不必要であり、計算負荷を避けるためにも行うべきではありません。

鍵は一年に一度程度の頻度で更新され、そのたびにミラーは /serversig ページを全て再フェッチします。クライアントは新しいサーバー鍵の信頼できるコピーを使う必要がありますが、そのための一つの方法は https://pypi.python.org/serverkey からダウンロードするというものであり、その際にはクライアントで mon-in-the-middle 攻撃を検出できるよう SSL サーバー証明書を検証する必要があります。この証明書は認証局によって署名されます。

実装の詳細

前節で説明した改良点の多くは Distutils2 で実装されています。setup.py ファイルはもう使われなくなり、プロジェクトは .ini ファイルに似た setup.cfg ファイルで完全に記述されます。こうすることでパッケージャは Python コードを読むことなくプロジェクトのインストールの動作を変更できるようになります。setup.cfg の例を示します:

[metadata]
name = MPTools
version = 0.1
author = Tarek Ziade
author-email = tarek@mozilla.com
summary = Set of tools to build Mozilla Services apps
description-file = README
home-page = http://bitbucket.org/tarek/pypi2rpm
project-url: Repository, http://hg.mozilla.org/services/server-devtools
classifier = Development Status :: 3 - Alpha
    License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1)

[files]
packages =
        mopytools
        mopytools.tests

extra_files =
        setup.py
        README
        build.py
        _build.py

resources =
    etc/mopytools.cfg {confdir}/mopytools

この設定ファイルを使って、Distutils2 は次のことを行います:

Distutils2 は他にも version モジュールを通して VERSION を実装しています。

INSTALL-DB の実装は Python 3.3 で標準ライブラリに組み込まれ、pkgutil モジュールに取り込まれます。現在でもこのモジュールは Distutils2 に含まれているので、すぐに使用できます。提供される API を使うと Python のインストール場所をブラウズでき、このモジュールはインストールされたプロジェクトを完全に知っています。

こういった API は次にあげる Distutils2 の便利な機能の基礎となります:

教訓

PEP が全て

Python のパッケージングほどに多様で複雑なアーキテクチャの変更は、PEP プロセスによる標準の変更を通じて注意深く行わなければなりません。私の経験から言うと、新しい PEP の作成や既存の PEP の変更には一年程度の時間がかかります。

このプロセスの中でコミュニティが犯した誤りの一つが、PEP を変更する代わりに、メタデータと Python アプリケーションのインストール方法をいじることで問題を“解決”するツールを提供したことでした。

言い換えると、使ったツールが標準ライブラリの Distutils なのか Setuptols なのかによって、アプリケーションのインストール方法が異なってしまったのです。新しいツールを使うコミュニティの一部は問題を解決できましたが、他の人々にとっては問題が増えただけでした。例えば OS パッケージャはいくつもの Python 標準に対応する必要が生じました: つまり公式にドキュメントされた標準と、Setuptools が使うデファクトスタンダードな標準です。

しかしその一方で、Setuptols には現実的な規模 (コミュニティ全体) での実験を行う機会が与えられ、速いペースで改良が進み、貴重なフィードバックを得ることもできました。新しい PEP を書くときには何が成功して何が失敗したかについて確信がありましたが、これは他の方法を取っていたら不可能だったでしょう。つまり、もしとあるサードパーティツールが革新を起こして問題を解決しているのであれば、PEP の変更を検討する良い機会だということです。

標準ライブラリに入ったパッケージは棺桶に片足を踏み入れる

これは Guido van Rossum の言葉を言い換えたものです。しかしこれは Python の“バッテリー搭載”哲学の一面であり、私たちの取り組みに様々な影響を及ぼしました。

Distutils は標準ライブラリに組み込まれており、Distutils2 も近いうちにそうなります。標準ライブラリに組み込まれたパッケージは進化させるのが非常に難しくなります。もちろん廃止するためのプロセスは用意されており、Python のマイナーバージョン二つ先で API を廃止または変更できます。しかし API が一度公開されれば、通常は何年もそのままです。

そのため標準ライブラリに加えられるバグフィックスでない変更は、エコシステムにとって混乱の元になりかねません。重要な変更を行うには、新しいパッケージを作る必要があります。

私はこのことを Distutils での苦労を通じて学びました。一年以上かけて取り組んできた変更を全て打ち消して、新しく Distutils2 を作らざるを得なかったのです。将来、標準がまた大きく変わるようなことがあれば、スタンドアローンの Distutils3 プロジェクトを最初に作る可能性が高いでしょう。標準ライブラリが別にリリースされるなら別ですが。

後方互換性

Python においてパッケージングの方式を変更するのはとても長い時間がかかります。Python エコシステムには古いパッケージングツールを使っているプロジェクトが大量にあるので、変更への抵抗が根強いからです (この章で説明した話題のいくつかについて合意に達するのには数年かかりました。私は数か月で済むと思っていたのですが)。Python 3 で全てのパッケージが新しい標準に移行するまでには数年はかかるでしょう。

そしてこのために、私たちは何をするときにも後方互換性を保たねばならないのです。これまでのツール、これまでのインストール方法、これまでの標準との後方互換性により、Distutils2 の実装は厄介な問題となります。

例えばあるプロジェクトが新しい標準を使っていて、その依存先のプロジェクトがまだ使っていなかったとしても、インストールを中止して「依存プロジェクトが不明なフォーマットを持っています!」とエンドユーザーに伝えるわけにはいきません。

もう一つ例をあげると、INSTALL-DB の実装にはオリジナルの Distutils, Pip, Distribute, Setuptools でインストールされたプロジェクトを閲覧するための互換性コードがあります。また Distutils2 には Distutils で作られたプロジェクトのメタデータを変換してインストールする機能があります。

参考文献と謝辞

この章のいくつかの節は、私たちがパッケージングシステムのために書いた様々な PEP ドキュメントからの直接の引用です。原文は http://python.org から読むことができます。

パッケージングについて取り組んでいる全ての人に感謝します。彼らの名前は上述の PEP に載っています。「パッケージング同盟」のメンバーにも特別な感謝を送ります。またこの章に対するフィードバックをくれた Alexis Metaireau, Toshio Kuratomi, Holger Krekel, Stefane Fermigier にも感謝します。

この章で触れたプロジェクトを次にまとめます:


  1. この章で触れる Python Enhancement Proposals (略して PEP) のリストは章の最後にまとめてあります。[return]

  2. かつては CheeseShop と呼ばれていました。[return]

  3. RFC 3280 SubjectPublicKeyInfo, with the algorithm 1.3.14.3.2.12.[return]

  4. RFC 3279 Dsa-Sig-Value, created by algorithm 1.2.840.10040.4.3[return]

広告