koturnの日記

普通の人です.ブログ上のコードはコピペ自由です.

GNU MakeでC/C++の依存関係を自動定義する

背景

C/C++用のMakefileを書いていると,依存関係を記述するのがなかなか厄介に感じられる. 何とか自動的に依存関係を定義したいと考えた.

gcc/g++による依存関係の抽出

gcc/g++は以下のようにして依存関係を抽出することが可能である.

$ g++ -MM foo.cpp
foo.o: foo.cpp aaa.h bbb.h

この機能を利用して,Makefileで依存関係の自動定義を行う.

外部ファイルに依存関係を書き出す

makeには他のMakefileをインクルードすることが可能である. これを利用し,依存関係を記述したファイルを掃き出し,Makefileからインクルードするとよさそうだ.

CXX     := g++
SRCS    := foo.cpp bar.cpp
DEPENDS := depends.mk

.PHONY: depends

depends:
    $(CXX) -MM $(SRCS) > $(DEPENDS)

-include $(DEPENDS)

ただし,この方法だと make depends を実行しなければ,依存関係を更新されない. また,初回はdepends.mkが存在せず,依存関係が定義されない. これを自動的に更新したい.

外部ファイルを用いず,依存関係を自動定義する

GNU Makeには,引数をシェルコマンドとして実行し,出力結果として返却する shell 関数,引数をMakefileに記述されたものとして扱う eval 関数が存在する. これらを利用することで,Makefileから依存関係を定義することができそうだ.

CXX  := g++
SRCS := foo.cpp bar.cpp

$(eval $(shell $(CXX) -MM $(SRCS)))

しかし,この方法では改行が無視されるため,うまくいかない. 具体的には,複数ファイルを一度に g++ -MM の引数として渡せない,行継続の \ によってエラーとなる. したがって,ソースファイルを1つずつに対し, g++ -MM を実行する必要し, \ を消去する必要がある.

GNU Makeには,文字列の置換を行う subst 関数, 与えられたリストの要素それぞれに,指定した処理を実行する foreach 関数がある. これらを利用し,前述の問題を解決する.

CXX  := g++
SRCS := foo.cpp bar.cpp

$(foreach SRC,$(SRCS),$(eval $(subst \,,$(shell $(CXX) -MM $(SRC)))))

具体的なMakefileの全貌は以下のようになるだろう. 圧倒的に依存関係の記述が楽になり,ヘッダファイル名は全く記述する必要が無くなっていることがわかる.

CXX      := g++ -std=gnu++14
CXXFLAGS := -Wall -Wextra -O3 -march=native -DNDEBUG
LDFLAGS  := -s
TARGET   := main
OBJS     := $(addsuffix .o, $(basename $(TARGET)) foo bar)
SRCS     := $(OBJS:.o=.cpp)

ifeq ($(OS),Windows_NT)
    TARGET := $(addsuffix .exe, $(TARGET))
else
    TARGET := $(addsuffix .out, $(TARGET))
endif

%.exe:
  $(CXX) $(LDFLAGS) $(filter %.c %.cpp %.cxx %.cc %.o, $^) $(LDLIBS) -o $@
%.out:
  $(CXX) $(LDFLAGS) $(filter %.c %.cpp %.cxx %.cc %.o, $^) $(LDLIBS) -o $@

.PHONY: all clean

all: $(TARGET)
$(TARGET): $(OBJS)

$(foreach SRC,$(SRCS),$(eval $(subst \,,$(shell $(CXX) -MM $(SRC)))))

clean:
    $(RM) $(TARGET) $(OBJS)

まとめ

GNU Make用のMakefileに以下のような記述を加えることで,オブジェクトファイル(.o)が依存するソースファイル(.c, .cpp)とヘッダファイル(.h, .hpp)依存関係を自動的に定義することができる. あとは,実行ファイルとオブジェクトファイル間の依存関係を定義するだけでよい.

CXX := g++
SRCS := foo.cpp bar.cpp

$(foreach SRC,$(SRCS),$(eval $(subst \,,$(shell $(CXX) -MM $(SRC)))))