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 コマンドを叩いた後の処理内容をまとめておく。

  1. root ディレクトリで make コマンドを叩く。
  2. root/Makefile の最初のビルド命令 exec.out: $(ALL_CH) に基づき、任意のソース・ヘッダファイルが変化していれば exec.out を更新しようとする。
  3. cd obj && $(MAKE) により、root/obj/Makefile の make を実行。
  4. root/obj/Makefile の最終行 -include $(ALL_D) により、A.d と main.d を読み込もうとする。
  5. %.d: ../src/%.c $(ALL_H) により、対象のソースか任意のヘッダファイルが変化していれば A.d と main.d を更新。
  6. A.d と main.d を読み込む。
  7. パターンマッチの %.o: ../src/%.c が A.d と main.d に書かれた依存関係で上書きされる。
  8. root/obj/Makefile の最初のビルド命令 dummy: $(ALL_O) を実行。
  9. dummyの処理は何もないが、依存ファイルとして main.o と A.o が指定されているので、それらを更新しようとする。
  10. (7で上書きした)依存関係に基づいて、main.o と A.o を更新する必要があるか判断。
  11. 必要と判断されれば $(CC) -c $< -o $@ を実行して、main.o と A.o を更新。
  12. dummy の処理(何もしない)が終わったので、root/Makefile に戻る。
  13. $(CC) $(ALL_O) -o $@ を実行し、exec.out を更新。

うーむややこしい。 処理順序としては 1,2,3 の後に 8,9,10,… としたいわけだが、8を行う前に root/obj/Makefile の事前準備として 4,5,6,7 が実行される感じだな。