CやC++の学習中にいちいちg++とか書いてコンパイルするのが面倒なのでMakefileにも手を出し始めました。shellscriptでも同じことはできるだろうけどやっぱりmakeしたい。。
使用しているmakeのバージョンは3.81。
$ make --version
GNU make 3.81
$ gcc --version
gcc (MacPorts gcc49 4.9.4_0) 4.9.4
概要
全く関係ない複数のソースコードを同じディレクトリで管理していて、別々に全部コンパイルしたい。これをMakefileで実現するために色々悩んだ際のメモ。
「wildcard」関数がかなり便利で、「*」記号を使って「.cpp」等の特定の拡張子のソースコードを全て自動で探し出してコンパイルできます。
Makefile特有の「foreach」関数やマクロの書き方で結構ハマった。。
なお、bashの以下のようなディレクトリ構造を想定しています。
.
├── GNUmakefile
│
├── src
│ ├── sourceA.cpp (***.cでも可)
│ ├── sourceB.cpp
│ └── ...
│
├── include
│ ├── header1.hpp (sourceAやsourceBで呼ばれるヘッダファイル)
│ ├── header2.hpp
│ └── ...
│
└── bin
└──
src内のソースファイルの数はいくつあっても問題ありません。
また、sourceA内でheader1を呼び出す際には「#include < >」を使います。(Makefile内でインクルードパスを通すため)
#include <header1.hpp>
Makefileの中身
# GNUmakefile
# (c) S.Suzuki 2017.7.21
SUFFIX = .cpp
COMPILER = g++
CFLAGS = -Wall -O2
SRCDIR = ./src
INCLUDE = ./include
EXEDIR = ./bin
SOURCES = $(wildcard $(SRCDIR)/*$(SUFFIX))
OBJECTS = $(notdir $(SOURCES:$(SUFFIX)=.o))
TARGETS = $(notdir $(basename $(SOURCES)))
define MAKEALL
$(1): $(1).o
$(COMPILER) -I$(INCLUDE) $(CFLAGS) -o $(EXEDIR)/$(1) $(1).o
@$(RM) $(1).o
$(1).o:
$(COMPILER) -I$(INCLUDE) $(CFLAGS) -c $(SRCDIR)/$(1)$(SUFFIX)
endef
.PHONY: all
all: $(TARGETS)
$(foreach VAR,$(TARGETS),$(eval $(call MAKEALL,$(VAR))))
#make clean
.PHONY: clean
clean:
$(RM) $(EXEDIR)/*
依存ファイルは一切なく、シンプルな構造になっているかと思います。
ちなみに「make」コマンドを実行すると、デフォルトでは「GNUmakefile」、「makefile」、「Makefile」の順に探してどれかが実行されます。
それでファイル名を「GNUmakefile」にしてるけど「Makefile」にしても問題ないっぽい。
使い方
拡張子、コンパイラの指定を行った上で、Makefileが置いてあるディレクトリ内で、
$ make
これだけ。Makefileで書くとスマートにコンパイルできます。
srcディレクトリ内に「sourceA.cpp」と「sourceB.cpp」というソースファイルがあれば、binディレクトリの中に「sourceA」と「sourceB」という実行ファイルが別々に作られます。
また、次のようにすると個別に1つだけのコンパイルも可能です。
$ make sourceA
binディレクトリ内の実行ファイルを全て消去したい場合には、
$ make clean
とします。
解説
Makefileを上から順に4つの部分に分けて説明していきます。
###1. 拡張子、コンパイラ、オプション、各ディレクトリの相対パスの指定
SUFFIX = .cpp # ソースの拡張子
COMPILER = g++ # 使用するコンパイラ
CFLAGS = -Wall -O2 # コンパイラオプション
「SUFFIX」、「COMPILER」等はMakefileにおける変数であり、自由に名前をつけられます。また、shellscriptと同様に「#」でコメントアウトできます。
「CFLAGS」で指定するコンパイラのオプションのうち、「-Wall」はWarning Allで全ての警告を出力、「-O2」はコンパイル時に最適化を行います。どちらのオプションも実際は無くても動くのでお好み。
SRCDIR = ./src
INCLUDE = ./include
EXEDIR = ./bin
それぞれのディレクトリのGNUmakefileから見た相対パス。
###2.コンパイルするソースの検索
SOURCES = $(wildcard $(SRCDIR)/*$(SUFFIX))
OBJECTS = $(notdir $(SOURCES:$(SUFFIX)=.o))
TARGETS = $(notdir $(basename $(SOURCES)))
Makefileでの関数の書き方は、以下のようになっています。
$(関数名 引数)
wildcard関数は「./src/*.cpp」の型に当てはまるファイルを取得できます。「SOURCES」を上のディレクトリ構造の場合で具体的に展開すると、
SOURCES = ./src/sourceA.cpp ./src/sourceB.cpp
また、中間ファイルOBJECTSと実行ファイルTARGETSは、以下を使用して展開されます。
notdir関数 … ディレクトリではない部分を抽出する
$(変数:a=b) … 変数内の文字列aを文字列bに置換する
basename関数 … 拡張子を除いた要素を抽出する
OBJECTS = sourceA.o sourceB.o
TARGETS = sourceA sourceB
###3.個別コンパイルのテンプレート(マクロ)
define MAKEALL
$(1): $(1).o
$(COMPILER) -I$(INCLUDE) $(CFLAGS) -o $(EXEDIR)/$(1) $(1).o
@$(RM) $(1).o
$(1).o:
$(COMPILER) -I$(INCLUDE) $(CFLAGS) -c $(SRCDIR)/$(1)$(SUFFIX)
endef
Makefileのマクロ定義は以下のように行います。また、$(1)はマクロの1つ目の引数を意味しており、call関数でマクロを呼び出す際に引数を与えることができます。
define マクロ
@echo "$(1)" #何らかの処理
endef
$(call マクロ,引数)
「@」マークが付いた行はmake実行時にコマンド名を表示しないという意味。
上の「MAKEALL」マクロでは、引数を「ARG1」と書くと以下のように展開されます。
define MAKEALL
ARG1: ARG1.o
g++ -I./include -Wall -O2 -o ./bin/ARG1 ARG1.o #ターゲット(実行ファイル)の生成
@rm -f ARG1.o #使用した中間ファイルの削除
ARG1.o:
g++ -I./include -Wall -O2 ./src/ARG1.cpp #中間ファイルの生成
endef
Makefileのコンパイル処理の書き方は、生成したいターゲットARG1(実行ファイル)の生成ルールを先に書き、その後に必要なパーツARG1.o(中間ファイル)の生成ルールを書きます。実行される順番はその逆となります。
###4.ソースファイルの数だけコンパイルする(forループ)
.PHONY:all
all: $(TARGETS)
$(foreach var,$(TARGETS),$(eval $(call MAKEALL,$(var))))
.PHONYは擬似ターゲットと呼ばれるもので、makefileのターゲット(生成しようとするもの)のうち実態がないもの(説明難しい…)。allというものは作らないけど実行するタスクを指し示すために仮に作っているターゲットです。
今回の場合、makeコマンド実行時に引数がない場合のデフォルトターゲットはallであり、TARGETSが指し示すターゲット「sourceA」、「sourceB」等を順番に生成していくことになります。
Makefileでのループにはshellのforループを利用したものが使えるようですが、今回の場合はMakefileのforeach関数で書くとループ部分は一行でスッキリ書けます。(他にいいのあればご教示願います)
srcディレクトリ内に「sourceA.cpp」、「sourceB.cpp」がある場合、TARGETS = sourceA sourceB となり、foreach関数
$(foreach VAR,$(TARGETS),$(eval $(call MAKEALL,$(VAR))))
はVARを変数として、TARGETSの内容を順に第3引数(eval関数)に渡していきます。すなわち以下のように展開されます。
$(eval $(call MAKEALL,sourceA))
$(eval $(call MAKEALL,sourceB))
ソースファイルの数の分だけ定義したMAKEALLマクロに展開されて実行ファイルが生成されていきます。
参考
–– Makefileの関数
–– Makefileの書き方(C言語)
–– 複数の実行ファイルを生成するMakefileの書き方