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

##はじめに

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

- [Makeでヘッダファイルの依存関係に対応する][link1]

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

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

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

##結論

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

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

- [Makeでヘッダファイルの依存関係を生成するルールを独立させると混乱する](https://qiita.com/shu_ohm1/items/6481c54df9162abb0aa1)

```Make: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
```

##解説

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

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

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

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

```Console
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 のマニュアル][link3]でもきちんと言及されていて、以下のように書いてある。

> An example of appropriate use is to avoid including .d files during clean rules (see Automatic Prerequisites), so make won&apos;t create them only to immediately remove them again:
> 
> ```Makefile
> sources = foo.c bar.c
> 
> ifneq ($(MAKECMDGOALS),clean)
> include $(sources:.c=.d)
> endif
> ```

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

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

###Dry-run か否かを取得する

- [In Makefile, know if GNU make is in --dry-run?][link4]

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

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

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

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

```Console
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` に格納される。

```Make
IS_DRYRUN := $(findstring n,$(filter-out --%,${MAKEFLAGS}))
```

```Console
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` に格納される。文字列の完全一致比較をする関数が見当たらなかったため、やや苦しい。

```Make
ONLY_CLEAN := $(findstring _clean_,_${MAKECMDGOALS}_)
```

```Console
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}` も空文字列であれば依存関係ファイルをインクルードする、というようにすればよい。

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

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

```Console
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.
```

##参考文献

- [Makeでヘッダファイルの依存関係に対応する][link1] (wagavulin's blog, 2012/04/05)
- [Makeでヘッダファイルの依存関係を生成するルールを独立させる][link2] (Qiita, 2018/03/22)
- [9.2 Arguments to Specify the Goals][link3] (GNU Make Manual, 2016/05/22)
- [In Makefile, know if GNU make is in --dry-run?][link4] (Stack Overflow, 2016/12/23)

[link1]:http://www.wagavulin.jp/entry/20120405/1333629926
[link2]:https://qiita.com/shu_ohm1/items/e84b5e5a3c461add03b4
[link3]:https://www.gnu.org/software/make/manual/html_node/Goals.html#Goals
[link4]:https://stackoverflow.com/questions/41302443/in-makefile-know-if-gnu-make-is-in-dry-run