C
C++
Makefile
GNUmake

Makeでヘッダファイルの依存関係を生成するルールを独立させる・続

はじめに

C や C++ で書かれたプログラムのビルドの際、Make でヘッダファイルの依存関係に対応するには、gcc の -MMD オプションを組み合わせればよいことが知られている。

通常はこの記事と同じように -MMD オプションを使ってコンパイルと依存関係ファイル .d の生成を同時に行うことが多いと思われるが、諸事情があり、コンパイルと依存関係ファイルの生成を別々に行いたくなった。そのため以下の記事を書いた。

しかし、これでもまだ include 関係の挙動に問題があることがわかった。

結論

完全に期待通りの動作というわけではないが、以下のような Makefile を書いて対応した。GNU Make 3.81 で確認。

続きの記事を書いたのでそちらを参照。

Makefile
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 $@

clean:
    ${RM} ${PROG} ${OBJS} ${DEPS}

IS_DRYRUN := $(findstring n,$(filter-out --%,${MAKEFLAGS}))
ONLY_CLEAN := $(findstring _clean_,_${MAKECMDGOALS}_)

ifeq ($(or ${IS_DRYRUN},${ONLY_CLEAN}),)
  -include ${DEPS}
endif

.PHONY: all clean

解説

「ファイルが存在するときだけインクルード」では解決になっていなかった

前に書いた記事では include 文は以下のように「ファイルが存在するときだけインクルードする」ようにしていた。

include $(shell ls ${DEPS} 2>/dev/null)

もう少しよく考えればすぐわかったことだったのだが、これでは解決になっていなかった。依存関係ファイル .d が存在していてもそれが古い場合は、無用な再生成が実行されることがある。以下の例ではやはりわざわざ依存関係ファイルを生成してから削除している。

shu@ubuntu:~/work$ make
g++ main.cc -MM -MP -MF main.d
g++ -c main.cc -o main.o
g++ foo.cc -MM -MP -MF foo.d
g++ -c foo.cc -o foo.o
g++ -o myapp main.o foo.o
shu@ubuntu:~/work$ touch foo.cc main.cc
shu@ubuntu:~/work$ make clean
g++ main.cc -MM -MP -MF main.d
g++ foo.cc -MM -MP -MF foo.d
rm -f myapp main.o foo.o main.d foo.d

よくよく調べてみると、この問題に関しては GNU Make のマニュアルでもきちんと言及されていて、以下のように書いてある。

An example of appropriate use is to avoid including .d files during clean rules (see Automatic Prerequisites), so make won't create them only to immediately remove them again:

sources = foo.c bar.c

ifneq ($(MAKECMDGOALS),clean)
include $(sources:.c=.d)
endif

要するに clean するときぐらいは依存関係ファイルのインクルードを抑制しようということである。

本記事ではもう一歩進めて、Dry-run または clean のときには依存関係ファイルのインクルードを抑制する方法について調査した結果をまとめる。

Dry-run か否かを取得する

この記事で一例が述べられている通り、make 実行時のフラグは ${MAKEFLAGS} という変数に保存されている。-n と --dry-run など、同じ効果のオプションで短い形式と長い形式の両方が用意されている場合には、常に短い形式で ${MAKEFLAGS} に格納されるようだ。

shu@ubuntu:~/work$ make --print-data-base --dry-run | grep MAKEFLAGS
MAKEFLAGS = pn

ちなみに -p (--print-data-base) は Makefile を読み込んで構成されたルールや変数を出力してくれるオプションである。

長い形式しか用意されていないオプション(例えば --warn-undefined-variables など)が混在していると、${MAKEFLAGS} にもやはり混在状態でフラグが格納される。

shu@ubuntu:~/work$ make -np --warn-undefined-variables --debug | grep MAKEFLAGS
MAKEFLAGS =  --warn-undefined-variables -pn --debug=basic

Dry-run か否かを知りたいだけであれば、filter-out 関数で長い形式のオプションを排除してから、findstring 関数で n フラグが含まれているかをチェックすればよいだろう。以下のようにすれば、dry-run のときには n が、そうでないときは空文字列が IS_DRYRUN に格納される。

IS_DRYRUN := $(findstring n,$(filter-out --%,${MAKEFLAGS}))
shu@ubuntu:~/work$ make -p | grep IS_DRYRUN
IS_DRYRUN :=
shu@ubuntu:~/work$ make -np | grep IS_DRYRUN
IS_DRYRUN := n

ターゲットが clean か否かを取得する

上で引用した GNU Make のマニュアルでも述べられている通り、make 実行時のターゲットは ${MAKECMDGOALS} に格納されている。以下のようにすれば、ターゲットに clean のみが指定されているときは _clean_ が、そうでないときは空文字列が ONLY_CLEAN に格納される。文字列の完全一致比較をする関数が見当たらなかったため、やや苦しい。

ONLY_CLEAN := $(findstring _clean_,_${MAKECMDGOALS}_)
shu@ubuntu:~/work$ make -np | grep ONLY_CLEAN
ONLY_CLEAN :=
shu@ubuntu:~/work$ make -np clean | grep ONLY_CLEAN
ONLY_CLEAN := _clean_
shu@ubuntu:~/work$ make -np clean all | grep ONLY_CLEAN
ONLY_CLEAN :=

3番目の例ではターゲットに clean と all の両方が指定されているため、ONLY_CLEAN ではない。

インクルードを抑制する

あとは ${IS_DRYRUN}${ONLY_CLEAN} のどちらかが有意な文字列であれば依存関係ファイルのインクルードを抑制する、逆に言えば、${IS_DRYRUN}${ONLY_CLEAN} も空文字列であれば依存関係ファイルをインクルードする、というようにすればよい。

ifeq ($(or ${IS_DRYRUN},${ONLY_CLEAN}),)
  -include ${DEPS}
endif

今回はここまでで諦めることにした。うっかりタイプミスなどで未定義のターゲットを指定して make を実行してしまうと、それでもとりあえず依存関係ファイルをインクルードしようとする、そして生成してしまう、という問題は残る。

shu@ubuntu:~/work$ make hoge
g++ foo.cc -MM -MP -MF foo.d
g++ main.cc -MM -MP -MF main.d
make: *** No rule to make target `hoge'.  Stop.

参考文献