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++のビルドをしてみる
#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ビルド両方を提供できる。
依存関係のあるビルド
まずはファイル分割されたソースを用意
#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;
}
#include <iostream>
#include "sub1.hpp"
void sub1()
{
std::cout << "sub1" << std::endl;
}
#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.o: main.cpp ../include/sub1.hpp
この様にmain.o
が依存しているファイルを列挙してくれます。
・-MP
依存するヘッダーファイルに偽のターゲットを追加してくれる。
これだけだとなんの意味があるのかわからないが、このオプションが効果を発揮するのはヘッダーファイルを消した時
-MMD
オプションだけを指定した場合の.d
ファイルを見ると.o
オブジェクトが特定のヘッダーに依存するという形になる。
この依存ファイルをmakeが読み込むのが実行時に行われるため、ソースから#include
を消しても次のコンパイルのタイミングではヘッダーファイルを消した場合でも必ず依存しているヘッダーファイルを探してしまう。
そのため消しても問題ないように-MP
を付けて偽のターゲット定義をしてやることでヘッダーを探しても大丈夫なようにしてやる。
-MP
オプションを追加するとこのような形になる。
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言語かなという感じ