OMake でディレクトリ単位の依存性(のような物)を記述する



大阪でおまけ言うたらコレ!

サンプルソースhttp://camlspotter@bitbucket.org/camlspotter/omake-depend-on-dir にあるよ。

OMake って大阪のオバチャンに言わせるところの「便利やけど使いにくいわぁ」「どっちやねん!」なツールですよね!

OMakeは素晴らしいツールであり,それ自体には特に問題はありません。

  • 各サブディレクトリを意図通りの順序でビルドするためには,ディレクトリをまたがってファイル間の依存関係を完璧に記述する必要がある。単純にディレクトリAからBの順にビルドすれば十分な場合でも簡単には表現できない場合がある(しばしば直感に反した動作をするため悩む)。
  • 標準のビルドルールを生成する関数(OCamlLibraryなど)の定義ではフラグの指定などが足りなくて改造する必要があった。改造するためにはOMake言語の仕様を泥縄式に学習する必要が生じ,OMakeで楽ができるはずがまったく楽ができなくなってしまった。
  • http://d.hatena.ne.jp/fet/20110407/1302168415


いやー、ほんま面倒やね! せやから皆で豆知識を共有しまへんかぁ?生活の知恵ってやつやね!


まず後者から。はい、OMake 付属の OCaml用ビルドルール関数はすべてのビルドニーズに対応していません。ごく一般的な使い方をした時、記述が便利なように細かい設定は無視しているのですね。(もし、どんなケースでも便利に使えるルール関数があればそれはそれで、すごく入りにくいモノになっているはず…)標準以上の事をしたければ OMake の書き方を自分で学習してルールを改造してください。そんなに難しくありませんが、ややこしいです。どっちやねん。終わり。


前者。 OMake のデザインは、そもそも Makefile と違って、ファイル生成間の依存関係をキチンと書こう、そうすれば、ゴールをビルドするのに無駄なファイルは一切生成しないし、マルチコアを使った並列ビルドや、 inotify や gamin を利用したファイル内容変更監視による on-the-fly ビルド (+必要あれば依存関係の再計算)が出来るよ! というものです。ですから、あるディレクトリ A でビルドが終わったら B に取り掛かるよ、のような古き良きダサき Makefile の様なことは、 OMake では、そもそもやるべきではないのです。もちろんドキュメントにもどうやってそんなルールを書くか言及がありません。


でも、依存性解析書くのメンドクサイよねぇ。


そこで。こんな物を私は使っています:

RequireFiles(files) =
    .SCANNER: scan-%: $(files)
    % : $(files)
    export


このルールは、このディレクトリ内の全ての依存性解析とファイル生成に files への依存性を加えています。 ですから、この関数を B/OMakefile で呼び出して、ディレクトリ A のソースファイルとそこで作られる全ての生成物への依存を加えてやると、「ディレクトリ A でビルドが終わるまで B には手を出さない」という挙動を実現できます。ただ、 files が一つでも変わってしまうと全てやり直しになるわけで、依存関係を記述尽くした場合と比べると、最小ビルド性が失われてしまい効率が悪くなります。 最後の export を忘れないでください。これを忘れると、折角作った依存ルールがこの式の外に出ていかず、意味がありません。

こっから先はあんまりテストしていません


ディレクトリ A のソースと作られる生成物のリストは…自分で作るのもまた面倒ですし、リストし忘れがあると B ディレクトリでのコンパイルに影響しまいコワイです。例えば A ディレクトリでの中間生成物があなたの知らないうちに B で参照されているような場合です。(A の最終生成物が偶然前のビルドと同じでも中間が異なっていた…といったこともありえますよね。) こんな時は、こんなのでどうでしょう:

$(dependencies-all $(targets))


この式は $(targets) の生成が依存するファイルを列挙します。これで、 A ディレクトリの最終生成物をこの関数にぶち込めば、ソースと中間生成物も含めたリストが得られます。 A ディレクトリでの最終生成物をちゃんとリストアップするのは…これはあなたの責任です。この結果を RequireFiles() に入れてやれば良いのですね。 ただし、この式を使うのは $(targets) が実際にビルドされてからでなければいけません。(実際のビルドを行わず、ビルドされる可能性のあるファイルをリストアップすることは、多分できません。ターゲット生成は動的なので…もしそんな方法があったら教えてください。)


さて、やることは、

  • $(targets) をあなたが決める。
  • $(targets) をビルドした後、$(dependencies-all $(targets)) を呼び出す
  • その結果を RequireFiles() に渡す
  • 最後の export を忘れずに


です。このルールを格好良く書けるのか、どうか、私はよくわかりません。どなたか教えてください。私は、ダサいですが、ファイルに結果を書き出す方法を使っています:

CreateCheckSum(pack, targets)=
    chan=$(fopen $(pack), w)
    fprintln($(chan), $(string $(targets)))
    fprintln($(chan), $(string $(digest $(targets))))
    close($(chan))

RequireBuild(targets) =
    dependencies.md5: $(targets)
        println($(string  $(sequence-sort $(compare), $(dependencies-all $(targets)))))
        CreateCheckSum($@, $(sequence-sort $(compare), $(dependencies-all $(targets))))
    RequireFiles(dependencies.md5)
    export


RequireBuild(targets) 関数は、ディレクトリ内の全ての依存解析とファイル生成を dependencies.md5 に依存させます。 (dependencies.md5 自体も依存しているという不思議。でも動くw) この dependencies.md5 の内容は $(targets) とその中間生成物の MD5 checksum になっています。


さて、準備できました。 RequireBuild(targets) を使えば B ディレクトリ内のビルドは全て A ディレクトリの最終生成物、及び、その中間生成物に依存させることができます:

# B/OMakefile
RequireBuild(../A/a.out ../A/libA.a)


と書けば、 B でのコンパイルは a.out や libA.a が依存する全ての .c .h .o ファイルに依存するわけですね!