どぶお/Pythonで遊ぼう!

distutilsでコンパイラ関連オプションを変更する  

作成したモジュールやパッケージを配布する際に利用するモジュールは、標準モジュールであるdistutilsがあります。ここでは、配布用のバイナリパッケージを作成するbdistコマンドを使用するときにコンパイラやコンパイルオプションを変更する場合の方法を考えてみます。

スタンダードな手段として、環境変数(CCやCFLAGS)にオプションを指定する方法が広く使えます。しかし、これである程度制御できますが、今回、Intel Compiler 11.1を使う設定を試そうとしたとき、気になった点がありましたのでdistutilsの挙動を検証しつつ、エレガント(?)な対応方法を探ってみました。なお、ちょっと(?)強引と思われる方法で実現していますので、どの程度の汎用性があるかは不明です [huh]。Vine Linux 5.2上でPython 2.5に付属のdistutilsで試しました。新しいPythonだとひょっとしたらIntel Compilerぐらいには対応しているかもしれませんが、それ以外、自分なりの特別なビルドオプションを配布時に組み込みたいときには役に立つかも。

コンパイラの設定をいじるには  

Extensionクラスのオプションを指定する  

distutilsのドキュメントによると、distutils.core.Extensionクラスを使用する際に以下のオプションが使えます。

名前説明
library_dirsライブラリの検索ディレクトリのリスト["./src/foolib"]
libraries追加でリンクするライブラリのリスト["foo"]
extra_compile_argsコンパイルオプションのリスト["-O3", "-xhost", "-no-prec-div"]
extra_link_argsリンクオプションのリスト["-static", "-static-intel"]

こんな感じですね。これらを指定すれば、おそらく特に問題なくオプションを指定できるでしょう。

環境変数を指定する  

一般によく用いられるconfigureでも環境変数で指定することができますが、distutilsでも同様です。例えばsetup.pyスクリプト中でも以下のようにすれば、指定することができます(Unixの場合)。

os.environ["CC"] = "icc -pthread"
os.environ["LDSHARED"] = "icc -pthread -shared"
os.environ["CFLAGS"] = "-O3 -xhost -no-prec-div"

これらの環境変数の読み取りはdistutils/sysconfig.pyのcustomize_compiler()(後述)中で指定されています。それによるとCC, CXX, LDSHARED, CPP, LDFLAGS, CFLAGS, CPPFLAGSが使用されるようです。

このようにすることで、とりあえずはオプションの制御ができるようです。しかし、実際に使ってみると、

icc -pthread -DNDEBUG -O2 -m32 -march=i386 -mtune=generic -fasynchronous-unwind-tables -fno-strict-aliasing -fno-schedule-insns2 -D_GNU_SOURCE -fPIC -O3 -xhost -no-prec-div -fPIC -I/usr/include/python2.5 -c src/memjpeg.c -o build/temp.linux-i686-2.5/src/memjpeg.o
icc: command line warning #10006: ignoring unknown option '-fasynchronous-unwind-tables'
icc: command line warning #10006: ignoring unknown option '-fno-schedule-insns2'
icc: command line warning #10120: overriding '-O2' with '-O3'
icc: command line warning #10121: overriding '-marchi386' with '-xhost'

このようにIntel Compilerにも関わらずgccのディフォルトオプション(赤字の部分)が読み込まれるため警告が出たり、最適化オプションの上書き(後から-O3を追加しているため)警告が出たりしました。まあ無視すれば済むのですが、もうちょっと完全に制御できないかということで別のやり方を試すことにしました。

CCompilerクラスの派生クラスで制御する  

distutilsにおけるコンパイラの動作は一連のCCompiler派生クラスにより行われているようです。それらと同様にLinuxのIntel Compilerを使う場合UnixCCompilerの派生クラス(例えばMyIntelCCompiler)を定義し、使用すれば(おそらく)ビルド時の動作は好きなように制御できるはずです。そこで、CCompilerクラスがどのように実体化されるかを追跡してみました。

まず実体であるコンパイラオブジェクトは通常拡張モジュールのビルドに使用されるため、distutils/command/build_ext.pyのbuild_ext#run()が使われます。その中でdistutils.ccompiler.new_compiler()が呼ばれ、これがCCompilerインスタンスを返します。new_compiler()内ではdistutils.ccompiler.compiler_classディクショナリ内で定義されているクラスを読み込みオブジェクトを生成しています。

distutils.ccompiler.compiler_classの定義(distutils/ccompiler.py:1096)
compiler_class = { 'unix':    ('unixccompiler', 'UnixCCompiler',
                               "standard UNIX-style compiler"),
                   'msvc':    ('msvccompiler', 'MSVCCompiler',
                               "Microsoft Visual C++"),
                   ...
                  }

このようにコンパイラタイプをキーとして(モジュール名, CCompilerの派生クラス, 説明)が定義されているようです。ここでコンパイラタイプにunixを指定すればdistutils.unixccompiler.UnixCCompilerクラスが使用されるわけです。その後、distutils.sysconfig.customize_compiler()にオブジェクトが渡され、コンパイラタイプがunixの場合はディフォルトオプション等が読み込まれてセットされます。従ってgccのディフォルトオプション等を避けるためにはコンパイラタイプをunix以外に指定しなければなりません。

このような仕組みなので、compiler_classに新しく、例えばintelというアイテムを追加すればよさそうですが、困ったことに、compiler_classを使用するnew_compiler()中で取得したモジュール名(例えばunixccompiler)にパッケージ名distutils.が付けられてしまうのでdistutilsパッケージ以下のモジュールしか指定できません。distutilsに直接モジュールを追加すれば実現できますが、それでは意味がないので、さらに他の方法を探ります。

仕方がないのでそのnew_compiler()をデコレートしてCCompilerオブジェクト生成部を変更してしまいます。例えばこんな感じ。

import distutils.ccompiler
def wrap_new_compiler(func):
    def _wrap_new_compiler(*args, **kwargs):
         try: return func(*args, **kwargs)
         except DistutilsPlatformError:
             return MyIntelCCompiler(None, kwargs["dry_run"], kwargs["force"])
     return _wrap_new_compiler
distutils.ccompiler.new_compiler = wrap_new_compiler(distutils.ccompiler.new_compiler)

デコレータの作成方法はここでは触れませんが、このコード片で定義しているwrap_new_compilerで元々のnew_compilerをデコレートします。compiler_class中で指定したコンパイラタイプが存在しない場合、new_compilerはdistutils.errors.DistutilsPlatformError例外を送出します。それを捕捉することで、特別なコンパイラが指定されたと判断可能です。実際には、オプションでコンパイラタイプに何が指定されたか等を判断する部分を組み込みます。

さて、ようやくここで返すためのMyIntelCCompilerクラスの定義を行います。MyIntelCCompilerクラスはUnixCCompilerクラスの派生クラスとして作成すれば最小限の変更でIntel Compilerを扱うためのクラスが作成できるはずです。例えばこんな感じ。

class MyIntelCCompiler(UnixCCompiler):
    compiler_type = "intel"
    executables = dict(UnixCCompiler.executables)
    executables.update({
        "compiler"      : ["icc", "-O3", "-xhost", "-no-prec-div"],
        "compiler_so"   : ["icc", "-O3", "-xhost", "-no-prec-div"],
        "compiler_cxx"  : ["icc", "-O3", "-xhost", "-no-prec-div"],
        "linker_so"     : ["icc", "-shared", "-static-intel"],
        "linker_exe"    : ["icc", "-static-intel"],
    })

これでようやくMyIntelCCompilerクラスを使用できるようになりました。

あ、ここまで来て気付きましたが、MyIntelCCompilerクラスなどを作らなくても、customize_compiler()をラップするなり置き換えてもよかったですね [worried]。その方が手っ取り早かったか・・・

拡張モジュールのビルドコマンドに組み込む  

上記の方法で使用するコンパイラは好きなように変更できるはずですが、実際に拡張モジュールのビルド段階で使用してみます。拡張モジュールのビルドはcommand.build_extなので、build_extクラスの派生クラスを作成し、runメソッドを実装します。

#!/usr/bin/env python
import os
from distutils.core import setup, Extension
from distutils.command import build_py, build_ext
from distutils.errors import DistutilsPlatformError
from distutils.unixccompiler import UnixCCompiler

class MyIntelCCompiler(UnixCCompiler):
    compiler_type = "intel"
    executables = dict(UnixCCompiler.executables)
    executables.update({
        "compiler"      : ["icc", "-O3", "-xhost", "-no-prec-div"],
        "compiler_so"   : ["icc", "-O3", "-xhost", "-no-prec-div"],
        "compiler_cxx"  : ["icc", "-O3", "-xhost", "-no-prec-div"],
        "linker_so"     : ["icc", "-shared", "-static-intel"],
        "linker_exe"    : ["icc", "-static-intel"],
    })

class diffraction_build_py(build_py.build_py):
    def run(self):
        os.link("src/diffraction.py", "diffraction.py")
        build_py.build_py.run(self)
        os.remove("diffraction.py")

class diffraction_build_ext(build_ext.build_ext):
    def run(self):
        import distutils.ccompiler
        def wrap_new_compiler(func):
            def _wrap_new_compiler(*args, **kwargs):
                try: return func(*args, **kwargs)
                except DistutilsPlatformError:
                    return MyIntelCCompiler(None, kwargs["dry_run"], kwargs["force"])
            return _wrap_new_compiler
        distutils.ccompiler.new_compiler = wrap_new_compiler(distutils.ccompiler.new_compiler)
        self.compiler = "intel"
        build_ext.build_ext.run(self)

setup(
    name="diffraction",
    version="0.0.2",
    cmdclass={"build_py":diffraction_build_py, "build_ext":diffraction_build_ext},
    py_modules=["diffraction"],
    ext_modules=[
        Extension(
            "diffractionmod",
            ["src/memjpeg.c", "src/diffraction.c", "src/diffractionmod.c"],
            libraries=["jpeg"],
            extra_link_args=["-static"],
        )
    ],
)

これはpure Pythonモジュールであるdiffraction.pyと拡張モジュールであるdiffractionmod.soから構成されるモジュールセットのsetup.pyを書いてみたものです。自家用なのでいろいろ適当ですが、build_extの部分でIntel Compilerを使用しています。

コメント  

コメント等あればお願いします。