Makeのことはじめ


Makeとは

Makefileと呼ばれるテキストファイルに依存関係を書き、makeというコマンドでMakefileに書かれた内容を実行してくれる。
主にC言語やC++の様にコンパイルが必要な言語で利用する場面の多いビルドツール。

差分コンパイルをしてくれるので、コンパイルが必要な言語だととても助かる。
またワイルドカードも使えるというのもいい所。

makefileの書き方

makeをとりあえず試してみる

Makefileのフォーマットは

.PHONY ターゲット
ターゲット: 依存ファイル
    [実行コマンド]

この様な形になっている。
[実行コマンド]行の手前にもタブ1文字を入れることに注意!!

そしてこのルールを実行する場合ターゲットの部分がmakeコマンドを実行する時に指定をするだけです。

% make ターゲット

.PHONYはタスクターゲットを宣言するためのもので、書かなくても問題はないです。
ただ、ターゲット名と同名のファイルが存在する場合Makefileのターゲットではなくターゲットファイルの指定されたと認識されルールの実行をしてくれません。
なのでMakefileだけで完結するターゲットを提供する場合 必ず.PHONYを書くぐらいの認識でいたほうが良いかもしれないです。
※.PHONYを書いたほうがパフォーマンスも良いらしい

例えばただechoをしたいだけのルールを提供するなら

.PHONY output
output:
    echo "test"

この様に記載を行うだけで良い。
このファイルを用意したディレクトリでmake outputと実行すると

echo "test" # makeの出力
test         # echoの出力

この様な出力が行われる

C++のビルドをしてみる

ざっくりとしたmakefileの記述はわかったのでこれで試しにC++のビルドをしてみる

main.cpp
#include <iostream>

int main(int argc, char const* argv[])
{
    std::cout << "make test" << std::endl;
    return 0;
}

こんな簡単なC++プログラムを用意してこのビルドを行うターゲットを提供したい場合

.PHONY: debug
debug: main.cpp
    clang++ -std=c++14 -O3 -g main.cpp

.PHONY: release
release: main.cpp
    clang++ -std=c++14 -O0 -DNDEBUG main.cpp

この様なMakefileを提供すればdebugビルド、releaseビルド両方を提供できる。

依存関係のあるビルド

まずはファイル分割されたソースを用意

main.cpp
#include <iostream>

#include "sub1.hpp"

int main(int argc, char const* argv[])
{
    std::cout << "make test" << std::endl;
    sub1();
#ifdef NDEBUG
    std::cout << "NO DEBUG" << std::endl;   
#else
    std::cout << "DEBUG" << std::endl;   
#endif
    return 0;
}
sub1.cpp
#include <iostream>

#include "sub1.hpp"

void sub1()
{
    std::cout << "sub1" << std::endl;
}
sub1.hpp
#pragma once

void sub1();

cppファイル1つのビルドはとても簡単でしたけど、これがファイル分割されていたプログラムの場合コマンドに全てのファイルのビルドをしてリンクするコマンドをかけばいいわけなんですけど

.PHONY: debug
debug: main.cpp sub1.cpp
    clang++ -std=c++14 -O3 -g main.cpp
    clang++ -std=c++14 -O3 -g sub1.cpp
    clang++ -o main main.o sub1.o

まだ2つで終わりなら良いけどここからsub2が増えたとかなったらビルドの行を追加して、実行ファイルのリンクの所編集してととても面倒!!

そこで 依存ファイルの指定サフィックスルール を使用してファイルが増える度に編集する箇所を減らしてやる。

依存ファイル指定

依存ファイル指定はdebug: main.cpp sub1.cppこのターゲットの後に書かれた内容のことです。
要はビルドを行う為には**に依存するという意味になります。
この部分に別のターゲットを書くことで別のターゲットを呼び出すことも可能です。
正直.cppならば書く必要はないです。

C言語、C++で分割コンパイルをする場合必ず.oファイルを作ってそれをリンクして実行ファイルを作るという流れなので実行ファイルを作るためこの指定には.oを指定します。

サフィックスルール

C言語、C++では必ず.cppから.oファイルが作られる,ということを利用し、ルール化したのがサフィックスルールです。
特殊なターゲットで

.cpp.o:
    コマンド

というのを用意します。
.cpp.o:というターゲットは,.oというファイルが必要になれば、これを.cppからつくる というルールである。

分割コンパイル

この2つを特性を利用してやると先ほどのルール記載がこの様に変わります。

.PHONY: debug
debug: main.o sub1.o
    clang++ main.o sub1.o

.cpp.o:
    clang++ -c -std=c++14 -O3 -g $< 

debugターゲットはmain.o sub1.oに依存する。
.o依存のターゲットが実行されるためmakeでは.oを生成するというフローに認識され.cpp.oを実行して.oを作るという流れになります。
$<は自動変数でサフィックス.cppつまりコンパイル対象の1ファイル名になります。
これでビルドしてやることで実行ファイルが出来ますがまだこれだとファイルが増えた時に面倒ですね。
なので変数等を利用してdebug、releaseビルドの切り替えやファイル名の追加を楽にしてやります。

CC=clang++
SRCS=main.cpp sub1.cpp
EXE_FILE=exe_
CXXFLAGS=-c -std=c++14
OBJS=$(SRCS:.cpp=.o)
DEFINE=NDEBUG

.PHONY: debug
debug: EXE_FILE=exe_debug
debug:CXXFLAGS+=-g -O3 -Wall -W
debug: build

.PHONY: release
release: EXE_FILE=exe_release
release: CXXFLAGS+=-O0 -DNDEBUG
release: build

.PHONY: build
build: $(OBJS)
    $(CC) -o $(EXE_FILE) $(OBJS)

.cpp.o:
    $(CC) -c -std=c++14 $(CXXFLAGS) $< 

.PHONY: clean
clean:
    rm -f $(EXE_FILE)* $(OBJS)

最終的にはこの形になりました。
SRCSのラインにファイル名を追加してやるだけで追加できます。(ワイルドカード使ってやることでこの作業も省略可能)

追記

上の最終的な形だとヘッダーを編集してもインクルードを行っているファイルでコンパイルしてくれないことがわかりました。
そのため分割コンパイルをする場合ヘッダーを編集した場合にもインクルード先のファイルをコンパイルしてくれる設定を追加する必要があります。

-MMD -MPオプションを使うことでこの解決は可能です。

-MMD
依存関係のファイルリストを拡張子.dのファイルに保存してくれて、コンパイルを行ってくれる。

clang++ -MMD -std=c++11 -c main.cpp

先ほどのコードに-MMDを指定してコンパイルした場合

main.d
main.o: main.cpp ../include/sub1.hpp

この様にmain.oが依存しているファイルを列挙してくれます。

-MP
依存するヘッダーファイルに偽のターゲットを追加してくれる。

これだけだとなんの意味があるのかわからないが、このオプションが効果を発揮するのはヘッダーファイルを消した時
-MMDオプションだけを指定した場合の.dファイルを見ると.oオブジェクトが特定のヘッダーに依存するという形になる。
この依存ファイルをmakeが読み込むのが実行時に行われるため、ソースから#includeを消しても次のコンパイルのタイミングではヘッダーファイルを消した場合でも必ず依存しているヘッダーファイルを探してしまう。
そのため消しても問題ないように-MPを付けて偽のターゲット定義をしてやることでヘッダーを探しても大丈夫なようにしてやる。

-MPオプションを追加するとこのような形になる。

main.d
main.o: main.cpp sub1.hpp

sub1.hpp:

・依存ファイルをmakeに教えてやる
-MMD -MPを利用することで依存ファイルができたが、これだけだとmakefileで依存関係が判明してないためそれを教えてやる必要がある。

そこで利用するのがmakefileのincludeを利用してやる。
includeはmakeに現在のmakefileの読み込みを中断させ、続ける前に一つないしそれ以上の他のmakefileを読ませます。
これで生成された.dファイルを読みこませれば依存関係の解消が行え、ヘッダーを変更しただけでもコンパイルされます。

これを踏まえたMakeファイルはこんな感じになりました。

CC=clang++
SRCS=main.cpp sub1.cpp
DEPS=$(SRCS:.cpp=.d)
EXE_FILE=exe_
CXXFLAGS=-c -std=c++14
OBJS=$(SRCS:.cpp=.o)
DEFINE=NDEBUG

-include $(DEPS)

.PHONY: debug
debug: EXE_FILE=exe_debug
debug:CXXFLAGS+=-g -O3 -Wall -W
debug: build

.PHONY: release
release: EXE_FILE=exe_release
release: CXXFLAGS+=-O0 -DNDEBUG
release: build

.PHONY: build
build: $(OBJS)
    $(CC) -o $(EXE_FILE) $(OBJS)

.cpp.o:
    $(CC) -c -MMD -MP -std=c++14 $(CXXFLAGS) $< 

.PHONY: clean
clean:
    rm -f $(EXE_FILE)* $(OBJS)

-include $(DEPS)とインクルードの前に-を付けてます。
これは最初のコンパイルの時に.dファイルが存在しないしコンパイルをして初めて生成されるファイルのため初回は探しても見つけることも創りだすこともできません。
そこで-includeとしてやることでそういったファイルを無視してくれます。

参考

Makeでヘッダファイルの依存関係に対応する

感想

ざっくりmakeを触って見たが正直C++のmakefile書くならcmakeのが構文的でわかりやすいなという感想
1ファイルだけコンパイルしたいなら普通にclang++だけでやる方が楽だしxcodeプロジェクトも作れるし
使う候補で考えるとGO言語かなという感じ