waf チュートリアル
waf - The flexible build system http://code.google.com/p/waf/
wafというものを最近知り一目惚れしてしまったので、紹介記事を書きます。ユーザーが増えると嬉しいな。
wafとは何か?特徴・利点・使うべき理由
wafはPythonベースのビルドシステムです。同様のことを行うツールとして、Autotools、Scons、CMake、Antなどがあります。Sconsからの派生で、比較的新しいソフトウェアです。
- 分かりやすい
Pythonで書かれており、スクリプトもPythonで記述します。シェルスクリプトと謎のマクロが入り混じるAutotoolsや、独自言語のCMakeなどに比べて扱い易いです。Pythonを知っていれば非常にすんなりと使いこなすことが出来ます。Pythonを知らなくても、他の独自言語を覚えるよりは実りがあるかと思います。Pythonで記述しますので、自分で機能を拡張することも非常に簡単にできます。このあたりはAutotoolsに苦しめられた経験のある方なら最も有力な乗り換え理由になると思います。
- 配布しやすい
Pythonで書かれているので、Pythonのインストールされているシステムでならどこでも動作します。幅広いバージョンのPythonで動作します。今やPythonはほとんどのLinuxにデフォルトでインストールされていますし、Windows、MacなどでもPythonが入っていれば動作させることができます。wafは80KB程度の単一のPythonスクリプトで、BSDライセンスなので、プロジェクトに含めて配布することが容易です。wafを同梱して配布することによりAutotoolsなどでは起こりがちなバージョン問題が起こりません(Autotoolsではconfigureで配布するとは思いますが)。configureスクリプトのラッパがあるので、これを用いると、Linux文化での標準的なソフトウェアのインストール方法である configure; make; make install にも簡単に対応することができます。
- 高速に動作する
configureが非常に高速に行われます。ccacheのようなオブジェクトキャッシュ、並列コンパイルの標準サポートにより、コンパイルも高速です。
- 多くの言語の組み込みサポート
C、C++、D、Java、OCaml、などのプログラミング言語のコンパイル、依存解析、実行がサポートされています。新しい言語のサポートも比較的容易です。
- 出力が分かりやすい
出力がカラーで、失敗すると赤く表示されるので、コンパイル結果がわかりやすいです。また、コンパイルすべきファイルがいくつ有って、現在何個目のファイルをコンパイルしているかが表示されるので、コンパイルの進捗状況がわかりやすいです。コンパイルの進捗をプログレスバーで表示する機能もあって、見ていて楽しいです。
wafの機能
などが標準でサポートされています。一通り揃っていると思います。ビルドフェーズを追加することも容易です。
インストール
wafをインストールするのは以下の理由から推奨されません。
- adminが要る・めんどい
- バージョン問題
- wafを同梱しない理由がない
- Windowsにはインストールできない
wafスクリプトをプロジェクトに含めてしまうのが一般的です。
wafのページから、最新版のwafをダウンロードして使います。
$ wget http://waf.googlecode.com/files/waf-1.5.11 $ mv waf-1.5.11 waf $ chmod +x waf $ ./waf Waf: Please run waf from a directory containing a file named "wscript" or run distclean
wafを使うにはwscriptというファイルを書く必要があるので、今のところはエラーが出ます。wafは実行の際に自身をカレントディレクトリに展開するので、書き込み可能なディレクトリで実行しなければならないことに注意してください。
プロジェクト作成の度にwafをダウンロードしたくない、実行の際に./を付けるのが嫌など、些細な点が気になるならば、上記デメリットを理解した上でインストールすることも可能です。その際にはwafのページからたどれる、waf bookの http://freehackers.org/~tnagy/wafbook/ch01s03.html のあたりが参考になると思います。
wafは単一スクリプトだけでなく、tar.bz2版も配布されています。これには、いろいろなサンプルプロジェクトや、Autotoolsからの移行ツールや、bash-completion、wafのソースなどが含まれていますので、wafを使っていこうという場合には一度確認されることをおすすめします。
wscriptの基本的な書き方
wafはwscriptというファイルに、ビルドに必要な情報を書きこみます。これは普通のPythonプログラムとして記述します。以下に雛形を示します。
APPNAME = 'test-project' VERSION = '1.0.0' srcdir = '.' blddir = 'build' def set_options(opt): # プロジェクトのオプションを設定する # 最初に呼ばれる pass def configure(conf): # ライブラリのチェックなど # waf configure 時に呼ばれる pass def build(bld): # ビルドの情報を書く # waf build 時に呼ばれる pass def shutdown(ctx): # 終了時に何かをさせたいとき # 最後に呼ばれる pass
まず、変数を四つ定義します。APPNAMEとVERSIONはプログラムの名前とバージョンを指定します。srcdirとblddirはソースの場所と、コンパイル時の一時ファイルを置くディレクトリを指定します。これらの変数は省略すると、それぞれ'noname'、'1.0'、'.'、'build'が使われます。
このままでは実行しても何も起こらないので、次のように書いてみます。
APPNAME = 'test-project' VERSION = '1.0.0' srcdir = '.' blddir = 'build' def set_options(opt): print "set_options" def configure(conf): print "configure" def build(bld): print "build" def shutdown(ctx): print "shutdown"
$ ./waf configure set_options configure 'configure' finished successfully (0.002s) shutdown $ ./waf build set_options Waf: Entering directory `/home/hideyuki/project/waf_test/build' build Waf: Leaving directory `/home/hideyuki/project/waf_test/build' 'build' finished successfully (0.003s) shutdown
configure、build実行時に、それぞれ対応する関数が呼ばれているのが分かります。ここにそれぞれ必要なことを書いていくことになります。
C/C++プログラムのコンパイル
def set_options(opt): opt.tool_options('compiler_cxx') def configure(conf): conf.check_tool('compiler_cxx') def build(bld): bld(features = 'cxx cprogram', source = 'main.cpp', target = 'main')
まず、set_optionsで、C++コンパイラ用のオプションを使えるようにします。'compiler_cxx'はC++コンパイラ用の設定です。Cコンパイラを使うなら、'compiler_cc'を指定します。次に、configureでC++コンパイラのチェックをします。最後にbuildにビルドルールを書きます。featuresには、プログラムをどうやってコンパイルするかを書きます。ここでは、cxxとcprogramを指定しています。これは、空白区切で指定してもいいですし、['cxx', 'cprogram']のように文字列のリストを渡してもいいです。これは以降出てくる文字列を複数している所でもすべて共通の仕様です。cxxは、C++のコンパイラでコンパイルせよと言う指定、cprogramは実行ファイルを作れと言う指定です。他にcshlib、cstaticlibなどがあります。sourceにはソースファイルを指定します。targetには生成する実行ファイルの名前を指定します。
ビルドを実行すると次のようになります。
$ cat main.cpp #include <iostream> using namespace std; int main() { cout<<"Hello, waf waf world!"<<endl; return 0; } $ ./waf configure Checking for program g++,c++ : ok /usr/bin/g++ Checking for program cpp : ok /usr/bin/cpp Checking for program ar : ok /usr/bin/ar Checking for program ranlib : ok /usr/bin/ranlib Checking for g++ : ok 'configure' finished successfully (0.049s) $ ./waf build Waf: Entering directory `/home/hideyuki/project/waf_test/build' [1/2] cxx: main.cpp -> build/default/main_1.o [2/2] cxx_link: build/default/main_1.o -> build/default/main Waf: Leaving directory `/home/hideyuki/project/waf_test/build' 'build' finished successfully (0.348s) $ build/default/main Hello, waf waf world!
configureで、C++コンパイラの存在がチェックされ、buildでmain.cppがコンパイルされ、build/default/mainに実行ファイルがリンクされます。ビルドの際に生成されるものはすべてbuild(blddirで指定したディレクトリ)以下に置かれます。
なお、waf buildのときのbuildは省略できるので、単にwafと実行すればbuildできます。
ファイル複数からなるプログラムはsourceに複数ファイルを指定すればできます。
def build(bld): bld(features = 'cxx cprogram', source = 'main.cpp foo.cpp', target = 'main', includes = '.')
includesにヘッダファイルのあるディレクトリを指定します。これを書けば、そこに含まれるヘッダの依存関係を自動で解析してくれます。
$ cat main.cpp #include <iostream> using namespace std; #include "foo.h" int main() { cout<<foo(123)<<endl; return 0; } $ cat foo.cpp #include "foo.h" int foo(int n) { return n*n; } $ cat foo.h int foo(int n); $ ./waf configure Checking for program g++,c++ : ok /usr/bin/g++ Checking for program cpp : ok /usr/bin/cpp Checking for program ar : ok /usr/bin/ar Checking for program ranlib : ok /usr/bin/ranlib Checking for g++ : ok 'configure' finished successfully (0.048s) $ ./waf Waf: Entering directory `/home/hideyuki/project/waf_test/build' [1/3] cxx: main.cpp -> build/default/main_1.o [2/3] cxx: foo.cpp -> build/default/foo_1.o [3/3] cxx_link: build/default/main_1.o build/default/foo_1.o -> build/default/main Waf: Leaving directory `/home/hideyuki/project/waf_test/build' 'build' finished successfully (0.439s) $ build/default/main 15129 $ ./waf Waf: Entering directory `/home/hideyuki/project/waf_test/build' Waf: Leaving directory `/home/hideyuki/project/waf_test/build' 'build' finished successfully (0.006s) $ emacs foo.cpp ... edit ... $ ./waf Waf: Entering directory `/home/hideyuki/project/waf_test/build' [2/3] cxx: foo.cpp -> build/default/foo_1.o [3/3] cxx_link: build/default/main_1.o build/default/foo_1.o -> build/default/main Waf: Leaving directory `/home/hideyuki/project/waf_test/build' 'build' finished successfully (0.164s) $ emacs foo.h ... edit ... $ ./waf Waf: Entering directory `/home/hideyuki/project/waf_test/build' [1/3] cxx: main.cpp -> build/default/main_1.o [2/3] cxx: foo.cpp -> build/default/foo_1.o [3/3] cxx_link: build/default/main_1.o build/default/foo_1.o -> build/default/main Waf: Leaving directory `/home/hideyuki/project/waf_test/build' 'build' finished successfully (0.408s)
必要なファイルだけ再コンパイルされているのが分かります。なお、ファイルの再コンパイルはmd5の比較により行われるので、内容が本当に変更されていないと再コンパイルは行われません。
ライブラリの作り方
ライブラリを作る場合は、featuresにcprogramの代わりにcshlibまたはcstaticlibと書くだけです。先程の例を、foo.cppをライブラリに、main.cppをそれを使うプログラムに変更してみます。
def build(bld): bld(features = 'cxx cstaticlib', source = 'foo.cpp', target = 'foo', includes = '.') bld(features = 'cxx cprogram', source = 'main.cpp', target = 'main', includes = '.', uselib_local = 'foo')
featuresにcstaticlibを指定しました。foo.cppからfooという静的ライブラリを作れという指示です。mainは今度はそのfooというライブラリをリンクしなければなりません。その指定をuselib_localというところに書きます。同じプロジェクトで作られるライブラリ(ローカルなライブラリ)への参照はuselib_localに書きます。
$ ./waf distclean configure build 'distclean' finished successfully (0.002s) Checking for program g++,c++ : ok /usr/bin/g++ Checking for program cpp : ok /usr/bin/cpp Checking for program ar : ok /usr/bin/ar Checking for program ranlib : ok /usr/bin/ranlib Checking for g++ : ok 'configure' finished successfully (0.048s) Waf: Entering directory `/home/hideyuki/project/waf_test/build' [1/4] cxx: foo.cpp -> build/default/foo_1.o [2/4] cxx: main.cpp -> build/default/main_2.o [3/4] static_link: build/default/foo_1.o -> build/default/libfoo.a [4/4] cxx_link: build/default/main_2.o -> build/default/main Waf: Leaving directory `/home/hideyuki/project/waf_test/build' 'build' finished successfully (0.426s)
featuresにcstaticlibを指定したので、foo.cppがコンパイルされ、staticライブラリlibfoo.aが生成されました(targetで指定した名前にlibが付いたものが生成されますのでlibをtargetにlibをつけてはいけません。liblibfoo.aになってしまいます)。それからmain.cppとlibfoo.aがリンクされ、mainが生成されます。ここで、mainを生成する前にlibfoo.aが生成されていなければなりませんが、このビルド順は、ビルドターゲット間の依存関係から自動的に解決されます。書いてある場所の前後は関係ありません。サブディレクトリ(後述)を含む場合も全体を考慮して解決されます。wafはwaf build -j2などとすると並列コンパイルができますが、それも依存関係に基づきます。依存関係が循環している場合、wafはそれを検知できません。エラーメッセージではなく、再帰がスタックオーバーフローしてwafが落ちます。依存関係はきちんとツリー(もしくは森)になるようにしましょう。
ちなみに、wafはコマンドを並べて書くと、上のようにそれらを順に実行します。
featuresにcshlibを指定すると、動的ライブラリが生成されます。その際も同様にuselib_localでリンクできます。
def build(bld): bld(features = 'cxx cshlib', source = 'foo.cpp', target = 'foo', includes = '.') bld(features = 'cxx cprogram', source = 'main.cpp', target = 'main', includes = '.', uselib_local = 'foo')
$ ./waf distclean configure build 'distclean' finished successfully (0.001s) Checking for program g++,c++ : ok /usr/bin/g++ Checking for program cpp : ok /usr/bin/cpp Checking for program ar : ok /usr/bin/ar Checking for program ranlib : ok /usr/bin/ranlib Checking for g++ : ok 'configure' finished successfully (0.065s) Waf: Entering directory `/home/hideyuki/project/waf_test/build' [1/4] cxx: foo.cpp -> build/default/foo_1.o [2/4] cxx: main.cpp -> build/default/main_2.o [3/4] cxx_link: build/default/foo_1.o -> build/default/libfoo.so [4/4] cxx_link: build/default/main_2.o -> build/default/main Waf: Leaving directory `/home/hideyuki/project/waf_test/build' 'build' finished successfully (0.507s)
サブディレクトリ
先程の例を、libfooをfooというディレクトリに移動させようと思います。サブモジュールを分割するのはプロジェクトをシンプルに保つために重要です。libfooを作る方法は独立していた方が良いので、wscriptも分割します。ディレクトリ構成としては次のようになります。
|- foo | |- foo.cpp | |- foo.h | `- wscript |- main.cpp `- wscript
ルートのwscriptから子のwscriptを呼び出すには次のようにします。
# wscript ef set_options(opt): opt.tool_options('compiler_cxx') def configure(conf): conf.check_tool('compiler_cxx') def build(bld): bld(features = 'cxx cprogram', source = 'main.cpp', target = 'main', includes = '. foo', uselib_local = 'foo') bld.recurse('foo')
# foo/wscript def build(bld): bld(features = 'cxx cshlib', source = 'foo.cpp', target = 'foo', includes = '.')
bld.recurse('foo') と書いてある部分がfoo/wscriptを呼び出す部分です。こうすると、foo/wscriptにある同じ関数が呼ばれます。この場合だとbuildなのでbuildが呼ばれます。もちろんconfigureからrecurseすることも可能です。recurseにはディレクトリのリストを渡せます。サブディレクトリのwscriptには、呼び出されない関数は書かなくても構いません。configureをrecurseできるので、各サブモジュールに必要なライブラリチェックを分散させることができます。Autotoolsのようにconfigure.acが肥大化したり、チェックの場所と使用の場所が離れたりすることが防げます。実行すると次のようになります。
$ ./waf distclean configure build 'distclean' finished successfully (0.000s) Checking for program g++,c++ : ok /usr/bin/g++ Checking for program cpp : ok /usr/bin/cpp Checking for program ar : ok /usr/bin/ar Checking for program ranlib : ok /usr/bin/ranlib Checking for g++ : ok 'configure' finished successfully (0.054s) Waf: Entering directory `/home/hideyuki/project/waf_test/build' [1/4] cxx: main.cpp -> build/default/main_1.o [2/4] cxx: foo/foo.cpp -> build/default/foo/foo_1.o [3/4] cxx_link: build/default/foo/foo_1.o -> build/default/foo/libfoo.so [4/4] cxx_link: build/default/main_1.o -> build/default/main Waf: Leaving directory `/home/hideyuki/project/waf_test/build' 'build' finished successfully (0.535s)
ライブラリ、ヘッダファイルのチェック
C/C++のライブラリ、ヘッダファイルのチェックはcheck_ccまたは、check_cxxによって行うことができます。これらはconfigure時に行うので、configure()に記述します。一通りの例を以下に示します。
def configure(conf): conf.check_tool('compiler_cxx') # libmの存在確認 conf.check_cxx(lib = 'm') # ディレクトリを指定して確認 conf.check_cxx(lib = 'superlib', libpath = '/var/super/lib') # time.hの存在確認 conf.check_cxx(header_name = 'time.h') # stdio.hと関数printfの存在確認。必須(mandatory) conf.check_cxx(function_name = 'printf', header_name = 'stdio.h', mandatory = True) # check_cxxはboolを返す if conf.check_cxx(lib = 'nuboo'): print "nuboo exists!" else: print "nuboo does not exist!" # コード片がコンパイルできるか調べる。コンパイルできるかどうかをboobahに格納 conf.check_cxx(fragment = 'int main(){ return 0; }', define_name = 'boobah') # コード片を実行して、出力を取り出す conf.check_cxx(fragment = """ #include <iostream> using namespace std; int main(){ cout<<sizeof(long)<<endl; return 0; } """, define_name = 'LONG_SIZE', execute = True, define_ret = True, msg = 'Checking for long size') # uselib_store(後述) conf.check_cxx(lib = 'm', cxxflags = '-Wall', defines = ['var=foo', 'x=y'], uselib_store = 'M') # チェックした結果をヘッダファイルとして書き出す conf.write_config_header('config.h') # envの中身を出力(デバッグに便利) conf.env.store('conf.log')
check_cxxはC++コンパイラを用いてコンパイルを試みます。同様にcheck_ccはCコンパイラを用いてコンパイルを試みます。それぞれ、使用するためには、set_optionsでopt.tool_options('compiler_cxx')、opt.tool_options('compiler_cc')されている必要があります。
libにライブラリ名を指定すると、そのライブラリがリンクできるかどうかを調べます。その際にlibpathでライブラリを探す場所を指定できます。
header_nameにヘッダファイル名を指定すると、そのファイルをインクルードできるか調べます。function_nameに関数名を指定すると、関数の存在を調べられます。このときライブラリ、ヘッダファイルがそれぞれ存在するとconf.env.HAVE_TIME_H、conf.env.HAVE_PRINTFなどが1に設定されます。
madantoryを指定すると、そのチェックを必須にできます。これを指定したチェックが失敗するとconfigureがこけます。
check_cxxはboolを返すので、Pythonのif文などで簡単に利用できます。とても便利です。
コード片のコードテストや、コード片の出力をconf.envに設定できます。上の例ですと、LONG_SIZEに"8"が設定されます。
wafにはuselibという仕組みがあります。詳しくは後述のライブラリの参照で述べますが、ここでは、ここに指定した文字列を接尾辞に、いろいろな変数が定義されます。ここでは、LIB_M、CXXDEFINES_M、CXXFLAGS_M、が定義されます。
write_config_header()を呼び出すと、集めた情報をC/C++のヘッダファイルに書きだします。例えば、上のコードなら、次のコードが生成されます。
/* Configuration header created by Waf - do not edit */ #ifndef _CONFIG_H_WAF #define _CONFIG_H_WAF #define HAVE_TIME_H 1 #define HAVE_PRINTF 1 #define boobah 1 #define LONG_SIZE "8" #endif /* _CONFIG_H_WAF */
conf.env.store()を呼び出すと、環境を書きだすことができます。これはwafコードのデバッグにとても便利です。
configureの実行結果を以下に載せておきます。
$ ./waf configure Checking for program g++,c++ : ok /usr/bin/g++ Checking for program cpp : ok /usr/bin/cpp Checking for program ar : ok /usr/bin/ar Checking for program ranlib : ok /usr/bin/ranlib Checking for g++ : ok Checking for library m : ok Checking for library superlib : not found Checking for header time.h : ok Checking for function printf : ok Checking for library nuboo : not found nuboo does not exist! Checking for custom code : ok Checking for long size : ok Checking for library m : ok 'configure' finished successfully (1.480s)
pkg-config
pkg-config、あるいは、hoge-configのような、Cのコンパイラフラグ、リンクフラグ、ライブラリパスなどを取得するためのプログラムを提供しているライブラリがあります。このようなライブラリがあるばあい、とても便利にチェックを行うことができます。
def configure(conf): conf.check_cfg(atleast_pkgconfig_version = '0.0.0') conf.check_cfg(package = 'pango', atleast_version = '0.0.0') conf.check_cfg(package = 'pango', exact_version = '0.21') conf.check_cfg(package = 'pango', max_version = '9.0.0') conf.check_cfg(package = 'pango', args='--cflags --libs') pango_version = conf.check_cfg(modversion = 'pango') conf.check_cfg(path = 'sdl-config', args = '--cflags --libs', package = '', uselib_store='SDL')
pkg-configの場合、check_cfgで、packageにパッケージ名を渡せば、バージョンのチェックなどができます。独自のconfigプログラムの場合は、pathに指定してやります(この場合はバージョンチェックなどはできない模様)。ここで、uselib_storeを指定して、argsにコンパイラフラグを取得するための引数を書いてやると、check_cfgはその返り値を解析して、-IxxxならCPPPATH_hogeに、-DxxxならCXXDEFINES_hogeに、-LxxxならLIBPATH_hogeに、-lxxxならLIB_hogeに、その他はCXXFLAGS_hogeなどに、自動的に振り分けて追加されます。
ライブラリの参照
bldの引数で、ライブラリの参照を追加できます。例えば、pthreadをリンクするなら、次のようになります。
def build(bld): bld(features = 'cxx cprogram', source = 'main.cpp', target = 'main', includes = '.', lib = ['pthread'])
ライブラリパスを指定したいなら、libpathを書きます。
def build(bld): bld(features = 'cxx cprogram', source = 'main.cpp', target = 'main', includes = '.', lib = ['pthread'], libpath = ['/usr/local/lib'])
その他、いろいろオプションを指定できます。
def build(bld): bld(features = 'cxx cprogram', source = 'main.cpp', target = 'main', includes = '.', defines = ['LINUX=1', 'BIDULE'], cxxflags = ['-O2', '-Wall'], lib = ['m'], libpath = ['/usr/lib'], linkflags = ['-g'])
uselib
uselibを用いると、上記のいろいろなコンパイルの設定を一気に設定できます。
def build(bld): bld(features = 'cxx cprogram', source = 'main.cpp', target = 'main', includes = '.', uselib = 'SDL')
bldの引数にuselibを指定してやると、その文字列を接尾辞とする設定がまとめてなされます。例えば、LIB_SDLがlibに、LIBPATH_SDLがlibpathに、CXXFLAGS_SDLがcxxflagsに設定されます。check_cfgもしくはcheck_cxxでのuselib_storeとuselibを組み合わせて使うと、コンパイル・リンクフラグの設定を大変簡単に行うことができます。
オプション
configure時のオプションを自由に追加することができます。プロジェクトの特定のモジュールの有効・無効を切り替えたりなどが典型的な利用例です。set_options()でadd_optionすることによりオプションの追加ができます。
def set_options(opt): opt.tool_options('compiler_cxx') # boolオプション opt.add_option('--enable-super-module', action = 'store_true', default = False, help='enable a super module') # 文字列オプション opt.add_option('--build_kind', action = 'store', default = 'debug,release', help = 'build the selected variants') def build(bld): import Options # オプションの参照 if Options.options.enable_super_module: build.recurse('super')
追加したオプションは waf --help にも表示されます。
$ ./waf --help waf [command] [options] Main commands (example: ./waf build -j4) build : builds the project clean : removes the build files configure: configures the project dist : makes a tarball for redistributing the sources distcheck: checks if the sources compile (tarball from 'dist') distclean: removes the build directory install : installs the build files uninstall: removes the installed files Options: --version show program's version number and exit -h, --help show this help message and exit -j JOBS, --jobs=JOBS amount of parallel jobs (1) -k, --keep keep running happily on independent task groups -v, --verbose verbosity level -v -vv or -vvv [default: 0] --nocache ignore the WAFCACHE (if set) --zones=ZONES debugging zones (task_gen, deps, tasks, etc) -p, --progress -p: progress bar; -pp: ide output --targets=COMPILE_TARGETS build given task generators, e.g. "target1,target2" --enable-super-module enable a super module --build-kind=BUILD_KIND build the selected variants ...
ユニットテスト
wafはユニットテスト支援機能があります。先のプロジェクトで、libfooのユニットテストを作成することにします。
def set_options(opt): opt.tool_options('compiler_cxx') opt.tool_options('UnitTest') def configure(conf): conf.check_tool('compiler_cxx') conf.check_cxx(lib = 'gtest_main', uselib_store = 'gtest') def build(bld): bld(features = 'cxx cprogram test', source = 'foo_test.cpp', target = 'foo_test', includes = '. foo', uselib_local = 'foo', uselib = 'gtest') bld.recurse('foo') import UnitTest bld.add_post_fun(UnitTest.summary)
まず、set_optionsでtool_optionsにUnitTestを追加します。次に、テストプログラムを作ります。これはfeaturesにtestを追加するだけです。testフィーチャーがついているプログラムは、コンパイル後に自動で実行され、その結果が集計されるようになります。それから、bld.add_post_funでUnitTest.summaryが最後に実行されるようにします。これでテストの結果が表示されるようになります。
#include <gtest/gtest.h> #include "foo.h" TEST(footest, test) { EXPECT_EQ(foo(123), 123+123); }
テストプログラムを書きます。今回はgtestを使いました。fooは二乗を返す関数だったので、これはfailするはずです。実行してみます。
$ ./waf distclean configure build 'distclean' finished successfully (0.002s) Checking for program g++,c++ : ok /usr/bin/g++ Checking for program cpp : ok /usr/bin/cpp Checking for program ar : ok /usr/bin/ar Checking for program ranlib : ok /usr/bin/ranlib Checking for g++ : ok Checking for library gtest_main : ok 'configure' finished successfully (0.224s) Waf: Entering directory `/home/hideyuki/project/waf_test/build' [1/5] cxx: foo_test.cpp -> build/default/foo_test_1.o [2/5] cxx: foo/foo.cpp -> build/default/foo/foo_1.o [3/5] static_link: build/default/foo/foo_1.o -> build/default/foo/libfoo.a [4/5] cxx_link: build/default/foo_test_1.o -> build/default/foo_test [5/5] utest: build/default/foo_test Waf: Leaving directory `/home/hideyuki/project/waf_test/build' execution summary FAIL /home/hideyuki/project/waf_test/build/default/foo_test command execution failed: /home/hideyuki/project/waf_test/build/default/foo_test -> 'Running main() from gtest_main.cc [==========] Running 1 test from 1 test case. [----------] Global test environment set-up. [----------] 1 test from footest [ RUN ] footest.test ../foo_test.cpp:7: Failure Value of: 123+123 Actual: 246 Expected: foo(123) Which is: 15129 [ FAILED ] footest.test [----------] Global test environment tear-down [==========] 1 test from 1 test case ran. [ PASSED ] 0 tests. [ FAILED ] 1 test, listed below: [ FAILED ] footest.test 1 FAILED TEST
コンパイルされた後、テストが実行されているのが分かります。失敗したテストがFAILと表示されています(これは実際には赤で表示されるので、失敗したことが分かりやすい)。wafはテストをコンパイルの後に実行します。コンパイルが起こらないとテストは実行されません。テストが失敗した場合も、再度waf buildを行ってもテストは実行されません。waf build --alltests オプションを使うと、ビルドされなかったテストを含む、全てのテストを実行することができます。
さて、先程のバグが分かったので、修正して再実行します。
include <gtest/gtest.h> #include "foo.h" TEST(footest, test) { EXPECT_EQ(foo(123), 123*123); }
$ ./waf Waf: Entering directory `/home/hideyuki/project/waf_test/build' [1/5] cxx: foo_test.cpp -> build/default/foo_test_1.o [4/5] cxx_link: build/default/foo_test_1.o -> build/default/foo_test [5/5] utest: build/default/foo_test Waf: Leaving directory `/home/hideyuki/project/waf_test/build' execution summary ok /home/hideyuki/project/waf_test/build/default/foo_test 'build' finished successfully (0.779s)
今度はokになりました(okは緑で表示されます)。なお、通ったテストの出力は表示されません。
インストーラ
waf install とすると、ビルドしたものをインストールできます。
$ sudo ./waf distclean configure build install 'distclean' finished successfully (0.002s) Checking for program g++,c++ : ok /usr/bin/g++ Checking for program cpp : ok /usr/bin/cpp Checking for program ar : ok /usr/bin/ar Checking for program ranlib : ok /usr/bin/ranlib Checking for g++ : ok Checking for library gtest_main : ok 'configure' finished successfully (0.209s) Waf: Entering directory `/home/hideyuki/project/waf_test/build' [1/5] cxx: foo_test.cpp -> build/default/foo_test_1.o [2/5] cxx: foo/foo.cpp -> build/default/foo/foo_1.o [3/5] cxx_link: build/default/foo/foo_1.o -> build/default/foo/libfoo.so [4/5] cxx_link: build/default/foo_test_1.o -> build/default/foo_test [5/5] utest: build/default/foo_test Waf: Leaving directory `/home/hideyuki/project/waf_test/build' execution summary ok /home/hideyuki/project/waf_test/build/default/foo_test 'build' finished successfully (0.965s) Waf: Entering directory `/home/hideyuki/project/waf_test/build' * installing build/default/foo/libfoo.so as /usr/local/lib/libfoo.so Waf: Leaving directory `/home/hideyuki/project/waf_test/build'
テストでない実行ファイル、動的ライブラリは自動的にインストール対象になります。
特定のファイル(例えばヘッダファイル)をインストールさせたい場合、bld.install_files()を使います。
def build(bld): ... # 特定のファイルをインストールする bld.install_files('${PREFIX}/include', ['foo/foo.h']) # ディレクトリ構造を保持する bld.install_files('${PREFIX}/include', ['foo/foo.h'], relative_trick = True) # 別名でインストールする bld.install_as('${PREFIX}/dir/bar.png', 'foo.png') # シンボリックリンクを作成する bld.symlink_as('${PREFIX}/lib/libfoo.so.1', 'libfoo.so.1.2.3')
install_filesの第一引数には、インストールするディレクトリを指定します。文字列中の${PREFIX}は、bld.env.PREFIXで置換されます。PREFIXでなくてもbld.envにある変数なら何でも展開できます。bld.env.PREFIXには、configure時に--prefix=で指定したディレクトリが入っています。指定しなかった場合は/usr/localになっています。
relative_trickを指定すると、ディレクトリ構造を保持してインストールされます。上の例なら、${PREFIX}/include/foo/foo.h にインストールされます(一つ目の例は${PREFIX}/foo.h になります)。
その他、別名でのインストールや、シンボリックリンク作成もできます。
パッケージ作成
さて、libfooもおおよそ完成したので、パッケージを作って配布しましょう。wafでは、Autotoolsと同様、waf distとすると、tarballが作成されます。
$ ./waf dist New archive created: test-project-1.0.0.tar.bz2 (sha='0af6ce61eb3661bfc54efa5d2b301442a7e84557') 'dist' finished successfully (0.033s) $ ls build foo_test.cpp main.cpp test-project-1.0.0.tar.bz2 wscript foo foo_test.cpp~ main.cpp~ waf wscript~ $ tar -jtf test-project-1.0.0.tar.bz2 test-project-1.0.0/ test-project-1.0.0/waf test-project-1.0.0/main.cpp test-project-1.0.0/foo/ test-project-1.0.0/foo/foo.cpp test-project-1.0.0/foo/wscript test-project-1.0.0/foo/foo.h test-project-1.0.0/wscript test-project-1.0.0/foo_test.cpp
test-project-1.0.0.tar.bz2というファイルが出来ています。デフォルトでは、bz2圧縮されます。圧縮方式を変更するには次のようにします。
import Scripting Scripting.g_gz = 'gz'
ところで、tarballに含めるファイルを指定した覚えはありませんでした。どうやってwafはtarballに含めるべきファイルを判断しているのでしょうか?
wafは基本的に、ディレクトリの内容をすべてtarballに含めます。そこから、特定の接尾辞を持つファイル・ディレクトリと、特定の名前のファイル・ディレクトリを除外します。ソースコードによると、、デフォルトの除外接尾辞は'~ .rej .orig .pyc .pyo .bak .tar.bz2 tar.gz .zip .swp'、デフォルトの除外ファイル・ディレクトリは'.bzr .bzrignore .git .gitignore .svn CVS .cvsignore .arch-ids {arch} SCCS BitKeeper .hg _MTN _darcs Makefile Makefile.in config.log'のようです。さらに、blddirは除外されます。
dist_hook()という関数を作れば、distで含めるファイルをカスタマイズすることができます。dist_hook()は、tarballに含めるファイルをテンポラリディレクトリにまとめて、圧縮する直前に、そこのディレクトリがカレントディレクトリになった状態で呼ばれます。
たとえば、main.cppを含めたくなければこうします。
def dist_hook(): import os os.remove('main.cpp')
$ ./waf dist New archive created: test-project-1.0.0.tar.gz (sha='4dbbd6948532b5b52d9ff52615739e4c68ad5f17') 'dist' finished successfully (0.017s) $ tar -ztf test-project-1.0.0.tar.gz test-project-1.0.0/ test-project-1.0.0/waf test-project-1.0.0/foo/ test-project-1.0.0/foo/foo.cpp test-project-1.0.0/foo/wscript test-project-1.0.0/foo/foo.h test-project-1.0.0/wscript test-project-1.0.0/foo_test.cpp
また、除外ルールに追加することもできます。
import Scripting Scripting.dist_exts += ['.cpp']
.cppファイルが含まれなくなります。
$ ./waf dist New archive created: test-project-1.0.0.tar.gz (sha='a777a3cc369955f489a7d76c947eb067812f1917') 'dist' finished successfully (0.014s) $ tar -ztf test-project-1.0.0.tar.gz test-project-1.0.0/ test-project-1.0.0/waf test-project-1.0.0/foo/ test-project-1.0.0/foo/wscript test-project-1.0.0/foo/foo.h test-project-1.0.0/wscript
tarballに含めたくないファイルをディレクトリ内に置きたい場合、特定の拡張子にして、その拡張子を除外ルールに追加するか、特定のディレクトリを除外にしてそこにまとめて置くか、あるいはdist_hook()を書いて独自の除去ルーチンを書けばなんでもできます。
結び
さて、wafいかがだったでしょうか。普通のPythonプログラムとしてビルドスクリプトを記述するのでとても見通しが良いのではないかと思います。どこに何が書けるのか、何を書いたらいいのかほとんど迷うことがありません。wafはコンパクトでありながら、ツボを抑えた機能をサポートしているので、普段使いではあれがないと困ることはほとんどないと思います。しかし、wafはflexibleと謳っているだけあって、機能を独自に拡張することが非常に容易にできるように設計されているので、最悪書けばなんとかなります。
今回はチュートリアルということで、wafのすべてを紹介できているわけではありません。もっとwafを知りたい、wafを是非使ってみたいという方は、下記のリンクを参照されると良いでしょう。
cabalで色付けされたソースにリンクしたドキュメントを生成する方法
$ cabal configure $ cabal hscolour $ cabal haddock "--haddock-options=--source-base=src/ --source-module=src/%{MODULE/./-}.html --source-entity=src/%{MODULE/./-}.html#%N"
Haskell binding for MessagePack
MessagePack の Haskell binding を作りました。
MessagePackとは
http://msgpack.sourceforge.jp/
id:viver さんが開発された高速なバイナリシリアライザです。
http://d.hatena.ne.jp/viver/20080816/p1 や http://d.hatena.ne.jp/nobu-q/20091209 が詳しいです。
インストール方法
githubにレポジトリを置いています。http://github.com/tanakh/hsmsgpack にあります。
パッケージをHackageにアップロードしています。http://hackage.haskell.org/package/msgpack-0.1.0 にて公開されています。
インストールは、MessagePackをインストールしてから、
$ cabal install msgpack
で行えます。
使い方
http://fxp.hp.infoseek.co.jp/doc/msgpack/
ここにhaddockドキュメントがあります(Hackageでのビルドがこけているため、Hackageにドキュメントが生成されていないので、暫定的にここに置いています。将来的には移動する可能性が高いです)。
Low Levelなインターフェースとして、MessagePackのC APIがほぼすべて一対一対応の関数が Data.MessagePack.Base にありますので、これを使えばだいたい何でもできるはずです。 http://msgpack.sourceforge.jp/c:doc.ja こちらが参考になるはずです。
import Control.Monad import Data.MessagePack main = do sb <- newSimpleBuffer -- Packerが使うバッファ作成 pc <- newPacker sb -- Packer作成 packArray pc 3 -- 三要素の配列をパックしますよ packS32 pc 12345 -- 整数 packDouble pc 3.14 -- 浮動小数 packTrue pc -- Bool bs <- simpleBufferData sb -- バッファにたまったデータを取り出す print bs -- > "\147\205\&09\203@\t\RS\184Q\235\133\US\195" up <- newUnpacker defaultInitialBufferSize -- Unpacker作成 unpackerFeed up bs -- Unpackerにデータを食わせる resp <- unpackerExecute up -- デシリアライズ実行 guard $ resp==1 -- 成功すると1が返る obj <- unpackerData up -- デシリアライズされたデータを取り出す print obj -- > ObjectArray [ObjectInteger 12345,ObjectDouble 3.14,ObjectBool True]
Low Levelなインターフェースの上に、High Levelなインターフェースを作っています。今のところ、汎用的なpack関数と、単一のpack, unpackを簡単に行う関数があります。
main = do sb <- newSimpleBuffer pc <- newPacker sb -- OBJECTクラスの値を適当にpackする pack pc [(1, 2), (3, 4), (5::Int, 6::Int)] ...
main = do bs <- packb [1,2,3::Int] -- お手軽pack Right dat <- unpackb bs -- お手軽unpack print (dat :: [Int]) -- > [1,2,3]
main = do let bs = packb' [1,2,3::Int] -- pure版 let Right dat = unpackb' bs -- pure版 print (dat :: [Int]) -- > [1,2,3]
ObjectとHaskellのデータの相互変換は toObject、 fromObject で行えます。
main = do bs <- packb [1,2,3::Int] Right (_, obj) <- withZone $ \z -> unpackObject z bs let Right dat = fromObject obj print (dat :: [Int])
Haskellerのためのプレゼンツール"MonadPoint"
去る11月20日、Haskellナイトというイベント ( http://hop.timedia.co.jp/ )で、"HaskellerのHaskellによるHaskellerのためのプレゼン"というなんだかよくわからないタイトルで発表してきました。
遅くなりましたが、作った物の公開と発表の補足をしておきます。
レポジトリ
http://github.com/tanakh/MonadPoint にてMonadPointを公開しています。
http://github.com/tanakh/haskell-night にて当日使ったスライドのソースとWindowsバイナリを公開しています。
使い方
まず、MonadPointをcabalでインストールしてください(Windowsに入れる場合、依存ライブラリのFTGLを入れるのが大変だと思いますので、前回のエントリを参照していただければ幸いです)。それから、てきとうな.hsファイルを作って、import MonadPoint してプレゼンをモナドでゴリゴリと書いて、runPresentation に渡せばいい感じになると思います。レポジトリにあるサンプルが参考になると思います。
http://github.com/tanakh/MonadPoint/blob/master/test/Main.hs
若干説明
今のところサポートしているのは、
- 矩形の中にそれっぽく文字列を書く
- 矩形を書く
- 画像ファイルの表示
- 矩形をレイアウトする
のような機能です。
基本的に、scaleでスケーリングして、縦横に並べてスライドを作ります。自動で高さとか幅を調節することはできないので、手でやらないといけません。
scale d $ do ... - 縦横に縮小。中央に配置。 scalehu d $ do ... -- 縦方向に縮小。上に配置。 scalehd d $ do ... -- 縦方向に縮小。下に配置。 scalevl d $ do ... -- 横方向に縮小。左に配置。 scalevr d $ do ... -- 横方向に縮小。右に配置。 txtc str -- 文字列。中央揃え。 txtl str -- 文字列。左揃え。 txtr str -- 文字列。右揃え。 pict filename -- 画像を表示。 txtarea strs -- 文字列リストを等幅フォントで表示。
他にも色々あるので、ソースをご参照下さい。
スライドによくつかわれるリスト記法は書きやすくする関数を提供しています。
list $ do ... -- 引数に与えられたリスト要素をうまいことレイアウトして表示する list $ do li "hoge" ul $ do li "moge"
ちなみに、liなどは型が違うので、listの外側には置けません。うーん、型ですね。
今後の予定
実装がやっつけで、完成度が低すぎるので、まだまだ色々改良したいところです。
FTGL on GHC on Windows
http://hackage.haskell.org/package/FTGL これをWindowsで使いたいという話。泣きそうなほど苦労したのでここに書いとく。
GHCのインストール
まずは、GHCのインストールです。WindowsだとHaskell Platform(http://hackage.haskell.org/platform/)を入れるのが楽。cabal-installが入ってパスもとおってうれしい。
MinGWのインストール
いきなりcabal installしても、ftglがなくて入るはずがないので、まずそっちを何とかする。そもそもWindowsで外部ライブラリの必要なcabalパッケージってどうやってインストールするのかよく知らない。調べたところ、cygwinかMinGWがあればいいらしい。cygwinの場合、cygwinをインストールして、その上にftglをインストールしてそこでcabal installすると結構簡単にインストール自体は成功したのだが、いざ動かそうとすると、cygwinとGHCのMinGWの食い合わせが悪いのか、うまく動かない(CPU100%食って固まる)。なので、cygwinは没となった。
まずはMinGWをインストールする。http://sourceforge.net/projects/mingw/files/ から、Automated MinGW Installerと、MSYS Base Systemをインストールする。msysのインストールの際にmingwの場所を教えて、msysのシェルからMinGWが参照できるようにする。
ftglのインストール
http://sourceforge.net/projects/ftgl/files/ から 2.1.3~rc5をダウンロードして、MinGWにインストールする。./configure; make; make install でいいのだが、./configureが、FreeType2が見つけられないと言ってこけるはずである。
FreeType2のインストール
http://ftp.twaren.net/Unix/NonGNU/freetype/ ここから、一番新しそうなのをダウンロードしてMinGWにインストールする。これはすんなり入ってくれるはず。
再びftglのインストール
freetypeをインストールして再度ftglの./configrueを実行すると、今度はGL libraryが見つからないとかでこける。GL libraryって何ぞ?という話だが、どうやら-lGLのことらしい。MinGWにはあらかじめopenglとGLUのライブラリがインストールされているので、見つけられないのはスクリプトがおかしいから。--with-gl-libでライブラリの場所を指定せよとのエラーメッセージが出てきているが、そもそもMinGWでのopenglのライブラリの名前は-lGLじゃなくて-lopengl32なので指定してもどうやっても見つからない。幸いm4ファイルが同梱されているので、これを書き換えることにする。m4/gl.m4というファイルがあって、これでopenglライブラリのチェックをしているので、これを書き換える。LIBSの-lGLとなっているところを書き換えればいいと思いきや、MinGW環境ではOpenGLに対するAC_LANG_CALLマクロがそもそもうまく動かないので(AC_LANG_CALLはmainの中に指定した関数を呼び出すコードを生成してコンパイルを試みるようだが、MinGWでのopenglの呼び出しはcdeclではないようなので、リンクがどうやっても成功しない)これを成功させることはあきらめ、失敗したときのコードを、無理やり成功したことにさせる。72行目付近:
if test "x$HAVE_GL" = xyes ; then AC_MSG_RESULT([yes]) GL_LIBS=$LIBS else # AC_MSG_RESULT([no]) # AC_MSG_ERROR([GL library could not be found, please specify its location with --with-gl-lib. If this still fails, please contact henryj@paradise.net.nz, include the string FTGL somewhere in the subject line and provide a copy of the config.log file that was left behind.]) AC_MSG_RESULT([yes]) GL_LIBS="-lopengl32" fi
また、同様の理由で-lGLUの検出もこけるので、そこも書き換えておく。116行目付近:
if test "x$HAVE_GLU" = xyes ; then AC_MSG_RESULT([yes]) GL_LIBS="$LIBS" else # AC_MSG_RESULT([no]) # AC_MSG_ERROR([GLU library could not be found, please specify its location with --with-gl-lib. If this still fails, please contact henryj@paradise.net.nz, include the string FTGL somewhere in the subject line and provide a copy of the config.log file that was left behind.]) AC_MSG_RESULT([yes]) GL_LIBS="-lglu32 $GL_LIBS" fi
autotoolsのインストール
m4/gl.m4を書き換えた後に、./autogen.shを実行しなければならないが、このときにautotoolsが必要になる。http://sourceforge.net/projects/mingw/files/ から、MSYS Supplementary ToolsのmsysDTK-1.0.1をインストールする。
msysDTKに入っているautoconfのバージョンは2.56だが、ftglが2.58以上を要求するので、http://ftp.gnu.org/gnu/autoconf/ ここからautoconfの新しいのを持ってきてインストールする。
さらにlibtoolもバージョンが古いといわれるので、1.4.2以上のを http://ftp.gnu.org/gnu/libtool/ ここから拾ってきて入れる。
三度ftglのインストール
./autogen.shを実行すると今度は成功するはずなので、続いて./configureする。これもようやくうまくいくはずである。make; make installでftglのインストール完了。
FTGL(Haskellのほう)のインストール
ようやくFTGLパッケージをインストールできる準備がととのったので、インストールする。
cabal install FTGL --user --extra-include-dirs=/usr/local/include --extra-lib-dirs=/usr/local/lib
http://blog.mestan.fr/2009/09/24/3d-text-rendering-in-haskell-with-ftgl/ ここにいいサンプルがあるので、実行してみる。すると、大量のリンクエラーメッセージが出るはずである。FTGLが使っているfreetypeとlibstdc++の関数がどうやらリンクできていないらしい。ghcのコマンドラインに--optl-lstdc++とかやってもうまくコンパイルできない(順序が大切?)。きっとftglかFTGLかのどちらかを書き換えないといけないのだろう。個人的に書き換えやすいFTGLのほうを書き換えることにする。
http://hackage.haskell.org/package/FTGL ここからアーカイブを取ってきて、FTGL.cabalの最後のほうのを
Library Build-Depends: OpenGL, base Exposed-Modules: Graphics.Rendering.FTGL extra-libraries: ftgl
から
Library Build-Depends: OpenGL, base Exposed-Modules: Graphics.Rendering.FTGL extra-libraries: ftgl stdc++ freetype
に変える。それから、
$ cabal configure --user --extra-include-dirs=/usr/local/include --extra-lib-dirs=/usr/local/lib $ cabal build $ cabal install --user --extra-include-dirs=/usr/local/include --extra-lib-dirs=/usr/local/lib
で、インストール。
テスト
再度先ほどのサンプルプログラムをコンパイルすると今度はリンクに成功する。実行するとglut32.dllがないといわれるので、http://jp.dll-download-system.com/dlls-download-g-/glut32.dll.html この辺から適当に拾ってきて同じディレクトリに入れる。
実行してみると、、、
動いたー
utf8-stringのencodeStringすれば日本語も出たー