C/C++中規模プロジェクトのための超シンプルなMakefile

私は多くの小規模プロジェクトでMakeを使ってきましたが、より大きな規模のプロジェクトになると、それは非常にうんざりするようなものでした。最近までは、自分のビルドシステムに行いたいことが4つあったのですが、Makeでの方法が分かりませんでした。

  • out-of-sourceビルド(オブジェクトファイルが、ソースとは分離されたディレクトリにダンプ出力されます)
  • 自動生成される(かつ正確!)ヘッダの依存関係
  • オブジェクト/ソースファイルのリストの自動的な決定
  • インクルードディレクトリのフラグの自動生成

以下にこれらの全てを行える、C、C++、およびアセンブリで動作するシンプルなMakefileを紹介します。

MAKEFILE

TARGET_EXEC ?= a.out

BUILD_DIR ?= ./build
SRC_DIRS ?= ./src

SRCS := $(shell find $(SRC_DIRS) -name *.cpp -or -name *.c -or -name *.s)
OBJS := $(SRCS:%=$(BUILD_DIR)/%.o)
DEPS := $(OBJS:.o=.d)

INC_DIRS := $(shell find $(SRC_DIRS) -type d)
INC_FLAGS := $(addprefix -I,$(INC_DIRS))

CPPFLAGS ?= $(INC_FLAGS) -MMD -MP

$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS)
    $(CC) $(OBJS) -o $@ $(LDFLAGS)

# assembly
$(BUILD_DIR)/%.s.o: %.s
    $(MKDIR_P) $(dir $@)
    $(AS) $(ASFLAGS) -c $< -o $@

# c source
$(BUILD_DIR)/%.c.o: %.c
    $(MKDIR_P) $(dir $@)
    $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

# c++ source
$(BUILD_DIR)/%.cpp.o: %.cpp
    $(MKDIR_P) $(dir $@)
    $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@


.PHONY: clean

clean:
    $(RM) -r $(BUILD_DIR)

-include $(DEPS)

MKDIR_P ?= mkdir –p

悪くないですね。

なお、out-of-sourceビルドにこだわらないという場合は、もっとシンプルなMakefileが使えます。これなら、built-inの持つ暗黙ルールが使えるという強みもあります。

MAKEFILE

TARGET ?= a.out
SRC_DIRS ?= ./src

SRCS := $(shell find $(SRC_DIRS) -name *.cpp -or -name *.c -or -name *.s)
OBJS := $(addsuffix .o,$(basename $(SRCS)))
DEPS := $(OBJS:.o=.d)

INC_DIRS := $(shell find $(SRC_DIRS) -type d)
INC_FLAGS := $(addprefix -I,$(INC_DIRS))

CPPFLAGS ?= $(INC_FLAGS) -MMD -MP

$(TARGET): $(OBJS)
    $(CC) $(LDFLAGS) $(OBJS) -o $@ $(LOADLIBES) $(LDLIBS)

.PHONY: clean
clean:
    $(RM) $(TARGET) $(OBJS) $(DEPS)

-include $(DEPS)

これらのいずれかを使うには、Makefile(タブ文字がコピーされることを確認しましょう! それに関してMakeはとてもうるさいです)、./src(このディレクトリは、SRC_DIRSを変更することにより変えることができます)のディレクトリあるいはサブディレクトリにあるソースやヘッダなどの全てを呼び出すファイルに、Makeコードを配します。そうしたら、あなたのプロジェクト用に必要となる値をCCとCFLAGSに設定するか、またはMakeのデフォルトを使うか、確認してください。

makeと入力します。

問題が発生した場合には、make -dを実行すると役に立つでしょう。

では、その動き方の概要を見てみます。

out-of-sourceビルド

私は、最終的には全てのビルドアーティファクトを、ソースとは切り離されたいくつかのディレクトリ(通常、 “./build” と名前を付けています)に生成したいと思っています。このようにMakeをすると、Makeを介して生成された他のビルドアーティファクトがそのディレクトリにそれぞれ並んだとしても、clean(単に、rm -rf ./build)がしやすいのです。また、ソースでgrepを行う場合など、他にも多くのことがずっとやりやすくなります。

Makeでこのようにするには通常、あなたの出力先のディレクトリを自分のパターンルールの先頭に追加する必要があります。例えば、.cファイルから同じディレクトリ内に.oファイルを作成することになる、%.o: %.cといったパターンの代わりとしては、$(BUILD_DIR)%.o: %.cを使うことになります。

自動生成されるヘッダの依存関係

ヘッダの依存関係の処理は、おそらく、古典的なMakeテクニックを使うものの中でも、最も退屈なものだと言えると思います。特に、その後で、あなたが何かしくじれば、明示的なエラーが全く出ず、再コンパイルもされるべき時にされないということになります。これは、タイプやプロトタイプがどういったものなのかについて、様々な考え方を持つ.oファイルが生成されることになってしまいます。

そこで、ここにドキュメントがあります。ただし、このドキュメントでは、依存関係のあるファイルはコンパイルステップとは別のステップで生成されていることが前提とされているようです。

コンパイルステップの一部として依存関係のあるファイルを生成するなら、もっとずっとシンプルになります。依存関係のあるファイルを生成するためにしなければならないことは、コンパイルコマンド(ClangGCCの両方でサポートされています)にいくつかフラグを追加するだけです。

  • -MMD –MP
    これで、.oファイルの次に.dファイルを生成します。その後、その.dファイルを使うために、次のコマンドでそれらを全て見つけなければなりません。
  • DEPS := $(OBJS:.o=.d)
    そして、次のようにして、それらを-includeします。
  • -include $(DEPS)

オブジェクト/ソースファイルのリストの自動的な決定

まず、定められたソースディレクトリで全てのソースファイルを検索します。私が見つけた、この検索を最もシンプルに素早く行う方法は、単にシェル関数でfindを使うことです。

  • SRCS := $(shell find $(SRC_DIRS) -name *.cpp -or -name *.c -or -name *.s)
    しかし、Makeはオブジェクトファイルからソースへと戻って動くので、欲しいオブジェクトファイルは全て、自分たちのソースファイルから算出しなければなりません。私の場合は基本的に、単純にファイルの先頭に$(BUILD_DIR)/を加え、次のように、それぞれソースファイルのパスの最後に.oを加えます。
  • OBJS := $(SRCS:%=$(BUILD_DIR)/%.o)
    そうして、オブジェクトファイルによって、ターゲットのMakeができます。
  • MAKEFILE
$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS)
  $(CC) $(OBJS) -o $@ $(LDFLAGS)

インクルードディレクトリのフラグの自動生成

インクルードディレクトリフラグを生成するためにも、似たようなテクニックを使っていました。定められたソースディレクトリの下にあるディレクトリ全てを検索します。

  • INC_DIRS := $(shell find $(SRC_DIRS) -type d)
    そして、それらの前には-Iを付けます。
  • INC_FLAGS := $(addprefix -I,$(INC_DIRS))

以上のテクニックがお役に立てば幸いです。