3. OMakeビルドサンプル

この章ではOMakeのビルド体系をさらにもう少し解説します。この議論を決定するただ一つの結論としては、OMakeは全体のプロジェクト解析を元にしているということです。これはあなたがプロジェクト全体の設定を決めて、そしてOMakeのインスタンスを実行することを意味しています。

一つのディレクトリで構成されたプロジェクトではあまり意味がないかもしれませんが、多数のディレクトリからなるプロジェクトでは大きな意味を持ちます。 GNU make では、あなたは通常、プロジェクトの各々のディレクトリにおいて make プログラムを再帰的に呼び出すでしょう。例えば、あなたが現在サブディレクトリ libmain が入っているソースディレクトリ src を含んでいるプロジェクトをいくつか持っているものとしましょう。具体的には、あなたのプロジェクト構成は以下のアスキーアートのようになります。

my_project/
|--> Makefile
`--> src/
     |---> Makefile
     |---> lib/
     |     |---> Makefile
     |     `---> source files...
     `---> main/
           |---> Makefile
           `---> source files...

一般的に GNU make では、初めに my_project/make インスタンスを呼び出します。この make インスタンスは src/ ディレクトリ内の make インスタンスを呼び出し、そして lib/main/ の新しいインスタンスを呼び出します。つまり、 GNU make では単純に、プロジェクト内の Makefile の数だけ make インスタンスが生成されることになります。

大量のプロセスを処理することは今日のコンピュータにとってさほど大きな問題ではありません(ときどき著者の意見に反対する人もいるようですが、私たちはもはや1970年代に住んでいるわけではないのです)。この構造に関する問題としては、各々の make プロセスの設定が分離されており、そしてそのすべてが調和のとれたものにするには、非常に多くの負担となってしまう点です。さらには、例えばプログラマが main/ ディレクトリで make プログラムを実行するが、 lib/ はもう時代遅れの代物であった場合を考えてみましょう。この場合、 make は楽しそうにあちこち曲がりくねった挙句、恐らく lib/ 内のファイルをリビルドしようと奮起して、恐らく諦めることとなるでしょう。

OMakeではこの構造を抜本的に変更します。とは言っても実際の変更点はそれほどありません。ソース構造は非常に似通っています。私たちは単純にいくつかの”O”を以下のアスキーアートのように加えただけです。

my_project/
|--> OMakeroot   (or Root.om)
|--> OMakefile
`--> src/
     |---> OMakefile
     |---> lib/
     |     |---> OMakefile
     |     `---> source files...
     `---> main/
           |---> OMakefile
           `---> source files...

各々の <dir>/OMakefile の役割は各々の <dir>/Makefile の役割と同様で、どのように <dir> のソースファイルをビルドするのかについて設定します。 OMakefileMakefile の構造や構文を保持しておりますが、ほとんどの場合 make よりも簡単に記述することができます。

一つ小さな違いがあるとすれば、プロジェクトのルートディレクトリに OMakeroot が存在している点です。このファイルの主な目的は、第一にプロジェクトのルートディレクトリがどこにあるか示すことです(omakeがサブディレクトリから呼び出されたときのためです)。 OMakeroot はブートストラップとして働きます。omakeはこのファイルを最初に読み込んで実行されます。それ以外では、 OMakeroot の構文と機能は他の OMakefile と全く同様です。

大きな 違いは、OMakeは グローバルな 解析を行うという点です。以下はどのようにomakeが動作するのかについて示します。

  1. omakeは OMakeroot ファイルがあるディレクトリに移動し、読んでいきます。

  2. 各々の OMakefile.SUBDIRS ターゲットを使用して OMakefile があるサブディレクトリを指し示します。例えば、 my_project/OMakefile は以下のルールを持っていたとします。

    .SUBDIRS: src
    
omakeはこれらのルールを読んで、プロジェクト内のすべての OMakefile を評価します。読み込みや評価は高速に行われるので、このプロセスは早期に終わります。
  1. 全体の設定が読まれたら、omakeはどのファイルが使われていないのかを決定し(グローバルな解析を使用します)、ビルド作業を開始します。これはどのファイルのビルドが実際に必要なのかに依存した、ビルド時間がかかります。

このモデルではいくつかの利点があります。初めに、解析をグローバルにしたことで、単一のビルド設定が用いられることとなり、ビルド設定を一定にするよう保証することがより簡単になるという点です。別の利点はビルドに関する設定が継承されて、再利用可能となり、さらに階層構造となる点です。概して、ルートの OMakefile はいくつかの標準的な決まり文句と設定を定義しており、これはサブディレクトリによって継承されて、調整したり、変更することができます(全体を書き換える必要はありません)。この方法の欠点は容量が増大することで、これは結局グローバルな解析を行っているためです。が、実際にはめったに考慮する必要があるようには見えません。OMakeは大きなプロジェクトにおいてでさえ、あなたが使っているウェブブラウザよりもはるかに小さい容量しか使いません。

GNU/BSDのmakeユーザは以下の点に留意してください。

  • OMakeは Makefile と同じくらい多くのファイルを作ります。構文は似ており、makeと同様に多くのビルドイン関数が用意されています。しかしながら、この2つのビルドシステムは同じではありません。OMakeではいくつかの酷い機能(これは著者の意見です)が外されており、それに代わって新しい機能が追加されています。
  • OMakeはWin32を含んだ、複数のプラットフォーム上で同様に動きます。あなたは複数のプラットフォーム上で動かすためにコードを変更したり、いくつかのトリッキーなテクを使ったり、 $(OSTYPE) 変数を使ってビルド設定を調節する必要はありません。
  • OMakeの依存関係の解析はMD5によるファイルの要約(digest)を元にしています。これはつまり、依存関係の解析はファイルの『修正日時』ではなくファイルの『内容』を元にしていることを表しています。さあ、ローカル日時とファイルサーバの日時から生じるミスマッチや、間違ったタイムスタンプの変更によるビルドミスからおさらばしましょう。

3.1 OMakeroot vs. OMakefile

さて、例を見せる前に、一つ質問をしてみましょう。それは「プロジェクトルートの OMakerootOMakefile の違いは何か?」というものです。その質問に関する端的な答えは「違いはないが、あなたは必ず OMakeroot ファイル(あるいは Root.om ファイル)を作らなければならない」です。

しかしながら、通常の範囲で用いるならば OMakeroot は変更する必要のない決まり文句が並んでいるファイルであり、すべてのプロジェクトの OMakeroot は多かれ少なかれ同じような内容となるでしょう。

OMakeを始めるために、あなたがこのような決まり文句を入力する必要はありません。ほとんどの場合において、あなたは以下の手順をプロジェクトのルートディレクトリで実行するだけです。

  • omake --install をプロジェクトのルートで実行する。

これで最初の OMakerootOMakefile が生成されて、編集できるようになりました。

3.2 Cプロジェクトのサンプル

OMakeを始めるために、まずは簡単なサンプルから始めてみましょう。いま私たちは以下のファイルを含んだディレクトリツリーを持っているものとします。

my_project/
|--> OMakeroot
|--> OMakefile
`--> src/
     |---> OMakefile
     |---> lib/
     |     |---> OMakefile
     |     |---> ouch.c
     |     |---> ouch.h
     |     `---> bandaid.c
     `---> main/
           |---> OMakefile
           |---> horsefly.c
           |---> horsefly.h
           `---> main.c

以下は OMakeroot , OMakefile リストのサンプルです。

my_project/OMakeroot:
    # Cアプリケーションの標準的な設定をインクルード
    open build/C

    # コマンドライン上の変数を処理
    DefineCommandVars()

    # このディレクトリのOMakefileをインクルード
    .SUBDIRS: .

my_project/OMakefile:
    # 標準的なコンパイルオプションを設定
    CFLAGS += -g

    # srcディレクトリをインクルード
    .SUBDIRS: src

my_project/src/OMakefile:
    # あなたの好きなようにオプションを付け加えます
    CFLAGS += -O2

    # サブディレクトリをインクルード
    .SUBDIRS: lib main

my_project/src/lib/OMakefile:
    # 静的ライブラリとしてライブラリをビルドします。
    # これはUnix/OSX上ではlibbug.aとして、
    # Win32上ではlibbug.libとしてビルドされます。
    # 引数にはソースファイルの拡張子を入れていないことに注意してください。
    StaticCLibrary(libbug, ouch bandaid)

my_project/src/main/OMakefile:
    # いくつかのファイルは../libディレクトリ上の
    # .hファイルをインクルードしています。
    INCLUDES += ../lib

    # どのライブラリに対してリンクしたいのかを指定
    LIBS[] +=
        ../lib/libbug

    # プログラムをビルドします。
    # Win32上ではhorsefly.exe、
    # Unix上ではhorseflyとしてビルドされます。
    # 最初の引数はアプリケーション名を指定します。
    # 二番目の引数はソースファイルの配列を指定します(拡張子は抜いてください)。
    # これらの配列はビルドするプログラムの一部となります。
    CProgram(horsefly, horsefly main)

    # デフォルトでこのプログラムをビルドします
    # (他の引数を指定しないでomakeが実行される場合です)。
    # EXE変数はWin32上では.exeとして定義されていますが、
    # 他のプラットフォーム上では空の変数です。
    .DEFAULT: horsefly$(EXE)

ほとんどの設定は build/C.om (これはOMakeがもつ全機能の一部です)ファイルに定義されています。このファイルはほとんどの作業の面倒を見てくれます。具体的には、

  • Cライブラリやプログラムを正当な方法でビルドするための、 StaticCLibraryCProgram 関数を定義しています。
  • 依存関係を特定するために各々のソースコードを調べていくメカニズムを定義しています。これはつまり、Cソースファイルのための .SCANNER ルールを定義していることを意味しています。変数はサブディレクトリにも継承されていき、例えば、 src/main/OMakefileCFLAGS 変数の値は "-g -O2" となります。

3.3 OCamlプロジェクトのサンプル

前回のCの代わりにOCamlを使った状態で、簡単なサンプルを作ってみましょう。今回では、ディレクトリツリーは以下のようになります。

my_project/
|--> OMakeroot
|--> OMakefile
`--> src/
     |---> OMakefile
     |---> lib/
     |     |---> OMakefile
     |     |---> ouch.ml
     |     |---> ouch.mli
     |     `---> bandaid.ml
     `---> main/
           |---> OMakefile
           |---> horsefly.ml
           |---> horsefly.mli
           `---> main.ml

OMakeroot , OMakefile のリストは前回と少し異なります。

my_project/OMakeroot:
    # OCamlアプリケーションの標準的な設定をインクルード
    open build/OCaml

    # コマンドライン上の変数を処理
    DefineCommandVars()

    # このディレクトリのOMakefileをインクルード
    .SUBDIRS: .

my_project/OMakefile:
    # 標準的なコンパイルオプションを設定
    OCAMLFLAGS += -Wa

    # バイトコードのコンパイラを使いたいですか?
    # それともネイティブコードのコンパイラを使いたいですか?
    # 今回は両方とも使ってみましょう。
    NATIVE_ENABLED = true
    BYTE_ENABLED = true

    # srcディレクトリをインクルード
    .SUBDIRS: src

my_project/src/OMakefile:
    # サブディレクトリをインクルード
    .SUBDIRS: lib main

my_project/src/lib/OMakefile:
    # ネイティブコードにおいて、積極的にインライン化を行う
    OCAMLOPTFLAGS += -inline 10

    # 静的ライブラリとしてライブラリをビルドします。
    # これはUnix/OSX上ではlibbug.aとして、
    # Win32上ではlibbug.libとしてビルドされます。
    # 引数にはソースファイルの拡張子を入れていないことに注意してください。
    OCamlLibrary(libbug, ouch bandaid)

my_project/src/main/OMakefile:
    # いくつかのファイルは../libディレクトリ上の
    # インターフェースに依存しています。
    OCAMLINCLUDES += ../lib

    # どのライブラリに対してリンクしたいのかを指定
    OCAML_LIBS[] +=
        ../lib/libbug

    # プログラムをビルドします。
    # Win32上ではhorsefly.exe、
    # Unix上ではhorseflyとしてビルドされます。
    # 最初の引数はアプリケーション名を指定します。
    # 二番目の引数はソースファイルの配列を指定します(拡張子は抜いてください)。
    # これらの配列はビルドするプログラムの一部となります。
    OCamlProgram(horsefly, horsefly main)

    # デフォルトでこのプログラムをビルドします
    # (他の引数を指定しないでomakeが実行される場合です)。
    # EXE変数はWin32上では.exeとして定義されていますが、
    # 他のプラットフォーム上では空の変数です。
    .DEFAULT: horsefly$(EXE)

この場合、ほとんどの設定は build/OCaml.om ファイルで定義されています。今回は特に、 my_project/src/lib ファイルを -inline 10 オプションを用いて積極的にコンパイルするが、 my_project/src/lib は普通にコンパイルする設定となっています。

3.4 新しい言語を扱う

前回の二つのサンプルは十分簡単なように見えますが、これはOMakeの標準ライブラリ( build/C/build/OCaml ファイル)がすべての仕事を行ってしまったためです。もし私たちがOMakeの標準ライブラリでサポートされていないような言語のビルド設定を書こうとしたら、一体どのようにすれば良いのでしょうか?

例えば、私たちはOMakeに新しい言語を適用させているものとしましょう。この言語は標準的なコンパイル/リンクモデルを用いていますが、OMakeの標準ライブラリには存在していません。今回は問題をはっきりさせるため、以下のように動作する手順について考えてみましょう。

  • .cat 拡張子(Categorical Abstract Terminology)では複数あるファイルの中のソースファイルを定義します。
  • -c オプションを加えた catc コンパイラを用いて、 .cat ファイルは .woof (Wicked Object-Oriented Format)ファイルにコンパイルされます。
  • .woof ファイルは実行可能な .dog (Digital Object Group)ファイルを生成するために、 -o オプションを用いた catc コンパイラによってリンクされます。 catc はまた -a オプションで、いくつかの .woof ファイルをライブラリに結合させることができます。
  • 各々の .cat ファイルは他のソースファイルに関連付けることができます。もしソースファイル a.cat が行 open b を含んでいたのなら、 a.catb.woof ファイルに依存しており、 a.catb.woof が変更されたときに再コンパイルしなければなりません。 catc 関数は -I オプションで依存関係を示した行を探索することができます。

ノート

訳注: これはcat, woof, dogにもじって作られた仮のソースコードであり、実際に存在しているわけではありません

ノート

訳注: 原文の時点で間違っているのではないかとご指摘を頂きました。確かにこちらの方が自然でしたので以下のように修正します:

  • catc コンパイラを用いて .cat ファイルは―

    -> -c オプションを加えた catc コンパイラを用いて、 .cat ファイルは―

  • woof ファイルは―(中略)― -c オプションを用いた catc コンパイラによって―

    -> -o オプションを用いた catc コンパイラによって―

ビルド設定を定義するために、私たちは以下の3つの作業を行う必要があります。

  1. 依存関係の情報をソースファイルから探索するための .SCANNER ルールを定義する。
  2. .cat ファイルを .woof ファイルにコンパイルするための普遍的なビルドルールを定義する。
  3. 実行可能な .dog ファイルを生成するために、 .woof ファイルをリンクするためのルールを一つの関数として定義する。

初めに、これらの定義はプロジェクトルートの OMakefile に置くことになります。

3.4.1 通常の編集ルールの定義

さて、パート2に移って、通常の編集ルールを定義していきましょう。今回私たちはビルドルールについて、ソースコードに直接ルールを定義することにします。まずはインクルードパスを扱うために、インクルードパスを指定した変数 CAT_INCLUDES を定義します。これはディレクトリが格納されている配列です。そしてオプションを定義するために、私たちは『遅延評価変数(lazy variable)(“7.5 遅延評価式“を参照)』を使用します。この場合は他にも標準的なフラグが存在していますので、 CAT_FLAGS 変数も定義することにしましょう。

# 私たちは今回CATC変数ををオーバーライドしたいので、catcコマンドを定義します
CATC = catc

# 通常のフラグは空にします
CAT_FLAGS =

# インクルードパスの辞書(通常は空です)
INCLUDES[] =

# インクルードパスによるインクルードオプションを計算します
PREFIXED_INCLUDES[] = $`(mapprefix -I, $(INCLUDES))

# 通常の方法で.woofファイルをビルドします
%.woof: %.cat
    $(CATC) $(PREFIXED_INCLUDES) $(CAT_FLAGS) -c $<

ノート

訳注: 今回の場合、 $`(mapprefix -I, $(INCLUDES)) という表記法がまさに遅延評価に相当しています。 $`(v)v を遅延評価するための表記法です。

最後の部分では、インクルードパスと前に定義されている CAT_FLAGS 変数を含んだ、 catc コンパイラを呼び出すというビルドルールを定義しています。 $< 変数はソースファイル名に置き換わります。

3.4.2 リンクするためのルールを定義

.woofファイルをリンクするために、どのようにビルド作業を行うのかについて記述した、別のルールを記述します。ここでソースに直接ルールを定義するかわりに、リンク作業を記述した関数を定義することにします。この関数は二つの引数をとり、最初の引数は実行ファイル名(拡張子なし)で、二つ目の引数はリンクするためのファイル名(これもまた拡張子はなし)を指定します。以下はコードの断片です。

# 副次的なリンクオプション
CAT_LINK_FLAGS =

# どのように.dogプログラムをビルドするのかを定義した関数
CatProgram(program, files) =
    # 拡張子を追加
    file_names = $(addsuffix .woof, $(files))
    prog_name = $(addsuffix .dog, $(program))

    # ビルドルール
    $(prog_name): $(file_names)
        $(CATC) $(PREFIXED_INCLUDES) $(CAT_FLAGS) $(CAT_LINK_FLAGS) -o $@ $+

    # プログラム名を返す
    value $(prog_name)

CAT_LINK_FLAGS 変数はちょうど私たちがリンク作業において、追加フラグを渡したいような場合に定義される変数です。さて、新しく関数が定義されましたので、私たちがプログラムをビルドするためのルールを定義したいと思った場合はいつでも、単純にこの関数を呼ぶだけで完了します。前回のような暗黙のルールを記述する場合ですと、どのように各々のソースファイルがコンパイルされるのかについていちいち指定する必要がありましたが、 CatProgram 関数はどのように実行ファイルをビルドするのか指定するだけで完了します。

ノート

訳注: $@ , $+ はそれぞれ『ターゲットの名前』と『依存ファイルのリスト』を表しています。詳細は “8. ビルドルール” を参照してください。

# rover.dogプログラムをソースファイルneko.catとchat.catからビルドします。
# rover.dogは普通にコンパイルされます。
.DEFAULT: $(CatProgram rover, neko chat)

3.4.3 依存関係の解析

これでほとんどの作業が終わりましたが、まだ依存関係の解析を自動的に行わせる部分が残っています。これはOMakeの利点の一つであり、さらにロバストで書きやすいビルド設定を作る手助けとなっています。厳密に言うと、この箇所は必要というわけではありませんが、あなたは切実にこの機能を欲しがっていると思います。

このメカニズムは通常のルールのように、 .SCANNER ルールを定義することで得られます。しかし .SCANNER ルールはどのように依存関係を解析するのかについて指定するのであって、ターゲット自身を指定しているわけではありません。私たちは以下のような形で .SCANNER ルールを定義したいものとします。

.SCANNER: %.woof: %.cat
    <commands>

このルールでは、「 .woof ファイルは収集した .cat ファイルから、 <commands> を実行することで展開できる」という、新しい依存関係を追加することを指定しています。 <commands> の実行結果は、通常の端末で出力できる、OMake形式の依存関係の配列である必要があります。

すでに述べた通り、各々の .cat ファイルは open 構文を用いて、 .woof ファイルに依存していることを指定していたとします。例えば、もし neko.cat ファイルが行 open chat コマンドを含んでいたとするならば、 neko.woof ファイルは chat.woof ファイルに依存しています。この場合、 <commands> は以下の行を出力しなければなりません。

neko.woof: chat.woof

この類推は、 .o ファイルが .c ファイルをコンパイルすることで生成されるC言語について考えるとより明瞭になります。もしファイル foo.c#include "fum.h" のような行を含んでいたとすると、 foo.cfum.h が変更されたときはいつでも再コンパイルを行う必要があります。これはつまり、ファイル foo.o がファイル fum.h に依存していることを表しています。OMakeの用語では、このことを『暗黙的な依存関係(implicit dependency)』と呼んでおり、 .SCANNER <commands> は以下のような行を出力する必要があるでしょう。

foo.o: fum.h

それでは動物の世界へと戻ってみましょう。 neko.woof の依存関係を解析するために、私たちは一行一行 neko.cat ファイルをスキャンして、 open <name> のような形の構文を含んだ行を探す必要があります。私たちはこのようなプログラムを書かなければなりませんが、OMakeはこのような作業を簡略化することができます。この例ですと、ソースファイルをスキャンする awk 関数がビルドインで用意されているので、これを使ってみましょう。一つ難しいことがあるとするならば、それは依存関係が INCLUDE パスに依存していることです。そのためにOMakeでは探索するための find-in-path 関数を用意しています。それでは以下のように書いてみます。

.SCANNER: %.woof: %.cat
    section
        # ファイルをスキャン
        deps[] =
        awk($<)
        case $'^open'
            deps[] += $2
            export

        # 重複を削除し、インクルードパスのファイルを探索する
        deps = $(find-in-path $(INCLUDES), $(set $(deps)))

        # 依存関係を出力
        println($"$@: $(deps)")

それでは上のソースコードを見てみましょう。初めに、全体の文はシェルコマンドのシーケンスとして扱われずに内部で計算されるよう、 section 文の中で定義されています。

今回私たちはすべての依存関係を集めるために、 deps 変数を用いました。 awk 関数はソースファイル ($<) を一行一行スキャンしていきます。正規表現 ^open (これはこの行が単語 open で始まることを表しています)が見つかった場合、 deps 変数に二番目の単語を追加します。具体的には、入力された行が open chat であった場合、 deps 配列に chat 文字列を追加することになります。ソースファイル中のその他すべての行は無視されます。

次に、 $(set $(deps)) 文によって deps 配列の重複された文字列は削除されます(このとき、アルファベット順に配列をソートします)。 find-in-path 関数はインクルードパス中の各々のファイルの絶対パスを探索します。

最後に、文字列 $"$@: $(deps)" を結果として出力します。クオーテーションには deps 配列を単純な文字列に変換した状態で追加されます。

3.4.4 まとめ

例がすべて終わったので、この成果を一つのプロジェクトにまとめてみましょう。前回の例は以下のような構成とします。

my_project/
|--> OMakeroot
|--> OMakefile
`--> src/
     |---> OMakefile
     |---> lib/
     |     |---> OMakefile
     |     |---> neko.cat
     |     `---> chat.cat
     `---> main/
           |---> OMakefile
           `---> main.cat

この全体のプロジェクトのリストは以下のようになります。私たちはまたライブラリにいくつかの .woof ファイルをリンクさせるために、 CatLibrary 関数を定義していることに注意してください。

my_project/OMakeroot:
    # コマンドライン上の変数を処理
    DefineCommandVars()

    # このディレクトリのOMakefileをインクルード
    .SUBDIRS: .

my_project/OMakefile:
   ########################################################################
   # .catファイルをコンパイルするための標準設定
   #

   # 私たちは今回CATC変数ををオーバーライドしたいので、catcコマンドを定義します
   CATC = catc

   # 通常のフラグは空にします
   CAT_FLAGS =

   # インクルードパスの辞書(通常は空です)
   INCLUDES[] =

   #  インクルードパスによるインクルードオプションを計算します
   PREFIXED_INCLUDES[] = $`(mapprefix -I, $(INCLUDES))

   # .catファイルの依存関係を解析するスキャナ
   .SCANNER: %.woof: %.cat
        section
            # ファイルをスキャン
            deps[] =
            awk($<)
            case $'^open'
                deps[] += $2
                export

            # 重複を削除し、インクルードパスのファイルを探索する
            deps = $(find-in-path $(INCLUDES), $(set $(deps)))

            # 依存関係を出力
            println($"$@: $(deps)")

   # 通常の方法で.catファイルをコンパイルする
   %.woof: %.cat
       $(CATC) $(PREFIXED_INCLUDES) $(CAT_FLAGS) -c $<

   # 副次的なリンクオプション
   CAT_LINK_FLAGS =

   # いくつかの.woofファイルを用いてライブラリをビルド
   CatLibrary(lib, files) =
       # 拡張子を追加
       file_names = $(addsuffix .woof, $(files))
       lib_name = $(addsuffix .woof, $(lib))

       # ビルドルール
       $(lib_name): $(file_names)
           $(CATC) $(PREFIXED_INCLUDES) $(CAT_FLAGS) $(CAT_LINK_FLAGS) -a $@ $+

       # プログラム名を返す
       value $(lib_name)

   # どのように.dogプログラムをビルドするのかを定義した関数
   CatProgram(program, files) =
       # 拡張子を追加
       file_names = $(addsuffix .woof, $(files))
       prog_name = $(addsuffix .dog, $(program))

       # ビルドルール
       $(prog_name): $(file_names)
           $(CATC) $(PREFIXED_INCLUDES) $(CAT_FLAGS) $(CAT_LINK_FLAGS) -o $@ $+

       # プログラム名を返す
       value $(prog_name)

   ########################################################################
   # これで正しくプログラムが動きます
   #

   # srcサブディレクトリをインクルード
   .SUBDIRS: src

my_project/src/OMakefile:
   .SUBDIRS: lib main

my_project/src/lib/OMakefile:
   CatLibrary(cats, neko chat)

my_project/src/main/OMakefile:
   # ../libディレクトリからのインクルードを許可
   INCLUDES[] += ../lib

   # プログラムをビルド
   .DEFAULT: $(CatProgram main, main ../cats)

注意点としては、 OMakeroot では依存関係の解析や、ソースファイルをコンパイルするための通常のルール、ライブラリやプログラムをビルドするいくつかの関数を含んだ、標準的な設定を定義しています。

これらのルールや関数はサブディレクトリに継承されていますので、 .SCANNER とビルドルールは自動的に各々のサブディレクトリに使われます。よってあなたはこれらを繰り返し記述する必要はありません。

3.4.5 終わりに

これで一通りの作業は終わりましたが、まだ考慮すべき点はいくつか残っています。

まず、 cat プログラムをビルドするためのルールはプロジェクトの OMakefile に定義しました。もしあなたがどこか別の cat プロジェクトを持っていたとすると、 OMakeroot をコピー(そしてもし必要ならば修正も)するかもしれません。その代わりに、あなたは設定ファイルを Cat.om のように名称を変更して、ライブラリの共有ディレクトリに移すべきです。これで、コードをコピーする代わりに、OMakeコマンド open Cat を用いてインクルードできるようになります。そのためには、あなたは共有ディレクトリを OMAKEPATH 環境変数に追加することで、omakeがどこを探せば良いのか分かるようにすべきです。

もしあなたが満足する仕事をしたのなら、標準の設定となるようにあなたの設定ファイルを送ることを考えてみてください(omake@metaprl.org 宛にリクエストを送ることで)。他の人の作業を省力化することになります。

3.5 階層構造、.SUBDIRSの内容を並列化させる

いくつかのプロジェクトは同一の設定を有した、数多くのディレクトリで構成されているものです。例えば、あなたは現在サブディレクトリが多数あり、その各々がウェブページの画像の集合であるというプロジェクトを持っているものとしましょう。ある特定の画像を除いて、各々のファイルの設定は同一です。

この設定をより強固に構築するため、以下のような場合を考えます。まず、このプロジェクトは4つのサブディレクトリ page1, page2, page3, page4 を含んでいるものとします。また、各々のサブディレクトリは二つのファイル image1.jpg, image2.jpg を含んでおり、それらはプログラム genhtml によって生成されるウェブページの一部であるとします。

各々のディレクトリ中に OMakefile を定義する代わりに、OMakeでは .SUBDIRS コマンドの内容として定義することができます。

.SUBDIRS: page1 page2 page3 page4
    index.html: image1.jpg image2jpg
        genhtml $+ > $@

.SUBDIRS の内容は、まるで OMakefile が内部にあるかのように正確にふるまい、通常の命令を任意の数だけ実行することができます。 .SUBDIRS の内容は各々のサブディレクトリの内部で評価されます。実際に何が行われているのかについては、現在のディレクトリ名を出力する命令 ($(CWD)) を追加することでより分かりやすくなるでしょう。

  .SUBDIRS: page1 page2 page3 page4
      println($(absname $(CWD)))
      index.html: image1.jpg image2jpg
          genhtml $+ > $@
# 出力
  /home/jyh/.../page1
  /home/jyh/.../page2
  /home/jyh/.../page3
  /home/jyh/.../page4

3.5.1 globパターンを扱う

もちろん、上述した指定は非常に強固なものとなっています。実際に、各々のサブディレクトリが異なった画像の集合であり、そのすべてがウェブページに含まれているような場合でも、記述方法は似ています。この問題に対するより簡単な解法の一つは、 globls のようなディレクトリのリストを出力する関数を用いることです。 glob 関数はシェルのパターンを引数に持ち、現在のディレクトリ上でマッチしているファイル名の配列を返す関数です。

.SUBDIRS: page1 page2 page3 page4
    IMAGES = $(glob *.jpg)
    index.html: $(IMAGES)
        genhtml $+ > $@

3.5.2 簡略化されたサブディレクトリの設定

別の方法は、各々のサブディレクトリ固有の情報を定義した設定ファイルを、それぞれのディレクトリに追加することです。例えば、私たちは現在、各々のサブディレクトリ中に、ディレクトリ内部にある画像のリストを定義した BuildInfo.om ファイルを設置しているものとします。 .SUBDIRS の行は似ていますが、 BuildInfo ファイルをインクルードしている点が異なっています。

.SUBDIRS: page1 page2 page3 page4
    include BuildInfo   # IMAGES変数を定義

    index.html: $(IMAGES)
        genhtml $+ > $@

それぞれの BuildInfo.om の内容は以下のようになっています。

page1/BuildInfo.om:
    IMAGES[] = image.jpg
page2/BuildInfo.om:
    IMAGES[] = ../common/header.jpg winlogo.jpg
page3/BuildInfo.om:
    IMAGES[] = ../common/header.jpg unixlogo.jpg daemon.jpg
page4/BuildInfo.om:
    IMAGES[] = fee.jpg fi.jpg foo.jpg fum.jpg

3.5.3 サブディレクトリのリストを計算

現在、サブディレクトリのリスト page1, ... , page4 は直接指定しています。他のディレクトリが追加される度に OMakefile を編集するよりも、( glob を用いて)計算させたほうがはるかに合理的です。

.SUBDIRS: $(glob page*)
    index.html: $(glob *.jpg)
        genhtml $+ > $@

ディレクトリ構造が階層的である場合を考えてみましょう。その場合 glob 関数を用いる代わりに、階層的に各々のディレクトリを返す subdirs 関数を使います。例えば、以下はOMakeプロジェクトのルート上で subdirs 関数を評価した結果です。最初の引数として渡した P オプションでは、OMakeのディレクトリ自身を含んでいない、『適切な』リストを返すことを指定しています。

osh> subdirs(P, .)
- : <array
        /home/jyh/.../omake/mk : Dir
        /home/jyh/.../omake/RPM : Dir
        ...
        /home/jyh/.../omake/osx_resources : Dir>

subdirs を使用することで、上の例は以下のように表現できます。

.SUBDIRS: $(subdirs P, .)
    index.html: $(glob *.jpg)
        genhtml $+ > $@

この場合ですと、プロジェクト中の すべての サブディレクトリが含まれることとなります。

私たちが BuildInfo.om オプションを使用する場合、すべてのサブディレクトリをインクルードする代わりに、 BuildInfo.om ファイルが含んであるディレクトリのみインクルードしたいと思うでしょう。これを実現するために、私たちはディレクトリを階層的に全走査し、特定の表現にマッチしたファイルを返す find 関数を使用します。この場合ですと、 BuildInfo.om という名前のファイルを探したいことになります。以下は find 関数を呼び出したサンプルです。

osh> FILES = $(find . -name BuildInfo.om)
- : <array
        /home/jyh/.../omake/doc/html/BuildInfo.om : File
        /home/jyh/.../omake/src/BuildInfo.om : File
        /home/jyh/.../omake/tests/simple/BuildInfo.om : File>
osh> DIRS = $(dirof $(FILES))
- : <array
        /home/jyh/.../omake/doc/html : Dir
        /home/jyh/.../omake/src : Dir
        /home/jyh/.../omake/tests/simple : Dir>

この例では、プロジェクト中に3つの BuildInfo.om ファイルが doc/html, src, tests/simple ディレクトリに存在しています。また、 dirof 関数は各々のファイルのディレクトリを返します。

先の例に戻って、私たちは以下のように修正することにしました。

.SUBDIRS: $(dirof $(find . -name BuildInfo.om))
    include BuildInfo   # IMAGES変数を定義

    index.html: $(IMAGES)
        genhtml $+ > $@

3.5.4 一時的なディレクトリ

時々、プロジェクトでは中間ファイルを置いておくための一時的なディレクトリが必要となる場合があります。これらの一時ディレクトリはプロジェクトがクリーンアップされたときはいつでも消去されます。これは特に、ディレクトリが消去されたら同ディレクトリの OMakefile も消去されるために、 OMakefile を一時的なディレクトリに置くべきではないことを意味しています。

もしあなたがこれらのディレクトリに関する設定を行いたいのなら、あなたは OMakefile を設置する代わりに、 .SUBDIRS の内容について記述する必要があります。

section
    CREATE_SUBDIRS = true

    .SUBDIRS: tmp
        # MD5ハッシュを計算
        %.digest: %.comments
           echo $(digest $<) > $@

        # ソースファイルからコメントを展開
        %.comments: ../src/%.src
           grep '^#' $< > $@

        .DEFAULT: foo.digest

.PHONY: clean

clean:
    rm -rf tmp

今回の例では、私たちは tmp ディレクトリが存在しない場合に新しくディレクトリを生成するため、 CREATE_SUBDIRS 変数を true に設定しました。 .SUBDIRS の内容は少々工夫してありますが、だいたいあなたが期待している通りに動作するはずです。 clean phony ターゲットでは、プロジェクトがクリーンアップされた場合は tmp ディレクトリが消去されるように指示しています。