Makefileでソース、ヘッダファイルの依存関係を処理
大規模なC言語プログラムでは、ソースやヘッダファイルが複雑な依存関係を持つため、それらを自動解決してくれるMakefileが欲しくなる。 欲望を具体化すると
- ソース、ヘッダ、オブジェクトファイルは異なるディレクトリに入れたい
- 依存関係を認識し、更新すべきオブジェクトファイルを自動検出
- 新しいソース、ヘッダファイルを作成した際、Makefileを書き換ずに済む
- 他のプロジェクトにも使いまわせる汎用性
- Makefile自体が短く、保守しやすい
だいぶ贅沢だが、頑張ってMakefileをこしらえたので、得られた知見を忘れないうちにまとめておく。 以前書いたMakefile文法ミニマムも参考にしてくれ。
ディレクトリ構成
root +----src/ | +---- main.c # mA.h を読み込む | +---- A.c # mA.h と A.h を読み込む | +----inc/ | +---- mA.h # 依存なし | +---- A.h # B.h を読み込む | +---- B.h # 依存なし | +----obj/ | +---- main.o # makeコマンドで作成される | +---- A.o # makeコマンドで作成される | +---- Makefile # *.o を作成 | +---- exec.out # makeコマンドで作成される +---- Makefile # exec.out を作成
*.c *.h *.o はそれぞれ別ディレクトリで管理する。 もちろん依存関係を考慮して
- A.c が更新されれば、A.o を更新
- mA.h が更新されれば、main.o と A.o を更新
- B.h が更新されれば、A.o を更新
といった具合でオブジェクトファイルを作り直し、exec.out に再リンクしたいのだ。
Makefile
この状況に対処するには、まず root/Makefile を作る
# root/Makefile CC := gcc ALL_C := $(wildcard src/*.c) # src/main.c src/A.c ALL_O := $(patsubst src/%.c,obj/%.o,$(ALL_C)) # obj/main.o obj/A.o ALL_CH := $(wildcard src/*.c inc/*.h) # src/*.c inc/*.h exec.out: $(ALL_CH) cd obj && $(MAKE) "CC=$(CC)" # obj/Makefile を実行する (ALL_Oが作成される) $(CC) $(ALL_O) -o $@ .PHONY: clean clean: @rm -rf *.out obj/*.o obj/*.d
このファイルの処理内容は Makefile文法ミニマム を見れば理解できるだろう。
要するにソースやヘッダファイルに更新があれば exec.out
をリビルドするのだが、
更新すべきオブジェクトファイルの検出と再コンパイルは root/obj/Makefileに丸投げしている。
この root/obj/Makefile は
# root/obj/Makefile ALL_C := $(wildcard ../src/*.c) # ../src/main.c ../src/A.c ALL_O := $(patsubst ../src/%.c,%.o,$(ALL_C)) # main.o A.o ALL_D := $(patsubst ../src/%.c,%.d,$(ALL_C)) # main.d A.d ALL_H := $(wildcard ../inc/*.h) # ../inc/mA.h ../inc/A.h ../inc/B.h .PHONY: dummy # dummy というファイルは作成されないので PHONY 指定 dummy: $(ALL_O) # 実行するには ALL_O が必要 --> 下の %.o:... で作成 %.o: ../src/%.c # A.c 以外の依存ファイルは、下の -include A.d で設定される $(CC) -c $< -o $@ %.d: ../src/%.c $(ALL_H) # 下の -include 命令から呼ばれる cpp -MM $< -MF $@ # A.c の依存関係をMakefile形式で書いた A.d を生成 -include $(ALL_D) # main.d と A.d を読み込む (無い場合は %.d:... で作成)
ファイルの冒頭の ALL_C
と ALL_O
は意味がわかると思うが、ALL_D
については説明が必要だろう。
実は gcc にはプリプロセッサの cpp が含まれており、
$ cpp -MM A.c -MF A.d
とすることで A.c が参照している他のソースやヘッダファイルの情報を Makefile 形式で取得できる。 A.d の中身は以下のような感じ
A.o: ../src/A.c ../inc/mA.h ../inc/A.h ../inc/B.h
これを見れば、A.d を読み込む Makefile は A.d と同じディレクトリに設置する必要があると分かるだろう。 そうした事情で root/Makefile から root/obj/Makefile を分離した。
cpp して得られた A.d をファイル末尾の -include $(ALL_D)
により読み込めば、
既に定義されているパターンマッチが以下のように上書きされる
# 上書き前 %.o: ../src/%.c $(CC) -c $< -o $@ # 上書き後 A.o: ../src/A.c ../inc/mA.h ../inc/A.h ../inc/B.h $(CC) -c $< -o $@
これでオブジェクトファイルの依存関係が解決し、正しく再コンパイルできるようになったわけだ。
まとめ
make コマンドを叩いた後の処理内容をまとめておく。
- root ディレクトリで make コマンドを叩く。
- root/Makefile の最初のビルド命令
exec.out: $(ALL_CH)
に基づき、任意のソース・ヘッダファイルが変化していれば exec.out を更新しようとする。 cd obj && $(MAKE)
により、root/obj/Makefile の make を実行。- root/obj/Makefile の最終行
-include $(ALL_D)
により、A.d と main.d を読み込もうとする。 %.d: ../src/%.c $(ALL_H)
により、対象のソースか任意のヘッダファイルが変化していれば A.d と main.d を更新。- A.d と main.d を読み込む。
- パターンマッチの
%.o: ../src/%.c
が A.d と main.d に書かれた依存関係で上書きされる。 - root/obj/Makefile の最初のビルド命令
dummy: $(ALL_O)
を実行。 - dummyの処理は何もないが、依存ファイルとして main.o と A.o が指定されているので、それらを更新しようとする。
- (7で上書きした)依存関係に基づいて、main.o と A.o を更新する必要があるか判断。
- 必要と判断されれば
$(CC) -c $< -o $@
を実行して、main.o と A.o を更新。 - dummy の処理(何もしない)が終わったので、root/Makefile に戻る。
$(CC) $(ALL_O) -o $@
を実行し、exec.out を更新。
うーむややこしい。 処理順序としては 1,2,3 の後に 8,9,10,… としたいわけだが、8を行う前に root/obj/Makefile の事前準備として 4,5,6,7 が実行される感じだな。