makefile と nmake ~ makefile を読み解く
今回は、分割コンパイル時に威力を発揮する makefile の書き方についてお話します。
1.準備
ここでは、例として記事「複数ファイルのリンク方法 (分割コンパイルとリンク)」で利用したソースコードを使います。 例で利用しているのは、conf.cpp, conf.h, log.cpp, log.h, prog1.cpp, prog1.h の 6 ファイルです。
makefile についての説明になりますので、必ずしも上記ファイルでなくとも構いません。 もし違う名前のファイルで試す場合は、ファイル名を読み替えてください。
準備はよろしいでしょうか?
2.nmake と makefile の実験
では、上記6ファイルをひとつのディレクトリに保存して、以下の内容を makefile という名前のファイルに保存してください。拡張子はありません。
TARGETNAME=test
OUTDIR=.\chk
LINK32=link.exe
ALL : $(OUTDIR)\$(TARGETNAME).exe
CLEAN :
-@erase /Q $(OUTDIR)\*
$(OUTDIR) :
@if not exist $(OUTDIR) mkdir $(OUTDIR)
CPPFLAGS=\
/nologo\
/W3\
/Fo"$(OUTDIR)\\"\
/Fd"$(OUTDIR)\\"\
/c\
/Zi\
/D_WIN32_WINNT=0x0600\
/DUNICODE\
/D_UNICODE
LINK32_FLAGS=\
/nologo\
/subsystem:console\
/pdb:"$(OUTDIR)\$(TARGETNAME).pdb"\
/out:"$(OUTDIR)\$(TARGETNAME).exe"\
/DEBUG
LINK32_OBJS=\
$(OUTDIR)\prog1.obj\
$(OUTDIR)\conf.obj\
$(OUTDIR)\log.obj
$(OUTDIR)\$(TARGETNAME).exe : $(OUTDIR) $(LINK32_OBJS)
$(LINK32) $(LINK32_FLAGS) $(LINK32_OBJS)
.cpp{$(OUTDIR)}.obj:
$(CPP) $(CPPFLAGS) $<
そして次に、Visual Studio コマンドプロンプトを開き、次のコマンドを実行します。
> nmake
これにより、ソースファイルを保存したディレクトリのサブディレクトリとして chk という名前のディレクトリが作成され、その中に *.obj, *.pdb, *.exe 等が生成 されたのではないでしょうか。
Visual Studio 2017 をインストールした環境では次のようなコマンド環境が選択できます。
このうち、x64 上で x64 向けのビルドを行うのは x64 Native Tools Command Prompt です。Cross Tools 系は x86 (32ビット) 上で x64 向けをビルドしたり、その逆だったりする場合です。
試しに cpp ファイルのどれかひとつだけを、ちょっとだけ変更して保存してください。 (何も無いところにスペースを入れる、など)
そして、もう一度 nmake を実行してください。
いかがでしたか?
今、変更したファイルのみが再コンパイルされて、新しい EXE ファイルが出来た のではないでしょうか。
このように、nmake と makefile を利用すると、必要なディレクトリを作成する、 必要なファイルだけを再コンパイルするなど、ビルドプロセスを効率よく行うことが できます。
makefile には、どのようなビルドを行うか、という「ルール」を書いておきます。
nmake コマンドは makefile を読み込み適切な処理を行います。
3.makefile の中身
それでは makefile はどのように書けばよいでしょうか。
上に示した makefile を例にとり、ひとつひとつ説明します。
まず、読み込まれるのは以下の部分です。
TARGETNAME=test
OUTDIR=.\chk
LINK32=link.exe
この部分はマクロをセットしています。こうしておくと、後で使うスクリプトで、 $(TARGETNAME) を test に、$(OUTDIR) を .\chk に置き換えることができます。 CPPFLAGS, LINK32_FLAGS, LINK32_OBJS も同様です。\ で改行しています。
さて、次に実行されるのは ALL と書かれている次の部分です。
ALL : $(OUTDIR)\$(TARGETNAME).exe
これは
「ALL は $(OUTDIR)\$(TARGETNAME).exe に依存しています」
という意味になります。
上述のように $(OUTDIR)\$(TARGETNAME).exe の $() の部分はマクロで展開できますから、結局、
「ALL は .\chk\test.exe に依存しています」
という意味になります。
ところで、.\chk\test.exe はこれから作るファイルです。まだ無いファイル に依存しているのですから、それを作らなければなりません。
そこで次のルールが必要になります。
$(OUTDIR)\$(TARGETNAME).exe : $(OUTDIR) $(LINK32_OBJS)
$(LINK32) $(LINK32_FLAGS) $(LINK32_OBJS)
この部分は、
「$(OUTDIR)\$(TARGETNAME).exe は $(OUTDIR) と $(LINK32_OBJS) に依存している」
という意味になります。
$() を読み下すと、
「.\chk\test.exe は .\chk、 .\chk\prog1.obj、.\chk\conf.obj、及び $(OUTDIR)\log.obj に依存している」
ということになります。
そこでまず、 $(OUTDIR) (=.\chk) を見てみると
$(OUTDIR) :
@if not exist $(OUTDIR) mkdir $(OUTDIR)
とあります。
コロン (:) の後ろに何も無いのは、依存関係が無いということです。
この場合、直ちに、その次の行のコマンドを実行します。すなわち、この部分です。
@if not exist $(OUTDIR) mkdir $(OUTDIR)
これは makefile の固有のコマンドではなく、普通の (?) バッチコマンドです。 Windows のコマンドプロンプトで単独で使えるコマンドです。
「$(OUTDIR) ディレクトリが無い場合、$(OUTDIR) を mkdir コマンドで作成せよ」
という意味ですから、 これによって、$(OUTDIR) が (存在しなければ新たに) 作成されます。
続いて、*.obj ファイルが必要になりますが、これは次の行で処理されています。
.cpp{$(OUTDIR)}.obj:
$(CPP) $(CPPFLAGS) $<
これは、
「{$(OUTDIR)} の .obj を作るには .cpp ファイルを次のコマンドで処理せよ」
と読み下せばよいものです。
CPP は事前定義されている nmake マクロで、cl として解釈されます。 また、$< は 「変更のあったファイル」 を表すファイル名マクロです。
この結果、.obj ファイルを作るため、cl (C/C++ コンパイラ) が実行され、 .cpp ファイルから .obj ファイルが作成されます。
以上で、EXE ファイルを作成する準備は整い、最終的に 次のコマンドで、EXE ファイルが作成されることになります。
$(LINK32) $(LINK32_FLAGS) $(LINK32_OBJS)
makefile は一見複雑に見えますが、依存関係をたどると、 単純なルールの積み重ねであることがわかります。
最後に EXE を作るルールのチェーンに出てこなかった次の二行について説明します。
CLEAN :
-@erase /Q $(OUTDIR)\*
これは、nmake が clean というオプションを受け取ったときに実行する コマンドです。(ALL はオプション省略時に実行されるコマンドなのです)
ここでは CLEAN には依存ファイルがありませんから、直ちに次のコマンドが 実行されます。
-@erase /Q $(OUTDIR)\*
ですから、結局、
> nmake clean
とすると、erase コマンドが実行されることになります。
一見すると、makefile によるビルドは無駄に小難しそうにみえるかもしれません。
しかし、コンパイラやリンカーから特定のオプションがなくなるなどしない限り、ほとんど何も変更せずに使いまわせますし、 昔からあるので数年程度ではほとんど変更もないです。
ながく使いたいコードは、なるべく環境依存しないように makefile でするといいです。