読者です 読者をやめる 読者になる 読者になる

余白の書きなぐり

aueweのブログ

Makefileでソース、ヘッダファイルの依存関係を処理

make C

大規模な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_CH = $(wildcard ../src/*.c ../inc/*.h)   # ../src/*.c ../inc/*.h

.PHONY: dummy             # dummy というファイルは作成されないので PHONY 指定
dummy: $(ALL_O)           # 実行するには ALL_O が必要 --> 下の %.o:... で作成

%.o: ../src/%.c %.d       # A.o が必要な際は A.o: ../src/A.c A.d として実行
  $(CC) -c $< -o $@

%.d: ../src/%.c $(ALL_CH) # 下の -include 命令から呼ばれる
  cpp -MM $< -MF $@       # A.c の依存関係をMakefile形式で書いた A.d を生成

-include $(ALL_D)         # main.d と A.d を読み込む --> 上の %.d:... で作成

ファイルの冒頭の ALL_CALL_O は意味がわかると思うが、ALL_Dについては説明が必要だろう。 実は gcc にはプリプロセッサの cpp が含まれており、

$ cpp -MM hoge.c -MF hoge.d

とすることで hoge.c が参照している他のソースやヘッダファイルの情報を Makefile 形式で取得できる。 こうして得られた *.d をファイル末尾の -include $(ALL_D) により読み込んでいるわけだ。 *.d を読み込む Makefile は *.d と同じディレクトリに設置する必要があるため、 root/Makefile から root/obj/Makefile を分離した。