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にデフォルトでインストールされていますし、WindowsMacなどでもPythonが入っていれば動作させることができます。wafは80KB程度の単一のPythonスクリプトで、BSDライセンスなので、プロジェクトに含めて配布することが容易です。wafを同梱して配布することによりAutotoolsなどでは起こりがちなバージョン問題が起こりません(Autotoolsではconfigureで配布するとは思いますが)。configureスクリプトのラッパがあるので、これを用いると、Linux文化での標準的なソフトウェアのインストール方法である configure; make; make install にも簡単に対応することができます。

  • 高速に動作する

configureが非常に高速に行われます。ccacheのようなオブジェクトキャッシュ、並列コンパイルの標準サポートにより、コンパイルも高速です。

  • 多くの言語の組み込みサポート

C、C++、D、JavaOCaml、などのプログラミング言語コンパイル、依存解析、実行がサポートされています。新しい言語のサポートも比較的容易です。

  • 出力が分かりやすい

出力がカラーで、失敗すると赤く表示されるので、コンパイル結果がわかりやすいです。また、コンパイルすべきファイルがいくつ有って、現在何個目のファイルをコンパイルしているかが表示されるので、コンパイルの進捗状況がわかりやすいです。コンパイルの進捗をプログレスバーで表示する機能もあって、見ていて楽しいです。

wafの機能

などが標準でサポートされています。一通り揃っていると思います。ビルドフェーズを追加することも容易です。

インストール

wafをインストールするのは以下の理由から推奨されません。

  • adminが要る・めんどい
  • バージョン問題
  • wafを同梱しない理由がない
  • Windowsにはインストールできない

wafスクリプトをプロジェクトに含めてしまうのが一般的です。

http://code.google.com/p/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++プログラムのコンパイル

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を是非使ってみたいという方は、下記のリンクを参照されると良いでしょう。