replyツイート形態素解析システムJUMANのPythonモジュールjumanyをPOSIX対応しました。本稿では、POSIXではソースからライブラリをビルドし、またWindowsにはビルド済ライブラリを提供する場合の、setup.pyの書き方についてレポートします。はじめに前回、形態素解析システムJUMANのPythonモジュールを作成しました。64bitのビルド済みの共有ライブラリ同梱の形で、POSIXには対応していません。そもそも、もともとのライブラリ(JUMAN)はPOSIXなので、setup.pyからmakeが出来れば、問題なくそのシステム対応の共有ライブラリが作れるはず。いうことで、setup.py内でライブラリをビルドするよう、POSIX対応することにしました。本稿は以下のシステムを対象としています。Python3.3以降setuptoolsを使ったsetup.pyの作成distutilsのみ使ったsetup.pyには対応しません。setup」にするだけで対応なのですが。)また本稿には、公式ドキュメントで裏を取っていない経験的な内容が多々含まれます。公式的にもっとスマートなやり方があるかもしれません。そのあたりをご了承ください。開発環境はWindows10です。結果(たぶん)POSIX対応はできていて、PYPIにもアップしました。(頑張ってclassfiers書いたのに表示されてませんね。jumany」でインストール可能です。Windowsで試しているので、パーミッションなどでトラブる可能性はありえます。learned)今回も学んだことを残しておきます。setuptoolsを使ったsetup.pyについてです。Setuptools」setuptoolsとdistutilsの違いsetuptoolsは、最近(Python3.4から?)はもう標準モジュール扱いのようです。最近のPythonのインストールにはもう入っていますので、こちらに乗り換えます。distutilsからの乗り換えは、先に書いたとおりで簡単です。私の気がついた、setuptoolsのdistutilsとの違いは以下です。setupの引数が増えている。MANIFESTが作られない。install_requires)、もうsetuptoolsを使ったほうがよいですね。2点目のwheelは、現時点で主流のビルド済みのパッケージのことらしいです。(ソースのパッケージをpipでインストールすると「Wheelを作ってるよ」というメッセージがしばらく表示されます。これにしておくと、ユーザーの時間短縮になりそうな。)3点目には少し悩みました。古いMANIFESTが残っていて「なぜMANIFEST.inが反映されないのだろう」と。「パッケージ名.egg-info」というディレクトリの中のSOURCE.txtがMANIFESTの代わりになります。共有ライブラリのビルド共有ライブラリの、ソース(*.c)からのビルドには、Extensionを使いました。教科書通りにしても、__init__.pyがあるパッケージのディレクトリにlibjuman.so(作ろうとしているライブラリ)を作ってくれません。パケージのディレクトリにsoを置きたい理由どこか他のPythonが探せる場所にできるらしいのですが、それを探すスクリプトは書きたくありません。システムにJUMANがインストールされていて、そちらのlibjuman.soが参照される可能性。(名前の変更はあまりしたくありません。)パケージディレクトリに置く、Windows用のビルド済みライブラリと同じ扱いにしたい。したらよい方法があって杞憂なのかもしれませんが、パッケージのディレクトリにsoを置くことを目指します。検索してもズバリの答えは見つかりません。自分の環境のsite-packages以下のモジュールでsoがあるものを探し、そのリポジトリ(github)からsetup.pyを見てみる、という手段でたどり着きました。結局のところ、以下ようなの書き方で目標は達せられました。(以下は実行していないコードですので、間違いがあるかもしれません。実際はスクリプトでこの構造を作っています。(後略))教科書的には、9-17行のext_modulesを、直接setupの引数にします。その結果は先述の通り。(もしかしたら教科書的なExtensionの指定だけで行けてたのかもしれません。私の検討が悪かっただけで...。)では、引数featuresと、そのdictに設定するクラスFeatureとは何でしょう?謎です。検索しても解説は見つかりませんでした。setuptoolsの中のソースコードを読むしかありません。コメントを眺めたところ、作るか作らないかをユーザーが選択できる拡張機能の書き方のようです。第一引数の文字列は、エラー表示するときに使われるそうです。仕様は謎ですが、パッケージディレクトリの中に、「libjuman.so」、或は「libjuman.<システムの略号>.so」というライブラリができるので、とにかく目標達成です。ちなみにExtensionの引数をご説明いたしますと...、11行のsourcesにはソースのファイル名をリストに列挙するのですが、面倒なのでglobでワイルドカードを展開し、ディレクトリ下のソースを丸ごと指定しています。それ以外のコンパイルオプションに対応します。configureが作成するconfig.hだけは、予め作ってscriptsというディレクトリの中に入れてあります。このディレクトリも、ソースのディレクトリとともに「include_dirs」に指定しています。「-Wno-error=format-security」は、buildの途中でコンパイルがエラー終了してしまいましたので入れました。パスの記述はsetup.py起点で良いようです。標準ライブラリのヘッダのある場所も特に指定していません。(C_INCLUDE_DIR等の環境変数は特に設定していません。)ソースをパッケージに入れるソースのような、インストール・パッケージには入れるけどインストールはしない、というファイルはMANIFEST.inに指定します。以下に、ライブラリに関連する部分を抜き出します。juman-7.01/lib/*.h予め作ったconfig.hだけ別に置いていますが、ソースはJUMANの展開ディレクトリから直接取得しています。こちらもパスはsetup.py起点です。読み込み側の書き方Pythonの側で、ライブラリを読み込むときの書き方は以下のようになりました。CDLL(LIBS[0])ライブラリの名前が「juman.so」なのか、それとも途中に環境の記述が入るのかはわかりませんが、とにかく同じディレクトリにある「libjuman.*so」にマッチするものを読み込みます。(CDLL()の代わりに前回の「cdll.LoadLibrary()」でもOKです。開発環境ではモジュールのディレクトリにsoがないので、実際はもうちょっと複雑にsoを探しています。)カスタムビルド以上でPOSIXの場合のライブラリのビルドができました。Windowsのビルド済ライブラリに関しては、今まではパケージのディレクトリにコピーして、どんなシステムでもいつでも入るようにしていました。POSIXの場合には、余計なWindows用のファイルがない方がスマートです。ライセンスの記述も、Windows用のはLGPLもくっついて長いので、これもシステム別にインストールしたいところ。build」と入力したときに、ビルド(インストールされるファイルの構成)をカスタマイズできる方法を検討しました。Overflowに見つかりました。(distutilsと共通の方法です。setuptoolsには実はスマートな方法が実装されているのかもしれません。)setup.pyの書き方カスタムビルドの書き方を書き下すと、以下のようになります。(後略))4行のように、distutils.command.build.buildクラスを継承したクラスを定義し、そのrunメソッドの中で、親クラスのrun()を呼んだ後にやりたい処理を書きます。そのクラスは、setupの引数cmdclassに、「build」の値として書いてあげます。(11行)run()メソッド内の書き方カスタムビルドのクラス(上でのCustomBuild)の、run()メソッド内の書き方について書きたいと思います。情報を渡す場合ですが、インスタンスは見えませんから、グローバル変数か、作成したクラスのスタティック・メンバを経由することになります。後者がよいのでしょうね。distutils.command.build.buildの親クラス、distutils.cmd.Commandには便利なメンバが定義されています。詳細はdistutils内のソース(cmd.py)を見てもらうとして、以下に使いそうなものを挙げます。:ライブラリのディレクトリ(build/lib)。パッケージ名を足せばパッケージディレクトリになり、そこにファイルを置くとインストールされる。distutils.file_util.copy_file()の呼び替え。ディレクトリのコピー。distutils.dir_util.copy_tree()の呼び替え。distutils.dir_util.mkpath()の呼び替え。私のやりたいこと(システムごとに異なるファイルをインストールすること)は、上から3つで事足りました。汎用的にrun()メソッドを、さら汎用的に書くと以下のようになります。self.spawn(*args)3行のCustomBuild._rulesには、したい処理をコマンドラインのような文字列のリストで設定してあげます。それを一行ごと、4,5行にてコマンドと引数に分解し、以降でコマンド毎に対応するメソッドを呼んであげています。mkpathのdestに対応する引数には、先のself.build_libとパッケージ名を前に付けてあげる、という処理をして、パスの記述が短くて済むようにしています。copy_tree以外は試していません。executeやspawnなどは危険な匂いがします。再利用可能にカスタムビルドのクラスの定義がsetup.pyに入ると、全体の見通しが悪くなります。これまで書いたことは使い回せそうですので、別ファイルにしたら、setup.py自体はスッキリするだろうと、思いました。パッケージディレクトリ内のREADMEを読み込んで、long_descriptionに設定する処理は多くの人が書いています。これも使い回せそうです。Pythonのバージョンについての依存性情報が、setupの引数python_requiresに書かれていて、さらにclassfiersの中にもすべてのバージョンを並べて書いていて、冗長です。私の場合はWindowsの64bitと32bitを別に判断する必要があって独自のシステム依存性チェックをしていました。これとclassfiersの中のOSの記述も重複します。これらの情報を重複して書かなくてもよくする処理も含めて、別ファイルにして再利用可能にします。したら、もうsetup.pyでsetuptoolsをimportする必要すらなくなりました。そのスッキリしたsetup.pyはこちら、汎用的な処理をまとめたファイルがこちらです。モジュールにしてはもう守破離の破ですが、共有ライブラリが絡んだためですので致し方なし。リソースファイルと即席テスト前々節にてシステムごとに違うファイルをインストールできるようにしましたが、これを使うとディレクトリ構成も自由に変更可能です。全システム共通の、リソースファイル、辞書ファイルも、開発環境の(__init__.pyのある)パッケージディレクトリに一旦コピーする必要もなくなります。setupの引数package_dataにしていた、これらのファイルのインクルードの指定ををMANIFEST.inに移動し、カスタムビルドの記述にパッケージディレクトリへのコピーの処理を加えてあげます。するとsetup.pyも少々スッキリしましたし、モジュールパッケージの開発ディレクトリもスッキリしました。'__main__':」以下の即席テストが実行できなくなるという...。(リソースファイルがパケージディレクトリにあること前提でしたので。)とりあえず、パッケージディレクトリにない場合はコピー元の場所を見に行くように変えて対処しました。このあたりは、setup.pyをスッキリさせるか即席テストを簡単にするか、開発者の好みになりそうです。(私は前者でしょうか。場合によりけりですが。)Windowsの64bitと32bitの区別これまで、Windowsの64bitと32bitは区別して別々のビルド済みライブラリを入れてあげるべき、という前提でやってきました。Pythonのドキュメントにも、Extensionクラスのくだりでしたか、「WindowsはPOSIXと異なり、コンパイラが予め入ってないので考慮すべし」というようなことが書かれていたと思います。POSIXのAPIもないのですが...。)しかしdistutilsやsetuptoolsのドキュメントの中に、そのような問題への対処が書かれている場所は、私には見つけられませんでした。(setuptoolsのドキュメントのDiscussionには、Todoで挙げられていましたが。64bit」のようなTipsであったり、またsetupの引数のclassfiersのOSの記述に64bit・32bitの区別がなかったりするところが、Python界では区別が必要でないと考えられていた証左かもしれません。それが出来たとしても、根本的な解決にはなりません。WindowsでもIntel以外のCPUが普及したら、もう個別のバイナリのサポートはできませんからね。いうことで根本解は、WindowsでPOSIXインタフェイスのソースのビルド・実行ができるミニマムな標準環境を、Microsoftが提供することです。Windowsで、もうPythonやその他OSSフレンドリなことは全部そこでやれ、ってことかもしれません。(まだWindows7を使ってらっしゃる方も多いので、まだそれを強制することはできません。)おわりにJUMANのPythonモジュールをPOSIXに対応しました。次回は、今回書けなかった、PYPIでの公開とテストについて書きたいと思います。WordPress%d人のブロガーが「いいね」をつけました。