Makefileでヘッダファイルの依存関係を扱う

依存関係を扱うことで、ヘッダファイルが更新された場合に、ヘッダファイルをインクルードするソースコードが再ビルドされるようにします。

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