前回はMakefileをヘッダファイルの依存関係に対応させてみた。そのMakefileは入力ファイル(ソース、ヘッダ)と出力ファイル(依存関係ファイル(.d)、オブジェクトファイル、実行ファイル)がすべて同じディレクトリにあることが前提となっていた。
しかし、多くのプロジェクトではビルドオプションによってコンパイルオプションや機能を切り替えるようになっているだろう。そのような場合は、出力先ディレクトリも変わるようになっている方が良い。
そこで今回はビルドオプションによる設定変更とサブディレクトリの対応を行ってみる。
サンプルプロジェクト概要
ソースツリー
今回は以下のようなC++プログラムのソースツリーを想定する。なお、できあがる実行ファイルはhelloにする。
myapp/ +- Makefile +- src/ +- main.cpp +- image/ | +- converter.h | +- converter.cpp +- text/ +- converter.h +- converter.cpp
ソースツリーのトップはmyapp、その直下にMakefileとsrcディレクトリがある。srcの直下にはmain.cppがあるが、それ以外はサブディレクトリ以下にある。意地悪なことに同じ名前のファイルが存在している。画像フォーマット変換機能がimage/converter.{h,cpp}にあり、文字コード変換機能がtext/converter.{h,cpp}にあるという想定だ。
ビルドオプション
このプロジェクトでは、2つのビルドオプションがある。
- buildtype
- debugかrelease。debugのときはコンパイルオプションに-O0 -gをつける。releaseのときは-O3をつける。デフォルトはrelease。
- text_conv
- 文字コード変換ライブラリの種類。iconvかicuを選択。デフォルトはiconv。また、icuの場合はコンパイルオプションに-DUSE_ICUをつける必要がある。
これらはmakeの引数で指定する。例えばmake buildtype=debug text_conv=icuなどのように使う。
出力ディレクトリ
出力ディレクトリはmyapp以下にBuildディレクトリを作り、さらにその下にビルドオプション毎にサブディレクトリを作る。サブディレクトリ名は"debug"または"release"に、text_conv=icuのときだけ"-icu"をつけるようにしてみる。
buildtype | text_conv | 出力ディレクトリ |
---|---|---|
releaseまたは省略 | iconvまたは省略 | release |
releaseまたは省略 | icu | release-icu |
debug | iconvまたは省略 | debug |
debug | icu | debug-icu |
また、実行ファイルは出力ディレクトリの直下に、オブジェクトファイルや依存関係ファイルはソースツリーと同じ構造のサブディレクトリ以下に作るようにする。
例えば、buildtype=debugでビルドが成功した場合、以下のような構造になる。
myapp/ +- Build/ | +- debug/ | +- hello | +- src/ | +- main.d | +- main.o | +- image/ | +- converter.d | +- converter.o | +- text/ | +- converter.d | +- converter.o +- src/ :
階層が深くなってしまったが仕方がない。「ソース/ヘッダファイルに同じ名前を持つものがない」という前提があれば、Build/debugフォルダ以下をフラットにして全てのファイルを直下に置くこともできるが、今回はそういうわけにはいかない。
最初のMakefile
さてまずは前回作ったMakefileをコピーし、PROGとSRCSを変更する。また、CではなくC++なのでその辺りも対応すると以下のようになる。なお、これではビルドはうまくいかない。
PROG := hello SRCS := src/main.cpp src/image/converter.cpp src/text/converter.cpp OBJS := $(SRCS:%.cpp=%.o) DEPS := $(SRCS:%.cpp=%.d) CXX := g++ all: $(PROG) -include $(DEPS) $(PROG): $(OBJS) $(CC) -o $@ $^ %.o: %.cpp $(CC) -c -MMD -MP $< clean: rm -f $(PROG) $(OBJS) $(DEPS)
各変数の設定
buildtypeと出力ディレクトリ
buildtypeの設定に応じてコンパイルオプションを変えるのはそれほど難しくない。Makeは条件分岐をサポートしているため、これを使えばよい。また、出力ディレクトリをOUTDIRに入れるようにする。これはbuildtypeの値に応じて変わる。
buildtype := release ifeq ($(buildtype),release) CXXFLAGS += -O3 else ifeq ($(buildtype),debug) CXXFLAGS += -O0 -g else $(error buildtype must be release, debug, profile or coverage) endif OUTDIR := Build/$(buildtype)
なお、どちらでもない場合に$(error)でMakeを強制的に終了させるようにしている。こうしておくと、make buildtype=deubgなどのように打ち間違えてもすぐに分かる。
text_conv
text_convオプションも特に難しいことはない。text_convの値に応じてDEFSやLIBSといった変数を変える。場合によってはSRCSに追加されることもあるだろう。また、icuの場合はOUTDIRを変える必要がある。
text_conv := iconv ifeq ($(text_conv),iconv) else ifeq ($(text_conv),icu) DEFS += -DUSE_ICU=1 LIBS += -licuuc OUTDIR := $(OUTDIR)-icu else $(error text_conv must be iconv or icu) endif
変数名の確認と整理
ディレクトリ構成がフラットでない場合は、ファイル/パス名の種類を明確にしなければならない。今回は以下のようにする。
変数名 | 意味 | パスの種類 |
---|---|---|
PROGNAME | 実行ファイル | ファイル名 |
PROG | 実行ファイル | OUTDIRからの相対パス |
SRCS | ソースファイル | myappからの相対パス |
OBJS | オブジェクトファイル | OUTDIRからの相対パス |
DEPS | 依存関係ファイル | myappからの相対パス |
Makefileに反映すると以下のようになる。
PROGNAME := hello SRCS := src/main.cpp src/image/converter.cpp src/text/converter.cpp PROG := $(OUTDIR)/$(PROGNAME) OBJS := $(SRCS:%.cpp=$(OUTDIR)/%.o) DEPS := $(SRCS:%.cpp=$(OUTDIR)/%.d)
各ファイルの生成
では上記を元に実際にファイルを生成する部分を書く。
実行ファイル
実行ファイルを作るところはそれほど難しくない。
$(PROG): $(OBJS) $(CXX) $(LDFLAGS) -o $@ $^ $(LIBS)
make buildtype=debugとしたときは、$(PROG)は"debug/hello"に、$(OBJS)は"debug/src/main.o debug/src/image/converter.o debug/src/text/converter.o"になる。次にこれらのオブジェクトファイルを作る部分を作ろう。
オブジェクトファイルと依存関係ファイル
これは少しややこしい。
$(OUTDIR)/%.o:%.cpp @if [ ! -e `dirname $@` ]; then mkdir -p `dirname $@`; fi $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(DEFS) -o $@ -c -MMD -MP -MF $(@:%.o=%.d) $<
最初の行の書き方に注意しよう。ここで書きたいのは当然、「.oファイルは.cppファイルに依存している」ということだが、サブディレクトリがあるためちょっと面倒になる。
前述の$(PROG)のルールにおいて、オブジェクトファイルには例えば"debug/src/image/converter.o"があった。このオブジェクトファイルに必要なソースファイルは"src/image/converter.cpp"である。つまり、オブジェクトファイルのパスから"$(OUTDIR)/"を除き、さらに.oを.cppに変換したものに依存している。これをMakefileで表すと$(OUTDIR)/%.o:%.cppとなる。
2行目はディレクトリの生成だ。gcc/g++は、出力先ディレクトリがない場合に自動的に作るようなオプションはないようなので、自分で作る必要がある。出力するファイルのパスは$@であるため、`dirname $@`が出力先ディレクトリになる。これが存在しない場合にはmkdir -pで作るようにする。また、このif文が毎回端末に出力されると見にくいので、先頭に"@"をつけることで抑制する。
3行目は前回とほぼ同じだが、-MFオプションを使って依存関係ファイルの出力先を明示的に指定している。これがないとカレントディレクトリに出力してしまう。
clean, distclean
cleanは$(OUTDIR)を消すようにし、distcleanはBuildディレクトリを消すようにしてみる。
clean: rm -rf $(OUTDIR) distclean: rm -rf Build
結果
すべてまとめて整理すると以下のようになる。
PROGNAME := hello buildtype := release text_conv := iconv SRCS := src/main.cpp src/image/converter.cpp src/text/converter.cpp CXX := g++ CPPFLAGS += -Isrc DEFS := LDFLAGS += LIBS += CXXFLAGS := -Wall -Wextra ifeq ($(buildtype),release) CXXFLAGS += -O3 else ifeq ($(buildtype),debug) CXXFLAGS += -O0 -g else $(error buildtype must be release, debug, profile or coverage) endif OUTDIR := Build/$(buildtype) ifeq ($(text_conv),iconv) else ifeq ($(text_conv),icu) DEFS += -DUSE_ICU=1 LIBS += -licuuc OUTDIR := $(OUTDIR)-icu else $(error text_conv must be iconv or icu) endif PROG := $(OUTDIR)/$(PROGNAME) OBJS := $(SRCS:%.cpp=$(OUTDIR)/%.o) DEPS := $(SRCS:%.cpp=$(OUTDIR)/%.d) .PHONY: install clean distclean all: $(PROG) -include $(DEPS) $(PROG): $(OBJS) $(CXX) $(LDFLAGS) -o $@ $^ $(LIBS) $(OUTDIR)/%.o:%.cpp @if [ ! -e `dirname $@` ]; then mkdir -p `dirname $@`; fi $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(DEFS) -o $@ -c -MMD -MP -MF $(@:%.o=%.d) $< clean: rm -rf $(OUTDIR) distclean: rm -rf Build