BLOG

「makeを使うと開発が非常に楽になる」というのは雰囲気でなんとなく分かりますが、 どういう問題をどう解決してくれるのかがイマイチ分からないので手をつけにくいですよね。

この記事ではmakefileの基本の書き方から、C/C++のコードを 依存関係を考慮して効率よくビルドできる makefileがどうできるのかを解説します。


Makeファイルを使うとコンパイルを自動化できます。

1. 基本構文

makefileの一番の基本ルールは

レシピ名: 依存ファイル
    手段

です。

例えば、

main: main.cpp header.h
    g++ main.cpp -o main

など。

このレシピを使うときは、これを"makefile"という名前で保存し、コマンドラインにmake mainと入力します。

普段何気なくmakeだけでビルドできていたと思いますが、これはmakeコマンドに何も引数を与えないと自動で 一番上にあるレシピを実行するというルールがあるからです。 大抵の場合、一番実行したいレシピを一番上に書くのでmakeだけで希望の動作が行えていたわけです。 なので、make cleanmake installというのはコマンドオプションではなく、makeファイル内の cleanレシピ、installレシピを実行しろというコマンドだったんですね。

makeコマンドは賢いので、出力ファイル(ターゲット)がすでに存在し、依存ファイルに前回ビルドとの変更がなかった場合は 仕事を停止します。(重要: あとでこの機能を活用するので覚えておいてください。)

2. 変数を定義して使う

出力するプログラムの名前を簡単に変えられるようにできると、makefileをコピペして簡単な編集で使い回せて楽ですね。 変数は VARNAME := hogehogeという構文で定義でき、使用するときは平文と区別するために$(VARNAME)と、$()で囲って展開します。

PROG := MyProgramName

main: main.cpp header.h
    g++ main.cpp -o $(PROG)

3. 複数のソースファイルに対応させる

main.cppだけでなく他にもhoge.cpp、piyo.cppがあったとしましょう。(このような状況は当たり前に発生しますよね) これらをビルドする愚直なmakefileは次のようになるかもしれません。

main: main.cpp hoge.cpp piyo.cpp
    g++ main.cpp hoge.cpp piyo.cpp

しかしこれでは無駄が多いです。というのも、この書き方では、3つのファイルのうちどれか一だけを 編集したときでも、すべてのファイルをコンパイルし直してしまうからです。

c++はビルドに大雑把にわけて2つのフェーズがあります。

一つはそれぞれのcppファイルをオブジェクトファイルに変換するフェーズ。 もう一つは全てのオブジェクトファイルを1つにまとめるフェーズです。 これを手動でコマンドで行うと、

$ g++ -c main.cpp # これでmain.oができる
$ g++ -c hoge.cpp # これでhoge.oができる
$ g++ -c piyo.cpp # これでpiyo.oができる
$ g++ main.o hoge.o piyo.o # これでターゲットa.outができる

このように一度オブジェクトファイルを経由し、その後リンクして全てのオブジェクトファイルを合成するという手法を取ることで、 編集したファイルのみをコンパイルし直すことができます。

main.cppだけをリコンパイルして成果物を作り直すには、

$ g++ -c main.cpp # main.oを作り直す
$ g++ main.o hoge.o piyo.o # hoge.o, piyo.oはそのまま

これでOKです。 しかし、いちいちこんなコマンドを入力するのは疲れてしまうので、makeを使って自動化しましょう。 だんだんmakeを使う価値が分かってきましたね。

makefileは次のようになります。

main: main.o hoge.o piyo.o
    g++ main.o hoge.o piyo.o

main.o: main.cpp
    g++ -c main.cpp

hoge.o: hoge.cpp
    g++ -c hoge.cpp

piyo.o: piyo.cpp
    g++ -c piyo.cpp

makeコマンドを発行すると、まず"main: main.o hoge.o piyo.o"の行に着目されます。 ここで、main.oに変更があるのでmain.o: main.cppのレシピが実行されます。 makeはターゲットがすでに存在し、依存ファイルに編集がなかった場合何もしないので、 hoge.oやpiyo.oに対しては何もしないんですね。

4. 複数のルールを一つにまとめる

先程のmakefileの

main.o: main.cpp
    g++ -c main.cpp

hoge.o: hoge.cpp
    g++ -c hoge.cpp

piyo.o: piyo.cpp
    g++ -c piyo.cpp

の部分、ほぼ同じことを繰り返し書いていて、なんだかひとまとめにできそうですね。 先に修正されたMakefileをお見せします。

SRCS := main.cpp hoge.cpp piyo.cpp
OBJS := main.o hoge.o piyo.o

main: $(OBJS)
    g++ $(OBJS) -o main

.cpp.o:
    g++ -c $<

main.o: main.cpp main.h
piyo.o: piyo.cpp piyo.h
hoge.o: hoge.cpp hoge.h

依存関係を示す部分はそのままで、実際の処理を記述している部分が``.cpp.o```という変わったターゲット名になっていますね。 これは、サフィックスルールと呼ばれているものです。

サフィックスルールはシンプルで、

.材料になるファイル拡張子.ターゲットの拡張子:

という記法になっています。 main.oが必要になったら、makeはこのサフィックスルールをみて、「main.cppをこのルールに適用すればmain.oが得られるんだな」 と解釈してくれるわけです。 手段の部分に$<という見慣れないシンボルがありますね。これは自動変数と呼ばれています。 最低限の説明をすると、自動変数は「自動で値が代入されている」変数で、その場に応じて欲しいデータを 取得することができます。ここで使っている``$<```はルール適用中の依存ファイルの名前が代入されています。 自動変数には他にもいろいろな種類があるので、詳しくは調べてみてください。

サフィックスルールによって少しすっきりしたものの、依然としてファイルが増えるたびに依存関係を記述しないといけないので、多少面倒ですね。 ソースファイルの依存関係はソース上でどのファイルがどのファイルを#includeしているかで決定されるので、プログラマとしてはもう記述済みの事実です。なんとか自動でmakeに反映することができないでしょうか。 特に、あるファイルから別のヘッダを突然includeしたくなるということは頻繁に発生しますよね?この度に書き換えますか?Makefile。 うっかり依存関係を書き忘れて一度make cleanしてからmakeしないとうまくビルドできなかったみたいな経験、誰でも一度はあるんじゃないんでしょうか。

実はあるんです。ソースファイルから依存関係を自動的に抽出してくれる魔法の方法が…

5.依存関係を自動で定義する

ソースファイルから依存関係を自動的に抽出するなんて魔法みたいなことがMakeだけで出来るんでしょうか。 残念ながらMakeの機能だけではできません。流石にCの構文を解析して依存関係を抽出するのは 第三者のアプリケーションには荷が重すぎます。

でも、gccのオプションを活用するとなんと可能です!本人に聞けって感じですね。 実は、gccには依存ファイルをmake形式で書き出すという神みたいなオプションがあるんです。 main.cにmain.hとCONST.hというincludeを書いた状態で次のコマンドを発行します。

g++ -MMD -MP -c main.cpp

すると、main.dというファイルがmain.oとともに出力されます main.dの中身は次のとおりです。

main.o: main.cpp main.h CONST.h

main.h:

CONST.h:

これをmakefileにincludeしてあげれば依存関係を自動で作ってくれるので完璧です。 また、初回のコンパイル時には.dファイルが存在していない為エラーになってしまうのを避ける為に、先頭に”-“をつけておきます。(急に雑な説明)

SRCS := main.cpp hoge.cpp piyo.cpp
OBJS := $(SRCS:%.cpp=%.o)
DEPS := $(SRCS:%.cpp=%.d)


main: $(OBJS)
	g++ $(OBJS) -o main

.cpp.o:
	g++ -MMD -MP -c $<

-include $(DEPS)

これでプロジェクトの変更に柔軟に対応し、効率よくビルドしてくれるMakefileができました。 また、SRCSをカレントディレクトリの全てのcppファイルを指すようにSRCS := $(wildcard *.cpp)と書いてしまうのもパンチが効いてていいですね。

以下に参考として万能Makefile例全文を掲載しておきます。この記事で紹介しなかった機能等に関しては少しコメントを添えたので、 詳しくは検索してみてください(無責任)。

PROG := TARGET_NAME
SRCS := $(wildcard *.cpp) # wildcard関数を用いてファイル内の.cppを全て取得する(配列)
OBJS := $(SRCS:%.cpp=%.o) # %マクロを用いて置換 SRC配列を元に"<ファイル名>.o"の配列を作る。 中間オブジェクトファイル用。
DEPS := $(SRCS:%.cpp=%.d) # %マクロを用いて置換 SRC配列を元に"<ファイル名>.d"の配列を作る。 依存ファイル用。

# 各種設定を変数として定義
CC := g++
CCFLAGS := -std=c++17
INCLUDEPATH := -I/usr/local/include
LIBPATH := -L/usr/local/lib
LIBS := -framework Cocoa -framework OpenGL -lz -ljpeg -lpng

# これが主レシピ
all: $(DEPENDS) $(PROG)

# リンク
$(PROG): $(OBJS)
	$(CC) $(CCFLAGS) -o $@ $^ $(LIBPATH) $(LIBS)

# コンパイル
.cpp.o:
	$(CC) $(CCFLAGS) $(INCLUDEPATH) -MMD -MP -MF $(<:%.cpp=%.d) -c $< -o $(<:%.cpp=%.o)

# "make clean"でターゲットと中間ファイルを消去できるようにする
.PHONY: clean
clean:
	$(RM) $(PROG) $(OBJS) $(DEPS)

-include $(DEPS) # include "ファイル名" でそのファイルの内容をここにコピペしたのと同じ効果を得られる