Makeでビルドオプションによって出力ディレクトリを変える

前回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