##はじめに
C や C++ で書かれたプログラムのビルドの際、Make でヘッダファイルの依存関係に対応するには、gcc の -MMD オプションを組み合わせればよいことが知られている。
通常はこの記事と同じように -MMD オプションを使ってコンパイルと依存関係ファイル .d の生成を同時に行うことが多いと思われるが、諸事情があり、コンパイルと依存関係ファイルの生成を別々に行いたくなった。簡単そうに見えるが、妙なところでハマった。
##結論
このような Makefile を作れば期待通りの動作を実現できた。GNU Make 3.81 で確認。
続きの記事を書いたのでそちらを参照。
PROG := myapp
SRCS := main.cc foo.cc
OBJS := ${SRCS:%.cc=%.o}
DEPS := ${SRCS:%.cc=%.d}
CXX := g++
all: ${PROG}
${PROG}: ${OBJS}
${CXX} -o $@ ${OBJS}
${DEPS}: %.d: %.cc
${CXX} $< -MM -MP -MF $@
${OBJS}: %.o: %.cc %.d
${CXX} -c $< -o $@
include $(shell ls ${DEPS} 2>/dev/null)
clean:
${RM} ${PROG} ${OBJS} ${DEPS}
.PHONY: all clean
「期待通りの動作」とは、ハマりどころの裏返しでもあるのだが、主に以下の2点がポイントである。
- オブジェクトファイル .o が依存関係ファイル .d にも依存するように指定する
- make -n (dry-run) の際には依存関係ファイル .d は生成されないようにする
1点目のポイントは、依存関係ファイル .d とオブジェクトファイル .o の生成ルールを以下のように「静的パターンルール」で書くことである。
${DEPS}: %.d: %.cc
${CXX} $< -MM -MP -MF $@
${OBJS}: %.o: %.cc %.d
${CXX} -c $< -o $@
2点目のポイントは、依存関係ファイル .d を -include 文に頼らずに以下のように書いてインクルードすることである。
include $(shell ls ${DEPS} 2>/dev/null)
##ハマりどころの顛末
先に後者のポイントから説明する。
###Dry-run の際にも依存関係ファイルが生成されてしまう
まず、最初に紹介した記事にならって、コンパイルと依存関係ファイルの生成を同時に行う Makefile を作ると以下のようになる。主要部分のみ抜粋。
-include ${DEPS}
%.o: %.cc
${CXX} -c -MMD -MP $<
これを、コンパイルと依存関係ファイルの生成を別々に行うように変更する。
-include ${DEPS}
%.d: %.cc
${CXX} $< -MM -MP -MF $@
%.o: %.cc %.d
${CXX} -c $< -o $@
さて、これでよいはずだと思ったが、念のためきちんと動くか dry-run で確認する。
shu@ubuntu:~/work$ ls
Makefile foo.cc foo.hpp main.cc
shu@ubuntu:~/work$ make -n
g++ foo.cc -MM -MP -MF foo.d
g++ main.cc -MM -MP -MF main.d
g++ -c main.cc -o main.o
g++ -c foo.cc -o foo.o
g++ -o myapp main.o foo.o
shu@ubuntu:~/work$ ls
Makefile foo.cc foo.d foo.hpp main.cc main.d
おかしい。実行される予定のコマンド列は正しいのだが、依存関係ファイル foo.d, main.d が実際に生成されてしまっている。ビルド自体は何ら問題なく実行できるため、気にしなければよいと言われればそれまでなのだが…。
shu@ubuntu:~/work$ ls
Makefile foo.cc foo.hpp main.cc
shu@ubuntu:~/work$ make clean
g++ foo.cc -MM -MP -MF foo.d
g++ main.cc -MM -MP -MF main.d
rm -f myapp main.o foo.o main.d foo.d
make clean に至ってはわざわざ依存関係ファイルを生成してから削除している。生成を要求していないのに…? 誰かが生成を要求している…?? 一体誰が…???
-include ${DEPS}
↑こいつである。
この記事で解説されている通り、いわゆる sub-Makefile を自動生成するルールが定義されていれば、include 文自体が当該 sub-Makefile の要求者となってくれる。
ところがこの sub-Makefile 自動要求は dry-run の対象外のようで、dry-run であろうとなかろうと sub-Makefile が生成されてしまう。よくよく考えれば一理あり、sub-Makefile がなければ Makefile 全体が完成しないのだから、これは当然の挙動とも思える。今回は include 文ではなく -include 文を使っているのだから、ファイルが存在しなければ無視してほしいものだが…。
とりあえずの回避策としては、自前で明示的に「存在するファイルだけインクルードする」ように書く方法がある。
include $(shell ls ${DEPS} 2>/dev/null)
…あまり美しくはない。
###オブジェクトファイルが依存関係ファイルにも依存するように指定できない
さてこれで解決かと思いきや、もうひとつ落とし穴が存在した。この時点では Makefile の主要部分は以下のようになっている。
%.d: %.cc
${CXX} $< -MM -MP -MF $@
%.o: %.cc %.d
${CXX} -c $< -o $@
include $(shell ls ${DEPS} 2>/dev/null)
実行してみる。
shu@ubuntu:~/work$ make
g++ -c -o main.o main.cc
g++ -c -o foo.o foo.cc
g++ -o myapp main.o foo.o
shu@ubuntu:~/work$ ls
Makefile foo.cc foo.hpp foo.o main.cc main.o myapp
おかしい。依存関係ファイルが生成されていない。よく見るとコンパイルする際の引数の順番が Makefile で定義したものと異なっており、デフォルトのサフィックスルールが走っているように見える。Makefile で定義したルールはどこへ行ったのか?
この記事の回答者の解説によると、「暗黙パターンルール」では依存するファイル(コロンより右側)が存在しない場合、そのルールは実行候補から除外されてしまうらしい。今回の場合は、main.d や foo.d が(最初の時点では)存在しないため、せっかく定義したルールが実行候補から外れてしまっているのである。
解決策は、暗黙パターンルールを静的パターンルールに書き換えることである。見た目は似ているが、静的パターンルールの場合は、存在しないファイルに依存しているようになっていると、その依存ファイルを生成する別なルールがないか探してくれるようだ。
${DEPS}: %.d: %.cc
${CXX} $< -MM -MP -MF $@
${OBJS}: %.o: %.cc %.d
${CXX} -c $< -o $@
ちなみに静的パターンルールは、1行の中にコロンが2回出てきてよくわからないかもしれないが、例えば ${DEPS}: %.d: %.cc
の場合は、${DEPS}
に対して for-each で %.d: %.cc
が定義されていると思えばよい。
##参考文献
- Makeでヘッダファイルの依存関係に対応する (wagavulin's blog, 2012/04/05)
- GNU Make は include 先が見つからなくてもルールさえあれば生成して include してくれる (Qiita, 2017/02/17)
- Makefile pattern rule with two or more dependencies %.sas7bdat: %.sas %.dat (Stack Overflow, 2015/02/05)