イントロダクション

Copyright © 2010-2011 Thomas Nagy

この本のコピーは非商用目的で再配布できます。 ライセンスは by-nc-nd license に従います。

ビルドシステムについて一言

ソフトウェアの複雑化に伴い、ソフトウェア作成のプロセスもさらに複雑になってきている。 今日のソフトウェアは様々な言語、コンパイラ、多くの分散した入力データファイルを必要とする。

ソフトウェアは今やソフトウェアのビルドプロセスを表現するのに用いられており、それは簡単なスクリプト(シェルスクリプトやMakefile)やコンパイラ(CMakeやQMake)、完全なアプリケーション(SCons, Maven, Waf)の形態である。 ビルドシステムという用語は、アプリケーションのビルドに用いられるツールの設計、として使われる。

Wafのフレームワーク

ビルドシステムはビルドするソフトウェアに関していくつかの想定をし、異なる言語やプロジェクトをビルドする際にそれらの想定は典型的に限定される。 例えば、AntはMakeよりもJavaのプロジェクトにおいては適しているが、単純なC言語のプロジェクトの管理に関してはMakeよりも制限されている。 プログラミング言語は一貫して進化しており、エンドユーザーにとって完全なビルドシステムを作ることは不可能である。

Wafフレームワークは伝統的なビルドシステムと比べて幾分異なっており、 特定の言語のサポートを提供しない。 ソフトウェアプロジェクトで遭遇する主なユースケースのサポートに焦点をおいている。 本質的に拡張性を強調した、ビルドシステムの使用に適したコンポーネントのライブラリである。 しかしながら、デフォルトのディストリビューションが様々なプログラミング言語(C言語やD言語, Ocaml, Javaなど)やツールへのプラグインを含んでおり、柔軟性を失った製品ではない。 新たな拡張を作ることが標準的かつ推奨されているプラクティスである。

この本の目的

この本の目的は実践を通してWafを使い、Waf拡張を記述し、Wafの内部構造を概観していくことで、Wafビルドシステムの使い方を明らかにすることである。 一般的なビルドシステムについては扱わないが、第二の目的は少数ではあるが、新しいテクニックやパターンをいくつかの例を通して示すことである。

章立ては難易度順に並べられており、WafとPythonの基本的な使い方から始まり、徐々により難しいトピックに掘り下げていくため、章立て順に読んでいくことを推奨する。 また、本書を読む前にWafのディストリビューションにある例 examples を見ることから始めるのもよいだろう。

2. ダウンロードおよびインストール

2.1. Wafファイルの入手

Wafプロジェクト Google Code から入手できる。 現行のWafは cPython の2.3から3.1、もしくは Jython の2.5以上を必要とする。

2.1.1. Wafバイナリのダウンロードおよび使い方

WafのバイナリはPythonのスクリプトで、その以外のインストールは必要はない。 書き込み可能なフォルダから実行できる。 必要ならば waf と名前を変更するとよい。

$ wget http://waf.googlecode.com/files/waf-1.6.10
$ mv waf-1.6.10 waf
$ python waf --version
waf 1.6.10 (54dc13ba5f51bfe2ae277451ec5ac1d0a91c7aaf)

waf ファイルのライブラリは圧縮されたバイナリストリームとしてファイル内に存在する。 実行されると、ライブラリはカレントディレクトリの隠しフォルダに展開される。 もしフォルダが削除されると、実行時に再度作成される。 この仕組みにより、異なるバージョンのWafを同じフォルダで実行できる。

$ ls -ld .waf*
.waf-1.6.10-2c924e3f453eb715218b9cc852291170

備考: バイナリファイルは自前でビルドしインストールされたcPythonでは利用できないかもしれない。 bzip2 による圧縮サポートが必要である。

2.1.2. ソースコードからのWafのビルド

WafのビルドにはPythonインタプリタのバージョンが2.6から3.1の範囲であることが要求される。 ソースコードはPython2.3, 2.4および2.5をサポートするように処理される。

$ wget http://waf.googlecode.com/files/waf-1.6.10.tar.bz2
$ tar xjvf waf-1.6.10.tar.bz2
$ cd waf-1.6.10
$ python waf-light
Configuring the project
'build' finished successfully (0.001s)
Checking for program python              : /usr/bin/python
Checking for python version              : (2, 6, 5, 'final', 0)
'configure' finished successfully (0.176s)
Waf: Entering directory `/waf-1.6.10/build'
[1/1] create_waf:  -> waf
Waf: Leaving directory `/waf-1.6.10/build'
'build' finished successfully (2.050s)

古いインタプリタについては、bzip2の替わりにgzipの圧縮で waf をビルドすることができる。

$ python waf-light --zip-type=gz

waflib/extras フォルダに存在するファイルはテストの段階にあるWafツールの拡張だ。 これらの拡張は --tools スイッチによってWafバイナリに追加される。

$ python waf-light --tools=compat15,swig,doxygen

compat15 は以前のWafのバージョンとの互換性を提供するツールだ。 削除するには --prelude スイッチを変えることによって初期化を変更する必要がある。

$ python waf-light --make-waf --prelude='' --tools=swig

最後に、外部ツールをインポートし、初期化時に読み込む方法を示す。 aba.py はカレントディレクトリに存在すると仮定する。

def foo():
        from waflib.Context import WAFVERSION
        print("This is Waf %s" % WAFVERSION)

次のように実行することで、実行時に foo をインポートして実行する独自のWafファイルが作られる。

$ python waf-light --make-waf --tools=compat15,$PWD/aba.py
   --prelude=$'\tfrom waflib.extras import aba\n\taba.foo()'
$ ./waf --help
This is Waf 1.6.10
[...]

extras に追加される外部ファイルは --tools スイッチに絶対パスで指定する。 それらのファイルはPythonファイルである必要はないが、Wafモジュールに存在する関数やクラスを変更する初期化コードの追加が典型的な用途だ。 build system kitにはWafから派生した独自のビルドシステムを作る様々な例がある。

2.2. Wafファイルの使い方

2.2.1. パーミッションとエイリアス

WafスクリプトはPythonのスクリプトなので、通常 python を呼ぶことで実行される。

$ python waf

Unix系のシステムでは、実行権限を付与することで毎回 python を呼ぶ必要はなく、便利である。

$ chmod 755 waf
$ ./waf --version
waf 1.6.10 (54dc13ba5f51bfe2ae277451ec5ac1d0a91c7aaf)

コマンドラインインタプリタがエイリアスをサポートするならば、次のようにエイリアスを設定することを推奨する。

$ alias waf=$PWD/waf
$ waf --version
waf 1.6.10 (54dc13ba5f51bfe2ae277451ec5ac1d0a91c7aaf)

もしくは、実行パスにWafバイナリの位置を追加することもできる。

$ export PATH=$PWD:$PATH
$ waf --version
waf 1.6.10 (54dc13ba5f51bfe2ae277451ec5ac1d0a91c7aaf)

本書の次のセクションでは、 waf で直接コマンドが呼べるように、エイリアスもしくは実行形式へのパスが設定されているものとする。

2.2.2. ローカルのwaflibフォルダ

バイナリファイルから自動的にWafのライブラリがアンパックされるが、ライブラリを可視フォルダに入れておく必要があることがある。 例えば、 waf-light はライブラリファイルを含まないので、 waf を作る際に waflib ディレクトリが使われる。

次のダイアグラムで waflib ディレクトリを探索する過程を示す。

waflibの探索

2.2.3. 移植性に関する懸念

デフォルトでは、推奨されるインタプリタはcPythonであるが、ユーザーの利便性のために、Jythonインタプリタのバージョン2.5のコピーをWafの実行形式と一緒に再配布することができる。

備考: waf, jython2.5.jar およびソースコードを含むプロジェクトはほぼどこでも使うことができる

注意: waf スクリプトはキャッシュファイルをアンパックするため、書込み可能なフォルダに配置されなくてはならない

3. プロジェクトとコマンド

waf スクリプトはソフトウェアプロジェクトのビルドを意味し、単独で使われたときにはあまり役に立たない。 この章ではWafプロジェクトのセットアップに必要なもの、そして waf スクリプトの使い方について述べる。

3.1. wafコマンド

WafプロジェクトはWafが使うことができる関数と変数を含むPythonスクリプトである wscript という名前のファイルを使う。 waf commands という名前の特別な関数をコマンドラインで使うことができる。

3.1.1. wafコマンドの宣言

wafコマンドは本当にシンプルな関数で他の関数を呼び出すような任意のPythonコードを実行できる。 これらのコマンドは一つのパラメータを入力としてとり、次の例のように、特に値を返す必要はない:

#! /usr/bin/env python
# encoding: utf-8

def 1 hello(ctx 2):
    print('hello world')
1 waf コマンド hello
2 スクリプト間でのデータの共有に使われるWafコンテキスト

そして、これがコマンドラインから waf に関数helloを呼び出させる方法だ:

$ waf hello
hello world
'hello' finished successfully (0.001s)

3.1.2. wafコマンドの連鎖

複数のコマンドを同一の wscript ファイルで宣言するとができる:

def ping(ctx):
    print(' ping! %d' % id(ctx))

def pong(ctx):
    print(' pong! %d' % id(ctx))

そして実行を連鎖させることができる:

$ waf ping pong ping ping
 ping! 140704847272272
'ping' finished successfully (0.001s)
 pong! 140704847271376
'pong' finished successfully (0.001s)
 ping! 140704847272336
'ping' finished successfully (0.001s)
 ping! 140704847272528
'ping' finished successfully (0.001s)

備考: コンテキストパラメータはそれぞれの実行されるコマンドのための新しいオブジェクトだ。また、クラスも異なる: configureのためのConfigureContext、ビルドのためのBuildContext、オプションのためのOptionContext、他のコマンドのためのContext。

3.1.3. 複数のスクリプトとフォルダの使用

Wafプロジェクトは最上位のディレクトリ階層に wscript を含まなくてはならないが、中身を複数のサブプロジェクトファイルに分割することができる。 ここでこのコンセプトを小さなプロジェクトで示す:

$ tree
|-- src
|   `-- wscript
`-- wscript

最上位のディレクトリ階層の wscript は同じコマンドをコンテキストオブジェクトの recurse という名前のメソッドを呼び出すことで、サブプロジェクトの wscript から呼び出す:

def ping(ctx):
        print('→ ping from ' + ctx.path.abspath())
        ctx.recurse('src')

そしてこれが src/wscript の内容だ

def ping(ctx):
        print('→ ping from ' + ctx.path.abspath())

実行すると結果が得られる:

$ cd /tmp/execution_recurse

$ waf ping
→ ping from /tmp/execution_recurse
→ ping from /tmp/execution_recurse/src
'ping' finished successfully (0.002s)

$ cd src

$ waf ping
→ ping from /tmp/execution_recurse/src
'ping' finished successfully (0.001s)

備考: メソッド recurse 、そして、アトリビュート path はすべてのWafコンテキストクラスから利用できる

3.2. Wafプロジェクトの定義

3.2.1. プロジェクトのconfigure (configure コマンド)

Wafは wscript を含むいかなるファルダからも呼び出すことができるが、通常単一のエントリーポイントを持つことはよい考えだ。 その上、整合的な振舞いを保証するために、同一のインポートの再定義と関数の再定義をすべてのwscriptファイルにも保存する。 次のコンセプトはWafプロジェクトの構造を考える上で助けになる:

  1. プロジェクトディレクトリ: パッケージ化され他の開発者やエンドユーザーに再配布されるソースファイルを含むディレクトリ

  2. ビルドディレクトリ: プロジェクトから生成されたファイルを含むディレクトリ(コンフィギュレーションセット、ビルドファイル、ログなど)

  3. システムファイル: プロジェクトに属さないファイルやフォルダ(オペレーティングシステムファイルなど)

configure という名前の既に定義されたコマンドはこれらのフォルダに関する情報を集めて保存するために使われる。 ここで前のセクションの例を次の最上位層のwscriptファイルで拡張する:

top = '.' 1
out = 'build_directory' 2

def configure(ctx): 3
        print('→ configuring the project in ' + ctx.path.abspath())

def ping(ctx):
        print('→ ping from ' + ctx.path.abspath())
        ctx.recurse('src')
1 プロジェクトディレクトリを表現する文字列。一般に、topは . に設定され、最上位にwscriptを追加できないいくつかのプロリエタリなプロジェクトを除いて、topは ../.. または /checkout/perforce/project のような他のフォルダにも設定できる。
2 ビルドディレクトリを表現する文字列。一般に、ビルドディレクトリが /tmp/build のような絶対パスに設定されているようないくつかのプロプリエタリなロジェクトを除いて build に設定される。安全にビルドディレクトリを削除できることが重要で、 ... に設定してはならない。
3 configure 関数は configure コマンドによって呼び出される。

スクリプト src/wscript は変更なし:

def ping(ctx):
        print('→ ping from ' + ctx.path.abspath())

実行結果は次のようになる:

$ cd /tmp/execution_configure 1
$ tree
|-- src
|   `-- wscript
`-- wscript

$ waf configure 2
→ configuring the project in /tmp/execution_configure
'configure' finished successfully (0.021s)

$ tree -a
|-- build_directory/ 3
|   |-- c4che/ 4
|   |   |-- build.config.py 5
|   |   `-- _cache.py 6
|   `-- config.log 7
|--.lock-wafbuild 8
|-- src
|   `-- wscript
`-- wscript

$ waf ping
→ ping from /tmp/execution_configure
→ ping from /tmp/execution_configure/src
'ping' finished successfully (0.001s)

$ cd src
$ waf ping 9
→ ping from /tmp/execution_configure
→ ping from /tmp/execution_configure/src
'ping' finished successfully (0.001s)
1 プロジェクトのconfigureを行うため、最上位のプロジェクトファイルを含むディレクトリに移動
2 waf configure を呼び出すことで実行される
3 ビルドディレクトリが作られた
4 コンフィギュレーションデータは c4che/ に保存される
5 コマンドラインオプションと使われる環境変数は build.config.py に保存される
6 ユーザーのコンフィギュレーションセットは _cache.py に保存される
7 コンフィギュレーションログ(コンフィギュレーション中に生成された出力の複製)
8 関連のあるプロジェクトファイルとビルドディレクトリを指し示す隠しファイル
9 サブフォルダから waf を呼出すとconfigureに使用したのと同一のwscriptファイルからコマンドを実行する

備考: waf configure は常にwscriptファイルを含むディレクトリから呼び出される

3.2.2. 生成されたファイルの削除(distclean コマンド)

コマンド distclean はビルドディレクトリとコンフィギュレーションで生成されたロックファイルを削除するために提供されている。 前のセクションでの例:

$ waf configure
→ configuring the project in /tmp/execution_configure
'configure' finished successfully (0.001s)

$ tree -a
|-- build_directory/
|   |-- c4che/
|   |   |-- build.config.py
|   |   `-- _cache.py
|   `-- config.log
|--.lock-wafbuild
`-- wscript

$ waf distclean 1
'distclean' finished successfully (0.001s)

$ tree 2
|-- src
|   `-- wscript
`-- wscript
1 distclean コマンドの定義は暗黙的(wscriptファイルで宣言されない)
2 ツリーは元の状態に戻される: ビルドディレクトリもロックファイルもない

distclean の振舞はごく一般的で、対応する関数はwscriptで定義される必要はない。 振舞を変更するには次の例を参照:

top = '.'
out = 'build_directory'

def configure(ctx):
        print('→ configuring the project')

def distclean(ctx):
        print(' Not cleaning anything!')

実行する:

$ waf distclean
 Not cleaning anything!
'distclean' finished successfully (0.000s)

3.2.3. プロジェクトソースのパッケージ化(dist コマンド)

dist コマンドはプロジェクトのアーカイブを生成するために提供されている。 前掲のスクリプトを使って:

top = '.'
out = 'build_directory'

def configure(ctx):
        print('→ configuring the project in ' + ctx.path.abspath())

dist コマンドを実行:

$ cd /tmp/execution_dist

$ waf configure
→ configuring the project in /tmp/execution_dist
'configure' finished successfully (0.005s)

$ waf dist
New archive created: noname-1.0.tar.bz2 (sha='a4543bb438456b56d6c89a6695f17e6cb69061f5')
'dist' finished successfully (0.035s)

デフォルトでは, プロジェクト名とバージョンはそれぞれ noname1.0 にセットされる。 これらを変更するには、トップレベルのプロジェクトファイルで2つの変数を追加的に与える必要がある:

APPNAME = 'webe'
VERSION = '2.0'

top = '.'
out = 'build_directory'

def configure(ctx):
        print('→ configuring the project in ' + ctx.path.abspath())

プロジェクトのconfigureは一度行われたため、もう一度configureを行う必要はない:

$ waf dist
New archive created: webe-2.0.tar.bz2 (sha='7ccc338e2ff99b46d97e5301793824e5941dd2be')
'dist' finished successfully (0.006s)

スクリプトで dist 関数を追加することでアーカイブを変更するために他にもパラメータを与えることができる;

def dist(ctx):
        ctx.base_name = 'foo_2.0' 1
        ctx.algo      = 'zip' 2
        ctx.excl      = ' **/.waf-1* **/*~ **/*.pyc **/*.swp **/.lock-w*' 3
        ctx.files     = ctx.path.ant_glob('**/wscript') 4
1 アーカイブ名は APPNAMEVERSION から計算されるのではなく直接与えられる。
2 デフォルトの圧縮フォーマットは tar.bz2 。他の有効なフォーマットは ziptar.gz
3 ファイルを探すために使われる ctx.path.ant_glob() に与えるための除外パターン
4 アーカイブに追加するファイルはWafのノードオブジェクトとして与えることができる(そのため excl は無視される)

3.2.4. コマンドラインオプションの定義(options コマンド)

Wafスクリプトはさまざまなデフォルトのコマンドラインオプションを提供し、 waf --help を実行することで使い方を確認することができる:

$ waf --help
waf [command] [options]

Main commands (example: ./waf build -j4)
  build    : executes the build
  clean    : cleans the project
  configure: configures the project
  dist     : makes a tarball for redistributing the sources
  distcheck: checks if the project compiles (tarball from 'dist')
  distclean: removes the build directory
  install  : installs the targets on the system
  list     : lists the targets to execute
  step     : executes tasks in a step-by-step fashion, for debugging
  uninstall: removes the targets installed

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 (2)
  -k, --keep            keep running happily even if errors are found
  -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)

  configure options:
    -o OUT, --out=OUT   build dir for the project
    -t TOP, --top=TOP   src dir for the project
    --prefix=PREFIX     installation prefix [default: '/usr/local/']
    --download          try to download the tools if missing

  build and install options:
    -p, --progress      -p: progress bar; -pp: ide output
    --targets=TARGETS   task generators, e.g. "target1,target2"

  step options:
    --files=FILES       files to process, by regexp, e.g. "*/main.c,*/test/main.o"

  install/uninstall options:
    --destdir=DESTDIR   installation root [default: '']
    -f, --force         force file installation

コマンドラインオプションへのアクセスはどのコマンドからでも可能だ。 ここに値 prefix にアクセスする方法を示す:

top = '.'
out = 'build_directory'

def configure(ctx):
        print('→ prefix is ' + ctx.options.prefix)

実行すると、次の結果が観測されるだろう:

$ waf configure
→ prefix is /usr/local/
'configure' finished successfully (0.001s)

プロジェクトのコマンドラインオプションを定義するには、特別なコマンド名 options をユーザースクリプトで定義する。 このコマンドは他のコマンドが実行される前に一度呼び出される。

top = '.'
out = 'build_directory'

def options(ctx):
        ctx.add_option('--foo', action='store', default=False, help='Silly test')

def configure(ctx):
        print('→ the value of foo is %r' % ctx.options.foo)

実行すると、次の結果が観測されるだろう:

$ waf configure --foo=test
→ the value of foo is 'test'
'configure' finished successfully (0.001s)

オプションのためのコマンドコンテキストはoptparseの機能へアクセスするためのショートカットだ。 optparseの詳細についてはPython documentationを参照。

3.3. build コマンド

3.3.1. Building targets (build コマンド)

build コマンドはターゲットのビルドに使われる。 ここで新しいプロジェクトを /tmp/execution_build/ に作り、空のファイル foo.txt を作って別のファイル bar.txt にコピーするためのスクリプトを追加する:

top = '.'
out = 'build_directory'

def configure(ctx):
    pass

def build(ctx):
    ctx(rule='touch ${TGT}', target='foo.txt')
    ctx(rule='cp ${SRC} ${TGT}', source='foo.txt', target='bar.txt')

waf build を直接呼ぶとエラーになる:

$ cd /tmp/execution_build/

$ waf build
The project was not configured: run "waf configure" first!

ビルドはソースファイルを探す場所および作成されたファイルをアウトプットする場所を知るためにconfigureされたフォルダを必要とする。 再びトライしよう:

$ waf configure build
'configure' finished successfully (0.007s)
Waf: Entering directory `/tmp/execution_build/build_directory'
[1/2] foo.txt:  -> build_directory/foo.txt 1
[2/2] bar.txt: build_directory/foo.txt -> build_directory/bar.txt
Waf: Leaving directory `/tmp/examples/execution_build/build_directory'
'build' finished successfully (0.041s)

$ tree -a
|-- build_directory/
|   |-- bar.txt 2
|   |-- c4che/
|   |   |-- build.config.py
|   |   `-- _cache.py
|   |-- foo.txt
|   |-- config.log
|   `-- .wafpickle 3
|--.lock-wafbuild
`-- wscript

$ waf build
Waf: Entering directory `/tmp/execution_build/build_directory'
Waf: Leaving directory `/tmp/execution_build/build_directory'
'build' finished successfully (0.008s) 4
1 bar.txtfoo.txt の後に作成されなくてはならないことをビルドから演繹される
2 ターゲットはビルドディレクトリに作成される
3 ピックル化されたファイルはターゲットに関する情報を格納するために使われる
4 ターゲットが最新の状態にあるため、もう一度作成される必要はない

コマンド waf build は通常非常に頻繁に実行されるため、暗黙に呼び出すショートカットが提供されている:

$ waf
Waf: Entering directory `/tmp/execution_build/build_directory'
Waf: Leaving directory `/tmp/execution_build/build_directory'

3.3.2. ターゲットのクリーン (clean コマンド)

clean コマンドはファイルとビルドで生成されたターゲットに関する情報を削除するために使われる。 wscriptの関数 build を使うのでwscriptに clean という名前の関数を追加する必要はない。

クリーンの後、最新の状態であったとしてもターゲットはもう一度作られる。

$ waf clean build -v
'clean' finished successfully (0.003s)
Waf: Entering directory `/tmp/execution_build/build_directory' 1
[1/2] foo.txt:  -> build_directory/foo.txt 2
14:58:34 runner 'touch foo.txt' 3
[2/2] bar.txt: build_directory/foo.txt -> build_directory/bar.txt
14:58:34 runner 'cp foo.txt bar.txt'
Waf: Leaving directory `/tmp/execution_build/build_directory'
'build' finished successfully (0.040s)
1 デファルトですべてのコマンドはビルドディレクトリから実行される
2 foo.txt に関する情報が失われたのでリビルドされる
3 -v フラグを使うと、実行されるコマンドラインは表示される

3.3.3. さらなるbuildコマンド

次のすべてのコマンドはwscriptファイル中の同一の関数 build を使う:

  1. build: ソースコードを処理してオブジェクトファイルを生成する

  2. clean: buildで生成されたオブジェクトファイルを削除する(distcleanとは異なり、configurationは削除しない)

  3. install: 生成されたすべてのオブジェクトファイルをチェックし、システムにコピーする(プログラム、ライブラリ、データファイルなど)

  4. uninstall: ビルドディレクトリ中のファイルには触れずにシステムからオブジェクトファイルを削除し, インストールを元に戻す

  5. list: ビルドセクション中のタスクジェネレータを列挙(waf --targets=nameで使うため)

  6. step: デバッグのために特定のファイルを強制的にリビルドする

アトリビュートcmd は実行されるコマンドの名前を保持する:

top = '.'
out = 'build_directory'

def configure(ctx):
        print(ctx.cmd)

def build(ctx):
        if ctx.cmd == 'clean':
                print('cleaning!')
        else:
                print(ctx.cmd)

実行結果の出力は次のようになる:

$ waf configure clean build
Setting top to : /tmp/execution_cmd
Setting out to : /tmp/execution_cmd/build_directory
configure
'configure' finished successfully (0.002s)
cleaning!
'clean' finished successfully (0.002s)
Waf: Entering directory `/tmp/execution_cmd/build_directory'
build
Waf: Leaving directory `/tmp/execution_cmd/build_directory'
'build' finished successfully (0.001s)

buildコマンドの利用方法については次章で詳しく述べる。

4. プロジェクトのconfigure

configuration コマンドはプロジェクトが要求するものを満しているかチェックし、その情報を格納するために使われる。 buildコマンドのような他のコマンドで使われるパラメータが格納される。

4.1. 永続データの使用

4.1.1. ビルドとのデータの共有

コンフィギュレーションコンテキストはビルドフェーズにおいて再利用することができるデータを格納するために使われる。 次の例から始めよう:

top = '.'
out = 'build'

def options(ctx):
        ctx.add_option('--foo', action='store', default=False, help='Silly test')

def configure(ctx):
        ctx.env.FOO = ctx.options.foo 1
        ctx.find_program('touch', var='TOUCH') 2

def build(bld):
        print(bld.env.TOUCH)
        print(bld.env.FOO) 3
        bld(rule='${TOUCH} ${TGT}', target='foo.txt') 4
1 env 変数(dictに似た構造)にオプション foo を格納
2 プログラム touch を探して ctx.env.TOUCH に格納するためのコンフィギュレーションルーチン
[find_program は探索中にOSの環境と同じ変数を使うことができる、たとえば CC=gcc waf configure]
3 configureでセットされた ctx.env.FOO の値を表示
4 変数 ${TOUCH}ctx.env.TOUCH に対応

実行結果:

$ waf distclean configure build --foo=abcd -v
'distclean' finished successfully (0.005s)
Checking for program touch               : /usr/bin/touch 1
'configure' finished successfully (0.007s)
Waf: Entering directory `/tmp/configuration_build/build'
/usr/bin/touch 2
abcd
[1/1] foo.txt:  -> build/foo.txt
10:56:41 runner '/usr/bin/touch foo.txt' 3
Waf: Leaving directory `/tmp/configuration_build/build'
'build' finished successfully (0.021s)
1 コンフィギュレーションテスト find_program のアウトプット
2 TOUCH の値
3 ターゲット foo.txt を作成するためのコマンドライン

変数 ctx.envコンフィギュレーションセット と呼ばれ、 ConfigSet クラスのインスタンスだ。 このクラスはPythonのdictをラップし、シリアライズ処理をする。 そのため、(関数やクラスではない)単純な値に対してのみ使われる 値はPythonに似たフォーマットでビルドディレクトリに格納される:

$ tree
build/
|-- foo.txt
|-- c4che
|   |-- build.config.py
|   `-- _cache.py
`-- config.log

$ cat build/c4che/_cache.py
FOO = 'abcd'
PREFIX = '/usr/local'
TOUCH = '/usr/bin/touch'

備考: ctx.env への値の読み書きはconfigureおよびbuildコマンドにおいて可能であるが、コンフィギュレーションフェーズにおいてのみ、値はファイルに格納される。

4.1.2. コンフィギュレーションセットの使い方

ここでコンフィギュレーションセットの使い方に関するより多くの例を提供する。 オブジェクト ctx.env はその中身にアクセスするための便利なメソッドを提供している:

top = '.'
out = 'build'

def configure(ctx):
        ctx.env['CFLAGS'] = ['-g'] 1
        ctx.env.CFLAGS = ['-g'] 2
        ctx.env.append_value('CXXFLAGS', ['-O2', '-g']) 3
        ctx.env.append_unique('CFLAGS', ['-g', '-O2'])
        ctx.env.prepend_value('CFLAGS', ['-O3']) 4

        print(type(ctx.env))
        print(ctx.env)
        print(ctx.env.FOO)
1 キーに基づいたアクセス; リストを格納
2 アトリビュートに基づいたアクセス(2つの形態は等価)
3 それぞれの要素をリスト ctx.env.CXXFLAGS に追加。リストであることを想定
4 先頭に値を挿入。 prepend_unique のようなメソッドは存在しないので注意

実行すると次のアウトプットが生成される:

$ waf configure
<class 'waflib.ConfigSet.ConfigSet'> 1
'CFLAGS' ['-O3', '-g', '-O2'] 2
'CXXFLAGS' ['-O2', '-g']
'PREFIX' '/usr/local'
[] 3

$ cat build/c4che/_cache.py 4
CFLAGS = ['-O3', '-g', '-O2']
CXXFLAGS = ['-O2', '-g']
PREFIX = '/usr/local'
1 オブジェクト conf.envwaflib/ConfigSet.py で定義されているConfigSetクラスのインスタンス
2 変更後の conf.env の内容
3 キーが未定義の場合、これはリストであることが想定されている(上記の append_value で使われている)
4 オブジェクト conf.env はデフォルトでこのファイルに格納される

コピーとシリアライズのAPIも提供されている:

top = '.'
out = 'build'

def configure(ctx):
        ctx.env.FOO = 'TEST'

        env_copy = ctx.env.derive() 1

        node = ctx.path.make_node('test.txt') 2
        env_copy.store(node.abspath()) 3

        from waflib.ConfigSet import ConfigSet
        env2 = ConfigSet() 4
        env2.load(node.abspath()) 5

        print(node.read()) 6
1 ctx.env のコピーを生成 - これは浅いコピー
2 ファイル test.txt を表すノードオブジェクトを生成するために ctx.path を使う
3 test.txtenv_copy の内容を格納
4 空の新たなConfigSetオブジェクトを生成
5 test.txt から値を読み込む
6 test.txt の内容を表示

実行すると次のアウトプットが得られる:

$ waf distclean configure
'distclean' finished successfully (0.005s)
FOO = 'TEST'
PREFIX = '/usr/local'
'configure' finished successfully (0.006s)

4.2. configureのユーティリティ

4.2.1. コンフィギュレーションメソッド

以前に見た ctx.find_program メソッドはコンフィギュレーションメソッドの例だ。 ここで他の例も示す:

top = '.'
out = 'build'

def configure(ctx):
        ctx.find_program('touch', var='TOUCH')
        ctx.check_waf_version(mini='1.6.10')
        ctx.find_file('fstab', ['/opt', '/etc'])

コンテキストクラス waflib.Configure.ConfigurationContext によってこれらのメソッドは提供されるが、 API documentation には掲載されていない。 モジュール性の理由により、これらは単純な関数として定義され動的に束縛される:

top = '.'
out = 'build'

from waflib.Configure import conf 1

@conf 2
def hi(ctx):
        print('→ hello, world!')

# hi = conf(hi) 3

def configure(ctx):
        ctx.hi() 4
1 デコレータ conf をインポート
2 メソッド hi をコンフィギュレーションコンテキストクラスにバインドするためにデコレータを使う。 実際には、コンフィギュレーションメソッドはconfigureフェーズでのみ使われる。
3 デコレータは単純なPythonの関数。 Python2.3は @ の構文をサポートしないため、関数は関数の宣言の後に呼ばなくてはならない
4 コンフィギュレーションコンテキストクラスにバインドしたメソッドを使う

実行すると次のアウトプットが得られる:

$ waf configure
→ hello, world!
'configure' finished successfully (0.005s)

4.2.2. Wafツールのロードと使い方

効率性のため、少数のメソッドのみがWafのコアに存在する。 ほとんどのコンフィギュレーションメソッドは Wafツール と呼ばれる拡張によってロードされる。 主要なツールはフォルダ waflib/Tools にあり、 waflib/extras にテストフェーズでのツールがある。 しかし、Wafツールはファイルシステムのどこからでも使うことができる。

ここでコマンドラインオプションから ctx.env.DANG に値をセットする dang.py という名前の非常に単純なWafツールのデモをおこなう:

#! /usr/bin/env python
# encoding: utf-8

print('→ loading the dang tool')

from waflib.Configure import conf

def options(opt): 1
        opt.add_option('--dang', action='store', default='', dest='dang')

@conf
def read_dang(ctx): 2
        ctx.start_msg('Checking for the variable DANG')
        if ctx.options.dang:
                ctx.env.DANG = ctx.options.dang 3
                ctx.end_msg(ctx.env.DANG)
        else:
                ctx.end_msg('DANG is not set')

def configure(ctx): 4
        ctx.read_dang()
1 コマンドラインオプションを提供
2 関数 read_dang をctx.read_dang()を下で呼ぶための新たなコンフィギュレーションメソッドにバインド
3 現在のコマンドラインオプションから永続的な値をセットする
4 ビルドコンテキストのインスタンスをパラメータとして受け付ける configure という名前のコマンドを提供する

ツールを読み込むために、configureの中でメソッド load を使わなくてはならない:

top = '.'
out = 'build'

def options(ctx):
    ctx.load('dang', tooldir='.') 1

def configure(ctx):
    ctx.load('dang', tooldir='.') 2

def build(ctx):
    print(ctx.env.DANG) 3
1 dang.py で定義されたオプションをロード
2 ツールdang.pyを読み込む。デフォルトでは、loadはツールで定義されている configure を呼び出す
3 configure中に ctx.env.DANG の値を変更する

実行すると、次のアウトプットになる:

$ waf configure --dang=hello
→ loading the dang tool
Checking for DANG                        : hello 1
'configure' finished successfully (0.006s)

$ waf
→ loading the dang tool 2
Waf: Entering directory `/tmp/configuration_dang/build'
hello
Waf: Leaving directory `/tmp/configuration_dang/build'
'build' finished successfully (0.004s)
1 最初にツールはPythonモジュールとしてインポートされ、メソッド configureload から呼び出される
2 configure中にロードされたツールはビルドフェーズでロードされる

4.2.3. 複数のconfigure

conf.env オブジェクトはWafツールもしくはユーザーが提供するコンフィギュレーション関数によってアクセスされ修正される、configureの重要なポイントだ。 Wafツールはビルドスクリプトのために特別な構造を強制しないので、ツールはデフォルトオブジェクトの内容の修正を行うだけだ。 ユーザースクリプトは複数の env オブジェクトをconfigfureで提供することができ、前後で特定の値をセットすることができる:

def configure(ctx):
        env = ctx.env 1
        ctx.setenv('debug') 2
        ctx.env.CC = 'gcc' 3
        ctx.load('gcc')

        ctx.setenv('release', env) 4
        ctx.load('msvc')
        ctx.env.CFLAGS = ['/O2']

        print ctx.all_envs['debug'] 5
1 conf.env への参照を保存
2 コピーして conf.env を置き換える
3 conf.env を変更
4 再びコピーして conf.env を最初の値に置き換える
5 名前でコンフィギュレーションセットを呼び出す

4.3. 例外処理

4.3.1. コンフィギュレーション例外の発生と補足

configureのヘルパーはconfオブジェクトが提供するメソッドで、例えば conf.find_program メソッドのように、パラメータを探すのを助ける。

top = '.'
out = 'build'

def configure(ctx):
        ctx.find_program('some_app')

テストが正常に完了しない場合、 waflib.Errors.ConfigurationError 型の例外が発生する。 これはオペレーティングシステムの環境で何かが欠けている場合や、特定の条件が満されない場合によく起きる。 例えば:

$ waf
Checking for program some_app         : not found
 error: The program some_app could not be found

これらの例外は conf.fatal を使って手動で発生させることができる:

top = '.'
out = 'build'

def configure(ctx):
        ctx.fatal("I'm sorry Dave, I'm afraid I can't do that")

これは同じようなエラーを表示する:

$ waf configure
 error: I'm sorry Dave, I'm afraid I can't do that
$ echo $?
1

次はコンフィギュレーション例外の補足の仕方だ:

top = '.'
out = 'build'

def configure(ctx):
        try:
                ctx.find_program('some_app')
        except ctx.errors.ConfigurationError: 1
                self.to_log('some_app was not found (ignoring)') 2
1 利便性のために、モジュール waflib.Errorsctx.errors にバインドされている
2 ログファイルに情報を追加

実行結果は次のようになる:

$ waf configure
Checking for program some_app            : not found
'configure' finished successfully (0.029s) 1

$ cat build/config.log 2
# project  configured on Tue Jul 13 19:15:04 2010 by
# waf 1.6.10 (abi 98, python 20605f0 on linux2)
# using /home/waf/bin/waf configure
#
Checking for program some_app
not found
find program=['some_app'] paths=['/usr/local/bin', '/usr/bin'] var=None -> ''
from /tmp/configuration_exception: The program ['some_app'] could not be found
some_app was not found (ignoring) 3
1 configureがエラーなしで完了
2 configureの実行に関する有用な情報を含んだログファイル
3 追加したログのエントリ

手動でエラーを補足するのは不便なため、すべての @conf メソッドは mandatory という名前のパラメータを受け付け、コンフィギュレーションエラーを抑制する。 前のコードスニペットは次と等価だ:

top = '.'
out = 'build'

def configure(ctx):
        ctx.find_program('some_app', mandatory=False)

一般的なルールとして、クライアントは決して終了コードや戻値を信頼してはならず、コンフィギュレーション例外を補足しなくてはならない。 ツールは常にコンフィギュレーションエラーを発生させることでエラーを表示し、クライアントに例外を処理する機会を与える。

4.3.2. トランザクション

configureで呼ばれるWafツールは conf.env の内容を意図的に使用、変更することができる。 これらの変更は複雑で追跡やアンドゥができなくなる。 幸運なことに、コンフィギュレーション例外によってロジックを簡略化し、前の状態に簡単に戻ることができる。 次の例では一度に複数のツールを使うためのトランザクションの使い方を示す:

top = '.'
out = 'build'

def configure(ctx):
        for compiler in ('gcc', 'msvc'):
                try:
                        ctx.env.stash()
                        ctx.load(compiler)
                except ctx.errors.ConfigurationError:
                        ctx.env.revert()
                else:
                        break
        else:
                ctx.fatal('Could not find a compiler')

stash の複数の呼出しを作ることができるが、コピーは浅く、(リストなどの)複雑なオブジェクトの変更は永続的だ。 そのため、次のコードはconfigureのアンチパターンだ:

def configure(ctx):
        ctx.env.CFLAGS += ['-O2']

代わりに常にこのメソッドを使うべきだ:

def configure(ctx):
    ctx.env.append_value('CFLAGS', '-O2')

5. ビルド

ここではビルドターゲットを処理するのに用られるビルドフェーズに関して詳細を提供する。

5.1. エッセンシャルなビルドの概念

5.1.1. ビルドの順序と依存性

ビルドプロセスの部分をなす、いくつかの概念を説明するため、ここでは新たな例を使う。 ファイル foo.txtbar.txtwscript のコピーによって作られ、ファイル foobar.txt は生成されたファイルの結合によって作られる。 ここに要約する:
[この例はデモ用であるが、実際、ファイルのコピーを避けるためのベストプラクティスと考えられる]

cp: wscript -> foo.txt
cp: wscript -> bar.txt
cat: foo.txt, bar.txt -> foobar.txt

3行のそれぞれの行は実行されるコマンドを表す。 cp コマンドがいかなる順序、もしくは並列に実行されようが、cat コマンドは他のすべてのコマンドの実行後に実行される。 ビルド順序 に対する制約は次で表現される 非循環有向グラフ:

同一のビルドのタスク表現

入力ファイルである wscript が変更されると、出力ファイルの foo.txt はもう一度作られる。 ファイル foo.txt は ファイル wscript に依存しているといえる。 ファイルの依存性 も非循環有向グラフで表現される:

単純なビルドにおける依存関係

プロジェクトのビルドは、これらの制約を考慮したスケジュールによるコマンドの実行から構成される。 ビルド順序を保ちならが並列に実行し、依存関係を用いて不要なコマンドの実行がスキップされると、ビルドは高速化される。

Wafでは、コマンドは task objects で表現される。 依存性はタスククラスによって使われ、それはファイルに基づくものか、特別な制約を課す抽象である。

5.1.2. 直接的なタスク宣言

ここでは、ビルドセクションで直接的にタスクを宣言することで、前のセクションのビルドを表現している:

def configure(ctx):
        pass

from waflib.Task import Task
class cp(Task): 1
        def run(self): 2
                return self.exec_command('cp %s %s' % (
                                self.inputs[0].abspath(), 3
                                self.outputs[0].abspath()
                        )
                )

class cat(Task):
        def run(self):
                return self.exec_command('cat %s %s > %s' % (
                                self.inputs[0].abspath(),
                                self.inputs[1].abspath(),
                                self.outputs[0].abspath()
                        )
                )

def build(ctx):

        cp_1 = cp(env=ctx.env) 4
        cp_1.set_inputs(ctx.path.find_resource('wscript')) 5
        cp_1.set_outputs(ctx.path.find_or_declare('foo.txt'))
        ctx.add_to_group(cp_1) 6

        cp_2 = cp(env=ctx.env)
        cp_2.set_inputs(ctx.path.find_resource('wscript'))
        cp_2.set_outputs(ctx.path.find_or_declare('bar.txt'))
        ctx.add_to_group(cp_2)

        cat_1 = cat(env=ctx.env)
        cat_1.set_inputs(cp_1.outputs + cp_2.outputs)
        cat_1.set_outputs(ctx.path.find_or_declare('foobar.txt'))
        ctx.add_to_group(cat_1)
1 タスククラスの宣言
2 Wafのタスクはターゲットを生成するための run メソッドをもつ
3 waflib.Task.Task のインスタンスは使用するファイルを表す入力および出力ファイルのオブジェクト(ノードオブジェクト)をもつ
4 手動による新たなタスクインスタンスの作成
5 waflib.Node.Node オブジェクトとして表現された入力および出力ファイルの設定
6 実行のためのビルドコンテキストにタスクを追加(しかしながら、すぐには実行されない)

実行の出力結果は次のようになる:

$ waf clean build 1
'clean' finished successfully (0.003s)
Waf: Entering directory `/tmp/build_manual_tasks/build'
[1/3] cp: wscript -> build/foo.txt
[2/3] cp: wscript -> build/bar.txt
[3/3] cat: build/foo.txt build/bar.txt -> build/foobar.txt
Waf: Leaving directory `/tmp/build_manual_tasks/build'
'build' finished successfully (0.047s)

$ waf build 2
Waf: Entering directory `/tmp/build_manual_tasks/build'
Waf: Leaving directory `/tmp/build_manual_tasks/build'
'build' finished successfully (0.007s)

$ echo " " >> wscript 3

$ waf build
Waf: Entering directory `/tmp/build_manual_tasks/build'
[1/3] cp: wscript -> build/foo.txt 4
[2/3] cp: wscript -> build/bar.txt
[3/3] cat: build/foo.txt build/bar.txt -> build/foobar.txt
Waf: Leaving directory `/tmp/build_manual_tasks/build'
'build' finished successfully (0.043s)
1 このタスクは clean コマンドでは実行されない
2 ビルドは再度生成されるのを避けるために、ファイルの痕跡を保つ
3 ソースファイルのひつとを変更する
4 依存関係グラフによりリビルドされる

覚えておくべきこと:

  1. 実行順序はタスクインスタンスの入力と出力のセットから 自動計算 される

  2. 依存関係はノードオブジェクト(ビルドとビルドの間にファイルのハッシュが保存され、比較される)から 自動計算 (ファイルは必要なときにリビルド)される

  3. 順序制約のないタスクはデフォルトで並列に実行される

5.1.3. タスクジェネレータによるタスクのカプセル化

直接タスクを宣言するのはtediousでスクリプトが長くなる。 機能的には、次は前の例と同等だ:

def configure(ctx):
        pass

def build(ctx):
        ctx(rule='cp ${SRC} ${TGT}', source='wscript', target='foo.txt')
        ctx(rule='cp ${SRC} ${TGT}', source='wscript', target='bar.txt')
        ctx(rule='cat ${SRC} > ${TGT}', source='foo.txt bar.txt', target='foobar.txt')

ctx(…) コールはクラス waflib.TaskGen.task_gen へのショートカットで、このクラスのインスタンスは タスクジェネレータオブジェクト と呼ばれる. タスクジェネレータは遅延コンテナで、必要なときにタスクとタスククラスの生成のみを行う:

def configure(ctx):
        pass

def build(ctx):
        tg = ctx(rule='touch ${TGT}', target='foo')
        print(type(tg))
        print(tg.tasks)
        tg.post()
        print(tg.tasks)
        print(type(tg.tasks[0]))

出力結果:

waf configure build
Setting top to   : /tmp/build_lazy_tg
Setting out to   : /tmp/build_lazy_tg/build
'configure' finished successfully (0.204s)
Waf: Entering directory `/tmp/build_lazy_tg/build'
<class 'waflib.TaskGen.task_gen'> 1
[] 2
[{task: foo  -> foo}] 3
<class 'waflib.Task.foo'> 4
[1/1] foo:  -> build/foo
Waf: Leaving directory `/tmp/build_lazy_tg/build'
'build' finished successfully (0.023s)
1 タスクジェネレータ型
2 生成されたタスクはリスト tasks (0..nタスクが追加され得る)に保存される
3 タスクはメソッドpost()の呼出後に作られる - 通常自動で内部的に呼び出される
4 ターゲット foo のために新しいタスククラスが作られる

5.1.4. ビルドフェーズの外観

ビルドプロセスの高レベルの外観は次のダイアグラムで表現される:

ビルドフェーズの外観

備考: すべてのタスクはすべての実行に先立って作られる。新しいタスクはビルド開始後に作られるが、依存関係は低レベルのAPIによって設定しなくてはならない。

5.2. さらなるビルドオプション

いかなるオペレーションもタスクの一部として実行することができ、いくつかのシナリオは典型的で、便利な関数を提供することは理にかなっている。

5.2.1. ビルド前後における特別なルーチンの実行

ユーザー関数はbuildコマンド(コールバック)中の2つの主要な時に実行されるようバインドすることができる:

  1. ビルド開始の直前(bld.add_pre_fun)

  2. ビルド成功完了直後(bld.add_post_fun)

これはビルド終了後のテストを実行する方法だ:

top = '.'
out = 'build'

def options(ctx):
        ctx.add_option('--exe', action='store_true', default=False,
                help='execute the program after it is built')

def configure(ctx):
        pass

def pre(ctx): 1
        print('before the build is started')

def post(ctx):
        print('after the build is complete')
        if ctx.cmd == 'install': 2
                if ctx.options.exe: 3
                        ctx.exec_command('/sbin/ldconfig') 4

def build(ctx):
        ctx.add_pre_fun(pre) 5
        ctx.add_post_fun(post)
1 コールバックはビルドコンテキストをユニークなパラメータ ctx として引数にとる
2 コマンドタイプにアクセス
3 コマンドラインオプションにアクセス
4 共通のシナリオとして、ファイルのインストール後にldconfigqを呼び出す
5 後の実行のために実行する関数のスケジューリング。Pythonの関数はオブジェクトでもある

上記を実行すると、次の出力が得られる:

$ waf distclean configure build install --exe
'distclean' finished successfully (0.005s)
'configure' finished successfully (0.011s)
Waf: Entering directory `/tmp/build_pre_post/build'
before the build is started 1
Waf: Leaving directory `/tmp/build_pre_post/build'
after the build is complete 2
'build' finished successfully (0.004s)
Waf: Entering directory `/tmp/build_pre_post/build'
before the build is started
Waf: Leaving directory `/tmp/build_pre_post/build'
after the build is complete
/sbin/ldconfig: Can't create temporary cache file /etc/ld.so.cache~: Permission denied 3
'install' finished successfully (15.730s)
1 bld.add_pre_fun によってバインドされた関数の出力
2 bld.add_post_fun によってバインドされた関数の出力
3 インストール時の実行

5.2.2. ファイルのインストール

3つのビルドコンテキストメソッドはビルド中もしくは後に作られたファイルのインストールのために提供される:

  1. install_files: フォルダにいくつかのファイルをインストール

  2. install_as: 異なる名前でターゲットをインストール

  3. symlink_as: サポートされるプラットフォームにシンボリックリンクを作成

def build(bld):
        bld.install_files('${PREFIX}/include', ['a1.h', 'a2.h']) 1
        bld.install_as('${PREFIX}/dir/bar.png', 'foo.png') 2
        bld.symlink_as('${PREFIX}/lib/libfoo.so.1', 'libfoo.so.1.2.3') 3

        env_foo = bld.env.derive()
        env_foo.PREFIX = '/opt'
        bld.install_as('${PREFIX}/dir/test.png', 'foo.png', env=env_foo) 4

        start_dir = bld.path.find_dir('src/bar')
        bld.install_files('${PREFIX}/share', ['foo/a1.h'],
                cwd=start_dir, relative_trick=True) 5

        bld.install_files('${PREFIX}/share', start_dir.ant_glob('**/*.png'), 6
                cwd=start_dir, relative_trick=True)
1 ターゲットに様々なファイルをインストール
2 1つのファイルをインストールし、名前を変更
3 シンボリックリンクの作成
4 コンフィギュレーションセットの上書き(env は3つのメソッドinstall_files、install_as、symlink_asでoptionalだ)
5 スクリプトからsrc/bar/foo/a1.hとして見えるファイルを ${PREFIX}/share/foo/a1.h にインストール
6 再帰的にpngファイルのインストールし、src/bar/のフォルダ構造を保つ

備考: メソッド install_filesinstall_assymlink_aswaf installwaf uninstall の間でのみ何かをなし、これらは他のbuildコマンドには影響を与えない

5.2.3. タスクジェネレータの列挙と特定のタスクジェネレータの強制的実行

list コマンドは宣言されたタスクジェネレータを表示するのに使われる:

top = '.'
out = 'build'

def configure(ctx):
        pass

def build(ctx):
        ctx(source='wscript', target='foo.txt', rule='cp ${SRC} ${TGT}')
        ctx(target='bar.txt', rule='touch ${TGT}', name='bar')

デフォルトでは、タスクジェネレータの名前は target アトリビュートから決まる:

$ waf configure list
'configure' finished successfully (0.005s)
foo.txt
bar
'list' finished successfully (0.008s)

名前の値の主な用途は --targets オプションによって強制的に部分的なビルドを行うことだ。 次を比較:

$ waf clean build
'clean' finished successfully (0.003s)
Waf: Entering directory `/tmp/build_list/build'
[1/2] foo.txt: wscript -> build/foo.txt
[2/2] bar:  -> build/bar.txt
Waf: Leaving directory `/tmp/build_list/build'
'build' finished successfully (0.028s)

$ waf clean build --targets=foo.txt
'clean' finished successfully (0.003s)
Waf: Entering directory `/tmp/build_list/build'
[1/1] foo.txt: wscript -> build/foo.txt
Waf: Leaving directory `/tmp/build_list/build'
'build' finished successfully (0.022s)

5.2.4. デバッグのためのステップバイステップの実行 ( step コマンド)

step は特定のタスクの実行に使われ、終了コードとエラーメッセージを返す。 これは特にデバッグに有用だ:

waf step --files=test_shlib.c,test_staticlib.c
Waf: Entering directory `/tmp/demos/c/build'
c: shlib/test_shlib.c -> build/shlib/test_shlib.c.1.o
 -> 0
cshlib: build/shlib/test_shlib.c.1.o -> build/shlib/libmy_shared_lib.so
 -> 0
c: stlib/test_staticlib.c -> build/stlib/test_staticlib.c.1.o
 -> 0
cstlib: build/stlib/test_staticlib.c.1.o -> build/stlib/libmy_static_lib.a
 -> 0
Waf: Leaving directory `/tmp/demos/c/build'
'step' finished successfully (0.201s)

この場合 .so ファイルもリビルドされる。 コマンドライン引数filesはカンマ区切りの正規表現のリストとして解釈されるため、次は異なる出力を生成する:

$ waf step --files=test_shlib.c$
Waf: Entering directory `/tmp/demos/c/build'
c: shlib/test_shlib.c -> build/shlib/test_shlib.c.1.o
 -> 0
Waf: Leaving directory `/tmp/demos/c/build'
'step' finished successfully (0.083s)

最後に、実行されるタスクはソースかターゲットかを特定するために、in: または out: を前につけることができる:

$ waf step --files=out:build/shlib/test_shlib.c.1.o
Waf: Entering directory `/tmp/demos/c/build'
cc: shlib/test_shlib.c -> build/shlib/test_shlib.c.1.o
 -> 0
Waf: Leaving directory `/tmp/demos/c/build'
'step' finished successfully (0.091s)

備考: waf step を使うと、たとえいくつかのタスクが0以外のリターンコードを返しても、すべてのタスクはシーケンシャルに実行される

6. ノードオブジェクト

ノードオブジェクトはファイルやフォルダを表し、ファイルシステムの操作を簡単にする。 この章では使用方法の概観を示す。

6.1. ノードクラスのデザイン

6.1.1. ノードツリー

Wafのノードは waflib.Node.Node クラスを継承しファイルシステムを表現するツリー構造を提供する:

  1. parent: 親ノード

  2. children: フォルダの中身 - ノードがファイルならば空

実際には、ファイルシステムツリーへのリファレンスは、Wafコマンドからのアクセスのためにコンテキストクラスにバインドされている。 ここで例示する:

top = '.'
out = 'build'

def configure(ctx):
    pass

def dosomething(ctx):
    print(ctx.path.abspath()) 1
    print(ctx.root.abspath()) 2
    print("ctx.path contents %r" % ctx.path.children)
    print("ctx.path parent   %r" % ctx.path.parent.abspath())
    print("ctx.root parent   %r" % ctx.root.parent)
1 ctx.path は実行される wscript ファイルへのパスを表す
2 ctx.root はファイルシステムもしくはドライブレターを含むフォルダ(win32システム)のルート

実行結果の出力は次のようになる:

$ waf configure dosomething
Setting top to    : /tmp/node_tree
Setting out to    : /tmp/node_tree/build
'configure' finished successfully (0.007s)
/tmp/node_tree 1
/
ctx.path contents {'wscript': /tmp/node_tree/wscript} 2
ctx.path parent   '/tmp' 3
ctx.root parent   None 4
'dosomething' finished successfully (0.001s)
1 しばしば絶対パスが使われる
2 フォルダの中身は名前をノードオブジェクトに結びつけるdict children に保存される
3 それぞれのノードはその parent ノードへの参照を保持する
4 ルートノードは parent をもたない

備考: ノードとファイルシステムの要素には厳格な対応関係がある: 1つのノードは正確に1つのファイルか1つのフォルダを表し、1つのノードのみがあるファイルやフォルダを表現できる

6.1.2. ノードキャッシング

デフォルトでは必要なノードのみが作られる:

def configure(ctx):
    pass

def dosomething(ctx):
    print(ctx.root.children)

ファイルシステムのルートはひとつのノードのみをもつが、現実のファイルシステムのルートは /tmp だけではなくそれ以外のフォルダを含む:

$ waf configure dosomething
Setting top to   : /tmp/nodes_cache
Setting out to   : /tmp/nodes_cache/build
'configure' finished successfully (0.086s)
{'tmp': /tmp}
'dosomething' finished successfully (0.001s)

$ ls /
bin boot dev etc home tmp usr var

これは特にいくつかのノードは使われる前にファイルシステムから読み込まれるか作成されなくてはならないことを意味する。

6.2. 一般的な用途

6.2.1. ノードの探索と作成

ノードは手動もしくはファイルシステムから読み込まれることで作られる。 この目的のために3つのメソッドが用意されている:

def configure(ctx):
        pass

def dosomething(ctx):
        print(ctx.path.find_node('wscript')) 1

        nd1 = ctx.path.make_node('foo.txt') 2
        print(nd1)

        nd2 = ctx.path.search('foo.txt') 3
        print(nd2)

        nd3 = ctx.path.search('bar.txt') 4
        print(nd3)

        nd2.write('some text') 5
        print(nd2.read())

        print(ctx.path.listdir())
1 ファイルシステムを読み込むことでノードを探索
2 ノードを探索し、存在しなければ作成する
3 ノードを探索するが、作成はしない
4 存在しないファイルの探索
5 ノードで指し示されるファイルへ書き出し、作成もしくは上書きする

出力結果は次のようになる:

$ waf distclean configure dosomething
'distclean' finished successfully (0.005s)
Setting top to    : /tmp/nodes_search
Setting out to    : /tmp/nodes_search/build
'configure' finished successfully (0.006s)
wscript
foo.txt
foo.txt
None
some text
['.lock-wafbuild', 'foo.txt', 'build', 'wscript', '.git']

備考: APIドキュメンテーション でさらに多くのメソッドを見つけることができる

警告: これらのメソッドは並行アクセスに対して安全ではない。ノードクラスメソッドはスレッドセーフではない。

6.2.2. ファイルとフォルダの列挙

メソッド ant_glob はファイルや再帰的なフォルダの列挙に使われる;

top = '.'
out = 'build'

def configure(ctx):
        pass

def dosomething(ctx):
        print(ctx.path.ant_glob('wsc*')) 1
        print(ctx.path.ant_glob('w?cr?p?')) 2
        print(ctx.root.ant_glob('usr/include/**/zlib*', 3 dir=False, src=True)) 4
        print(ctx.path.ant_glob(['**/*py', '**/*p'], excl=['**/default*'])) 5
1 メソッド ant_glob はビルドコンテキストではなく、ノードオブジェクトに対して呼ばれ、デフォルトではファイルのみを返す
2 *? のようなワイルドカードを含むパターンだが、Antパターンで、正規表現ではない
3 シンボル ** は再帰を有効にする。複雑なフォルダ階層には多くの時間がかかるため、使用には注意が必要だ
4 再帰が有効になったとしても、デフォルトではファイルのみが返る。フォルダを返すためには、'dir=True'を使う
5 パターンは文字列のリストかスペース区切の値だ。除外するパターンは waflib.Node.exclude_regs で定義される。

実行結果の出力は次のようになる:

$ waf configure dosomething
Setting top to    : /tmp/nodes_ant_glob
Setting out to    : /tmp/nodes_ant_glob/build
'configure' finished successfully (0.006s)
[/tmp/nodes_ant_glob/wscript]
[/tmp/nodes_ant_glob/wscript]
[/usr/include/zlib.h]
[/tmp/nodes_ant_glob/build/c4che/build.config.py]

.. は正確に2つのドット文字を表し、親ディレクトリではない。 これは探索が終了することを保証し、同じファイルが何度も列挙されることはない。 次を考慮:

ctx.path.ant_glob('../wscript') 1
ctx.path.parent.ant_glob('wscript') 2
1 無効、このパターンは何も返さない
2 親ディレクトリから ant_glob の呼出し

6.2.3. パスの操作: abspath, path_from

メソッド abspath はノードの絶対パスを取得するために使われる。 次の例では3つのノードが使われる:

top = '.'
out = 'build'

def configure(conf):
        pass

def build(ctx):
        dir = ctx.path 1
        src = ctx.path.find_resource('wscript')
        bld = ctx.path.find_or_declare('out.out')

        print(src.abspath(ctx.env)) 2
        print(bld.abspath(ctx.env))
        print(dir.abspath(ctx.env)) 3
        print(dir.abspath())
1 ディレクトリノード、ソースノード、そしてビルドノード
2 パラメータにコンフィギュレーションセットをとり、ソースノードやビルドノードの絶対パスを計算
3 ディレクトリの絶対パスの計算にはコンフィギュレーションセットを使うこともできる

これは実行のトレースだ:

$ waf distclean configure build
'distclean' finished successfully (0.002s)
'configure' finished successfully (0.005s)
Waf: Entering directory `/tmp/nested/build'
/tmp/nested/wscript 1
/tmp/nested/build/out.out 2
/tmp/nested/build/ 3
/tmp/nested 4
Waf: Leaving directory `/tmp/nested/build'
'build' finished successfully (0.003s)
1 ソースノードへの絶対パス
2 ビルドノードの絶対パスは使用中のvariantに依存する
3 コンフィギュレーションセットが与えられた場合、ディレクトリノードへの絶対パスはvariantを含むビルドディレクトリ表現だ
4 コンフィギュレーションセットが与えられない場合、ディレクトリノードへの絶対パスはソースディレクトリへの絶対パスとなる

備考: relpath_gensrcpath などの他のいくつかのメソッドが提供されている。 APIドキュメントを参照。

6.3. BuildContext特有のメソッド

6.3.1. ソースとビルドノード

wscript ファイルの中で sourcestargets はカレントディレクトリに存在するかのように宣言されるが 、ターゲットファイルはビルドディレクトリに出力される。 この振舞いを有効にするには、top ディレクトリ以下の構造が out ディレクトリ以下に複製されなくてはならない。 例えば、 demos/cprogram フォルダは同等な構造をビルドディレクトリにもつ:

$ cd demos/c
$ tree
.
|-- build
|   |-- c4che
|   |   |-- build.config.py
|   |   `-- _cache.py
|   |-- config.h
|   |-- config.log
|   `-- program
|       |-- main.c.0.o
|       `-- myprogram
|-- program
|   |-- a.h
|   |-- main.c
|   `-- wscript_build
`-- wscript

これをサポートするために、ビルドコンテキストは2つのノードを提供する:

  1. srcnode: 最上位ディレクトリを表すノード

  2. bldnode: ビルドディレクトリを表すノード

ソースノードからビルドノードを得る、およびその逆を行うには、次のメソッドが使える:

  1. Node.get_src()

  2. Node.get_bld()

6.3.2. ビルドフェーズ中のノードの使用

srcnodebldnode を直接使うこともできるが、次の3つのラッパーメソッドがより使い易い。 これらはターゲットを表す文字列を入力として受け付け、単一のノードを返す:

  1. find_dir: ノードを返す。フォルダがシステムに存在しない場合はNoneを返す。

  2. find_resource: ソースディレクトリ次のノード、ビルドディレクトリ次のノード、もしくはそのようなノードが存在しない場合はNoneを返す。もしファイルがビルドディレクトリにない場合、ノードのシグネチャは計算されてキャッシュにいれられる(ファイルの中身のハッシュ).

  3. find_or_declare: ノードを返すかビルドディレクトリに対応するノードを作る。

そのうえ、それらはすべて、ビルドディレクトリに必要なディレクトリ構造を作る find_dir を内部的に使う。 なぜならば、ビルドが開始される前にビルドディレクトリでフォルダが複製され得るため、可能な場所ではすべてそれを使うことが推奨される:

def build(bld):
    p = bld.path.parent.find_dir('src') 1
    p = bld.path.find_dir('../src') 2
1 非推奨、 find_dir を代わりに使うべき
2 パスのセパレータはプラットフォームに応じて自動的に変換される

6.3.3. ノード、タスク、タスクジェネレータ

前の章で見たように、タスクオブジェクトは入力および出力ノードのリストとして表現されるファイルの処理を行う。 タスクジェネレータは通常、文字列で与えられた入力ファイルを処理し、ノードを生成してタスクにバインドする。

ビルドディレクトリは有効、無効にできるため、次のファイルは不正である:
[ファイルのコピーを避けられない場合、ベストプラクティスはファイル名を変更することだ]

def build(bld):
    bld(rule='cp ${SRC} ${TGT}', source='foo.txt', target='foo.txt')

実際に対応するビルドディレクトリに同じ名前でファイルをコピーするには、曖昧さをなくさなくてはならない:

def build(bld):
    bld(
        rule   = 'cp ${SRC} ${TGT}',
        source = bld.path.make_node('foo.txt'),
        target = bld.path.get_bld().make_node('foo.txt')
    )

7. 一歩進んだビルド定義

7.1. コマンドのカスタマイズ

7.1.1. コンテキストの継承

waflib.Context.Context のインスタンスはカスタムコマンドにデフォルトで用いられる。 カスタムコンテキストのオブジェクトを提供するには、コンテキストのサブクラスを作る必要がある。

def configure(ctx):
        print(type(ctx))

def foo(ctx): 1
        print(type(ctx))

def bar(ctx):
        print(type(ctx))

from waflib.Context import Context

class one(Context):
        cmd = 'foo' 2

class two(Context):
        cmd = 'tak' 3
        fun = 'bar'
1 デフォルトのコンンテキストを使ったコマンド
2 foo コマンドのためにコンテキストクラスを構築
3 tak という名前の新しいコマンドを宣言するが、スクリプト内の bar 関数を呼び出す

実行結果の出力は次のようになるだろう。

$ waf configure foo bar tak
Setting top to    : /tmp/advbuild_subclass
Setting out to    : /tmp/advbuild_subclass/build
<class 'waflib.Configure.ConfigurationContext'>
'configure' finished successfully (0.008s)
<class 'wscript.one'>
'foo' finished successfully (0.001s)
<class 'waflib.Context.Context'>
'bar' finished successfully (0.001s)
<class 'wscript.two'>
'tak' finished successfully (0.001s)

カスタムコンテキストの典型的な応用例は ctx.env に読み込まれたコンフィギュレーションデータを使うためにビルドコンテキストのサブクラスを作ることだ。

def configure(ctx):
        ctx.env.FOO = 'some data'

def build(ctx):
        print('build command')

def foo(ctx):
        print(ctx.env.FOO)

from waflib.Build import BuildContext
class one(BuildContext):
        cmd = 'foo'
        fun = 'foo'

出力は次のようになる

$ waf configure foo
Setting top to    : /tmp/advbuild_confdata
Setting out to    : /tmp/advbuild_confdata/build
'configure' finished successfully (0.006s)
Waf: Entering directory `/disk/comp/waf/docs/book/examples/advbuild_confdata/build'
some data
Waf: Leaving directory `/disk/comp/waf/docs/book/examples/advbuild_confdata/build'
'foo' finished successfully (0.004s)

備考: buildコマンドはこのシステムを使っている: waf installwaflib.Build.InstallContext, waf stepwaflib.Build.StepContext, など

7.1.2. コマンドの合成

既存のコマンドと互換性のないコンテキストクラスを再利用するには、それらの互換性のないクラスを コマンドスタック に挿入する。

def configure(ctx):
        pass

def build(ctx):
        pass

def cleanbuild(ctx):
        from waflib import Options
        Options.commands = ['clean', 'build'] + Options.commands

このテクニックはテストケースを書くときに便利だ。 waf test を実行することで、次のスクリプトはプロジェクトのconfigureを行い、ソースファイルをsourceディレクトリに作り、プログラムをビルドし、ソースを変更し、プログラムを再ビルドする。 このケースでは、ヘッダが変更されたため(暗黙的な依存性)、プログラムは再ビルドされる。

def options(ctx):
        ctx.load('compiler_c')

def configure(ctx):
        ctx.load('compiler_c')

def setup(ctx):
        n = ctx.path.make_node('main.c')
        n.write('#include "foo.h"\nint main() {return 0;}\n')

        global v
        m = ctx.path.make_node('foo.h')
        m.write('int k = %d;\n' % v)
        v += 1

def build(ctx):
        ctx.program(source='main.c', target='app')

def test(ctx):
        global v 1
        v = 12

        import Options 2
        lst = ['configure', 'setup', 'build', 'setup', 'build']
        Options.commands = lst + Options.commands
1 異なるコマンド間でデータを共有するためにグローバル変数を使う
2 testコマンドはコマンドを追加するために使われる

次のような出力が見られるだろう。

$ waf test
'test' finished successfully (0.000s)
Setting top to                           : /tmp/advbuild_testcase
Setting out to                           : /tmp/advbuild_testcase/build
Checking for 'gcc' (c compiler)          : ok
'configure' finished successfully (0.092s)
'setup' finished successfully (0.001s)
Waf: Entering directory `/tmp/advbuild_testcase/build'
[1/2] c: main.c -> build/main.c.0.o
[2/2] cprogram: build/main.c.0.o -> build/app
Waf: Leaving directory `/tmp/advbuild_testcase/build'
'build' finished successfully (0.137s)
'setup' finished successfully (0.002s)
Waf: Entering directory `/tmp/advbuild_testcase/build'
[1/2] c: main.c -> build/main.c.0.o
[2/2] cprogram: build/main.c.0.o -> build/app
Waf: Leaving directory `/tmp/advbuild_testcase/build'
'build' finished successfully (0.125s)

7.1.3. Wafツールからのコマンドのバインディング

トップレベルのwscriptが読み込まれると、Pythonのモジュールに変換され、メモリ上に保持される。 コマンドは関数をモジュールに加えることで、動的に追加される。 プロジェクト中のタスクジェネレータの数を数えるWafツールを披露しよう。

top = '.'
out = 'build'

def options(opt):
        opt.load('some_tool', tooldir='.')

def configure(conf):
        pass

Wafはconfigureとビルドのために一度読み込まれる。 たとえ実際にはoptionsを提供しない場合でも、ツールが常に有効であることを確かなものにするために、optionsを読み込まなくてはならない。 wscript ファイルと同階層にある、我々のツール some_tool.py は次のコードを含む。

from waflib import Context

def cnt(ctx):
        """do something"""
        print('just a test')

Context.g_module.__dict__['cnt'] = cnt

実行結果は次のようになる。

$ waf configure cnt
Setting top to   : /tmp/examples/advbuild_cmdtool
Setting out to   : /tmp/advbuild_cmdtool/build
'configure' finished successfully (0.006s)
just a test
'cnt' finished successfully (0.001s)

7.2. ビルド出力のカスタマイズ

7.2.1. 複数のconfigure

環境変数 WAFLOCK はconfigureのロックやデフォルトのビルドディレクトリを指定するのに使われる。 次のプロジェクトを見てみよう。

def configure(conf):
        pass

def build(bld):
        bld(rule='touch ${TGT}', target='foo.txt')

実行すると WAFLOCK は変更される。

$ export WAFLOCK=.lock-wafdebug 1

$ waf
Waf: Entering directory `/tmp/advbuild_waflock/debug'
[1/1] foo.txt:  -> debug//foo.txt 2
Waf: Leaving directory `/tmp/advbuild_waflock/debug'
'build' finished successfully (0.012s)

$ export WAFLOCK=.lock-wafrelease

$ waf distclean configure
'distclean' finished successfully (0.001s)
'configure' finished successfully (0.176s)

$ waf
Waf: Entering directory `/tmp/advbuild_waflock/release' 3
[1/1] foo.txt:  -> release/foo.txt
Waf: Leaving directory `/tmp/advbuild_waflock/release'
'build' finished successfully (0.034s)

$ tree -a
.
|-- .lock-debug 4
|-- .lock-release
|-- debug
|   |-- .wafpickle-7
|   |-- c4che
|   |   |-- build.config.py
|   |   `-- _cache.py
|   |-- config.log
|   `-- foo.txt
|-- release
|   |-- .wafpickle-7
|   |-- c4che
|   |   |-- build.config.py
|   |   `-- _cache.py
|   |-- config.log
|   `-- foo.txt
`-- wscript
1 ロックファイルはプロジェクトのコンフィギュレーションと使われるビルドディレクトリを指す
2 これらのファイルはビルドディレクトリ debug の出力結果
3 release コンフィギュレーションでは異なるロックファイルとビルドディレクトリが使われる
4 プロジェクトディレクトリの中には2つのロックファイルと2つのビルドフォルダが含まれる

ロックファイルはWafファイルの中の変数が変わることにより変更され得る。

from waflib import Options
Options.lockfile = '.lock-wafname'

備考: Wafロックファイルにより参照される出力ディレクトはWafスクリプトで付えられないときのみ、有効になる

7.2.2. 出力ディレクトリの変更

Variant ビルド

前のセクションでは2つの異なるコンフィギュレーションは同じようなビルドに使われていた。 ここでは同一のコンフィギュレーションを継承する、異なる出力ターゲットのフォルダをもつ2つのビルドの作り方を示す。 まずはプロジェクトファイルから始めよう。

def configure(ctx):
        pass

def build(ctx):
        ctx(rule='touch ${TGT}', target=ctx.cmd + '.txt') 1

from waflib.Build import BuildContext
class debug(BuildContext): 2
        cmd = 'debug'
        variant = 'debug' 3
1 呼びだされるコマンドは self.cmd
2 ビルドコンテキストを継承した debug コマンドの作成
3 debug コマンドのターゲットの用フォルダの宣言

このプロジェクトは2つのことなるビルド builddebug を宣言する。 主力結果を検証しよう。

waf configure build debug
Setting top to   : /tmp/advbuild_variant
Setting out to   : /tmp/advbuild_variant/build
'configure' finished successfully (0.007s)
Waf: Entering directory `/tmp/advbuild_variant/build'
[1/1] build.txt:  -> build/build.txt
Waf: Leaving directory `/tmp/advbuild_variant/build'
'build' finished successfully (0.020s)
Waf: Entering directory `/tmp/build_variant/build/debug'
[1/1] debug.txt:  -> build/debug/debug.txt 1
Waf: Leaving directory `/tmp/advbuild_variant/build/debug'
'debug' finished successfully (0.021s)

$ tree
.
|-- build
|   |-- build.txt 2
|   |-- c4che
|   |   |-- build.config.py
|   |   `-- _cache.py
|   |-- config.log
|   `-- debug
|       `-- debug.txt 3
`-- wscript
1 コマンドは build/variant から実行される
2 デフォルトコマンド build はvariantを持たない
3 debug ターゲットはbuildディレクトリの中のvariant
variantに対するコンフィギュレーション

variantはconfigureの段階で作られた異なるコンフィギュレーションセットを必要とする。 ここに例を示す。

def options(opt):
        opt.load('compiler_c')

def configure(conf):
        conf.setenv('debug') 1
        conf.load('compiler_c')
        conf.env.CFLAGS = ['-g'] 2

        conf.setenv('release')
        conf.load('compiler_c')
        conf.env.CFLAGS = ['-O2']

def build(bld):
        if not bld.variant: 3
                bld.fatal('call "waf build_debug" or "waf build_release", and try "waf --help"')
        bld.program(source='main.c', target='app', includes='.') 4

from waflib.Build import BuildContext, CleanContext, \
        InstallContext, UninstallContext

for x in 'debug release'.split():
        for y in (BuildContext, CleanContext, InstallContext, UninstallContext):
                name = y.__name__.replace('Context','').lower()
                class tmp(y): 5
                        cmd = name + '_' + x
                        variant = x
1 conf.env から返され c4che/debug_cache.py に保存される新たなコンフィギュレーションセットを作成
2 コンフィギュレーションセットのいくつかのデータの変更
3 variantがsetであることを確認することで, 通常の buildcleaninstall コマンドを無効にする
4 bld.env は適切なvariantのコンフィギュレーションsetを読み込む(debug の中の場合 debug_cache.py)
5 clean_debuginstall_debug などの新たなコマンドの作成(クラス名は何でもよい)

実行結果の出力は次のようなものになるだろう。

$ waf clean_debug build_debug clean_release build_release
'clean_debug' finished successfully (0.005s)
Waf: Entering directory `/tmp/examples/advbuild_variant_env/build/debug'
[1/2] c: main.c -> build/debug/main.c.0.o
[2/2] cprogram: build/debug/main.c.0.o -> build/debug/app
Waf: Leaving directory `/tmp/examples/advbuild_variant_env/build/debug'
'build_debug' finished successfully (0.051s)
'clean_release' finished successfully (0.003s)
Waf: Entering directory `/tmp/examples/advbuild_variant_env/build/release'
[1/2] c: main.c -> build/release/main.c.0.o
[2/2] cprogram: build/release/main.c.0.o -> build/release/app
Waf: Leaving directory `/tmp/examples/advbuild_variant_env/build/release'
'build_release' finished successfully (0.052s)

8. タスク処理

この章ではビルドフェーズで使われるタスククラスについて記述する。

8.1. タスクの実行

8.1.1. 主要なアクター

ビルドコンテキストはタスクと、並行に実行することができるタスクのリストを返すためにのみ使われる。 スケジューリングはタスク消費者に実行させるタスク生産者に移譲される。 タスク生産者は処理されたタスクやエラーの数などのビルド状態を記録する。

タスクを処理するアクター

消費者の数はプロセッサの数、もしくは -j オプションにより手動で決定される。

$ waf -j3

8.1.2. ビルドグループ

タスク生産者はビルドコンテキストによって返されたタスクのリストをイテレートする。 ひとつリスト中のタスクは消費者スレッドにより並列に実行されるが、あるリスト中のタスクは他のリスト中のタスクより前にすべて処理される。 処理すべきタスクがなくなるとビルドは終了する。

これらのタスクのリストは ビルドグループ と呼ばれ、 ビルドスクリプトからアクセスできる。 例を通してこの振舞いを見ていこう:

def build(ctx):
    for i in range(8):
        ctx(rule='cp ${SRC} ${TGT}', source='wscript', target='wscript_a_%d' % i,
            color='YELLOW', name='tasks a')
        ctx(rule='cp ${SRC} ${TGT}', source='wscript_a_%d' % i, target='wscript_b_%d' % i,
            color='GREEN', name='tasks b')
    for i in range(8)
        ctx(rule='cp ${SRC} ${TGT}', source='wscript', target='wscript_c_%d' % i,
            color='BLUE', name='tasks c')
        ctx(rule='cp ${SRC} ${TGT}', source='wscript_c_%d' % i, target='wscript_d_%d' % i,
            color='PINK', name='tasks d')

それぞれのグリーンのタスクは1つのイエローのタスクの後に実行されなくてはならない。 そしてそれぞれのピンクのタスクは1つのブルーのタスクの後に実行されなくてはならない。 デフォルトでは1つのグループしかないため、並列実行は次と似たものになるだろう:

1つのビルドグループ

ここでもうひとつビルドグループを追加するように例を変更しよう。

def build(ctx):
    for i in range(8):
        ctx(rule='cp ${SRC} ${TGT}', source='wscript', target='wscript_a_%d' % i,
            color='YELLOW', name='tasks a')
        ctx(rule='cp ${SRC} ${TGT}', source='wscript_a_%d' % i, target='wscript_b_%d' % i,
            color='GREEN', name='tasks b')
    ctx.add_group()
    for i in range(8):
        ctx(rule='cp ${SRC} ${TGT}', source='wscript', target='wscript_c_%d' % i,
            color='BLUE', name='tasks c')
        ctx(rule='cp ${SRC} ${TGT}', source='wscript_c_%d' % i, target='wscript_d_%d' % i,
            color='PINK', name='tasks d')

ここで、セパレータは黄色と緑色のタスクのグループと青色と紫色のタスクのグループとの間に表われる:

2つのビルドグループ

タスクとタスクジェネレータは暗黙的に現在のグループに追加される。 グループに名前を与えることで何が何処へ行くのかを簡単に制御できる:

def build(ctx):

    ctx.add_group('group1')
    ctx.add_group('group2')

    for i in range(8):
        ctx.set_group('group1')
        ctx(rule='cp ${SRC} ${TGT}', source='wscript', target='wscript_a_%d' % i,
            color='YELLOW', name='tasks a')
        ctx(rule='cp ${SRC} ${TGT}', source='wscript_a_%d' % i, target='wscript_b_%d' % i,
            color='GREEN', name='tasks b')

        ctx.set_group('group2')
        ctx(rule='cp ${SRC} ${TGT}', source='wscript', target='wscript_c_%d' % i,
            color='BLUE', name='tasks c')
        ctx(rule='cp ${SRC} ${TGT}', source='wscript_c_%d' % i, target='wscript_d_%d' % i,
            color='PINK', name='tasks d')

前の例では、すべてのビルドグループからなる、すべてのタスクジェネレータはビルドが実際に開始する前に処理される。 このデフォルトの挙動は可能な限り正確にタスク数を数えるために提供されている。 ここではビルドグループをチューンする方法を示す:

def build(ctx):
        from waflib.Build import POST_LAZY, POST_BOTH, POST_AT_ONCE
        ctx.post_mode = POST_AT_ONCE 1
        #ctx.post_mode = POST_LAZY 2
        #ctx.post_mode = POST_BOTH 3
1 すべてのタスクジェネレータはビルドが開始される前にタスクを生成する(デフォルトの挙動)
2 グループは順番に処理される: 次のグループのタスクジェネレータが処理される前に前のグループのすべてのタスクは実行される
3 前の2つの挙動の組合せ: 次のグループのタスクによって生成されるタスクジェネレータはタスクを生成することができる

ビルドグループはソースファイルの生成のためのコンパイラをビルドするために使うことができる。

8.1.3. 生産者-消費者システム

ほとんどのPythonインタープリタでは、グローバルインタープリタロックによって一度に1つ以上のCPUでの並列化が行われない。 そのため、1つのタスクプロデューサ上でタスクのスケジューリングを制限することは理にかなっており、スレッドにタスクの実行のみをアクセスさせる。

生産者と消費者間のコミュニケーションは readyout の2つのキューに基づいている。 生産者は ready にタスクを追加し、結果を out から読み込む。 消費者は ready からタスクを取得し、 task.run を実行後、結果を生産者に返すために out に返す。

生産者はタスクをイテレートしてキュー ready にどのタスクを入れるかを決めるために outstanding と名付けられたリストを内部で使う。 処理することのできないタスクは、タスクが他のタスクを待つエンドレスループを避けるためにリスト frozen に一時的に出力される。

次の図ではビルド中にタスク生産者と消費者が演じる関係について示す。

並列実行

8.1.4. タスクの状態とステータス

実行を追跡するために、それぞれのタスクに状態が割当てられている(task.hasrun = state)。 可能な値は次のようになる:

状態 数値 Description

NOT_RUN

0

タスクは未処理

MISSING

1

タスクの出力がない

CRASHED

2

タスクメソッド run が非ゼロを返した

EXCEPTION

3

タスクメソッド run で例外が発生した

SKIPPED

8

タスクはスキップされた(最新の状態だった)

SUCCESS

9

実行は成功した

タスクを実行するか否かを決定するために、生産者はタスクメソッド runnable_status の戻値を使う。 可能な戻値は次のようになる:

コード 説明

ASK_LATER

タスクが実行が終了していない他のタスクに依存している(not ready)

SKIP_ME

タスクは実行される必要はない、最新

RUN_ME

タスクは実行の準備完了

次の図は主要なタスクメソッドとタスクの状態およびステータス間の相互作用を表現した図だ:

タスクの状態

8.2. ビルド順序制約

8.2.1. set_run_afterメソッド

メソッド set_run_after はタスク間の順序制約を宣言するために使われる:

task1.set_run_after(task2)

待機中のタスクは run_after アトリビュートに格納される。 これらのタスクは実行されていない場合にメソッド runnable_status によって使われ、 ASK_LATER ステータスをyieldする。 これは単にビルド順序および前のタスクのうちのひとつが実行された場合にリビルドを強制するためのものだ。

8.2.2. 計算された制約

after/beforeアトリビュート

アトリビュート beforeafter はタスク間の順序制約を宣言するために使われる:

from waflib.Task import TaskBase
class task_test_a(TaskBase):
    before = ['task_test_b']
class task_test_b(TaskBase):
    after  = ['task_test_a']
ext_in/ext_out

順序を強制する他の方法はクラスアトリビュートで抽象的なシンボルのリストを宣言することだ。 この方法ではクラスは明示的には名付けられない、例えば:

from waflib.Task import TaskBase
class task_test_a(TaskBase):
    ext_in  = ['.h']
class task_test_b(TaskBase):
    ext_out = ['.h']

拡張子 ext_inとext_outは、タスクがそのような拡張子のファイルを生成しなくてはならないことを意味するわけではなく、優先順序制約を使うための単なるシンボルだ。

順序抽出

タスクを生産者-消費者システムに投入する前に、入力および出力を持つタスクに対して制約の抽出が行われる。 アトリビュート run_after は待つタスクで初期化される。

タスクのリストに対して呼ばれる2つの関数は:

  1. waflib.Task.set_precedence_constraints: タスククラスのアトリビュートext_in/ext_out/before/afterからビルド順序の抽出

  2. waflib.Task.set_file_constraints: 入力および出力を持つタスクから制約の抽出

8.2.3. 弱い順序制約

多くの実行時間が必要となることがわかっているタスクを最初に実行することでビルド時間が改善される。 制約付きの並列環境おいて最適な実行順序を見つける一般的な問題はJob Shop問題と呼ばれる。

実際にはこの問題はクリティカルパスの問題に帰着できる(近似)。

次の図では異なる独立のタスクでビルドする際のスケジューリングでの遅いタスクが特定され最初に起動される違いを示す:

def build(ctx):
    for x in range(5):
        ctx(rule='sleep 1', color='GREEN', name='short task')
    ctx(rule='sleep 5', color='RED', name='long task')
特別な順序なし

生産に渡される前にグループから再度タスクの順序を決めるために関数を使う。 長いタスクを最初の位置に並べ換えるために関数を置き換える:

from waflib import Task
old = Task.set_file_constraints
def meth(lst):
    lst.sort(cmp=lambda x, y: cmp(x.__class__.__name__, y.__class__.__name__)) 1
    old(lst) 2
Task.set_file_constraints = meth 3
1 長いタスクを最初の位置に設定
2 オリジナルのコードを実行
3 メソッドを置き換える

ここで影響を表現する:

一番遅いタスクを最初に実行

8.3. 依存関係

8.3.1. タスクシグネチャ

waflib.Task.TaskBase の直接のインスタンスは非常に限定されていてファイルの変更を追跡できない。 サブクラスである waflib.Task.Task は、ソースファイルがターゲットファイルの生成に使われる、最も一般的なビルドに必要な特徴を提供する。

依存関係の追跡は タスクシグネチャ と呼ばれる依存関係のハッシュの使用に基づいて行われる。 シグネチャは入力ファイルやコンフィギュレーションセットの値などのさまざまな依存するソースから計算される。

次の図ではどのように waflib.Task.Task インスタンスが処理されるかを示す:

シグネチャ

次のデータはシグネチャの計算に使われる:

  1. 明示的な依存関係: 入力ノードbld.depends_on によって明示的に示された依存関係のセット

  2. 暗黙の依存関係: スキャナメソッド(scan メソッド)によって探索された依存関係

  3. 値: コンパイルフラグなどのコンフィギュレーションセットの値

8.3.2. 明示的な依存関係

入力および出力ノード

タスクオブジェクトは他のタスクに直接依存はしない。 他のタスクは存在するかしないか、そして実行されるかノードである。 むしろ、入力および出力ノードは異なるソースに由来する、それ自身のシグネチャの値を保持する:

  1. ビルドファイルのためのノードは通常ファイルを生成したタスクのシグネチャを継承する

  2. 他のどこかからのノードはファイルの中身(ハッシュ)から計算されたシグネチャを持つ

他のノードに対するグローバルな依存関係

いくつかのファイルは入力に存在しなくても他のファイルに推移的に依存する。 これはビルドコンテキストの add_manual_dependency メソッドによりなされる:

def configure(ctx):
    pass

def build(ctx):
    ctx(rule='cp ${SRC} ${TGT}', source='wscript', target='somecopy')
    ctx.add_manual_dependency(
        ctx.path.find_node('wscript'),
        ctx.path.find_node('testfile'))

somecopy ファイルは wscript もしくは testfile がたとえ一文字でも変更されるとリビルドされる。

$ waf build
Waf: Entering directory `/tmp/tasks_manual_deps/build'
[1/1] somecopy: wscript -> build/somecopy
Waf: Leaving directory `/tmp/tasks_manual_deps/build'
'build' finished successfully (0.034s)

$ waf
Waf: Entering directory `/tmp/tasks_manual_deps/build'
Waf: Leaving directory `/tmp/tasks_manual_deps/build'
'build' finished successfully (0.006s)

$ echo " " >> testfile

$ waf
Waf: Entering directory `/tmp/tasks_manual_deps/build'
[1/1] somecopy: wscript -> build/somecopy
Waf: Leaving directory `/tmp/tasks_manual_deps/build'
'build' finished successfully (0.022s)

8.3.3. 暗黙の依存関係(スキャナメソッド)

いくつかのタスクはビルド開始後に動的に生成することができ、依存関係を前もって知ることはできない。 タスクサブクラスは scan という名前のメソッドを提供することができ、暗黙に追加のノードを得る。 次の例では、 copy タスクはスキャナメソッドを提供し、入力ファイルと同階層で見つかるwscriptファイルに依存する。

import time
from waflib.Task import Task
class copy(Task):

    def run(self):
        return self.exec_command('cp %s %s' % (self.inputs[0].abspath(), self.outputs[0].abspath()))

    def scan(self): 1
        print('→ calling the scanner method')
        node = self.inputs[0].parent.find_resource('wscript')
        return ([node], time.time()) 2

    def runnable_status(self):
        ret = super(copy, self).runnable_status() 3
        bld = self.generator.bld 4
        print('nodes:       %r' % bld.node_deps[self.uid()]) 5
        print('custom data: %r' % bld.raw_deps[self.uid()]) 6
        return ret

def configure(ctx):
    pass

def build(ctx):
    tsk = copy(env=ctx.env) 7
    tsk.set_inputs(ctx.path.find_resource('a.in'))
    tsk.set_outputs(ctx.path.find_or_declare('b.out'))
    ctx.add_to_group(tsk)
1 スキャナメソッド
2 戻り値は依存するノードのリストとシリアライズ可能なカスタムデータのタプル
3 runnable_statusメソッドをオーバーライドしてロギングを追加
4 このタスクに関連付けられたビルドコンテキストへの参照を得る
5 スキャナメソッドによって返されるノードはマップ bld.node_deps に格納される
6 スキャナメソッドによって返されるカスタムデータマップ bld.raw_deps に格納される
7 タスクを手動で作成(タスクジェネレータによるカプセル化については次の章で述べる)
$ waf
→ calling the scanner method 1
nodes:  [/tmp/tasks_scan/wscript]
custom data: 55.51
[1/1] copy: a.in -> build/b.out
'build' finished successfully (0.021s)

$ waf 2
nodes:  [/tmp/tasks_scan/wscript]
custom data: 1280561555.512006
'build' finished successfully (0.005s)

$ echo " " >> wscript 3

$ waf
→ calling the scanner method
nodes:  [/tmp/tasks_scan/wscript]
custom data: 64.31
[1/1] copy: a.in -> build/b.out
'build' finished successfully (0.022s)
1 スキャナメソッドはクリーンビルドで常に呼ばれる
2 返されるデータが検索された場合でも、何も変更がないときにはスキャナメソッドは呼ばれない
3 依存関係が変化すると、スキャナメソッドはもう一度実行される(カスタムデータが変化した)

警告: ビルド順序が正しくない場合、メソッド scan は依存するノードの発見に失敗する(ノードが見つからない)か、シグネチャの計算で例外を投げる(依存するノードのシグネチャが見つからない).

8.3.4. 値

コンパイルフラグのような慣習的なコマンドラインパラメータは 値に対する依存関係 を生成し、より具体的にはコンギュレーションセットの値を生成する。 タスククラスのアトリビュート vars はシグネチャの計算にどの値が使われるかを制御するために使われる。 次の例では、作成されたタスクは出力も入力も持たないノードを持ち、値にのみ依存する。

from waflib.Task import Task
class foo(Task): 1
        vars = ['FLAGS'] 2
        def run(self):
                print('the flags are %r' % self.env.FLAGS) 3

def options(ctx):
        ctx.add_option('--flags', default='-f', dest='flags', type='string')

def build(ctx):
        ctx.env.FLAGS = ctx.options.flags 4
        tsk = foo(env=ctx.env)
        ctx.add_to_group(tsk)

def configure(ctx):
        pass
1 foo という名前のタスククラスを作成
2 self.env.FLAGS が変更されるといつでもタスクインスタンスは実行される
3 デバッグのために値をプリント
4 コマンドラインから値を読む

実行によって次の出力が生成される:

$ waf --flags abcdef
[1/1] foo:
the flags are 'abcdef' 1
'build' finished successfully (0.006s)

$ waf --flags abcdef 2
'build' finished successfully (0.004s)

$ waf --flags abc
[1/1] foo: 3
the flags are 'abc'
'build' finished successfully (0.006s)
1 このタスクは最初の実行で実行される
2 依存関係が変化しないため、タスクは実行されない
3 フラグが変更されたため、タスクは実行される

8.4. タスクのチューニング

8.4.1. クラスアクセス

タスクが次の例のように run_str という名前のアトリビュートを提供するとき:

def configure(ctx):
        ctx.env.COPY      = '/bin/cp'
        ctx.env.COPYFLAGS = ['-f']

def build(ctx):
        from waflib.Task import Task
        class copy(Task):
                run_str = '${COPY} ${COPYFLAGS} ${SRC} ${TGT}'
        print(copy.vars)

        tsk = copy(env=ctx.env)
        tsk.set_inputs(ctx.path.find_resource('wscript'))
        tsk.set_outputs(ctx.path.find_or_declare('b.out'))
        ctx.add_to_group(tsk)

run_str はコマンドラインを表し、 COPYFLAGS のような ${} の中の変数は依存関係に追加される変数を表すことが想定されている。 メタクラスは run_str を処理し、 run メソッド(タスクを実行するために呼ばれる)と vars アトリビュート(存在する変数とマージされた)中の変数を獲得する。 作成された関数は次の出力で表示される:

$ waf --zones=action
13:36:49 action   def f(tsk):
        env = tsk.env
        gen = tsk.generator
        bld = gen.bld
        wd = getattr(tsk, 'cwd', None)
        def to_list(xx):
                if isinstance(xx, str): return [xx]
                return xx
        lst = []
        lst.extend(to_list(env['COPY']))
        lst.extend(to_list(env['COPYFLAGS']))
        lst.extend([a.path_from(bld.bldnode) for a in tsk.inputs])
        lst.extend([a.path_from(bld.bldnode) for a in tsk.outputs])
        lst = [x for x in lst if x]
        return tsk.exec_command(lst, cwd=wd, env=env.env or None)
[1/1] copy: wscript -> build/b.out
['COPY', 'COPYFLAGS']
'build' finished successfully (0.007s)

waflib.Task.TaskBase のすべてのサブクラスはモジュールアトリビュート waflib.Task.classes に格納される。 そのため、 copy タスクは次のようにアクセスできる:

from waflib import Task
cls = Task.classes['copy']

8.4.2. スクリプトレット拡張

run_str はコンフィギュレーションセットの変数を使うが、利便性のためにいくつかの特別なケースが提供されている:

  1. 値が env, gen, bld もしくは tsk で始まる場合、メソッド呼出しは作られる

  2. 値がSRC[n]もしくはTGT[n]で始まる場合、入力/出力ノード n のためのメソッド呼出しは作られる

  3. SRCはビルドディレクトリのルートから見たタスクの入力のリストを表す

  4. TGTはビルドディレクトリのルートから見たタスクの出力のリストを表す

ここにいくつかの例を示す:

${SRC[0].parent.abspath()} 1
${bld.root.abspath()} 2
${tsk.uid()} 3
${CPPPATH_ST:INCPATHS} 4
1 タスクの最初のソースファイルの親フォルダへの絶対パス
2 ファイルシステムのルート
3 タスクのユニークな識別子を表示
4 [env.CPPPATH_ST % x for x in env.INCPATHS] と同等なmapの置換を実行

8.4.3. 直接のクラス変更

常に実行

関数 waflib.Task.always_run はビルドが実行されたときにあるタスクを常に強制的に実行するために使われる。 この関数はメソッド runnable_status が常に RUN_ME を返すように設定する。

def configure(ctx):
    pass

def build(ctx):
    from waflib import Task
    class copy(Task.Task):
        run_str = 'cp ${SRC} ${TGT}'
    copy = waflib.Task.always_run(copy)

    tsk = copy(env=ctx.env)
    tsk.set_inputs(ctx.path.find_resource('wscript'))
    tsk.set_outputs(ctx.path.find_or_declare('b.out'))
    ctx.add_to_group(tsk)

利便性のため、ルールベースのタスクジェネレータは同じ結果を得るのに always アトリビュートを宣言できる:

def build(ctx):
    ctx(
        rule   = 'echo hello',
        always = True
    )
ファイルハッシュ

出力が本当にタスクから生成されたかを検知するために、タスクのシグネチャはタスクノードの出力のシグネチャとして使われる。 その結果、ソースディレクトリで作られたファイルは毎回リビルドされる。 これを避けるためにノードのシグネチャは実際のファイルの中身に一致する。 これはタスクラスの waflib.Task.update_outputs 関数を使うことで強制される。 この関数は出力ノードにハッシュファイルの中身を設定するために、タスククラスの post_runrunnable_status メソッドを置き換える。

利便性のために、ルールベースのタスクジェネレータは update_outputs アトリビュートを宣言することがで同じ結果を得ることができる。

def build(ctx):
    ctx(
        rule           = 'cp ${SRC} ${TGT}',
        source         = 'wcript',
        target         = ctx.path.make_node('wscript2'),
        update_outputs = True
    )

9. タスクジェネレータ

9.1. ルールベースのタスクジェネレータ(Makeに類似)

この章では、簡単なターゲットのビルドのためのルールベースのタスクジェネレータの使い方について説明する。

9.1.1. 宣言と使い方

ルールに基づいたタスクジェネレータは正確に1つのタスクを生成する特定のタスクジェネレータのカテゴリーだ。

次では、プロジェクトファイル wscript からコピーのために cp コマンドを実行し、 foobar を生成するタスクジェネレータを例示する。

top = '.'
out = 'build'

def configure(conf):
        pass

def build(bld):
        bld( 1
                rule   = 'cp ${SRC} ${TGT}', 2
                source = 'wscript', 3
                target = 'foobar.txt', 4
        )
1 新しいタスクジェネレータを初期化する。すべての引数は key=value の形式だ
2 アトリビュート rule は可読な方法で実行するコマンドを表現する(これについては次の章で詳しく触れる).
3 ソースファイル。スペース区切の文字列か文字列のリスト
4 ターゲットファイル。スペース区切の文字列か文字列のリスト

実行すると次のような出力が得られるだろう:

$ waf distclean configure build -v
'distclean' finished successfully (0.000s)
'configure' finished successfully (0.021s)
Waf: Entering directory `/tmp/rules_simple/build'
[1/1] foobar.txt: wscript -> build/foobar.txt 1
10:57:33 runner 'cp ../wscript foobar.txt' 2
Waf: Leaving directory `/tmp/rules_simple/build'
'build' finished successfully (0.016s)

$ tree
.
|-- build
|   |-- c4che
|   |   |-- build.config.py
|   |   `-- _cache.py
|   |-- config.log
|   `-- foobar.txt
`-- wscript

$ waf 3
Waf: Entering directory `/tmp/rules_simple/build'
Waf: Leaving directory `/tmp/rules_simple/build'
'build' finished successfully (0.006s)

$ echo " " >> wscript 4

$ waf
Waf: Entering directory `/tmp/rules_simple/build'
[1/1] foobar.txt: wscript → build/foobar.txt 5
Waf: Leaving directory `/tmp/rules_simple/build'
'build' finished successfully (0.013s)
1 初回の実行でターゲットは正しく作られる
2 コマンドラインは -v オプションをつけた verboseモード でのみ表示される
3 ターゲットは最新なのでタスクは実行されない
4 空白文字を追加し、ソースファイルを変更
5 ソースファイルが変更されたので、ターゲットは再度生成される

ルールへの文字列も依存関係の計算に追加される。 ルールが変更されるとタスクは再度コンパイルされる。

9.1.2. ルール関数

ルールは表現文字列もしくはPython関数として与えられる。 関数は生成されたタスククラスに割当られる:

top = '.'
out = 'build'

def configure(conf):
        pass

def build(bld):
        def run(task): 1
                src = task.inputs[0].abspath() 2
                tgt = task.outputs[0].abspath() 3
                cmd = 'cp %s %s' % (src, tgt)
                print(cmd)
                return task.exec_command(cmd) 4

        bld(
                rule   = run, 5
                source = 'wscript',
                target = 'same.txt',
        )
1 ルール関数はタスクインスタンスをパラメータとしてとる
2 ソースとターゲットは内部でタスクインスタンスにバインドされたノードオブジェクトとして表現
3 コマンドはビルドディレクトリのルートから実行される。 bldpath のようなノードのメソッドはコマンドラインの生成を簡単にする
4 タスククラスはコマンドを実行するsubprocess.Popen(…)のラッパーを保持する
5 文字列表現の代わりに関数を使用

実行のトレースは次と似たものになる:

$ waf distclean configure build
'distclean' finished successfully (0.001s)
'configure' finished successfully (0.001s)
Waf: Entering directory `/tmp/rule_function/out'
[1/1] same.txt: wscript -> out/same.txt
cp /tmp/rule_function/wscript /tmp/rule_function/build/same.txt
Waf: Leaving directory `/tmp/rule_function/out'
'build' finished successfully (0.010s)

ルール関数は成功を意味するためにヌル値(0やNone、False)を返さなくてはならない。 また、アウトプットに対応するファイルを生成しなくてはならない。 ルール関数は内部的にスレッドによって実行されるためスレッドセーフなコード(ノードオブジェクトの探索や作成はできない)を書くことが重要となる。

文字列表現とは異なり、関数は一度に複数のコマンドを実行できる。

9.1.3. シェルの使用

アトリビュート shell を使うとコマンドの実行のためにシステムのシェルを有効にできる。 ルールベースのタスクジェネレータを宣言するときに、いくつかの点を覚えておくとよい:

  1. Wafツールはコマンドの実行にシェルを使わない

  2. ユーザーコマンドとカスタムタスクジェネレータのためにデフォルトでシェルが使われる

  3. シンボル ‘>’ 、 ‘<’ もしくは ‘&’ を含む文字列表現は、シェルを使わずにコマンドを実行するために関数に変換しようとしてもできない

  4. 一般に、クオートの問題(例えば、空白文字を含むパス)を避けるために、シェルを使うのは可能であれば常に避けたほうがよい

  5. シェルは、win32システムでより顕著な、パフォーマンス上のペナルティをうみだす。

例:

top = '.'
out = 'build'

def configure(conf):
        pass

def build(bld):
        bld(rule='cp ${SRC} ${TGT}', source='wscript', target='f1.txt', shell=False)
        bld(rule='cp ${SRC} ${TGT}', source='wscript', target='f2.txt', shell=True)

実行すると、結果は次と似たものになるだろう:

$ waf distclean configure build --zones=runner,action
'distclean' finished successfully (0.004s)
'configure' finished successfully (0.001s)
Waf: Entering directory `/tmp/rule/out'
23:11:23 action 1
def f(task):
        env = task.env
        wd = getattr(task, 'cwd', None)
        def to_list(xx):
                if isinstance(xx, str): return [xx]
                return xx
        lst = []
        lst.extend(['cp'])
        lst.extend([a.srcpath(env) for a in task.inputs])
        lst.extend([a.bldpath(env) for a in task.outputs])
        lst = [x for x in lst if x]
        return task.exec_command(lst, cwd=wd)

23:11:23 action
def f(task):
        env = task.env
        wd = getattr(task, 'cwd', None)
        p = env.get_flat
        cmd = ''' cp %s %s ''' % (" ".join([a.srcpath(env) for a in task.inputs]), 2
                " ".join([a.bldpath(env) for a in task.outputs]))
        return task.exec_command(cmd, cwd=wd)

[1/2] f1.txt: wscript -> out/f1.txt
23:11:23 runner system command -> ['cp', '../wscript', 'f1.txt'] 3
[2/2] f2.txt: wscript -> out/f2.txt
23:11:23 runner system command ->  cp ../wscript f2.txt
Waf: Leaving directory `/tmp/rule/out'
'build' finished successfully (0.017s)
1 文字列表現は関数に変換される(シェルを使わない).
2 シェルによって実行されるコマンド。文字列の連結を大量に行わないように気をつけよう
3 実行するコマンドは waf --zones=runner を呼び出すと表示される。シェルなしで呼び出されると、引数はリストとして表示される

備考: パフォーマンスとメンテナンス性のため、可能な限りシェルを使うのは避けるよう努力しよう

9.1.4. インプットとアウトプット

ソースとターゲット引数はMakeに似たタスクジェネレータにはオプショナルで、一度に一つもしくは複数のファイルを指す。 いくつかの例:

top = '.'
out = 'build'

def configure(conf):
        pass

def build(bld):
        bld( 1
                rule   = 'cp ${SRC} ${TGT[0].abspath()} && cp ${SRC} ${TGT[1].abspath()}',
                source = 'wscript',
                target = 'f1.txt f2.txt',
                shell  = True
        )

        bld( 2
                source = 'wscript',
                rule   = 'echo ${SRC}'
        )

        bld( 3
                target = 'test.k3',
                rule   = 'echo "test" > ${TGT}',
        )

        bld( 4
                rule   = 'echo 1337'
        )

        bld( 5
                rule   = "echo 'task always run'",
                always = True
        )
1 インプットまたはルールが変更されたらいつでも 2つのファイル を生成する。同様に、ルールベースのタスクジェネレータは複数のインプットファイルをとることができる
2 インプットまたはルールが変更されたらいつでもコマンドが実行される。宣言されたアウトプットはない
3 インプットなし、コマンドが変更されたらいつでもコマンドが実行される
4 インプットもアウトプットもなし、文字列表現が変更されたときにのみコマンドが実行される
5 インプットもアウトプットもなし、ビルドが呼び出される度にコマンドが実行される

記録するために、これがビルドのアウトプットだ:

$ waf distclean configure build
'distclean' finished successfully (0.002s)
'configure' finished successfully (0.093s)
Waf: Entering directory `/tmp/rule/out'
[1/5] echo 1337:
1337
[2/5] echo 'task always run':
[3/5] echo ${SRC}: wscript
../wscript
[4/5] f1.txt f2.txt: wscript -> out/f1.txt out/f2.txt
task always run
[5/5] test.k3:  -> out/test.k3
Waf: Leaving directory `/tmp/rule/out'
'build' finished successfully (0.049s)

$ waf
Waf: Entering directory `/tmp/rule/out'
[2/5] echo 'task always run':
task always run
Waf: Leaving directory `/tmp/rule/out'
'build' finished successfully (0.014s)

9.1.5. ファイルの内容に対する依存関係

2番目の例として、現在の日付からファイル r1.txt を作る。 ビルドが実行される度に更新される。 2番目のファイル r2.txtr1.txt から作られる。

top = '.'
out = 'build'

def configure(conf):
        pass

def build(bld):
        bld(
                name   = 'r1', 1
                target = 'r1.txt',
                rule   = '(date > ${TGT}) && cat ${TGT}', 2
                always = True, 3
        )

        bld(
                name   = 'r2', 4
                target = 'r2.txt',
                rule   = 'cp ${SRC} ${TGT}',
                source = 'r1.txt', 5
                after  = 'r1', 6
        )
1 タスクジェネレータに名前を与え、タスクジェネレータはコマンドを実行するための同じ名前のタスククラスを作る
2 日付とともに r1.txt を作成する
3 依存すべきソースファイルがなく、ルールは決して変更されない。タスクは always アトリビュートを使ってビルドが開始される度に実行されるようにする
4 名前が与えられないとき、ルールがタスククラスの名前として使われる
5 r1.txtr2.txt のソースとして使う。r1.txt は前に宣言されたため、依存関係が自動的に追加される(r2.txtr1.txt が変更されると再作成される)
6 r1.txt を生成するコマンドの後で、 r2.txt を生成するコマンドが実行されるようにセットする。アトリビュート after はタスクジェネレータではなく、タスククラス名を参照する。ここでは、ルールベースのタスクジェネレータは name アトリビュートを継承するため、動作する

実行結果は次のようになる;

$ waf distclean configure build -v
'distclean' finished successfully (0.003s)
'configure' finished successfully (0.001s)
Waf: Entering directory `/tmp/rule/out'
[1/2] r1:  -> out/r1.txt
16:44:39 runner system command ->  (date > r1.txt) && cat r1.txt
dom ene 31 16:44:39 CET 2010
[2/2] r2: out/r1.txt -> out/r2.txt
16:44:39 runner system command ->  cp r1.txt r2.txt
Waf: Leaving directory `/tmp/rule/out'
'build' finished successfully (0.021s)

$ waf -v
Waf: Entering directory `/tmp/rule/out'
[1/2] r1:  -> out/r1.txt
16:44:41 runner system command ->  (date > r1.txt) && cat r1.txt
dom ene 31 16:44:41 CET 2010
Waf: Leaving directory `/tmp/rule/out'
'build' finished successfully (0.016s)

r2は r1.txt依存 するが、r2は2度目のビルドで実行されなかった。 実際のところ、タスクr1のシグネチャは変化しておらず、r1はシグネチャとは関係なしに毎回実行されるようにセットされただけだ。 r1.txt のシグネチャは変化していないため、r2のシグネチャも変化せず、 r2.txt は最新の状態と判断される。

on_results アトリビュートを有効にすることで、アウトプットがファイルの内容を反映し、依存するタスクのリビルドを引き起こすようにする方法を示す:

top = '.'
out = 'build'

def configure(conf):
        pass

def build(bld):
        bld(
                name   = 'r1',
                target = 'r1.txt',
                rule   = '(date > ${TGT}) && cat ${TGT}',
                always = True,
                on_results = True,
        )

        bld(
                target = 'r2.txt',
                rule   = 'cp ${SRC} ${TGT}',
                source = 'r1.txt',
                after  = 'r1',
        )

r2.txt は毎回再生成される:

$ waf distclean configure build -v
'distclean' finished successfully (0.003s)
'configure' finished successfully (0.001s)
Waf: Entering directory `/tmp/rule/out'
[1/2] r1:  -> out/r1.txt
16:59:49 runner system command ->  (date > r1.txt) && cat r1.txt 1
dom ene 31 16:59:49 CET 2010 2
[2/2] r2: out/r1.txt -> out/r2.txt
16:59:49 runner system command ->  cp r1.txt r2.txt
Waf: Leaving directory `/tmp/rule/out'
'build' finished successfully (0.020s)

$ waf -v
Waf: Entering directory `/tmp/rule/out'
[1/2] r1:  -> out/r1.txt
16:59:49 runner system command ->  (date > r1.txt) && cat r1.txt
dom ene 31 16:59:49 CET 2010 3
Waf: Leaving directory `/tmp/rule/out'
'build' finished successfully (0.016s)

$ waf -v
Waf: Entering directory `/tmp/rule/out'
[1/2] r1:  -> out/r1.txt
16:59:53 runner system command ->  (date > r1.txt) && cat r1.txt
dom ene 31 16:59:53 CET 2010 4
[2/2] r2: out/r1.txt -> out/r2.txt
16:59:53 runner system command ->  cp r1.txt r2.txt
Waf: Leaving directory `/tmp/rule/out'
'build' finished successfully (0.022s)
1 クリーンビルドから始めて、r1.txtr2.txt の両方が生成される
2 日時が通知される
3 2番目のビルドが同一の日時に実行されたため、r1.txt は変化せず、r2.txt は最新の状態
4 3番目のビルドが別の日時に実行されたため、r1.txt は変化し、 r2.txt は再生成される

9.2. 名前と拡張子に基づくファイル処理

ファイル名や拡張子に基づいて自動的に変換される。

9.2.1. ルールベースの繰返しのタスクジェネレータを暗黙のルールへリファクタリング

前の章で述べた明示的なルールは同じ種類のファイルの処理に限られる。 次のコードはメインテナンス不能なスクリプトへ誘うものであり、さらにビルドが遅い(forループ):

def build(bld):
        for x in 'a.lua b.lua c.lua'.split():
                y = x.replace('.lua', '.luac')
                bld(source=x, target=y, rule='${LUAC} -s -o ${TGT} ${SRC}')
                bld.install_files('${LUADIR}', x)

むしろ、ルールはユーザースリクプトから取り除かれるべきで、次のようなものだ:

def build(bld):
        bld(source='a.lua b.lua c.lua')

同等なロジックは次のコードで実現される。 これは同じ wscript もしくはWafツールに配置することができる:

from waflib import TaskGen
TaskGen.declare_chain(
        name         = 'luac', 1
        rule         = '${LUAC} -s -o ${TGT} ${SRC}', 2
        shell        = False,
        ext_in       = '.lua', 3
        ext_out      = '.luac', 4
        reentrant    = False, 5
        install_path = '${LUADIR}', 6
)
1 使用するタスククラスに対応する名前
2 ルールはルールベースのタスクジェネレータと同じ
3 拡張子によって処理される入力ファイル
4 スペースで区切られた出力ファイルの拡張子。この場合は唯一の出力ファイル
5 reentrantアトリビュートは、他の暗黙なルールによる処理のために、出力ファイルを再度入力として追加するのに使われる
6 アウトプットファイルへのパスの文字列表現は、 bld.install_files からの目的の場所へのパスと似ている。インストールを無効にするならFalseに設定。

9.2.2. 2つ以上のコマンドの連鎖

ここでは長い連鎖を考える uh.inuh.auh.buh.c 。 次の暗黙のルールは最小限のユーザースクリプトのメンナンスでファイルを生成するデモを行う:

top = '.'
out = 'build'

def configure(conf):
        pass

def build(bld):
        bld(source='uh.in')

from waflib import TaskGen
TaskGen.declare_chain(name='a', rule='cp ${SRC} ${TGT}', ext_in='.in', ext_out='.a',)
TaskGen.declare_chain(name='b', rule='cp ${SRC} ${TGT}', ext_in='.a',  ext_out='.b',)
TaskGen.declare_chain(name='c', rule='cp ${SRC} ${TGT}', ext_in='.b',  ext_out='.c', reentrant = False)

ビルドフェーズの間、拡張子に基づいて正しいコンパイル順序が計算される:

$ waf distclean configure build
'distclean' finished successfully (0.000s)
'configure' finished successfully (0.090s)
Waf: Entering directory `/comp/waf/demos/simple_scenarios/chaining/build'
[1/3] a: uh.in -> build/uh.a
[2/3] b: build/uh.a -> build/uh.b
[3/3] c: build/uh.b -> build/uh.c
Waf: Leaving directory `/comp/waf/demos/simple_scenarios/chaining/build'
'build' finished successfully (0.034s)

9.2.3. スキャナメソッド

変換の連鎖は暗黙の変換に基づいているため、いくつかのファイルをスクリプトから除くことが望ましい。 もしくは、いくつかの依存関係は条件的に導入され、事前にはわからないかもしれない。 スキャナメソッド は追加的な依存関係をターゲットが生成される直前に探すある種のコールバックだ。 例示するために、3つのファイル wscriptch.inch.dep が含まれる空のプロジェクトから始めよう:

$ cd /tmp/smallproject

$ tree
.
|-- ch.dep
|-- ch.in
`-- wscript

ビルドは ch.out と呼ばれる ch.in のコピーを作る。 また ch.outch.dep が変更されるたびにリビルドされなくてはならない。 これは多かれ少なかれ次のMakefileに対応する:

ch.out: ch.in ch.dep
        cp ch.in ch.out

ユーザースクリプトは次のコードのみを含む:

top = '.'
out = 'build'

def configure(conf):
        pass

def build(bld):
        bld(source = 'ch.in')

次のコードはユーザースクリプトからは独立で、Wafツールによって読み込まれる。

def scan_meth(task): 1
        node = task.inputs[0]
        dep = node.parent.find_resource(node.name.replace('.in', '.dep')) 2
        if not dep:
                raise ValueError("Could not find the .dep file for %r" % node)
        return ([dep], []) 3

from waflib import TaskGen
TaskGen.declare_chain(
        name      = 'copy',
        rule      = 'cp ${SRC} ${TGT}',
        ext_in    = '.in',
        ext_out   = '.out',
        reentrant = False,
        scan      = scan_meth, 4
)
1 スキャナメソッドはタスクオブジェクトを入力として受け付ける(タスクジェネレータではない)
2 依存関係を見つけるためにノードメソッドを使用 (そして見つからなければエラーを挙げる)
3 スキャナメソッドは2つのリストを含むタプルを返す。最初のリストは依存するノードオブジェクトのリストを含む。2番目のリストはデバッグ情報などのプライベートなデータを含む。結果はビルドの呼出間でキャッシュされ、中身はシリアライズされる。
4 宣言を連鎖するためにスキャナメソッドを追加

実行のトレースは次のようになる:

$ echo 1 > ch.in
$ echo 1 > ch.dep 1

$ waf distclean configure build
'distclean' finished successfully (0.001s)
'configure' finished successfully (0.001s)
Waf: Entering directory `/tmp/smallproject/build'
[1/1] copy: ch.in -> build/ch.out 2
Waf: Leaving directory `/tmp/smallproject/build'
'build' finished successfully (0.010s)

$ waf
Waf: Entering directory `/tmp/smallproject/build'
Waf: Leaving directory `/tmp/smallproject/build'
'build' finished successfully (0.005s) 3

$ echo 2 > ch.dep 4

$ waf
Waf: Entering directory `/tmp/smallproject/build'
[1/1] copy: ch.in -> build/ch.out 5
Waf: Leaving directory `/tmp/smallproject/build'
'build' finished successfully (0.012s)
1 ch.inch.dep のファイルの中身を初期化
2 最初のクリーンビルドを実行。ファイル ch.out が生成される
3 何も変更されていないので、ターゲット ch.out は最新の状態
4 ch.dep の中身を変更
5 依存関係が変化したのでリビルド

ここにスキャナメソッドのいくつかの重要なポイントを示す:

  1. ターゲットが最新の状態でない場合に限り実行される

  2. task オブジェクトやコンフィギュレーションセット task.env の中身を変更しない

  3. 並列実行における問題を回避するため、メインのシングルスレッドでのみ実行される

  4. スキャナの結果(2つのリストのタプル)はビルドの実行間で再利用される(プログラミングによりこれらの結果にアクセス可能)

  5. Makeに似たルールは scan 引数も受け付ける (スキャナメソッドはタスクジェネレータよりもむしろタスクにバインドされる)

  6. C/C++サポートでは、ヘッダファイルの依存関係(.c.h)を動的に追加するため、Wafによって内部的に使われる

9.2.4. 拡張子コールバック

前のセクションでの連鎖の宣言で、reentrant アトリビュートは生成されたファイルが処理されるべきかそうでないかを制御するためのものとして述べた。 しかし、それ自身のなかではソースファイルとして考慮されてない(C/C++でのヘッダファイルのように)が、生成された2つのファイルのうちの1つが宣言されるべき場合もある(依存関係として使われるため). ここで(uh.inuh.a1 + uh.a2)と(uh.a1uh.b)の2つの連鎖について次の例で考える:

top = '.'
out = 'build'

def configure(conf):
        pass

def build(bld):
        obj = bld(source='uh.in')

from waflib import TaskGen
TaskGen.declare_chain(
        name      = 'a',
        action    = 'cp ${SRC} ${TGT}',
        ext_in    = '.in',
        ext_out   = ['.a1', '.a2'],
        reentrant = True,
)

TaskGen.declare_chain(
        name      = 'b',
        action    = 'cp ${SRC} ${TGT}',
        ext_in    = '.a1',
        ext_out   = '.b',
        reentrant = False,
)

次のエラーメッセージが生成される:

$ waf distclean configure build
'distclean' finished successfully (0.001s)
'configure' finished successfully (0.001s)
Waf: Entering directory `/tmp/smallproject'
Waf: Leaving directory `/tmp/smallproject'
Cannot guess how to process bld:///tmp/smallproject/uh.a2 (got mappings ['.a1', '.in'] in
   class TaskGen.task_gen) -> try conf.load(..)?

エラーメッセージは uh.a2 を処理する方法が存在しないことを示唆する。 .a1.in を拡張子にもつファイルのみが処理される。 内部的に、拡張子の名前はコールバックメソッドにバインドされている。 エラーはメソッドが見つからなかったために発生し、次はグローバルに拡張子コールバックを登録する方法だ:

@TaskGen.extension('.a2')
def foo(*k, **kw):
        pass

拡張子コールバックをローカルに登録するためには、タスクジェネレータへの参照は保たれなくてはならない:

def build(bld):
        obj = bld(source='uh.in')
        def callback(*k, **kw):
                pass
        obj.mappings['.a2'] = callback

拡張子コールバックの正確なメソッドシグネチャと典型的な用法は次のようになる:

from waflib import TaskGen
@TaskGen.extension(".a", ".b") 1
def my_callback(task_gen_object2, node3):
        task_gen_object.create_task(
                task_name, 4
                node,  5
                output_nodes) 6
1 カンマ区切りの拡張子のリスト(strings)
2 データを保持するタスクジェネレータインスタンス
3 ファイルを表すノードインスタンス(ソースもしくはビルド)
4 タスクを生成するための最初の引数はタスククラスの名前
5 2番目の引数は入力ノード(もしくは複数の入力に対するノードのリスト)
6 最後のパラメータは出力ノード(もしくは複数の出力に対するノードのリスト)

新しいタスククラスの生成については次のセクションで記述する。

9.2.5. タスククラスの宣言

WafタスクはTask.TaskBaseクラスのインスタンスだ。 しかし、ベースクラスは実に最小限であるため、直接のサブクラスである Task.Task が通常ユーザースクリプトで使われる。 ここでは1つの wscript プロジェクトファイルと ah.in という名前のサンプルファイルのみを含む単純なプロジェクトから始める。 タスククラスはあとで追加される。

top = '.'
out = 'build'

def configure(conf):
        pass

def build(bld):
        bld(source='uh.in')

from waflib import Task, TaskGen

@TaskGen.extension('.in')
def process(self, node):
        tsk = self.create_task('abcd') 1
        print(tsk.__class__)

class abcd(Task.Task): 2
        def run(self): 3
                print('executing...')
                return 0 4
1 abcd の新たなインスタンスの作成。メソッド create_task はタスクがそのタスクジェネレータへのリファレンスを保つことを保証するためのショートカット
2 モジュールTask.pyのTaskクラスを継承
3 メソッドrunはタスクが実行されたときに呼ばれる
4 タスクのリターンステータスは整数でなくてはならず、ゼロは成功を意味する。失敗したタスクはその後に続くビルドで実行される

ビルド実行の出力は次のようになる:

$ waf distclean configure build
'distclean' finished successfully (0.002s)
'configure' finished successfully (0.001s)
Waf: Entering directory `/tmp/simpleproject/build'
<class 'wscript_main.abcd'>
[1/1] abcd:
executing...
Waf: Leaving directory `/tmp/simpleproject/build'
'build' finished successfully (0.005s)

plainなPythonでタスククラスを書き下すことができるが、2つの関数(ファクトリ)が簡略化のために提供されている、例えば:

Task.simple_task_type( 1
        'xsubpp', 2
        rule    = '${PERL} ${XSUBPP} ${SRC} > ${TGT}', 3
        color   = 'BLUE', 4
        before  = 'cc') 5

def build_it(task):
        return 0

Task.task_type_from_func(6
        'sometask', 7
        func    = build_it, 8
        vars    = ['SRT'],
        color   = 'RED',
        ext_in  = '.in',
        ext_out = '.out') 9
1 ルール文字列を実行する新しいタスククラスを生成
2 タスククラスの名前
3 ビルド中に実行されるルール
4 実行中の出力の色
5 cc という名前のタスククラスのインスタンスに先立って実行。 before の反対は after
6 カスタムのPython関数から新しいタスククラスの生成。vars アトリビュートは依存関係として使われる追加のコンフィギュレーションセットの値を表す
7 タスククラスの名前
8 使用する関数
9 このコンテキストでは、他のタスククラスを明示的に名付けなくても、拡張子の名前は実行順序の計算に使われることを意味する

殆どのアトリビュートは2つのファクトリ関数の間で共通であることに注意してほしい。 他にも多くの使用例がWafツールの中で見つかるだろう。

9.2.6. ソースアトリビュートの処理

ソースファイルアトリビュートの処理での最初の処理はすべてのファイル名をノードに変換することだ。 正確な(拡張子なしの)ファイル名のエントリによって、特別なメソッドがintercept namesにマッピングされる。 ノードオブジェクトはタスクジェネレータの source アトリビュートに追加される。

ノードのリストは通常の拡張子のマッピングにより処理される。 拡張メソッドはその後の処理のために、 source アトリビュートに追加することで、出力ノードを再度挿入する。 (従って、リエントラントという名前はdeclare_chainで提供される)。

ソースアトリビュートの処理

9.3. 一般的な目的のタスクジェネレータ

これまで、さまざまなタスクジェネレータの使用例を示してきた。 この章ではタスクジェネレータの構造および用途を詳述する。

9.3.1. タスクジェネレータの定義

Makeに類似したルールに関する章でどのようにアトリビュート rule が処理されるかを示した。 名前と拡張子に基づくファイル処理に関する章でどのようにアトリビュート source が処理されるかを示した(ruleアトリビュートの不在)。 どのようなアトリビュート でも処理するために次のプロパティを保持する:

  1. アトリビュートはタスクジェネレータがタスクを生成するためにセットされたときにのみ処理される(遅延処理)

  2. オーソライズされたアトリビュートのリストは存在しない(タスクジェネレータはユーザースクリプトによって拡張される)

  3. タスクジェネレータのインスタンス単位でアトリビュートの処理が制御される(特定のタスクジェネレータのための特別なルール)

  4. 拡張は独立したフィイルに分割される(Wafツール間の低いカップリング)

そのようなシステムを実装することは難しい問題で、非常に異なるデザインの生成を引起こす:

  1. タスクジェネレータのサブクラスの階層 Wafツール間の高いカップリングのため廃棄された: ハイブリッドなアプリケーションのビルドではCツールがDツールの知識を必要とする

  2. メソッド修飾(メソッド呼出しのリンクリストの生成) 安全にメソッドを置換もしくは無効にすることはもはや不可能(追加のみ可能)で、このシステムはすぐになくなった

  3. フラットメソッドと実行制約の宣言 この概念はアスペクト指向プログラミングに近く、プログラマを恐れさせる

これまで、3番目のデザインがもっとも柔軟性があり保たれてきた。 これはタスクジェネレータメソッドを定義する方法だ:

top = '.'
out = 'build'

def configure(conf):
        pass

def build(bld):
        v = bld(myattr='Hello, world!')
        v.myattr = 'Hello, world!' 1
        v.myMethod() 2

from waflib import TaskGen

@TaskGen.taskgen_method 3
def myMethod(tgen): 4
        print(getattr(self, 'myattr', None)) 5
1 アトリビュートは引数もしくはオブジェクトへのアクセスによってセットされる。この例では2度セットされる
2 タスクジェネレータのメソッドの明示的な呼出し
3 Pythonデコレータの使用
4 タスクジェネレータのメソッドは現在のインスタンスを表すユニークな引数をとる
5 アトリビュート myattr が存在する場合(この例の場合)、処理する

ビルドのアウトプットは次のようになる:

$ waf distclean configure build
'distclean' finished successfully (0.001s)
'configure' finished successfully (0.001s)
Waf: Entering directory `/tmp/simpleproject/build'
hello world
Waf: Leaving directory `/tmp/simpleproject/build'
'build' finished successfully (0.003s)

備考: Pythonのクラスに新しいメソッドをバインドするように、メソッドは setattr を直接使うことでバインドすることができる

9.3.2. ビルド中のメソッドの実行

これまで、定義されたタアスクジェネレータメソッドは明示的な呼出しによってのみ実行されていた。 ビルドフェーズでタスクジェネレータを実行させるためには別のデコレータが必要だ。 アップデートされた例:

top = '.'
out = 'build'

def configure(conf):
        pass

def build(bld):
        bld(myattr='Hello, world!')

from waflib import TaskGen

@TaskGen.taskgen_method 1
@TaskGen.feature('*') 2
def methodName(self):
        print(getattr(self, 'myattr', None))
1 メソッドをタスクジェネレータクラスにバインド(TaskGen.feature のような他のメソッドが使われる場合、冗長)
2 メソッドをシンボル myfeature にバインド

実行結果は次のようになる:

$ waf distclean configure build --zones=task_gen 1
'distclean' finished successfully (0.004s)
'configure' finished successfully (0.001s)
Waf: Entering directory `/tmp/simpleproject/build'
23:03:44 task_gen posting objects (normal)
23:03:44 task_gen posting >task_gen '' of type task_gen defined in dir:///tmp/simpleproject> 139657958706768 2
23:03:44 task_gen -> exec_rule (139657958706768) 3
23:03:44 task_gen -> process_source (139657958706768) 4
23:03:44 task_gen -> methodName (139657958706768) 5
Hello, world!
23:03:44 task_gen posted 6
Waf: Leaving directory `/tmp/simpleproject/build'
23:03:44 task_gen posting objects (normal)
'build' finished successfully (0.004s)
1 デバッグゾーン task_gen は実行されたタスクジェネレータのメソッドを表示するために使用
2 どのタスクジェネレータが実行されたかを表示
3 メソッド exec_rulerule を処理するために使われる。これは常に実行される
4 メソッド process_sourcesource アトリビュートを処理するために使われる。これはメソッド exec_rulerule アトリビュートを処理する場合を除いて常に実行される
5 タスクジェネレータは実行され Hello, world! を出力
6 タスクジェネレータのメソッドが実行されタスクジェネレータは実行済とマークされる(posted)

9.3.3. タスクジェネレータの特徴

これまで、追加されたタスクジェネレータメソッドはすべてのタスクジェネレータインスタンスによって実行されるためのものだ。 特別なタスクジェネレータに実行を制限するためには feature デコレータを使う:

top = '.'
out = 'build'

def configure(conf):
        pass

def build(bld):
        bld(features='ping')
        bld(features='ping pong')

from waflib import TaskGen

@TaskGen.feature('ping')
def ping(self):
        print('ping')

@TaskGen.feature('pong')
def pong(self):
        print('pong')

実行結果の出力は次のようになる:

$ waf distclean configure build --zones=task_gen
'distclean' finished successfully (0.003s)
'configure' finished successfully (0.001s)
Waf: Entering directory `/tmp/simpleproject/build'
16:22:07 task_gen posting objects (normal)
16:22:07 task_gen posting <task_gen '' of type task_gen defined in dir:///tmp/simpleproject> 140631018237584
16:22:07 task_gen -> exec_rule (140631018237584)
16:22:07 task_gen -> process_source (140631018237584)
16:22:07 task_gen -> ping (140631018237584)
ping
16:22:07 task_gen posted
16:22:07 task_gen posting <task_gen '' of type task_gen defined in dir:///tmp/simpleproject> 140631018237776
16:22:07 task_gen -> exec_rule (140631018237776)
16:22:07 task_gen -> process_source (140631018237776)
16:22:07 task_gen -> pong (140631018237776)
pong
16:22:07 task_gen -> ping (140631018237776)
ping
16:22:07 task_gen posted
Waf: Leaving directory `/tmp/simpleproject/build'
16:22:07 task_gen posting objects (normal)
'build' finished successfully (0.005s)

警告: タスクジェネレータのインスタンスは順番に処理されるが、タスクジェネレータのメソッドの実行には実行順序のための特別な宣言が必要だ。 ここではメソッド pong はメソッド ping の前に実行される。

9.3.4. タスクジェネレータメソッドの実行順序

実行順序を制御するために、2つの新たなデコレータの追加が必要だ。 method1method2 の順序で実行される2つのタスクジェネレータのカスタムメソッドで新たな例を示す:

top = '.'
out = 'build'

def configure(conf):
        pass

def build(bld):
        bld(myattr='Hello, world!')

from waflib import TaskGen

@TaskGen.feature('*')
@TaskGen.before('process_source', 'exec_rule')
def method1(self):
        print('method 1 %r' % getattr(self, 'myattr', None))

@TaskGen.feature('*')
@TaskGen.before('process_source')
@TaskGen.after('method1')
def method2(self):
        print('method 2 %r' % getattr(self, 'myattr', None))

実行結果の出力は次のようになる:

$ waf distclean configure build --zones=task_gen
'distclean' finished successfully (0.003s)
'configure' finished successfully (0.001s)
Waf: Entering directory `/tmp/simpleproject/build'
15:54:02 task_gen posting objects (normal)
15:54:02 task_gen posting <task_gen of type task_gen defined in dir:///tmp/simpleproject> 139808568487632
15:54:02 task_gen -> method1 (139808568487632)
method 1 'Hello, world!'
15:54:02 task_gen -> exec_rule (139808568487632)
15:54:02 task_gen -> method2 (139808568487632)
method 2 'Hello, world!'
15:54:02 task_gen -> process_source (139808568487632)
15:54:02 task_gen posted
Waf: Leaving directory `/tmp/simpleproject/build'
15:54:02 task_gen posting objects (normal)
'build' finished successfully (0.005s)

9.3.5. 実行のためのメソッドの追加および削除

メソッドの順序制約(前/後)はアトリビュート meths のメソッドのリストのソートに使われる。 ソートは一度行われ、リストはメソッドが実行されるにともなって消費される。 最初のメソッドが実行されても新たなフィーチャーが追加されないこともあるが、新たなメソッドはself.methsに動的に追加される。 ここに、最後に同じメソッドを追加することで無限ループを作る方法を示す:

from waflib.TaskGen import feature

@feature('*')
def infinite_loop(self):
        self.meths.append('infinite_loop')

同様に、メソッドは実行されるメソッドのリストから削除することができる:

from waflib.TaskGen import feature

@feature('*')
@before_method('process_source')
def remove_process_source(self):
        self.meths.remove('process_source')

タスクジェネレータメソッドのワークフローは次の図で表現される:

タスクジェネレータワークフロー

9.3.6. タスクジェネレータ間の抽象的な依存関係の表現

タスクジェネレータオブジェクト間の抽象的な依存関係を表現するために使われるタスクジェネレータをどのように使うことができるかについてはここでは説明しない。 ここに新たなプロジェクトファイルが /tmp/targets/ 以下にある:

top = '.'
out = 'build'

def configure(conf):
        pass

def build(bld):
        bld(rule='echo A', always=True, name='A')
        bld(rule='echo B', always=True, name='B')

waf --targets=B を実行することで、タスクジェネレータ B のみがそのタスクを生成し、実行結果の出力は次のようになる:

$ waf distclean configure build --targets=B
'distclean' finished successfully (0.000s)
'configure' finished successfully (0.042s)
Waf: Entering directory `/tmp/targets/build'
[1/1] B:
B
Waf: Leaving directory `/tmp/targets/build'
'build' finished successfully (0.032s)

これは B が実行されたときにタスクジェネレータ A がタスクを生成することを保証する方法だ:

top = '.'
out = 'build'

def configure(conf):
    pass

def build(bld):
    bld(rule='echo A', always=True, name='A')
    bld(rule='echo B', always=True, name='B', depends_on='A')

from waflib.TaskGen import feature, before_method
@feature('*') 1
@before_method('process_rule')
def post_the_other(self):
    deps = getattr(self, 'depends_on', []) 2
    for name in self.to_list(deps):
        other = self.bld.get_tgen_by_name(name) 3
        print('other task generator tasks (before) %s' % other.tasks)
        other.post() 4
        print('other task generator tasks (after) %s' % other.tasks)
1 このメソッドはすべてのタスクジェネレータについてアトリビュート rule が処理される前に実行される
2 アトリビュート depends_on が存在するならば処理を試みる
3 名前によって同じバリアントのためのタスクジェネレータを獲得
4 他のタスクジェネレータにタスクを生成することを強制

アウトプットは次のようになる:

$ waf distclean configure build --targets=B
'distclean' finished successfully (0.001s)
'configure' finished successfully (0.001s)
Waf: Entering directory `/tmp/targets/build'
other task generator tasks (before) [] 1
other task generator tasks (after) [ 2
        {task: A  -> }]
[1/2] B:
B
[2/2] A: 3
A
Waf: Leaving directory `/tmp/targets/build'
'build' finished successfully (0.014s)
1 他のタスクジェネレータはまだ何もタスクを生成していない
2 タスクジェネレータはそのすべてのタスクを post() を呼出すことで生成する。
3 --targets=B が必要とされるにも拘わらず、ターゲット A からのタスクは生成され実行される

実際には、依存関係はしばしば他のタスクジェネレータから生成されたタスクオブジェクトを再利用する: ノード、コンフィギュレーションセット、など。 これはuselibシステムによって使われる(次の章のC/C++ビルドを参照).

10. CとC++プロジェクト

Wafは言語ニュートラルであるが、CとC++プロジェクトに非常によく使われる。 この章ではこれらの言語に使われるWafツールと関数について述べる。

10.1. C、C++、Dアプリケーションへの共通のスクリプト

10.1.1. 前もって定義されたタスクジェネレータ

C/C++のビルドはソースファイルをオブジェクトファイルに変換(コンパイル)すること、そして最後にオブジェクトファイルをアセンブル(リンク)することから構成される。 理論的には、単一のプログラミング言語でどんなアプリケーションを書くのにも十分であるべきだが、通常、状況はより複雑だ:

  1. ソースファイルは他の言語で書かれたコンパイラ(IDL, ASN1など)から生成されることがある

  2. 追加のファイルがリンクの段階で入ってくる(ライブラリ、オブジェクトファイル)ことがあるし、アプリケーションは動的もしくは静的ライブラリに分割されうる

  3. 異なるプラットフォームでは異なる処理ルールが必要となる(MS-Windowsでのマニフェストファイルなど)

実装の詳細を隠すこと、およびポータビリティーのため、次の例のようにそれぞれのターゲット(プログラム、ライブラリ)は単一のタスクジェネレータオブジェクトにラップすることができる:

def options(opt):
        opt.load('compiler_c')

def configure(conf):
        conf.load('compiler_c') 1

def build(bld):
        bld.program(source='main.c', target='app', use='myshlib mystlib') 2
        bld.stlib(source='a.c', target='mystlib') 3
        bld.shlib(source='b.c', target='myshlib', use='myobjects') 4
        bld.objects(source='c.c', target='myobjects')
1 Cルーチンをロードしコンパイラを見つけるためにcompiler_cを使う(C++では compiler_cxx 、D言語では compiler_d)
2 main.c と他の2つのライブラリを使って実行ファイルを宣言
3 スタティックライブラリの宣言
4 myobjects のオブジェクトファイルを使った共有ライブラリの宣言

ターゲットはプラットフォームで異なる拡張子と名前を持つ。 例えば、Linuxではビルドディレクトリの中身は:

$ tree build
build/
|-- c4che
|   |-- build.config.py
|   `-- _cache.py
|-- a.c.1.o
|-- app 1
|-- b.c.2.o
|-- c.c.3.o
|-- config.log
|-- libmyshlib.so 2
|-- libmystlib.a
`-- main.c.0.o 3
1 実行ファイルはLinuxでは拡張子をもたないが、Windowsでは .exe
2 共有ライブラリの拡張子はLinuxでは .so 、Windowsでは .dll
3 .o オブジェクトファイルはオリジナルのファイル名とインデックスを使って複数コンパイルでのエラーを避ける

ビルドコンテキストのメソッド programshlibstlibobjects は単一のタスクジェネレータとソースリストで検知された適切なフィーチャーを返す。 例えば、ソースアトリビュートに .c ファイルを持つプログラムでは、追加されるフィーチャーは "c cprogram" で、 d の静的ライブラリでは "d dstlib" だ。

10.1.2. 追加のアトリビュート

前述のメソッドは単に use 以外にも多くのアトリビュートを処理できる:

def options(opt):
        opt.load('compiler_c')

def configure(conf):
        conf.load('compiler_c')

def build(bld):
        bld.program(
                source       = 'main.c', 1
                target       = 'appname', 2
                features     = ['more', 'features'], 3

                includes     = ['.'], 4
                defines      = ['LINUX=1', 'BIDULE'],

                lib          = ['m'], 5
                libpath      = ['/usr/lib'],
                stlib        = ['dl'], 6
                stlibpath    = ['/usr/local/lib'],
                linkflags    = ['-g'], 7
                rpath        = ['/opt/kde/lib'] 8
                vnum         = '1.2.3',

                install_path = '${SOME_PATH}/bin', 9
                cflags       = ['-O2', '-Wall'], 10
                cxxflags     = ['-O3'],
                dflags       = ['-g'],
        )
1 ソースファイルのリスト
2 ターゲット。プラットフォームやタイプに応じて自動的に target.exelibtarget.so に変換される
3 追加するフィーチャー(Cのファイルからなるプログラムの場合、デフォルトは 'c cprogram')
4 includeとdefine
5 共有ライブラリと共有ライブラリのリンクパス
6 静的ライブラリとリンクパス
7 特定のリンクフラグのためにlinkflagsを使う(ライブラリには適用されない)
8 rpathとvnum。これらをサポートしないプラットフォームでは無視される
9 プログラムと共有ライブラリはデフォルトでインストールされる。インストールを無効にするにはNoneにセット
10 フラグをサポートするソースファイルに適用されるさまざまなフラグ(もし存在するならば)

10.2. includeの処理

10.2.1. 実行パスとフラグ

インクルードパスはヘッダを探すためにC/C++コンパイラによって使われる。 ヘッダが変更されると、ファイルは自動的に再コンパイルされる。 例えば次のような構成のプロジェクトでは:

$ tree
.
|-- foo.h
|-- src
|   |-- main.c
|   `-- wscript
`-- wscript

ファイル src/wscript は次のコードを含む:

def build(bld):
    bld.program(
        source   = 'main.c',
        target   = 'myapp',
        includes = '.. .')

コマンドライン(waf -v のアウトプット)は次のようになる:

cc -I. -I.. -Isrc -I../src ../src/main.c -c -o src/main_1.o

コマンドはビルドディレクトリから実行されるため、フォルダは次のようにインクルードフラグに変換される:

.. -> -I..      -I.
.  -> -I../src  -Isrc

覚えておくべき重要な点がいくつかある:

  1. インクルードは常にwscriptファイルを含むディレクトリからの相対パスで与えられる

  2. インクルードはタスクジェネレータバリアントのためにソースディレクトリと対応するビルドディレクトリを追加する

  3. コマンドはビルドディレクトリから実行されるため、インクルードパスは変換されなくてはならない

  4. システムのインクルードパスはconfigure中に定義されINCLUDES変数に追加される(uselib)

10.2.2. Wafプリプロセッサ

Wafはヘッダの依存関係を追加するためにPythonで書かれたプリプロセッサを使う。 #include文を見るだけの単純なパーサでは次のような構造を見逃がしてしまう:

#define mymacro "foo.h"
#include mymacro

依存関係を見つけるためにコンパイラを使うのはQtのようなファイル処理を必要とするアプリケーションではうまくいかない。 Qtでは、拡張子 .moc の特別なインクルードファイルをビルドシステムが検知し、前もって生成しなくてはならない。 Cコンパイラはそのようなファイルをパースできない。

#include "foo.moc"

デフォルトではシステムのヘッダは追跡されないため、Wafのプリプロセッサは次のように書かれた依存関係を見逃すかもしれない:

#if SOMEMACRO
        /* an include in the project */
        #include "foo.h"
#endif

ポータブルでデバッグしやすいコードにするために、プロジェクトで使われるすべての条件を config.h ファイルに書き出すことを強く推奨する。

def configure(conf):
        conf.check(
                fragment    = 'int main() { return 0; }\n',
                define_name = 'FOO',
                mandatory   = True)
        conf.write_config_header('config.h')

パフォーマンス上の理由から、デフォルトではシステムのヘッダの暗黙の依存関係は無視される。 次のコードはこれを有効にするために使われる:

from waflib import c_preproc
c_preproc.go_absolute = True

gccdepsdumbpreproc などのツールは代替的な依存関係のスキャナを提供し、あるケース(boost)においてより高速だ。

備考: Wafエンジンはタスクがコンパイルに必要なヘッダを生成しビルド順序を計算するか否かを検知する。ヘッダを生成するタスクがヒント ext_out=[".h"] を提供するならばスキャナのパフォーマンスが改善されることもある

10.2.3. 依存関係のデバッグ

Wafプリプロセッサは特定のデバッグゾーンを含む:

$ waf --zones=preproc

獲得したもしくは見逃された依存関係を表示するには、次を使う:

$ waf --zones=deps

23:53:21 deps deps for src:///comp/waf/demos/qt4/src/window.cpp: 1
  [src:///comp/waf/demos/qt4/src/window.h, bld:///comp/waf/demos/qt4/src/window.moc]; 2
  unresolved ['QtGui', 'QGLWidget', 'QWidget'] 3
1 プリプロセスされたファイル
2 見つかったヘッダ
3 廃棄されたシステムヘッダ

依存関係の計算はファイルが最新ではないときにのみ行われ、これらのコマンドはコンパイルすべきファイルが存在するときにのみ表示を行う。

備考: スキャナはCファイルや依存関係が変化したときにのみ呼ばれる。 コンパイルが成功した後でヘッダを追加するまれなケースでは、フルスキャンを強制的に行うために waf clean build を実行する必要があるかもしれない

10.3. ライブラリの相互作用(use)

10.3.1. ローカルライブラリ

アトリビュート use は(静的もしくは共有)ライブラリとのリンクを有効にし、参照されるタスクジェネレータがライブラリでない場合、オブジェクトファイルを含めることを有効にする。

def build(bld):
        bld.stlib(
                source   = 'test_staticlib.c',
                target   = 'mylib',
                name     = 'stlib1') 1

        bld.program(
                source   = 'main.c',
                target   = 'app',
                includes = '.',
                use      = ['stlib1']) 2
1 nameアトリビュートは正確に一つのタスクジェネレータを指し示さなくてはならない
2 アトリビュート use は使うタスクジェネレータの名前を含む

この例では、mylib が変更されるとファイル app は再度生成される(順序と依存関係)。 タスクジェネレータの名前を使うことで、実行形式とライブラリの宣言はどのような順序で行ってもよい。 利便性のために、名前は定義する必要はなく、ターゲットの名前から前もってセットされる:

def build(bld):
        bld.stlib(
                source   = 'test_staticlib.c',
                target   = 'mylib')

        bld.program(
                source   = 'main.c',
                target   = 'app',
                includes = '.',
                use      = ['mylib'])

また、 use は再帰的な振舞いの処理を禁止している。 これを次の例で示そう:

def build(bld):
        bld.shlib(
                source = 'a.c', 1
                target = 'lib1')

        bld.stlib(
                source = 'b.c',
                use    = 'cshlib', 2
                target = 'lib2')

        bld.shlib(
                source = 'c.c',
                target = 'lib3',
                use    = 'lib1 lib2') 3

        bld.program( 4
                source = 'main.c',
                target = 'app',
                use    = 'lib3')
1 単純な共有ライブラリ
2 cshlib フラグはライブラリと実行形式両方に伝搬する。
[伝搬を防ぐには http://code.google.com/p/waf/source/browse/trunk/docs/book/examples/cprog_propagation/wscript を参照]
3 lib3 は共有ライブラリと静的ライブラリを使う
4 実行形式は lib3 を使う

共有ライブラリの依存関係 lib1lib2 のため、実行形式 applib1lib3 に対してリンクされるが、 lib2 に対してはリンクされない:

$ waf -v
'clean' finished successfully (0.004s)
Waf: Entering directory `/tmp/cprog_propagation/build'
[1/8] c: a.c -> build/a.c.0.o
12:36:17 runner ['/usr/bin/gcc', '-fPIC', '../a.c', '-c', '-o', 'a.c.0.o']
[2/8] c: b.c -> build/b.c.1.o
12:36:17 runner ['/usr/bin/gcc', '../b.c', '-c', '-o', 'b.c.1.o']
[3/8] c: c.c -> build/c.c.2.o
12:36:17 runner ['/usr/bin/gcc', '-fPIC', '../c.c', '-c', '-o', 'c.c.2.o']
[4/8] c: main.c -> build/main.c.3.o
12:36:17 runner ['/usr/bin/gcc', '../main.c', '-c', '-o', 'main.c.3.o']
[5/8] cstlib: build/b.c.1.o -> build/liblib2.a
12:36:17 runner ['/usr/bin/ar', 'rcs', 'liblib2.a', 'b.c.1.o']
[6/8] cshlib: build/a.c.0.o -> build/liblib1.so
12:36:17 runner ['/usr/bin/gcc', 'a.c.0.o', '-o', 'liblib1.so', '-shared']
[7/8] cshlib: build/c.c.2.o -> build/liblib3.so
12:36:17 runner ['/usr/bin/gcc', 'c.c.2.o', '-o', 'liblib3.so', '-Wl,-Bstatic', '-L.', '-llib2', '-Wl,-Bdynamic', '-L.', '-llib1', '-shared']
[8/8] cprogram: build/main.c.3.o -> build/app
12:36:17 runner ['/usr/bin/gcc', 'main.c.3.o', '-o', 'app', '-Wl,-Bdynamic', '-L.', '-llib1', '-llib3']
Waf: Leaving directory `/tmp/cprog_propagation/build'
'build' finished successfully (0.144s)

use アトリビュートの最も重要な点をまとめると:

  1. タスクジェネレータを異なるファイルにどのような順序でも作ることができるが、use アトリビュートのためにユニークな名前をつけなくてはならない

  2. use の処理は必要なすべてのタスジクジェネレータに対して再帰的に処理するが、追加されたフラグはターゲットの種類(共有/静的ライブラリ)に依存する

10.3.2. 特別なローカルライブラリ

includeフォルダ

useキーワードは実際にはターゲットを宣言しない特別なライブラリ指定することができる。 例えば、ヘッダのみのライブラリは複数のターゲットに対して共通のインクルードパスを追加することで使われる:

def build(bld):
        bld(
                includes        = '. src',
                export_includes = 'src', 1
                name            = 'com_includes')

        bld.stlib(
                source          = 'a.c',
                target          = 'shlib1',
                use             = 'com_includes') 2

        bld.program(
                source          = 'main.c',
                target          = 'app',
                use             = 'shlib1', 3
                )
1 includes アトリビュートはプライベートであるが、 export_includes は他のタスクジェネレータによって使われる
2 パスは他のタスクジェネレータに対する相対パスとして追加される
3 export_includes は他のタスクジェネレータにも伝搬する
オブジェクトファイル

これは特定のファイルに対して特別なコンパイルフラグを有効にする方法だ:

def build(bld):
        bld.objects( 1
                source  = 'test.c',
                cflags  = '-O3',
                target  = 'my_objs')

        bld.shlib(
                source  = 'a.c',
                cflags  = '-O2', 2
                target  = 'lib1',
                use     = 'my_objs') 3

        bld.program(
                source  = 'main.c',
                target  = 'test_c_program',
                use     = 'lib1') 4
1 ファイルはCモードでコンパイルされるが、実行形式もライブラリも生成されない
2 異なるコンパイルフラグが使われる
3 オブジェクトは自動的にリンクステージで追加される
4 シンボルの重複エラーを回避するために、オブジェクトは他の実行形式やライブラリには伝搬しない

警告: 静的ライブラリのように、オブジェクトファイルはバイナリコードをコピーアンドペーストするために乱用されがちだ。可能ならば常に共有ライブラリを使うことで実行ファイルのサイズを最小化しよう。

フェイクライブラリ

ローカルライブラリは変更されると再コンパイルをトリガーする。 メソッド read_shlibread_stlib はこの振舞いを外部のライブラリやプロジェクトに存在するバイナリファイルに追加するために使うことができる。

def build(bld):
        bld.read_shlib('m', paths=['.', '/usr/lib64'])
        bld.program(source='main.c', target='app', use='m')

このメソッドは必要なパスと依存関係を計算するために、指定されたパスにある libm.solibm.dll のようなファイルを探す。 この例では、 /usr/lib64/libm.so が変更されるとターゲット app は再度生成される。 ローカルで宣言された共有ライブラリや静的ライブラリのように、これらのライブラリはタスクジェネレータ間を伝搬する。

10.3.3. 外部ライブラリとフラグ

アトリビュート use の要素がローカルライブラリにマッチしない場合、システムライブラリを表現し、必要なフラグはコンフィギュレーションセット env に存在することが想定される。 このシステムは、次の例のように、いくつかのコンパイルフラグとリンクフラグを一度に追加する:

import sys

def options(opt):
        opt.load('compiler_c')

def configure(conf):
        conf.load('compiler_c')
        conf.env.INCLUDES_TEST      = ['/usr/include'] 1

        if sys.platform != 'win32': 2
                conf.env.DEFINES_TEST   = ['TEST']
                conf.env.CFLAGS_TEST   = ['-O0'] 3
                conf.env.LIB_TEST       = ['m']
                conf.env.LIBPATH_TEST   = ['/usr/lib']
                conf.env.LINKFLAGS_TEST = ['-g']
                conf.env.INCLUDES_TEST  = ['/opt/gnome/include']

def build(bld):
        mylib = bld.stlib(
                source   = 'test_staticlib.c',
                target   = 'teststaticlib',
                use      = 'TEST') 4

        if mylib.env.CC_NAME == 'gcc':
                mylib.cxxflags = ['-O2'] 5
1 移植性の理由から、-I/includeのような形態でフラグを与える替わりにINCLUDESを使うことが推奨される。INCLUDESはCとC++両方から使われることに注意
2 プラットフォーム固有の設定でundefinedとなりうる変数。しかしながらビルドスクリプトは同一のままだ。
3 configureでいくつかの変数を宣言する。変数はVAR_NAMEの慣習に従う
4 use variable NAMEに対応するすべてのVAR_NAMEを追加。この例では TEST
5 避けるべきモデル: フラグの設定およびconfigureのチェックはconfigureセクションで行われるべきだ

C/C++のための変数は次のようになる:

Table 1. C/C++用の変数とタスクジェネレータのアトリビュートの使用
Uselib変数 アトリビュート 用法

LIB

lib

プレフィックスや拡張子のついていない使用する共有ライブラリ名のリスト

LIBPATH

libpath

共有ライラリのサーチパスのリスト

STLIB

stlib

プレフィックスや拡張子のついていない使用する静的ライブラリ名のリスト

STLIBPATH

stlibpath

静的ライブラリのサーチパスのリスト

LINKFLAGS

linkflags

リンクフラグのリスト(可能ならば他の変数を使った方がよい)

RPATH

rpath

リンク時にバイナリにハードコードされるパスのリスト

CFLAGS

cflags

Cファイル用のコンパイルフラグのリスト

CXXFLAGS

cxxflags

C++ファイル用のコンパイルフラグのリスト

DFLAGS

dflags

D言語用のコンパイルフラグのリスト

INCLUDES

includes

インクルードパス

CXXDEPS

変更されたときにC++の再コンパイルをトリガーする変数/リスト

CCDEPS

変更されたときにCの再コンパイルをトリガーする変数/リスト

LINKDEPS

変更されたときに再リンクをトリガーする変数/リスト

DEFINES

defines

defineのリスト in the form [‘key=value’, …]

FRAMEWORK

framework

使用するフレームワークのリスト

FRAMEWORKPATH

frameworkpath

使用するフレームワークへのパスのリスト

ARCH

arch

アーキテクチャのリスト in the form [ppc, x86]

後の使用のために変数を空にすることができるが、エラーは起こらない。 開発中は、異なるconfigureを試すために、プロジェクト全体の再configureをすることなくコンフィギュレーションキャッシュファイル(例えば、_cache.py)をテキストエディタから変更することができるが、影響を受けるファイルはリビルドされる。

10.4. configureのヘルパ

10.4.1. コンフィギュレーションテスト

メソッド check は小さなビルドプロジェクトを使うパラメータの検知に使われる。 主なパラメータは次のようになる

  1. msg: 実行するテストのタイトル

  2. okmsg: テストが成功したときに表示するメッセージ

  3. errmsg: テストが失敗したときに表示するメッセージ

  4. env: ビルドのために使用する環境(conf.envはデフォルトで使われる)

  5. compile_mode: cc もしくは cxx

  6. define_name: テストが成功ときにコンフィギュレーションヘッダ用のdefineを追加(ほとんどの場合、自動的に計算される)

発生する例外は waflib.Errors.ConfigurationError のインスタンスだ。 リターンコードはない。

主要なパラメータの他に、C/C++のタスクジェネレータのアトリビュートが使われる。 ここに完全な例を示す:

def configure(conf):

        conf.check(header_name='time.h', features='c cprogram') 1
        conf.check_cc(function_name='printf', header_name="stdio.h", mandatory=False) 2
        conf.check_cc(fragment='int main() {2+2==4;}\n', define_name="boobah") 3
        conf.check_cc(lib='m', cflags='-Wall', defines=['var=foo', 'x=y'],
                uselib_store='M') 4
        conf.check_cxx(lib='linux', use='M', cxxflags='-O2') 5

        conf.check_cc(fragment='''
                        #include <stdio.h>
                        int main() { printf("4"); return 0; } ''',
                define_name = "booeah",
                execute     = True,
                define_ret  = True,
                msg         = "Checking for something") 6

        conf.check(features='c', fragment='int main(){return 0;}') 7

        conf.write_config_header('config.h') 8
1 システムに存在するならば、コンフィギュレーションヘッダtime.hを用いてコンパイルを試行し、テストが成功すればdefine HAVE_TIME_Hが追加される
2 ヘッダstdio.hを追加し関数printfを使ってコンパイルを試行(header_nameは追加されるヘッダのリストでもよい)。デフォルトですべてのコンフィギュレーションテストが必要で(@confメソッド)、コンフィギュレーション例外を発生する。隠すためにはアトリビュート mandatory をFalseにセット。
3 コードの断片のコンパイルを試行し、テストが成功すればboobahが定義される
4 タスクジェネレータ環境への変更は保存されない。テストが成功しアトリビュートuselib_storeが与えられているとき、lib、cflagsとdefinesはuse variables LIB_MとCFLAGS_M、DEFINES_Mに変換され、フラグの値はコンフィギュレーション環境に追加される。
5 linux と呼ばれるライブラリに対して単純なCのプログラムのコンパイルを試行し、 use でlibm用のパラメータを再利用する
6 単純なプログラムを実行し、成功したらアウトプットを収集してdefineにつっこむ
7 単一のタスクジェネレータによってテストがビルドを作る。 features アトリビュートを直接渡すことでコンパイルを無効にしたり、より複雑なコンフィギュレーションテストを作ることができる。
8 すべてのテストが実行された後で、コンフィギュレーションヘッダをビルドディレクトリに書き出す(optional)。コンフィギュレーションヘッダはコマンドラインのサイズを制限するために使われる。

ここに前掲のテストコードによって生成された config.h の例を示す:

/* Configuration header created by Waf - do not edit */
#ifndef _CONFIG_H_WAF
#define _CONFIG_H_WAF

#define HAVE_PRINTF 1
#define HAVE_TIME_H 1
#define boobah 1
#define booeah "4"

#endif /* _CONFIG_H_WAF */

ファイル _cache.py は次の変数を含む:

DEFINES_M = ['var=foo', 'x=y']
CXXFLAGS_M = ['-Wall']
CFLAGS_M = ['-Wall']
LIB_M = ['m']
boobah = 1
booeah = '4'
defines = {'booeah': '"4"', 'boobah': 1, 'HAVE_TIME_H': 1, 'HAVE_PRINTF': 1}
dep_files = ['config.h']
waf_config_files = ['/compilation/waf/demos/adv/build/config.h']

10.4.2. アドバンステスト

メソッド conf.check はビルドコンテキストとタスクジェネレータを内部的に作る。これはアトリビュート includesdefinescxxflags が使われうることを意味する(これですべてではない)。 アドバンステストはフィーチャー引数を渡すことによって作られる:

from waflib.TaskGen import feature, before_method

@feature('special_test')
@before_method('process_source')
def my_special_test(self):
        self.bld(rule='touch ${TGT}', target='foo') 1
        self.bld(rule='cp ${SRC} ${TGT}', source='foo', target='bar')
        self.source = [] 2

def configure(conf):
        conf.check_cc(features='special_test', msg='my test!') 3
1 別のタスクジェネレータからタスクジェネレータを生成
2 test.c sourceに空のリストをセットすることで test.c のコンパイルを無効にする
3 special_testフィーチャーを使う

10.4.3. コンフィギュレーションヘッダの作成

大量のdefineの値をコマンドラインに追加すると、コマンドラインのサイズが増加し有用な情報が隠される(differences)。 いくつかのプロジェクトはconfigureで生成されたヘッダを使い、それらはビルド中に変更されないしインストールも再頒布もされない。 このシステムは巨大なプロジェクトに有効で、autoconfに基づいたプロジェクトで一般的だ。

コンフィギュレーションヘッダの書き出しは次のメソッドを使うことで行える:

def configure(conf):
        conf.define('NOLIBF', 1)
        conf.undefine('NOLIBF')
        conf.define('LIBF', 1)
        conf.define('LIBF_VERSION', '1.0.2')
        conf.write_config_header('config.h')

このコードスニペットはビルドディレクトリに次のように config.h を生成する:

build/
|-- c4che
|   |-- build.config.py
|   `-- _cache.py
|-- config.log
`-- config.h

この例のconfig.hの中身は:

/* Configuration header created by Waf - do not edit */
#ifndef _CONFIG_H_WAF
#define _CONFIG_H_WAF

/* #undef NOLIBF */
#define LIBF 1
#define LIBF_VERSION "1.0.2"

#endif /* _CONFIG_H_WAF */

備考: デフォルトでは、defineはコマンドラインからコンフィギュレーションヘッダに移動する。これはオペレーションによってアトリビュート conf.env.DEFINE が作られることを意味する。この振舞いを阻止するには、 conf.write_config_header(remove=False) を使う

10.4.4. Pkg-config

依存するすべてのプロジェクトのコンフィギュレーションを検出する替わりに、コンフィギュレーションファイルはライブラリがインストールされたときに書き出される。 (データベースやAPIに問い合せることのできない)Makeに基づいたビルドシステムとのインタラクションを簡単にするために、キャッシュファイルを読み込み、 pkg-config やwx-config、sdl-configなどのパラメータ(伝統的に -config で終わる名前)を解釈するために小さなアプリケーションが作られた。

メソッド check_cfg はこれらのアプリケーションとのインタラクションを簡単にするために提供されている。 ここにいくつかの例を示す:

def options(opt):
        opt.load('compiler_c')

def configure(conf):
        conf.load('compiler_c')

        conf.check_cfg(atleast_pkgconfig_version='0.0.0') 1
        pango_version = conf.check_cfg(modversion='pango') 2

        conf.check_cfg(package='pango') 3
        conf.check_cfg(package='pango', uselib_store='MYPANGO',
                args=['--cflags', '--libs']) 4

        conf.check_cfg(package='pango', 5
                args=['pango >= 0.1.0', 'pango < 9.9.9', '--cflags', '--libs'],
                msg="Checking for 'pango 0.1.0'") 6

        conf.check_cfg(path='sdl-config', args='--cflags --libs',
                package='', uselib_store='SDL') 7
        conf.check_cfg(path='mpicc', args='--showme:compile --showme:link',
                package='', uselib_store='OPEN_MPI', mandatory=False) 8
1 pkg-configのバージョンをチェック
2 パッケージ向のモジュールのバージョンを文字列として検索。エラーがないならば、 PANGO_VERSION が定義される。アトリビュート uselib_store='MYPANGO' によって上書きできる。
3 pangoパッケージが存在するか否かをチェックし、 HAVE_PANGO を定義する(パッケージ名から自動的に計算される)
4 HAVE_MYPANGO を定義すると伴に, 関係するフラグを抽出し use variable MYPANGO (LIB_MYPANGO, LIBPATH_MYPANGO など)に格納する
5 前のテストと同様だが、特定のバージョン番号を強制するためのpkg-config節を追加
6 アウトプットにカスタムのメッセージを表示。アトリビュート okmsgerrmsg はそれぞれ成功および失敗したときに表示するメッセージを表す
7 sdl-config向けのフラグを取得。この例はwx-configやpcre-configなどの他のコンフィギュレーションプログラムにも適用できる
8 実行されるプログラムが見つからないときや終了ステータスがゼロでないときに発生するコンフィギュレーションエラーを抑制する

フラグが大量で、アプリケーション間のコンフィグに標準が欠けており、コンパイラ依存のフラグ(gccでは -I、msvcでは /I)のため、pkg-configのアウトプットは対応する use variables を設定する前にパースされる。 Wafモジュールc_config.pyの関数 parse_flags(line, uselib, env) はフラグの抽出を行う。

アウトプットはビルドディレクトリのファイル config.log に書き出される:

# project  configured on Tue Aug 31 17:30:21 2010 by
# waf 1.6.10 (abi 98, python 20605f0 on linux2)
# using /home/waf/bin/waf configure
#
---
Setting top to
/disk/comp/waf/docs/book/examples/cprog_pkgconfig
---
Setting out to
/disk/comp/waf/docs/book/examples/cprog_pkgconfig/build
---
Checking for program pkg-config
/usr/bin/pkg-config
find program=['pkg-config'] paths=['/usr/local/bin', '/usr/bin'] var='PKGCONFIG' -> '/usr/bin/pkg-config'
---
Checking for pkg-config version >= 0.0.0
['/usr/bin/pkg-config', '--atleast-pkgconfig-version=0.0.0']
yes
['/usr/bin/pkg-config', '--modversion', 'pango']
out: 1.28.0

---
Checking for pango
['/usr/bin/pkg-config', 'pango']
yes
---
Checking for pango
['/usr/bin/pkg-config', 'pango']
yes
---
Checking for pango 0.1.0
['/usr/bin/pkg-config', 'pango >= 0.1.0', 'pango < 9.9.9', '--cflags', '--libs', 'pango']
out: -pthread -I/usr/include/pango-1.0 -I/usr/include/glib-2.0 -I/usr/lib64/glib-2.0/include
     -pthread -lpango-1.0 -lgobject-2.0 -lgmodule-2.0 -lgthread-2.0 -lrt -lglib-2.0

yes
---
Checking for sdl-config
['sdl-config', '--cflags', '--libs']
out: -I/usr/include/SDL -D_GNU_SOURCE=1 -D_REENTRANT
-L/usr/lib64 -lSDL -lpthread

yes
---
Checking for mpicc
['mpicc', '--showme:compile', '--showme:link']
out: -pthread libtool: link: -pthread -L/usr/lib64 -llammpio -llamf77mpi -lmpi -llam -lutil -ldl

そのようなconfigureの後、コンフィギュレーションセットの中身は次と似たようなものになる:

'CFLAGS_OPEN_MPI' ['-pthread']
'CFLAGS_PANGO' ['-pthread']
'CXXFLAGS_OPEN_MPI' ['-pthread']
'CXXFLAGS_PANGO' ['-pthread']
'DEFINES' ['HAVE_PANGO=1', 'HAVE_MYPANGO=1', 'HAVE_SDL=1', 'HAVE_OPEN_MPI=1']
'DEFINES_SDL' ['_GNU_SOURCE=1', '_REENTRANT']
'INCLUDES_PANGO' ['/usr/include/pango-1.0', '/usr/include/glib-2.0', '/usr/lib64/glib-2.0/include']
'INCLUDES_SDL' ['/usr/include/SDL']
'LIBPATH_OPEN_MPI' ['/usr/lib64']
'LIBPATH_SDL' ['/usr/lib64']
'LIB_OPEN_MPI' ['lammpio', 'lamf77mpi', 'mpi', 'lam', 'util', 'dl']
'LIB_PANGO' ['pango-1.0', 'gobject-2.0', 'gmodule-2.0', 'gthread-2.0', 'rt', 'glib-2.0']
'LIB_SDL' ['SDL', 'pthread']
'LINKFLAGS_OPEN_MPI' ['-pthread']
'LINKFLAGS_PANGO' ['-pthread']
'PKGCONFIG' '/usr/bin/pkg-config'
'PREFIX' '/usr/local'
'define_key' ['HAVE_PANGO', 'HAVE_MYPANGO', 'HAVE_SDL', 'HAVE_OPEN_MPI']

11. アドバンスのシナリオ

この章ではあまり一般的ではない、より複雑でシナリオのためのWafライブラリの例をいくつか示す。

11.1. プロジェクトの構成

11.1.1. 最初にコンパイラをビルド

次の例は残りのターゲットをビルドするためのコンパイラをビルドする方法を示す。 要求は次のようなものとする:

  1. コンパラとすべての中間タスクを生成

  2. 第2ステップのビルドでコンパイラを再利用

  3. コンパイラは .src ファイルを後で処理される .cpp ファイルに変換

  4. リビルドされたときにはコンパラを再度呼び出す(コンパイラに依存関係を追加)

最初にするべきことは期待されるユーザースクリプトを書くことだ:

top = '.'
out = 'build'

def configure(ctx):
    ctx.load('g++')
    ctx.load('src2cpp', tooldir='.')

def build(ctx):
    ctx.program( 1
        source = 'comp.cpp',
        target = 'comp')

    ctx.add_group() 2

    ctx.program(
        source = 'main.cpp a.src', 3
        target = 'foo')
1 最初にコンパイラをビルドし、 comp という名前のバイナリが得られる
2 次のタスクの処理を開始する前にコンパイラが完成するように、新しいビルドグループを追加
3 ファイル a.srccomp によって a.cpp に変換される

src → cpp 変換のためのコードは次のようになる:

from waflib.Task import Task
class src2cpp(Task): 1
    run_str = '${SRC[0].abspath()} ${SRC[1].abspath()} ${TGT}'
    color   = 'PINK'

from waflib.TaskGen import extension

@extension('.src')
def process_src(self, node): 2
    tg = self.bld.get_tgen_by_name('comp') 3
    comp = tg.link_task.outputs[0]
    tsk = self.create_task('src2cpp', [comp, node], node.change_ext('.cpp')) 4
    self.source.extend(tsk.outputs) 5
1 自前のコンパイラでソースを処理するための新たなタスククラスを宣言
2 このメソッドによって処理される、拡張子が .src のファイル
3 コンパイラを生成するタスクジェネレータへのリファレンスを獲得
4 タスク src → cpp を生成し、コンパイラは最初のソースファイルとして使われる
5 生成された cpp ファイルも処理するために追加

コンパイル結果は次のようになる:

$ waf distclean configure build -v
'distclean' finished successfully (0.006s)
Setting top to                           : /tmp/scenarios_compiler
Setting out to                           : /tmp/scenarios_compiler/build
Checking for program g++,c++             : /usr/bin/g++
Checking for program ar                  : /usr/bin/ar
'configure' finished successfully (0.118s)
Waf: Entering directory `/tmp/scenarios_compiler/build'
[1/5] cxx: comp.cpp -> build/comp.cpp.0.o
10:21:14 runner ['/usr/bin/g++', '../comp.cpp', '-c', '-o', 'comp.cpp.0.o']
[2/5] cxxprogram: build/comp.cpp.0.o -> build/comp 1
10:21:14 runner ['/usr/bin/g++', 'comp.cpp.0.o', '-o', '/tmp/scenarios_compiler/build/comp']
[3/5] cxx: main.cpp -> build/main.cpp.1.o
10:21:15 runner ['/usr/bin/g++', '../main.cpp', '-c', '-o', 'main.cpp.1.o']
[4/5] src2cpp: a.src -> build/a.cpp
10:21:15 runner ['/tmp/scenarios_compiler/build/comp', '../a.src', 'a.cpp'] 2
[5/5] cxxprogram: build/main.cpp.1.o -> build/foo 3
10:21:15 runner ['/usr/bin/g++', 'main.cpp.1.o', '-o', 'foo']
Waf: Leaving directory `/tmp/scenarios_compiler/build'
'build' finished successfully (1.009s)
1 comp 実行形式の生成
2 a.cpp を生成するためにコンパイラを使う
3 a.cppmain.cpp をコンパイルしリンクして実行形式 foo を生成する

備考: ‘waf --targets=foo’ が呼び出されたとき、タスクジェネレータ ‘comp’ はタスクも生成する(前のグループのタスクジェネレータは処理される)。

11.1.2. 任意のコンフィギュレーションファイルを与える

ビルドが開始する前にファイルがビルドディレクトリにコピーされる。 ビルドは他のターゲットをビルドするためにこのファイルを使うことができる。

cfg_file = 'somedir/foo.txt'

def configure(conf):

    orig = conf.root.find_node('/etc/fstab')
    txt = orig.read() 1

    dest = conf.bldnode.make_node(cfg_file)
    dest.parent.mkdir() 2
    dest.write(txt) 3

    conf.env.append_value('cfg_files', dest.abspath()) 4

def build(ctx):
    ctx(rule='cp ${SRC} ${TGT}', source=cfg_file, target='bar.txt')
1 ファイル /etc/fstab を読み込む
2 ファイル作成先のディレクトリが存在しない場合にそなえてディレクトリを作成
3 新たなファイルをビルドディレクトリに作成
4 アウトプットをコンフィギュレーションファイルとしてマークしビルド中に使うことができる

実行のアウトプットは次のようになる:

$ waf configure build
Setting top to     : /tmp/scenarios_impfile
Setting out to     : /tmp/scenarios_impfile/build
'configure' finished successfully (0.003s)
Waf: Entering directory `/tmp/scenarios_impfile/build'
[1/1] bar.txt: build/somedir/foo.txt -> build/bar.txt
Waf: Leaving directory `/tmp/scenarios_impfile/build'
'build' finished successfully (0.008s)

$ tree
.
|-- build
|   |-- bar.txt
|   |-- c4che
|   |   |-- build.config.py
|   |   `-- _cache.py
|   |-- config.log
|   `-- somedir
|       `-- foo.txt
`-- wscript

11.2. 混在する拡張子とC/C++のフィーチャー

11.2.1. 単一のタスクジェネレータによって処理されるファイル

ここでidlファイルの処理で@extensionデコレータを説明しよう。 拡張子.idlのファイルは処理され.cと.hファイルを生成する(foo.idlfoo.c + foo.h)。 .cファイルは生成された後にコンパイルされなくてはならない。

最初に、ユーザースクリプトで期待される宣言だ:

top = '.'
out = 'build'

def configure(conf):
    conf.load('g++')

def build(bld):
    bld.program(
        source = 'foo.idl main.cpp',
        target = 'myapp'
        )

ファイル foo.idl はソースとしてリストされる。 これは処理されて foo.cpp になり、コンパイルされて main.cpp とリンクされる。

これはこのシナリオをサポートするコードだ:

from waflib.Task import Task
from waflib.TaskGen import extension

class idl(Task):
    run_str = 'cp ${SRC} ${TGT[0].abspath()} && touch ${TGT[1].abspath()}' 1
    color   = 'BLUE'
    ext_out = ['.h'] 2

@extension('.idl')
def process_idl(self, node):
    cpp_node = node.change_ext('.cpp')
    hpp_node = node.change_ext('.hpp')
    self.create_task('idl', node, [cpp_node, hpp_node]) 3
    self.source.append(cpp_node) 4
1 デモのためのダミーのコマンド。実際にはルールは omniidl -bcxx ${SRC} -C${TGT} のようなものになる
2 idlタスクはヘッダを生成するので、 cpp ファイルがコンパイルされる前に実行されなくてはならない
3 拡張子 .idl からタスクを作成
4 C++コンパイラによってコンパイルされるように再度ファイルを追加

実行結果は次のようになる:

$ waf distclean configure build -v
'distclean' finished successfully (0.002s)
Setting top to   : /tmp/scenarios_idl
Setting out to   : /tmp/scenarios_idl/build
Checking for program g++,c++   : /usr/bin/g++
Checking for program ar        : /usr/bin/ar
'configure' finished successfully (0.072s)
Waf: Entering directory `/tmp/scenarios_idl/build'
[1/4] idl: foo.idl -> build/foo.cpp build/foo.hpp
19:47:11 runner 'cp ../foo.idl foo.cpp && touch foo.hpp'
[2/4] cxx: main.cpp -> build/main.cpp.0.o
19:47:11 runner ['/usr/bin/g++', '-I.', '-I..', '../main.cpp', '-c', '-o', 'main.cpp.0.o']
[3/4] cxx: build/foo.cpp -> build/foo.cpp.0.o
19:47:11 runner ['/usr/bin/g++', '-I.', '-I..', 'foo.cpp', '-c', '-o', 'foo.cpp.0.o']
[4/4] cxxprogram: build/main.cpp.0.o build/foo.cpp.0.o -> build/myapp
19:47:11 runner ['/usr/bin/g++', 'main.cpp.0.o', 'foo.cpp.0.o', '-o', 'myapp']
Waf: Leaving directory `/tmp/scenarios_idl/build'
'build' finished successfully (0.149s)

備考: この宣言による難点は、idlによって生成されたソースファイルは単一のタスクジェネレータからしか使うことができない点だ

11.2.2. 複数のタスクジェネレータによって共有されるリソース

複数のタスクジェネレータがidlのアウトプットを共有するとしよう。 まずはそれっぽいユーザースクリプトを書くことから始める:

top = '.'
out = 'out'

def configure(ctx):
    ctx.load('g++')

def build(ctx):
    ctx( 1
        source   = 'notify.idl',
        name     = 'idl_gen')

    ctx.program( 2
        source   = ['main.cpp'],
        target   = 'testprog',
        includes = '.',
        add_idl  = 'idl_gen') 3
1 最初のタスクジェネレータにあるidlファイルを処理する。このタスクジェネレータを idl_gen と名付ける
2 他のどこか(おそらく別のスクリプト)で、別のタスクジェネレータがidlの処理によって生成されたソースファイルを使う
3 名前 idl_gen でidlを処理するタスクジェネレータを参照する

このシナリオをサポートするコードは次のようになる:

from waflib.Task import Task
from waflib.TaskGen import feature, before_method, extension

class idl(Task):
        run_str = 'cp ${SRC} ${TGT[0].abspath()} && touch ${TGT[1].abspath()}'
        color   = 'BLUE'
        ext_out = ['.h'] 1

@extension('.idl')
def process_idl(self, node):
        cpp_node = node.change_ext('.cpp')
        hpp_node = node.change_ext('.hpp')
        self.create_task('idl', node, [cpp_node, hpp_node])
        self.more_source = [cpp_node] 2

@feature('*')
@before_method('process_source') 3
def process_add_source(self):
        for x in self.to_list(getattr(self, 'add_idl', [])): 4
                y = self.bld.get_tgen_by_name(x)
                y.post() 5
                if getattr(y, 'more_source', None):
                        self.source.extend(y.more_source) 6
1 C++タスクが実行される前にidlの処理は行われなくてはならない
2 アウトプットファイルを新たなアトリビュートにバインドする
3 別のタスクジェネレータオブジェクトからソースを追加する
4 add_idl を処理し他のタスクジェネレータを見つける
5 他のタスクジェネレータにタスクを作成させる
6 ソースのリストを更新する

タスク実行のアウトプットは最初の例のアウトプットとよく似たものとなるだろう:

$ waf distclean configure build -v
'distclean' finished successfully (0.007s)
Setting top to  : /tmp/scenarios_idl2
Setting out to  : /tmp/scenarios_idl2/build
Checking for program g++,c++    : /usr/bin/g++
Checking for program ar         : /usr/bin/ar
'configure' finished successfully (0.080s)
Waf: Entering directory `/tmp/scenarios_idl2/build'
[1/4] idl: foo.idl -> build/foo.cpp build/foo.hpp
20:20:24 runner 'cp ../foo.idl foo.cpp && touch foo.hpp'
[2/4] cxx: main.cpp -> build/main.cpp.1.o
20:20:24 runner ['/usr/bin/g++', '-I.', '-I..', '../main.cpp', '-c', '-o', 'main.cpp.1.o']
[3/4] cxx: build/foo.cpp -> build/foo.cpp.1.o
20:20:24 runner ['/usr/bin/g++', '-I.', '-I..', 'foo.cpp', '-c', '-o', 'foo.cpp.1.o']
[4/4] cxxprogram: build/main.cpp.1.o build/foo.cpp.1.o -> build/testprog
20:20:24 runner ['/usr/bin/g++', 'main.cpp.1.o', 'foo.cpp.1.o', '-o', 'testprog']
Waf: Leaving directory `/tmp/scenarios_idl2/build'
'build' finished successfully (0.130s)

11.3. タスクジェネレータメソッド

11.3.1. 特定のアトリビュートの置換

一般に、タスクジェネレータのアトリビュートは置換されず、次は main.c をコンパイルしない:

bld.env.FOO = '/usr/includes'
bld.env.MAIN = 'main.c'
bld(
    features = 'c cprogram',
    source   = '${MAIN}',
    target   = 'app',
    includes = '. ${FOO}')

このデザインの決定は2つの理由によるものだ:

  1. アトリビュートを処理するとパフォーマンスが悪化する

  2. 整合性のためにはすべてのアトリビュートが処理されるべきである

それにもかかわらず、いくつかのアトリビュートを処理するためにWafにメソッドを提供する方法を示す。 新たなタスクジェネレータメソッドを追加するには、他のメソッドとのインテグレーションについて考える必要がある: 特別な順序が存在するのか? 答えはyesで、例えば、ソースアトリビュートはコンパイルタスクを作成するために使われる。 何のメソッドが使われているかを表示するために、次のロギングキーをつけてWafを実行する:

$ waf --zones=task_gen
...
19:20:51 task_gen posting task_gen 'app' declared in 'scenarios_expansion' 1
19:20:51 task_gen -> process_rule (9232720) 2
19:20:51 task_gen -> process_source (9232720)
19:20:51 task_gen -> apply_link (9232720)
19:20:51 task_gen -> apply_objdeps (9232720)
19:20:51 task_gen -> process_use (9232720)
19:20:51 task_gen -> propagate_uselib_vars (9232720)
19:20:51 task_gen -> apply_incpaths (9232720)
19:20:51 task_gen posted app
1 タスクジェネレータの実行
2 メソッド名と()の中のタスクジェネレータID

メソッドのリストから、 process_ruleprocess_sourcesource アトリビュートを処理していることがわかる。 includes アトリビュートは apply_incpaths によって処理される。

from waflib import Utils, TaskGen
@TaskGen.feature('*') 1
@TaskGen.before('process_source', 'process_rule', 'apply_incpaths') 2
def transform_strings(self):
        for x in 'includes source'.split(): 3
                val = getattr(self, x, None)
                if val:
                        if isinstance(val, str):
                                setattr(self, x, Utils.subst_vars(val, self.env)) 4
                        elif isinstance(val, list):
                                for i in xrange(len(val)):
                                        if isinstance(val[i], str):
                                                val[i] = Utils.subst_vars(val[i], self.env)
1 すべてのタスクジェネレータでこのメソッドを実行する
2 考慮するメソッド
3 すべての興味のあるアトリビュートについてイテレートする
4 アトリビュートを代入する

11.3.2. 特別なインクルードフラグの挿入

C/C+プロジェクトではときどき、フラグが通常どのように処理されるかに係わらず、特定のフラグを他のフラグより前に挿入する必要がある。 次のケースを考慮する: フラグ ‘-I.’ を最初に(他のどのインクルードよりも前に)つけてすべてのC+のコンパイルを実行する。

最初に、C++のコンパイルルールの定義を見ると、変数 INCPATHS がインクルードフラグを含むことがわかる:

class cxx(Task.Task):
    color   = 'GREEN'
    run_str = '${CXX} ${CXXFLAGS} ${CPPPATH_ST:INCPATHS} ${CXX_SRC_F}${SRC} ${CXX_TGT_F}${TGT}'
    vars    = ['CXXDEPS']
    ext_in  = ['.h']
    scan    = c_preproc.scan

インクルードフラグはメソッド apply_incpaths によってセットされる。 そのメソッドが実行された後で INCPATHS を変更するのがトリックだ:

top = '.'
out = 'build'

def configure(conf):
    conf.load('g++')

def build(bld):
    bld.program(features='cxx cxxprogram', source='main.cpp', target='test')

from waflib.TaskGen import after, feature

@feature('cxx')
@after_method('apply_incpaths')
def insert_blddir(self):
    self.env.prepend_value('INCPATHS', '.')

関連するケースとして、コンフィギュレーションヘッダを含んだ最上位ディレクトリを追加する方法を示す:

@feature('cxx')
@after_method('apply_incpaths', 'insert_blddir')
def insert_srcdir(self):
    path = self.bld.srcnode.abspath()
    self.env.prepend_value('INCPATHS', path)

11.4. カスタムタスク

11.4.1. 特定のタスクのコンパイルを強制する

いくつかのアプリケーションでは、最終ビルドの日時を記録するとおもしろいことがある。 Cでは、これはマクロ ‘DATE’ と ‘TIME’ を使うとできる。 例えば、 about.c ファイルが次を含む:

void ping() {
    printf("Project compiled: %s %s\n", __DATE__, __TIME__);
}

ファイルは変更があるときにのみコンパイルされるので、 about.c を強制的に再コンパイルする方法を見つける必要がある。 まとめると、コンパイルは次のケースではいつでも行われるべきだ:

  1. プロジェクト中の一つのCファイルがコンパイルされた

  2. タスクのためのリンクフラグが変更された

  3. マクロのためのオブジェクトを含むリンクタスクが削除された

この振舞いを示すために、さまざまなCファイルを使うプロジェクトをセットアップする:

def options(opt):
    opt.load('compiler_c')

def configure(conf):
    conf.load('compiler_c')

def build(bld):
    bld.program(
        source   = 'main.c about.c',
        target   = 'app',
        includes = '.',
        use      = 'my_static_lib')

    bld.stlib(
        source   = 'test_staticlib.c',
        target   = 'my_static_lib')

mainファイルは日時を表示するために、 about.c で定義された関数 ping を呼び出すだけだ:

#include "a.h"

int main() {
    ping();
    return 0;
}

依存関係を考慮するために、タスクメソッド runnable_status は上書きされなくてはならない:

import os
from waflib import Task
def runnable_status(self):
    if self.inputs[0].name == 'about.c': 1
        h = 0 2
        for g in self.generator.bld.groups:
            for tg in g:
                if isinstance(tg, TaskBase):
                    continue 3

                h = hash((self.generator.bld.hash_env_vars(self.generator.env, ['LINKFLAGS']), h))
                for tsk in getattr(tg, 'compiled_tasks', []): # all .c or .cpp compilations
                    if id(tsk) == id(self):
                        continue
                    if not tsk.hasrun:
                        return Task.ASK_LATER
                    h = hash((tsk.signature(), h)) 4
        self.env.CCDEPS = h

        try:
            os.stat(self.generator.link_task.outputs[0].abspath()) 5
        except:
            return Task.RUN_ME

    return Task.Task.runnable_status(self) 6

from waflib.Tools.c import c 7
c.runnable_status = runnable_status
1 もしタスクが about.c を処理するならば
2 タスクが依存するハッシュ値(CCDEPS)を定義する
3 プロジェクトのすべてのタスクジェネレータについてイテレートする
4 リンクフラグと他のすべてのコンパイルタスクのシグネチャのハッシュ
5 まだ実行されていないのならば、タスクを実行する
6 通常の振舞い
7 c タスククラスを変更する

実行すると次のようなアウトプットが生成される:

$ waf
Waf: Entering directory `/tmp/scenarios_end/build'
[2/5] c: test_staticlib.c -> build/test_staticlib.c.1.o
[3/5] cstlib: build/test_staticlib.c.1.o -> build/libmy_static_lib.a
[4/5] c: about.c -> build/about.c.0.o
[5/5] cprogram: build/main.c.0.o build/about.c.0.o -> build/app
Waf: Leaving directory `/tmp/scenarios_end/build' 1
'build' finished successfully (0.088s)

$ ./build/app
Project compiled: Jul 25 2010 14:05:30

$ echo " " >> main.c 2

$ waf
Waf: Entering directory `/tmp/scenarios_end/build'
[1/5] c: main.c -> build/main.c.0.o
[4/5] c: about.c -> build/about.c.0.o 3
[5/5] cprogram: build/main.c.0.o build/about.c.0.o -> build/app
Waf: Leaving directory `/tmp/scenarios_end/build'
'build' finished successfully (0.101s)

$ ./build/app
Project compiled: Jul 25 2010 14:05:49
1 すべてのファイルは最初のビルドでコンパイルされる
2 ファイル main.c を変更する
3 ビルド時刻の文字列を更新するためにビルドは再度 about.c を生成する

11.4.2. 事前に名前のわからないソースファイルを生成するコンパイラ

この問題の要求点は次のようになる:

  1. コンパイラは ソースファイルを作成する (1つの .src ファイル → 複数の .c ファイル)

  2. 作成されるソーフファイルの名前は コンパイラが実行されたときにのみわかる

  3. コンパイラは遅いので 必要なときにのみ 実行される

  4. 他のタスクは 生成されたファイルに依存する (.cファイルをコンパイルしリンクして実行形式を作る)

これを満すために、ソースファイルに関する情報はビルドの実行のをまたがって共有されなくてはならない:

top = '.'
out = 'build'

def configure(conf):
    conf.load('gcc')
    conf.load('mytool', tooldir='.')

def build(bld):
    bld.env.COMP = bld.path.find_resource('evil_comp.py').abspath() 1
    bld.stlib(source='x.c foo.src', target='astaticlib') 2
1 コンパイラへのパス
2 .src ファイルをもつ例

mytool の中身は次のようになる:

import os
from waflib import Task, Utils, Context
from waflib.Utils import subprocess
from waflib.TaskGen import extension

@extension('.src')
def process_shpip(self, node): 1
    self.create_task('src2c', node)

class src2c(Task.Task):
    color = 'PINK'
    quiet = True 2
    ext_out = ['.h'] 3

    def run(self):
        cmd = '%s %s' % (self.env.COMP, self.inputs[0].abspath())
        n = self.inputs[0].parent.get_bld()
        n.mkdir()
        cwd = n.abspath()
        out = self.generator.bld.cmd_and_log(cmd, cwd=cwd, quiet=Context.STDOUT) 4

        out = Utils.to_list(out)
        self.outputs = [self.generator.path.find_or_declare(x) for x in out]
        self.generator.bld.raw_deps[self.uid()] = [self.signature()] + self.outputs 5
        self.add_c_tasks(self.outputs) 6

    def add_c_tasks(self, lst):
        self.more_tasks = []
        for node in lst:
            if node.name.endswith('.h'):
                continue
            tsk = self.generator.create_compiled_task('c', node)
            self.more_tasks.append(tsk) 7

            tsk.env.append_value('INCPATHS', [node.parent.abspath()])

            if getattr(self.generator, 'link_task', None): 8
                self.generator.link_task.set_run_after(tsk)
                self.generator.link_task.inputs.append(tsk.outputs[0])

    def runnable_status(self):
        ret = super(src2c, self).runnable_status()
        if ret == Task.SKIP_ME:

            lst = self.generator.bld.raw_deps[self.uid()]
            if lst[0] != self.signature():
                return Task.RUN_ME

            nodes = lst[1:]
            for x in nodes:
                try:
                    os.stat(x.abspath())
                except:
                    return Task.RUN_ME

            nodes = lst[1:]
            self.set_outputs(nodes)
            self.add_c_tasks(nodes) 9

        return ret
1 処理はタスクに移譲される
2 タスクのアウトプットがない場合、警告を無効にする
3 .h ファイルを使うタスクの前に処理が実行されるようにする
4 タスクが実行されたときに、生成されたファイル名を含んでいる、プロセスの標準出力を収集する
5 アウトプットファイルのノードを永続化キャッシュへ格納する
6 アウトプットをコンパイルするためにタスクを作る
7 Cタスクは現在のタスクが終わった後で処理される。これはCタスクが常に実行されるわけではないことを意味する
8 src ファイルのタスクジェネレータがリンクタスクをもつならば、ビルド順序をセット
9 このタスクがスキップされたときに、強制的に動的なCタスクを作成

アウトプットは次のようになる:

$ waf distclean configure build build
'distclean' finished successfully (0.006s)
Setting top to  : /tmp/scenarios_unknown
Setting out to  : /tmp/scenarios_unknown/build
Checking for program gcc,cc              : /usr/bin/gcc
Checking for program ar                  : /usr/bin/ar
'configure' finished successfully (0.115s)
Waf: Entering directory `/tmp/scenarios_unknown/build'
[1/3] src2c: foo.src
[2/5] c: build/shpip/a12.c -> build/shpip/a12.c.0.o
[3/5] c: build/shpop/a13.c -> build/shpop/a13.c.0.o
[4/5] c: x.c -> build/x.c.0.o
[5/5] cstlib: build/x.c.0.o build/shpip/a12.c.0.o build/shpop/a13.c.0.o -> build/libastaticlib.a
Waf: Leaving directory `/tmp/scenarios_unknown/build'
'build' finished successfully (0.188s)
Waf: Entering directory `/tmp/scenarios_unknown/build'
Waf: Leaving directory `/tmp/scenarios_unknown/build'
'build' finished successfully (0.013s)

12. 開発バージョンの使用

Wafの開発フローに関するいくつかのノート。

12.1. 実行のトレース

12.1.1. ロギング

スタックトレースやメッセージに情報を追加する一般的なフラグは -v (verbosity)で、ビルド中に実行されるコマンドラインの表示に使われる:

$ waf -v

すべてのトレース(バグレポートに便利)を表示するには、次のフラグを使う:

$ waf -vvv

zones フラグで簡単にデバッグ情報をフィルターできる:

$ waf --zones=action

トレースするゾーンはカンマ区切でなくてはならない、例えば:

$ waf --zones=action,envhash,task_gen

Wafの Logs モジュールはPythonのloggingモジュールを置換する。 ソースコードでは、トレースは debug 関数を使うことで与えられ、次のように"zone: message"のフォーマットに従わなくてはならない:

Logs.debug("task: executing %r - it was never run before or its class changed" % self)

Wafでは次のゾーンが使われる:

Table 2. デバッグゾーン
ゾーン 説明

runner

実行されるコマンドライン(デバッグゾーンなしで-vが与えられると有効になる)

deps

暗黙の依存関係の検出(タスクスキャナ)

task_gen

(タスクジェネレータからの)タスク生成とタスクジェネレータメソッドの実行

action

ターゲットのビルドを実行するための関数

env

環境オブジェクト

envhash

環境オブジェクトのハッシュ - 変化したものを確認するのを助ける

build

ファイルシステムへのアクセスなどのビルドコンテキストのオペレーション

preproc

プリプロセッサの実行

group

グループとタスクジェネレータ

警告: デバッグ情報はコマンドラインが解析された後にのみ表示することができる。例えば、コマンドラインオプション opt.load() もしくはグローバルな初期化関数 init.tool() によってWafツールが存在する場合にはデバッグ情報は表示されない。

12.1.2. ビルドの視覚化

parallel_debug という名前のWafツールはWafモジュールにコードをインジェクションするために使われ、詳細な実行トレースを取得する。 このモジュールはフォルダ waflib/extras で提供され、使用前にプロジェクトでインポートされなくてはならない:

def options(ctx):
        ctx.load('parallel_debug', tooldir='.')

def configure(ctx):
        ctx.load('parallel_debug', tooldir='.')

def build(ctx):
        bld(rule='touch ${TGT}', target='foo')

実行するとファイル pdebug.svg にビルド中に実行されたタスクのダイアグラムを生成する:

並列実行ダイアグラム

詳細はスペース区切の値としてファイル pdebug.dat に生成される。 このファイルは他のダイアグラムを得るためにGnuplotなどの他のアプリケーションによって処理することができる:

#! /usr/bin/env gnuplot
set terminal png
set output "output.png"
set ylabel "Amount of active threads"
set xlabel "Time in seconds"
set title "Active threads on a parallel build (waf -j5)"
unset label
set yrange [-0.1:5.2]
set ytic 1
plot 'pdebug.dat' using 3:7 with lines title "" lt 2
ビルド中のスレッドアクティビティ

データファイルフォーマットは次のようになる:

Table 3. pdebugファイルフォーマット
カラム 説明

1

int

開始もしくは終了したタスクをもつスレッドの識別子

2

int

処理されるタスクの識別子

3

float

イベント時刻

4

string

処理されるタスクの型

5

int

処理されるタスク数

6

int

タスクコンシューマによって処理されるのを待機しているタスク数

7

int

アクティブなスレッド数

12.2. プロファイリング

12.2.1. プロジェクトのベンチマーク

スクリプト utils/genbench.py は巨大なCライクなプロジェクトファイルを作るベースとなる。 一般的な使い方は次のようになる:

$ utils/genbench.py /tmp/build 50 100 15 5
$ cd /tmp/build
$ waf configure
$ waf -p -j2

作成されたC++プロジェクトは100のクラスからそれぞれ50のライブラリを生成し、それぞれのソースファイルは同一のライブラリを指し示す15のインクルードヘッダと5つのランダムに選ばれた他のヘッダを指し示すヘッダをもつ。

コンパイル時間は実際のコンパイルを無効にすることで簡単に捨てることができる。 例:

def build(bld):
        from waflib import Task
        def touch_func(task):
                for x in task.outputs:
                        x.write('')
        for x in Task.TaskBase.classes.keys():
                cls = Task.TaskBase.classes[x]
                cls.func = touch_func
                cls.color = 'CYAN'

12.2.2. トレースのプロファイリング

プロファイリングの情報はcProfileモジュールを呼出し、特定のコードを埋め込むことで取得される。 プロファイリングのためのもっとも興味深いメソッドは waflib.Build.BuildContext.compile だ。 関数呼出しの数は通常ボトルネックとなり、減らすことで大きなスピードアップが得られる。 これはコンパイルメソッドでの例だ:

from waflib.Build import BuildContext
def ncomp(self):
        import cProfile, pstats
        cProfile.runctx('self.orig_compile()', {}, {'self': self}, 'profi.txt')
        p = pstats.Stats('profi.txt')
        p.sort_stats('time').print_stats(45)

BuildContext.orig_compile = BuildContext.compile
BuildContext.compile = ncomp

これは前のセクションで説明したビルドのベンチマークのアウトプットだ:

Fri Jul 23 15:11:15 2010    profi.txt

         1114979 function calls (1099879 primitive calls) in 5.768 CPU seconds

   Ordered by: internal time
   List reduced from 139 to 45 due to restriction 45

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   109500    0.523    0.000    1.775    0.000 /comp/waf/waflib/Node.py:615(get_bld_sig)
     5000    0.381    0.000    1.631    0.000 /comp/waf/waflib/Task.py:475(compute_sig_implicit_deps)
   154550    0.286    0.000    0.286    0.000 {method 'update' of '_hashlib.HASH' objects}
   265350    0.232    0.000    0.232    0.000 {id}
40201/25101    0.228    0.000    0.228    0.000 /comp/waf/waflib/Node.py:319(abspath)
    10000    0.223    0.000    0.223    0.000 {open}
    20000    0.197    0.000    0.197    0.000 {method 'read' of 'file' objects}
    15000    0.193    0.000    0.349    0.000 /comp/waf/waflib/Task.py:270(uid)
    10000    0.189    0.000    0.850    0.000 /comp/waf/waflib/Utils.py:96(h_file)

既知のいくつかのホットスポットがライブラリに存在する:

  1. cPickleモジュールによって実装された永続化(シリアライズのためのキャッシュファイルは数メガバイト消費することがある)

  2. Environmentインスタンスからのコンフィギュレーションデータへのアクセス

  3. 一般的な暗黙の依存関係の計算

12.2.3. 最適化のTips

wafのソースコードはすでにさまざまな方法で最適化が施されている。 実際、プロジェクトでは追加的な想定が使われておりビルドスクリプトのあるメソッドやパラメータは置換される。 例えば、Windowsで実行されるならば、変数 frameworkrpath は削除される:

from waflib.Tools.ccroot import USELIB_VARS
USELIB_VARS['cprogram'] = USELIB_VARS['cxxprogram'] = \
        set(['LIB', 'STLIB', 'LIBPATH', 'STLIBPATH', 'LINKFLAGS', 'LINKDEPS'])

12.3. Wafプログラミング

12.3.1. 開発用のWafディレクトリのセットアップ

Wafは Google code でホスティングされており、ソースコントロールにSubversionを使用している。 開発バージョンのコピーを得るには、次を使用:

$ svn checkout http://waf.googlecode.com/svn/trunk/ waf-read-only
$ cd waf-read-only
$ ./waf-light --make-waf

Wafを毎回生成するのを避けるために、環境変数 WAFDIRwaflib を含むディレクトリを指し示すべきだ:

$ export WAFDIR=/path/to/directory/

12.3.2. 特定のガイドライン

Wafはpythonで書かれているが、ソースコードには他にも追加で制限が適用されている:

  1. インデントはタブのみで1行の最大の文字数は200

  2. 開発コードはToolsディレクトリ以下のデコレータは例外だが、Python2.3との互換性を保っている。特にWafのバイナリはPython2.3で生成できる

  3. Wafのコアを小さく、言語から独立に保つために waflib モジュールは Tools モジュールから隔離されなくてはならない

  4. APIの互換性はマイナーバージョンのサイクルでメンテナンスされる(1.5.0から1.5.n)

備考: コードが多くなると常にバグが増える。可能ならばいつでも不要なコードは削除されなくてはならないし、存在するコードベースはシンプルであるべきだ。

13. Wafアーキテクチャ概要

この章では、Wafライブラリとコンポーネント間のインタラクションについて記述する。

13.1. モジュールとクラス

13.1.1. コアとなるモジュール

Wafはコアライブラリを構成する次のモジュールにより構成される。 これらのモジュールは waflib/ ディレクトリに置かれている。 waflib/Toolswaflib/extras に配置されているモジュールはWafのコアモジュールには含まれない拡張モジュールだ。

Table 4. コアモジュールのリスト
モジュール 役割

Build

ひとつのビルドに対するデータ(パス、コンフィギュレーションデータ)を保持するビルドコンテキストクラス(build, clean, install, uninstall)の定義

Configure

コンフィギュレーションコンテキストクラスを含む。言語のコンフィギュレーションテストおよびビルド用のコンフィギュレーションセッティングの記述のために使われる

ConfigSet

軽量のコピースキームをサポートし、永続サービスを提供するディクショナリクラスを提供する

Context

すべてのWafコマンドのベースクラスを含む (Wafコマンドのコンテキストパラメータ)

Errors

Wafのコードで使われる例外

Logs

Pythonのloggingモジュールの呼出をラップするロギングシステム

Node

ファイルシステムを表現するクラスを含む

Options

optparseを元にした、カスタムのコマンドラインを処理するシステムを提供する

Runner

タスク実行システムを含む (スレッドベースのプロデューサ-コンシューマ)

Scripting

Wafのエントリポイント、ビルドなどのユーザーコマンドの実行、コンフィギュレーション、インストールを構成する

TaskGen

タスクジェネレータシステムとメソッドの追加に基づくその実行システムを提供する

Task

タスククラスの定義と新たなタスククラスを作るためのファクトリ関数を含む

Utils

他のWafモジュールから使われる補助関数およびクラスを含む

Wafをライブラリとして使うためにすべてのコアモジュールが必要というわけではない。 モジュール間の依存関係は次のダイアグラムにより表現される。 例えば、 Node モジュールは UtilsErrors モジュールを必要とする。 逆に、 Build モジュールが単独で使われるならば 、 ScriptingConfigure モジュールは安全に取り除くことができる。

モジュールの依存関係

13.1.2. コンテキストクラス

configurebuild などのユーザーコマンドは waflib.Context.Context から派生したクラスとして表現される。 関連付けられたクラスがないコマンドの場合、 waflib.Context.Context が替わりに使われる。

execute メソッドはコンテキスト実行のスタートポイントで、ユーザーのスクリプトを読込み、 fun クラスアトリビュートで参照される関数の実行を開始する recurse メソッドを度々呼び出す。

コマンドは cmd クラスアトリビュートによってコンテキストクラスに関連付けられる。 コンテキストサブクラスは store_context メタクラスによって waflib.Context.classes に追加され、 waflib.Context.create_context を通してロードされる。 最後に定義されたクラスによって既存のコマンドを上書きされる。

例のように、次のコンテキストクラスは configure コマンドを定義または上書きする。 waf configure を呼び出す場合、wscriptから foo 関数が呼び出される:

from waflib.Context import Context
class somename(Context):
    cmd = 'configure'
    fun = 'foo'
コンテキストクラス

13.1.3. ビルドクラス

クラス waflib.Build.BuildContextwaflib.Build.InstallContextwaflib.Build.StepContext のような、そのサブクラスはユーザースクリプトを読んだときに作られるタスクジェネレータをもつ。 タスクジェネレータは通常タスクインスタンスをもち、すべてのタスクジェネレータ処理後に実行されるオペレーションに依存する。

ConfigSet インスタンスはタスク(waflib.ConfigSet.ConfigSet.derive)に対するビルドコンテキストからコピーされ、コンフィギュレーションフラグのような値が伝搬する。 コピーオンライトはそのクラスのほとんどのメソッド(append_value, prepend_value, append_unique)を通じて実行される。

Parallel オブジェクトはビルドコンテキストのすべてのタスクに対するイテレーションをカプセル化し、スレッドオブジェクトに実行を移譲する(producer-consumer)。

全体の構造は次のダイアグラムに表現される:

ビルドクラス

13.2. コンテキストオブジェクト

13.2.1. コンテキストコマンドと再帰

コンテキストコマンドは可能な限り独立なものとして設計されており、並列に実行することができる。 主な適用例はコンフィギュレーションテストの一部として小さなビルドを実行することである。 例えば、メソッド waflib.Tools.c_config.run_c_code はテストを実行するためにプライベートなビルドコンテキストを内部的に作成する。 これは単純なコンフィギュレーションコンテキストの作成と実行を並列に行うビルドの例だ:

import os
from waflib.Configure import conf, ConfigurationContext
from waflib import Task, Build, Logs

def options(ctx):
        ctx.load('compiler_c')

def configure(ctx):
        ctx.load('compiler_c')

def build(ctx):
        ctx(rule=run_test, always=True, header_name='stdio.h') 1
        ctx(rule=run_test, always=True, header_name='unistd.h')

def run_test(self):
        top = self.generator.bld.srcnode.abspath()
        out = self.generator.bld.bldnode.abspath()

        ctx = ConfigurationContext(top_dir=top, out_dir=out) 2
        ctx.init_dirs() 3

        ctx.in_msg = 1 4
        ctx.msg('test') 5

        header = self.generator.header_name
        logfile = self.generator.path.get_bld().abspath() + os.sep \
                + header + '.log'
        ctx.logger = Logs.make_logger(logfile, header) 6

        ctx.env = self.env.derive() 7
        ctx.check(header_name=header) 8
1 下で定義された run_test メソッドを実行するタスクジェネレータの作成
2 Task.run の呼出しの一部である新たなコンフィギュレーションコンテキストの作成
3 ctx.srcnodeとctx.bldnodeの初期化 (ビルドコンテキストとコンフィギュレーションコンテキストのみ)
4 メソッド msgstart_msgend_msg への内部カウンタを設定
5 コンソール出力を無効化(カウンター値が非ゼロでネストされたメッセージの無効化)
6 それぞれのコンテキストはエラーメッセージに対してリダイレクトするロガーをもつことができる。
7 コピーされたタスクに対するデフォルト環境の初期化
8 コンフィギュレーションチェックの実行

waf build の実行後、プロジェクトフォルダは新しいログファイルができる:

$ tree
.
|-- build
|   |-- c4che
|   |   |-- build.config.py
|   |   `-- _cache.py
|   |-- config.log
|   |-- stdio.h.log
|   `-- unistd.h.log
`-- wscript

コンテキストが並列に実行されることを保証するために、いくつかのmeasuresが設定されている:

  1. コンテキストオブジェクトは waflib.Logs モジュールから派生した異なるロガーを使うことができる。

  2. それぞれのコンテキストオブジェクトは waflib.Node.Node のプライベートなサブクラスに関連付けられており、ノードオブジェクトがユニークであることが保証される。 ノードオブジェクトをピックル化するために、ロックオブジェクト waflib.Node.pickle_lock によって並行アクセスを避けることが重要である。

13.2.2. ビルドコンテキストと永続性

ビルドコンテキストはビルドに必要なすべての情報を保持する。 起動を高速化するために、一部の情報は起動の合間で保存され読み込まれる。 永続化されるアトリビュートを次に示す:

Table 5. Persistent attributes
アトリビュート Description タイプ

root

ファイルシステムのルートを表現するノード

ノード

node_deps

暗黙の依存性

ノードとシグネチャを結びつけるdict

raw_deps

解決できない暗黙のファイル依存性

ノードIDとシリアライズの型を結びつけるdict

task_sigs

実行されるタスクのシグネチャ

タスクの計算されたuidとハッシュを結びつけるdict

13.3. c-likeな言語のサポート

13.3.1. コンパイルタスクとリンクタスク

waflib.Tools.ccroot はオブジェクトファイルを生成し、それらをひとつの最終的なファイルにリンクするシステムを提供する。 メソッド waflib.Tools.ccroot.apply_link はメソッド waflib.TaskGen.process_source の後に呼び出されリンクタスクを生成する。 疑似コード:

call the method process_source:
  for each source file foo.ext:
    process the file by extension
      if the method create_compiled_task is used:
        create a new task
        set the output file name to be foo.ext.o
        add the task to the list self.compiled_tasks

call the method apply_link
  for each name N in self.features:
    find a class named N:
      if the class N derives from 'waflib.Tools.ccroot.link_task':
        create a task of that class, assign it to self.link_task
        set the link_task inputs from self.compiled_tasks
        set the link_task output name to be env.N_PATTERN % self.target
        stop

このシステムは assembly, C, C++, Dfortran にデフォルトで使われる。 ちなみにメソッド apply_link はメソッド process_source の後で呼ばれることが想定されている。

ここで、ミニ言語のサポートの仕方を示そう:

cp: .ext -> .o
cat: *.o -> .exe

これがプロジェクトファイルだ:

def configure(ctx):
        pass

def build(ctx):
        ctx(features='mylink', source='foo.ext faa.ext', target='bingo')

from waflib.Task import Task
from waflib.TaskGen import feature, extension, after_method
from waflib.Tools import ccroot 1

@after_method('process_source')
@feature('mylink')
def call_apply_link(self): 2
        self.apply_link()

class mylink(ccroot.link_task): 3
        run_str = 'cat ${SRC} > ${TGT}'

class ext2o(Task):
        run_str = 'cp ${SRC} ${TGT}'

@extension('.ext')
def process_ext(self, node):
        self.create_compiled_task('ext2o', node) 4
1 このインポートは create_compiled_taskapply_link_task のようなメソッドをバインドする
2 代替の定義は waflib.TaskGen.feats[‘mylink’] = [‘apply_link’] を呼ぶ
3 リンクタスクは他のリンクタスククラスのサブクラスでなくてはならない
4 create_compiled_task メソッドの呼出し

実行結果は次のようになる:

$ waf distclean configure build -v
'distclean' finished successfully (0.005s)
Setting top to   : /tmp/architecture_link
Setting out to   : /tmp/architecture_link/build
'configure' finished successfully (0.008s)
Waf: Entering directory `/tmp/architecture_link/build'
[1/3] ext2o: foo.ext -> build/foo.ext.0.o
12:50:25 runner ['cp', '../foo.ext', 'foo.ext.0.o']
[2/3] ext2o: faa.ext -> build/faa.ext.0.o
12:50:25 runner ['cp', '../faa.ext', 'faa.ext.0.o']
[3/3] mylink: build/foo.ext.0.o build/faa.ext.0.o -> build/bingo
12:50:25 runner 'cat foo.ext.0.o faa.ext.0.o > bingo'
Waf: Leaving directory `/tmp/architecture_link/build'
'build' finished successfully (0.041s)

備考: タスクジェネレータのインスタンスは最大1つのリンクタスクのインスタンスを持つ

13.4. 再利用可能なwafツールを書く

13.4.1. Wafツールの追加

コードのインポート

Wafツールの目的は、すべての概念的に関連したメソッドとクラスを分離されたファイルに移動し、Wafのコアから隠蔽し、可能な限り互いを独立させることで高い凝縮度をすすめることだ。

カスタムのWafツールはプオジェクトに残すことができ、waflib/extras フォルダもしくは sys.path の変更によってカスタムのWafファイルに追加することができる。

ツールは import キーワードによって直接他のツールをインポートすることができる。 しかしながら、カップリングを制限するために常に ctx.load にツールをインポートするべきである。 例を比較:

def configure(ctx):
    from waflib.extras.foo import method1
    method1(ctx)

そして:

def configure(ctx):
    ctx.load('foo')
    ctx.method1()

method1 がモジュール foo からきたのか否か、モジュール foo が存在する場所、それぞれに関しての想定が少ないため、2番目のバージョンの方が望ましい。

C/C++/Fortranの命名の慣習

ツール compiler_ccompiler_cxxcompiler_fc は特定のコンパイラの検出のために他のWafツールを使う。 ユーザースクリプトで新しいツールにそれ自身を自動的に登録しインポートを保存する機会を与えるために、特定の命名の慣習を提供する。 名前が c_cxx_ もしくは fc_ ではじまるツールがテストされる。

レジストレーションコードは次のようなものになるだろう:

from waflib.Tools.compiler_X import X_compiler
X_compiler['platform'].append('module_name')

ここで X はコンパイラのタイプ(c, cxxfc)を表し, platform は判定が行われるプラットフォーム(linux, win32, など)を表し、module_name はツールの名前を表す。

13.4.2. コマンドメソッド

サブクラス化はコマンドのためだけ

一般的なルールとして、 waflib.Context.Context のサブクラスは新しいコマンドが必要なときにのみ作られる。 これは特定のvariant(アウトプットフォルダ)向けのコマンドが必要な場合や、新たな振舞いを提供する例でのケースだ。 このことが起きる場合、クラスメソッド recurseexecute やクラスアトリビュート cmdfun は通常上書きされる。

備考: もし新しいコマンドが必要ないならばサブクラス化は使わなくてよい

エンドユーザーへの利便性のためのドメイン固有のメソッド

Wafフレームワークはタスクジェネレータを宣言する最大限柔軟な方法を推進するが、巨大なプロジェクトではドメイン固有のラッパーを宣言することがしばしばより便利なことがある。 例えば、sambaプロジェクトでは次のように使われる関数を提供する:

bld.SAMBA_SUBSYSTEM('NDR_NBT_BUF',
    source    = 'nbtname.c',
    deps      = 'talloc',
    autoproto = 'nbtname.h'
    )
新しいメソッドへのバインド方法

新しいメソッドは @conf デコレータを使って、ビルドコンテキストもしくはコンフィギュレーションコンテキストに共通にバインドされる:

from waflib.Configure import conf

@conf
def enterprise_program(self, *k, **kw):
    kw['features'] = 'c cprogram debug_tasks'
    return self(*k, **kw)

def build(bld):
    # no feature line
    bld.enterprise_program(source='main.c', target='app')

サブクラス化では異なる目的のために書かれたツール間での衝突が起り得るため、メソッドは常にこのような方法か手動でバインドするべきだ。

14. Further reading

Wafが提供する機能が多いため、この本では機能を網羅し最新の情報を伝えることはできない。 さらなる理解と実践のために、読者に次のリンクを推奨する:

Table 6. Recommended links
リンク 説明

http://docs.waf.googlecode.com/git/apidocs_16/index.html

APIドキュメント

http://code.google.com/p/waf

Wafプロジェクトのページ

http://code.google.com/p/waf/w/list

FAQを含むWafのwiki

http://groups.google.com/group/waf-users

Wafのメーリングリスト

http://waf-devel.blogspot.com/2011/01/python-32-and-build-system-kit.html

ビルドシステムキットに関する情報

15. 用語集

ビルド順序

ビルド順序はタスクが実行される順序だ。タスクは並列に実行される可能性があり、ビルド順序はタスク間の制約から計算することができる。(通常、矛盾する順序制約によって)ビルド順序が計算できない場合、ビルドはデッドロックの状態にあるといわれる。

依存関係

依存関係はタスクが最新の状態とみなせるか否かの条件を表す(実行ステータス)。依存関係は明示的(ファイルのインプットとアウトプット)もしくは抽象的(例えば値に対する依存関係)だ。

タスクジェネレータ

タスクジェネレータはTask.task_genクラスのインスタンスだ。タスクジェネレータはさまざまなタスクインスタンスの生成をカプセル化し、それらのタスク間の順序制約の生成を簡略化する(例えば、コンパイルタスクはリンクタスクの前に実行される)。

タスク

WafのタスクはTask.TaskBaseクラスのインスタンスだ。Wafのタスクは単純なもの(Task.TaskBase)、もしくはファイルシステムに関連付けられている(Task.Task)。タスクはビルドでの成果物(一般的にファイル)を表現し、(順序制約により)シーケンシャルもしくは並列に実行される。

ツール

WafツールはWaf特有の拡張を含んだPythonモジュールだ。Wafツールは waflib/Tools/ フォルダに存在し、通常、configureで実行される関数を参照するグローバル変数 configure を含む。

ノード

Nodeクラスはファイルシステムを効率的に表現するために使われるデータ構造だ。ノードオブジェクトはファイルもしくはフォルダだ。ファイルノードはシグネチャオブジェクトと関連付けられている。シグネチャはファイルの中身のハッシュ(ソースファイル)かタスクシグネチャ(ビルドファイル)だ。

コマンド

トップレベルのプロジェクトファイル(wscript)に存在する関数で waflib.Context.Context インスタンスを唯一の入力パラメータとして受け付ける。関数名がコマンドラインで与えられるとその関数が実行される(例えば waf configure を実行すると関数 configure が実行される)

バリアント

異なるコンパイルフラグで同じターゲットを生成するための(ビルド)コマンドを有効にするために使われる、追加のアウトプットディレクトリ