依存関係を扱うことで、ヘッダファイルが更新された場合に、ヘッダファイルをインクルードするソースコードが再ビルドされるようにします。
Table of Contents
1 ヘッダファイルの依存関係を扱う必要性
ヘッダファイルの依存関係を扱わないと、ヘッダファイルが更新されても、それをインクルードするソースコードがビルドされない問題があります。オブジェクトファイルをクリーンして全ビルドを実行する必要があり、コンパイル時間が膨大になります。C++の場合は特に気をつけるべきです。
例えば、以下の構成のソースツリーがあります。
$ tree . ├── Makefile ├── include │ └── sample.h ├── sample └── sample.c 1 directory, 4 files
sample.cは自分で作成したsample.hをインクルードします。
$ cat sample.c #include <stdio.h> #include <sample.h> int main(void) { return 0; }
MakefileでCFLAGSにインクルードパスを設定します。PROGにはsampleという文字列が格納されます。
$ cat Makefile CFLAGS := -I. -I./include SRC := $(wildcard *.c) PROG := $(patsubst %.c,%,$(SRC)) all: $(PROG) clean: @$(RM) $(PROG)
allターゲットの依存に$(PROG)を指定することで、Makefileはsampleを作成するための暗黙のルールを用意してくれます。Makefileで設定したCFLAGSを用いてccコマンドが実行されます。再度makeを実行してもsampleというファイルがあるので実行されません。
$ make cc -I. -I./include sample.c -o sample $ make: Nothing to be done for `all'.
なお、暗黙のルールを利用しない場合は以下の様なターゲットを定義します。
%:%.c $(CC) $(CFLAGS) $< -o $@
touchコマンドでsample.cのタイムスタンプを更新すると、sample.cとsampleのタイムスタンプを比較して、新しくsampleを作成してくれます。
$ touch sample.c $ make cc -I. -I./include sample.c -o sample
問題はsample.hが更新されても、makeは新しくsampleを作成しない点です。sample.cはsample.hをインクルードしているので、sample.cの内容が変更されたことになります。sampleは新しく作成されるべきです。
$ touch include/sample.h $ make make: Nothing to be done for `all'.
2 $(CC)の-MMオプション
-Mオプションや-MMオプションを用いることでインクルードされているヘッダファイルを出力します。
-Mオプションの場合は標準インクルード(コンパイラに組み込まれたサーチパスにあるヘッダファイル)も含んで表示します。stdio.hの延長で多くのファイルがインクルードされています。
$ cc -M -I./include sample.c | head sample.o: sample.c /usr/include/stdc-predef.h /usr/include/stdio.h \ /usr/include/features.h /usr/include/sys/cdefs.h \ /usr/include/bits/wordsize.h /usr/include/gnu/stubs.h \ <snip>
-MMオプションの場合は標準インクルードを含めないで表示します。-MMオプションを用いてMakefileでヘッダファイルの依存を扱います。
$ cc -MM -I./include sample.c sample.o: sample.c include/sample.h
3 依存関係を記述した%.dファイル
そこで従来から%.dファイルの自動作成が推奨されています。
- $(CC)の-MMオプションで"sample.o: sample.c include/sample.h"を出力する。
- sedでsample.dの依存関係を追加する。これはsample.cとsample.hが更新された場合にsample.oだけでなく、sample.dを再作成するようにするためである。
これを踏まえたMakefileは以下のようになります。allターゲットで%.dファイルの作成を依存しておき、make $(PROG)ターゲットを呼びます。$(PROG)もallの依存に記述するとうまく動作しない点に注意してください。$(PROG)を満たす為に%ターゲットが呼ばれ、%の生成に暗黙のルールが利用されます。
CFLAGS := -I. -I./include SRC := $(wildcard *.c) OBJ := $(patsubst %.c,%.o,$(SRC)) DEP := $(patsubst %.c,%.d,$(SRC)) PROG := $(patsubst %.c,%,$(SRC)) all: $(DEP) @$(MAKE) $(PROG) clean: @$(RM) $(DEP) $(OBJ) $(PROG) ifneq ($(filter clean,$(MAKECMDGOALS)),clean) -include $(DEP) endif %.d: %.c $(info GEN $@) @$(CC) -MM $(CFLAGS) $< | sed 's/\($*\)\.o[ :]*/\1.o $@ : /g' > $@ %: %.d
先ほどとは異なり、sample.hを更新した場合も新しくsampleが生成されます。
$ make GEN sample.d cc -I. -I./include -c -o sample.o sample.c cc sample.o -o sample $ make make[1]: `sample' is up to date. $ touch sample.c $ make GEN sample.d cc -I. -I./include -c -o sample.o sample.c cc sample.o -o sample $ touch include/sample.h $ make GEN sample.d cc -I. -I./include -c -o sample.o sample.c cc sample.o -o sample