自動化のための nmake 入門講座 - ディレクトリ階層を扱う方法
自動化のための nmake 入門講座 -
ディレクトリ階層を扱う方法
はじめに
make は 1 つのディレクトリ内にあるファイルを扱うには非常に優れたツールですが,複数にまたがるディレクトリ階層を扱うのが不得意です.
例えば Java で開発する場合を考えてみます.Java では,パッケージごとにディレクトリを作る必要がありますが, make を使ってディレクトリ階層を一気にコンパイルするにはどうすればいいでしょうか? あるいは,あるディレクトリから下のディレクトリ階層にあるテストケースをすべて実行したい場合はどうでしょう? 他にも,各ディレクトリで共通のタスクターゲットを定義し,どのディレクトリでも同じような作業を行うようにしたいところです.
ここでは, これらの問題を扱うために筆者が作成した subdirs.mak という nmake ライブラリを紹介します.この記事を読めば,ある程度規模が大きい Java 開発でも make が十分使えるツールであることが理解できると思います.subdirs.mak は自由に使ってください(すごく単純です).
目次
subdirs.mak を利用したメイクファイルの書き方
subdirs.mak を使うのは非常に簡単ですが,ある程度準備が必要です.ここでは,例として
というディレクトリ階層を考えましょう.ルートとなるディレクトリは project ディレクトリです.その下に dir1, dir2, dir3 のサブディレクトリがあり, dir3 ディレクトリにはさらに dir3a, dir3b というサブディレクトリがあります.
この場合,次の 2 種類のメイクファイルを作る必要があります.
ファイル名 書くこと ファイルを置く場所 common.mak 各ディレクトリ共通のことがら ルートディレクトリ (project) Makefile 各ディレクトリに特化したことがら すべてのディレクトリ
(project, dir1, dir2, dir3, dir3a, dir3b)
つまりサンプル例では, common.mak × 1 と Makefile × 6 の計 7 つのメイクファイルをつくることになります.これを読んでちょっとひいてしまった方もいるかもしれませんが,各ファイルは本当に必要最小限のことしか書きません.とくに Makefile に書く内容は少ないですので,安心してください.
subdirs.mak の配置
まず,下準備として subdirs.mak を適当なディレクトリに保存します.筆者の場合は nmake のライブラリ用のディレクトリを作成し,そこに nmake 関係のライブラリを集めています.例えば c:\lib\nmake というディレクトリを作成し,そこに subdirs.mak を保存してください.
環境変数の設定
次に,上で保存した subdirs.mak があるディレクトリのパスを INCLUDE 環境変数に追加します.環境変数の設定の仕方は省略しますが, Visual C++ をインストールされているなら,すでに
c:\app\msdevstd\include;\include;%include%
のように設定されていると思います. subdirs.mak の保存先が c:\lib\nmake なら,最後にセミコロンをつけ,次のように追加しておいてください.
c:\app\msdevstd\include;\include;%include%;c:\lib\nmake
INCLUDE 環境変数が設定されていない場合は,この環境変数を新しく作成してください.
ここで nmake でメイクファイルから他のメイクファイルをインクルードする方法について補足をしておきます.基本的に C 言語のプリプロセッサ指令 #include に似ています.
書き方 意味 !include filename カレントディレクトリにある filename を読み込みインクルードする. !include <filename> filename がカレントディレクトリにない場合は,INCLUDE 環境変数に設定されたディレクトリ内にある filename を読み込みインクルードする.
なお,!include <filename> で指定したとき,メイクファイルで INCLUDE マクロを設定している場合は INCLUDE マクロが優先されます.
common.mak の作成
以上で nmake を用いたディレクトリ階層を扱うための下準備は終わりです.ここからは,特定のディレクトリ階層で subdirs.mak を利用するための説明に入ります.
まず, project ディレクトリ直下に common.mak という名前のメイクファイルを作ります.common.mak は各ディレクトリの Makefile にインクルードされます.ここに共通のマクロやサフィックスルール,ターゲットを書いておきます.
ここではタスクターゲットの例として message を定義することにしましょう.このターゲットは,各ディレクトリを巡回して Makefile ごとに定義された MESSAGE マクロを表示する,というものです.message ターゲットは,共通のターゲットとなるので common.mak に書くことになります.
以上をふまえて,message タスクターゲットが定義された common.mak の内容は以下のようになります.
message: $(MAKE) /$(MAKEFLAGS) subdirs SUBDIRS_PRE_TARGET=message-self message-self: echo $(MESSAGE) !include <subdirs.mak>
上の例では,message ターゲットのほかに message-self ターゲットが定義されています.echo コマンドで MESSAGE マクロをエコーするという単純なものです.そのすぐ下に subdirs.mak ライブラリがインクルードされています.!include <subdirs.mak> というように山形カッコ (< >) をつけたのは, INCLUDE 環境変数に設定された検索パスから subdirs.mak ファイルを見つけるためです.
通常 message ターゲットは
message: echo $(MESSAGE)
と書きますが,ディレクトリ階層を扱う場合は上記のように message と message-self に分解し,subdirs.mak をインクルードします.このターゲット変換ルールをまとめると以下のようになります.
変換前 target: [target のコマンドライン]↓ 変換後 target: $(MAKE) /$(MAKEFLAGS) subdirs SUBDIRS_PRE_TARGET=target-self target-self: [target のコマンドライン]
この target-self ターゲットは,デザインパターン用語で言い換えると Template Method パターンのフックメソッドに相当します.筆者は,フックメソッドのネーミングに self というサフィックスをつけるのが好みです.GoF のスタイルが好きな方は, do-target でもいいですね.しかしここでは便宜上 self ターゲット と呼ぶことにします.self ターゲットは, そのディレクトリだけが対象のターゲットです.
話が前後しますが,message ターゲットのコマンドラインにある MAKE マクロ は nmake コマンドそのものを表し,MAKEFLAGS マクロは最初に nmake を起動したときのオプションを表しています.再帰 make を使う場合はこれらのマクロがよく使われます.
Makefile の作成
最後に,各ディレクトリで Makefile を作成していきます.Makefile では,以下のマクロを定義する必要があります.
マクロ名 内容 ROOT_DIR ルートディレクトリへの相対パス (. や ..\.. など) SUBDIRS サブディレクトリ名のリスト(空白で区切ること.省略可.)
これらのマクロを定義して,最後にルートディレクトリの common.mak をインクルードします.
!include $(ROOT_DIR)\common.mak
例えば, project ディレクトリにある Makefile は以下のようになります.
# project Makefile ROOT_DIR = . SUBDIRS = dir1 dir2 dir3 MESSAGE = This is project Makefile. !include $(ROOT_DIR)\common.mak
project ディレクトリは,ルートディレクトリであるため ROOT_DIR マクロは自分自身への相対パスです.また,このディレクトリは dir1, dir2, dir3 という 3 つのサブディレクトリを持っているため, SUBDIRS マクロは上のようになります.message ターゲットで用いられる MESSAGE マクロも定義されています.
今度は, サブディレクトリを持たない dir1 の Makefile を見てみましょう.
# dir1 Makefile ROOT_DIR = .. MESSAGE = This is dir1 Makefile. !include $(ROOT_DIR)\common.mak
dir1 は,ルートディレクトリから 1 階層下のディレクトリなので ROOT_DIR は .. になります.またサブディレクトリを持たないため SUBDIRS マクロは定義されていません.dir2 のMakefile も dir1 の Makefile と同様です. dir3 は,サブディレクトリ dir3a, dir3b を持つため次のようになります.
# dir3 Makefile ROOT_DIR = .. SUBDIRS = dir3a dir3b MESSAGE = This is dir3 Makefile. !include $(ROOT_DIR)\common.mak
最後に 2 階層下の dir3a の Makefile も書いておきましょう.
# dir3a Makefile ROOT_DIR = ..\.. MESSAGE = This is dir3a Makefile. !include $(ROOT_DIR)\common.mak
dir3b も同様です.以上でメイクファイルすべて書くことができました.
ターゲットを実行してみよう
さて,いよいよ nmake を使って message ターゲットを実行します. project ディレクトリに移り,次のコマンドを実行してみてください.
c:\work\project> nmake /NOLOGO /S message
実行結果は,以下のようになると思います.
This is project Makefile. This is dir1 Makefile. This is dir2 Makefile. This is dir3 Makefile. This is dir3a Makefile. This is dir3b Makefile.
project 以下すべてのディレクトリの message ターゲットが再帰的に実行されています.
namke のオプションについて少し補足しておきます. /NOLOGO は Microsoft のロゴ表示を抑制するオプションです./S は nmake が実行するコマンドラインをエコーしない,というオプションです.これらのオプションをつけないで実行すれば余分な表示がたくさん出てきます.気になる方は,一度 /NOLOGO や /S オプションを省いて nmake を実行してみてください.
もしこれらのオプションを毎回つけるのが嫌な方は, common.mak の message ターゲットを次のように変更してみましょう.
message: $(MAKE) /NOLOGO /S subdirs SUBDIRS_PRE_TARGET=message-self
サブディレクトリ上で実行してみよう
project ディレクトリで行ったコマンドは,dir1 や dir3 のディレクトリでも同じように実行できます.例えば, dir1 に移動して実行みると,次のような結果になります.
c:\work\project\dir1> nmake /NOLOGO /S message This is dir1 Makefile.
dir3 の場合は次のようになります.
c:\work\project\dir3> nmake /NOLOGO /S message This is dir3 Makefile. This is dir3a Makefile. This is dir3b Makefile.
ちゃんと dir3 のサブディレクトリ dir3a dir3b が実行されていますね.
サブディレクトリを巡回してほしくない場合
ターゲットを実行するのは自分のディレクトリだけにしたい,という場合もあるでしょう.その場合は, self ターゲットを実行することになります.
c:\work\project> nmake /NOLOGO /S message-self This is project Makefile.
これは当たり前の結果ですが,いずれにせよ self ターゲットのネーミングルールを統一しておくことが重要です.筆者のお勧めは target-self です.
サブディレクトリを先に巡回してほしい場合
これまでの例では,先に自分自身のディレクトリを処理してから各サブディレクトリに移って処理を実行していました.しかし,場合によってはサブディレクトリの処理を先に行いたい場合があります.例えば,ビルド関係のタスクターゲットでは,各サブディレクトリで先にビルドしておいたファイルを利用した後自分自身のディレクトリをビルドするようなことがあります.
このような場合にも subdirs.mak は対応しています.message ターゲットの場合は, common.mak を次のように修正します.
message: $(MAKE) /$(MAKEFLAGS) subdirs SUBDIRS_POST_TARGET=message-self
SUBDIRS_PRE_TARGET ではなく SUBDIRS_POST_TARGET になっていることに注意してください.実際に実行してみましょう.
c:\work\project> nmake /NOLOGO /S message This is dir1 Makefile. This is dir2 Makefile. This is dir3a Makefile. This is dir3b Makefile. This is dir3 Makefile. This is project Makefile.
このように, dir3 と project の処理が後回しになっていることがわかります.
巡回時,あるディレクトリに特化した処理をフックさせたい場合
巡回しているときに,あるディレクトリについては特殊な処理を入れ,先にその処理を行ってから通常の処理を実行したい場合があります.
例えば,Java で開発している場合を考えましょう.ほとんどのパッケージは java ファイルをコンパイルして class ファイルを生成すればよいのですが,あるパッケージでは, java ファイルを自動生成して,その後その java ファイルをコンパイルしなければならない場合があります( 例えば,パーサジェネレータである JavaCC を使った開発などがそうです.jj ファイルから java ファイルを生成し,その後 javac でコンパイルしなくてはなりません).
この場合も common.mak を少し工夫するだけで可能です.例えば, dir2 の message ターゲットを処理する時にここだけ **** dir2 **** というバナー表示をしたいとしましょう.
まず, common.mak の message-self ターゲットを次のように変更します.
message-self:$(MESSAGE_SELF_PRE_TARGET) echo $(MESSAGE)
その後 dir2 の Makefile を次のように修正します(太字部分を追加).
# dir2 Makefile ROOT_DIR = .. MESSAGE = This is dir2 Makefile. MESSAGE_SELF_PRE_TARGET = print-banner print-banner: echo **** dir2 **** !include $(ROOT_DIR)\common.mak
project ディレクトリから実行してみましょう.
C:\work\project> nmake /NOLOGO /S message This is project Makefile. This is dir1 Makefile. **** dir2 **** This is dir2 Makefile. This is dir3 Makefile. This is dir3a Makefile. This is dir3b Makefile.
ちゃんと **** dir2 **** が表示されていますね.このテクニックは,nmake で未定義のマクロは単に空白(またはヌル文字列)として解釈される,ということを利用しています.dir2 の Makefile を実行するときは,message-self ターゲットは
message-self:print-banner echo $(MESSAGE)
として解釈されますが,その他のディレクトリでは元の
message-self: echo $(MESSAGE)
として解釈されるからです.
このテクニックを使う場合,筆者はフック用マクロのネーミングルールを TARGET_PRE_TARGET にしています.
巡回時,あるディレクトリに特化した処理をフックさせたい場合 Part 2
さて今度は 処理が終わってからも dir2 のバナー表示を入れたい,例えば
**** dir2 **** This is dir2 Makefile. ==== dir2 ====
のように表示させるにはどうすればいいでしょうか? これがすぐわかる人は make をきちんと理解している人だと思います.答えは次の通りです.
まず,common.mak の message-self ターゲットを修正し,新しく message-self-self ターゲットを導入します.
message-self:$(MESSAGE_SELF_PRE_TARGET) message-self-self $(MESSAGE_SELF_POST_TARGET) message-self-self: echo $(MESSAGE)
次に, dir2 の Makefile を次のように修正します(太字部分).
# dir2 Makefile ROOT_DIR = .. MESSAGE = This is dir2 Makefile. MESSAGE_SELF_PRE_TARGET = print-banner-header MESSAGE_SELF_POST_TARGET = print-banner-footer print-banner-header: echo **** dir2 **** print-banner-footer: echo ==== dir2 ==== !include $(ROOT_DIR)\common.mak
project ディレクトリからの実行結果は以下の通りです.
C:\work\project> nmake /NOLOGO /S message This is project Makefile. This is dir1 Makefile. **** dir2 **** This is dir2 Makefile. ==== dir2 ==== This is dir3 Makefile. This is dir3a Makefile. This is dir3b Makefile.
お気づきになった方も多いと思いますが,この TARGET_PRE_TARGET, TARGET_POST_TARGET というマクロを使ったテクニックは subdirs.mak 自身が利用しているテクニックでもあります.
ファイルの共有
最後に,各ディレクトリの Makefile でファイルを共有する方法について説明しましょう.例えば,Ruby スクリプトを作り,それを各ディレクトリで利用したい場合を考えます.
まず, ルートディレクトリに共有したいファイルを置きます.そして common.mak に
$(ROOT_DIR)\filename
という形でそのファイルを利用すれば OK です(これが,ROOT_DIR マクロを 各 Makefile で定義した理由です).
例として,message-upcase ターゲットという MESSAGE マクロをすべて大文字で表示するタスクターゲットを定義することにしましょう.ここでは,Ruby スクリプト: message-upcase.rb でその処理を行うものとします.まず,ルートディレクトリに message-upcase.rb を作ります:
#! ruby puts ARGV[0].upcase
このファイルを利用する common.mak は以下のようになります.
message-upcase: ruby $(ROOT_DIR)\message.rb "$(MESSAGE)"
これでどこからでも MESSAGE を大文字表示するタスクターゲットが利用できます(ここではサブディレクトリを再帰的に呼び出すことは考慮していません).
もっとも nmake の場合は,インラインファイルを使ってもっと簡単に実装することができます.この場合は, message-upcase.rb というファイルを作る必要はありません.common.mak に
message-upcase: ruby << puts "$(MESSAGE)".upcase <<
とするだけでよいのです.nmake のインラインファイルは非常に便利で,直接 メイクファイルのマクロを埋め込んだスクリプトが書けます(ただし,ファイル内で nmake の特殊文字 $ をそのまま使いたい場合は $$ とします).
共有ファイルを使った集計処理
ディレクトリ階層を巡回して,集計処理を行いたい場合があります.例えば,あるディレクトリ以下の java ファイルが合計何ファイルあるのか,という処理を行いたい場合を考えましょう.
こういった処理を行うのは subdirs.mak は不得意です.打開策としては,ルートディレクトリに共有ファイルを用意して,各ディレクトリを巡回しながら結果を書き込んでいき,最後に Ruby などのスクリプト言語を使って集計をとる,という方法が考えられます.
この方法を使った Java ファイルをカウントするサンプルは,以下のようになります.これを common.mak に書き込んで実行してみてください(適当に拡張子 java のファイルを各ディレクトリに置いて確かめてみましょう).
COUNT_JAVA_FILES_RESULT_FILE = $(ROOT_DIR)\count-java-files.result count-java-files: if exist $(COUNT_JAVA_FILES_RESULT_FILE) del $(COUNT_JAVA_FILES_RESULT_FILE) $(MAKE) /NOLOGO /S subdirs SUBDIRS_PRE_TARGET=count-java-files-self ruby << count = 0 open('$(COUNT_JAVA_FILES_RESULT_FILE)').each do |line| count += line.to_i end puts "Java のファイル数は #{count} 個です." << count-java-files-self: ruby << >> $(COUNT_JAVA_FILES_RESULT_FILE) puts Dir['*.java'].size <<
まず,最初に 共有ファイルのファイル名を COUNT_JAVA_FILES_RESULT_FILE マクロに設定しています.その後 count-java-files のターゲットのコマンドライン処理は,次のようになっています.
- あらかじめ共有ファイルを削除しておく
- ディレクトリ階層を巡回し,各ディレクトリの Java ファイル数を共有ファイルに書き込む
- インライン Ruby スクリプトを使って共有ファイルをオープンし,集計をとる.
count-java-files の self ターゲットである count-java-files-self では, Ruby スクリプトで拡張子が java のファイル数を計算しています.見づらいかもしれませんが, nmake のインラインファイルを使ってリダイレクトで共有ファイルに追加書き込みするのは,上のような書き方になります.