設定 - Makefile

提供:MochiuWiki - SUSE, Electronic Circuit, PCB
ナビゲーションに移動 検索に移動

概要

C / C++で記述されたソースコードにおいて、コンパイルする時の定義を記述するMakefileについて記載する。

Makefileは、1970年代にUnix環境で開発されたビルド自動化ツールであり、ソフトウェア開発における「どのファイルをどの順番でコンパイルするか」という問題を解決するために生まれた。
特に、複数のソースファイルから構成される大規模なプロジェクトでは、手動でコンパイルコマンドを実行することは非常に煩雑でミスも起こりやすい。

Makefileを使用することにより、この煩雑さから解放され、開発者はコードの記述に集中できるようになる。

makeコマンドの優れた点は、単にコンパイルを自動化するだけでなく、ファイルの依存関係とタイムスタンプを追跡することで、変更されたファイルとそれに依存するファイルのみを再コンパイルする点である。
これにより、大規模プロジェクトにおいても、修正後の再ビルド時間を大幅に短縮できる。

例えば、100個のソースファイルがあるプロジェクトで1つのファイルだけを修正した場合、makeコマンドは変更されたファイルとそれに依存する部分のみを再コンパイルし、他の99個のファイルはそのまま使用する。


Makefileとは

Makefileとは、C/C++のコンパイルに必要なコマンド、ソースコード、オプション、依存関係等を定義したファイルのことである。
makeコマンドを実行することにより、Makefileを使用してコンパイルを実行する。

Makefileのメリットは、以下の通りである。

  • コンパイルの手間が減り、ミスも少なくなる。
    特に、大規模なプロジェクトであるほど恩恵は大きい。
    複雑なコンパイルオプションや多数のライブラリのリンク指定を毎回手入力する必要がなくなる。
  • 共通のMakefileを用意することで、開発者間でコンパイルの定義が統一できる。
    チーム開発において、誰がビルドしても同じ結果が得られることは品質管理の面で非常に重要である。
  • 差分ビルドにより、変更されたファイルのみを再コンパイルするため、ビルド時間を大幅に短縮できる。



Makefileの記述方法

以下の例では、C/C++で記述されたソースコードをコンパイルするためのMakefileを記述している。

 # (1) コンパイラの指定(フルパスを記述してもよい)
 CC  = g++
 
 # (2) コンパイルオプション
 CFLAGS    = -Wall -O2
 
 # (3) 実行ファイル名
 TARGET  = Sample
 
 # (4) コンパイル対象のソースコード
 SRCS    = Sample.cpp
 
 # (5) オブジェクトファイル名
 OBJS    = $(SRCS:.cpp=.o)
 
 # (6) インクルードファイルのあるディレクトリパス
 INCDIR  = -I../inc
 
 # (7) ライブラリファイルのあるディレクトリパス
 LIBDIR  = -L/usr/local/lib
 
 # (8) 追加するライブラリファイル
 LIBS    = -lpthread
 
 # (9) ターゲットファイル生成
 $(TARGET): $(OBJS)
 	$(CC) -o $@ $^ $(LIBDIR) $(LIBS)
 
 # (10) オブジェクトファイル生成
 $(OBJS): $(SRCS)
 	$(CC) $(CFLAGS) $(INCDIR) -c $(SRCS)
 
 # (11) make allコマンドを実行することにより、make cleanコマンドとmakeコマンドを実行する。
 all: clean $(OBJS) $(TARGET)
 
 # (12) .oファイル、実行ファイル、.dファイルを削除する。
 clean:
 	-rm -f $(OBJS) $(TARGET) *.d
 
 # (13) 偽物のターゲット宣言
 .PHONY: all clean


上記の例において、3つのブロックに分けて説明する。

  1. Makefileに必要な情報
  2. ターゲットの生成
  3. Makefileの実行オプション


Makefileに必要な情報

コンパイルの下準備として、どのコンパイラを使用し、どのようなオプションを付けるか、どのソースファイルをコンパイルするかといった基本的な設定を行う。

1. コンパイラ : CC
使用するコンパイラを記載する。
ただし、上記の例では、C++をコンパイルするため、g++としている。
Cのコンパイルであれば、gccを使用することが一般的である。

2. コンパイルオプション : CFLAGS
コンパイルに使用するオプションを記述する。
使用できる機能やコンパイル時に出力される警告等を制御することができる。
例えば、-Wallはすべての警告を表示するオプションであり、-O2は最適化レベル2でコンパイルするオプションである。
デバッグビルドの場合は-gオプションを追加してデバッグ情報を含めることが多い。

3. 実行ファイル名 : TARGET
実行ファイル名を決める。
この変数で指定した名前が、最終的に生成される実行可能ファイルの名前となる。

4. コンパイル対象のソースコード : SRCS
コンパイル対象のソースコードを指定する。
ソースファイルが複数ある場合は、以下のように、複数指定する。
SRCS = hoge1.cpp
SRCS += hoge2.cpp
SRCS += hoge3.cpp
+= 演算子を使用することで、既存の変数に値を追加できる。

5. オブジェクトファイル名 : OBJS
オブジェクトファイルの名称を定義する。
ソースファイル名と同一のオブジェクトファイルを作成することが多い。
上記の例では、$(SRCS:.cpp=.o)という変数置換を使用しており、これはSRCS変数内の.cpp拡張子を.o拡張子に置き換えることを意味する。

6. インクルードファイルのあるディレクトリパス : INCDIR
参照するインクルードファイルが存在するパスを指定する。
なお、インクルードファイル名は不要である。
-Iオプションの後にディレクトリパスを指定することで、そのディレクトリ内のヘッダーファイルをインクルードできるようになる。

7. ライブラリファイルのあるディレクトリパス : LIBDIR
参照するライブラリファイルが存在するパスを指定する。
-Lオプションの後にディレクトリパスを指定する。

8. 追加するライブラリファイル : LIBS
参照するライブラリファイル名を指定する。
-lオプションの後にライブラリ名を指定する。例えば、-lpthreadはpthreadライブラリ(libpthread.so)をリンクすることを意味する。


ターゲットの生成

Makefileにおけるターゲットとは、TARGETとOBJSのことを指す。
上記のセクションで示した情報を使用して、オブジェクトファイルと実行ファイルの生成ルールを記述する。

9. ターゲットファイル生成
以下の2行の構成は、ターゲットファイルの生成ルールを記述している。
$(TARGET): $(OBJS)
$(CC) -o $@ $^ $(LIBDIR) $(LIBS)
1行目は、$(TARGET): $(OBJS)と記述して、TARGETがOBJSに依存することを示している。これにより、OBJSが更新された場合にのみTARGETが再生成される。
2行目は、コマンド行であり、リンク対象のライブラリを指定する。
コマンド行は必ずタブ文字でインデントしなければならない点に注意が必要である。スペースではエラーになる。
上記の例では、リンクするライブラリを指定しているため、OBJSとの依存関係の管理に加えて、適切なライブラリとのリンクも行われる。

10. オブジェクトファイル生成
TARGETが依存するOBJSの生成ルールを指定する。
オブジェクトファイルは、ソースと機械語の中間ファイルに当たるため、ソースコードに依存する。
基本的な記載ルールは、上記のターゲットファイルの生成と同じである。
-cオプションは、リンクせずにコンパイルのみを行い、オブジェクトファイルを生成するためのオプションである。


Makefileの実行オプション

上記の"Makefileに必要な情報""ターゲットの生成"のみでコンパイル可能であるが、
ここでは、makeコマンドのルールを自由に定義することができる。

この設定を使用することにより、効率良くコンパイルできるようになる。
以下に示す3つは、よく使用される設定である。

11. make allコマンドを実行することで、make cleanコマンドとmakeコマンドを実行する。
上記の例では、make allコマンドを実行するだけで、前のビルド時に生成したファイルを削除した上で、再ビルドを実行する。
これにより、クリーンな状態からの完全なリビルドが簡単に行える。

12. .oファイル、実行ファイル、.dファイルを削除する。
ビルドで生成したファイルを削除する。
上記の例において、削除対象は、オブジェクトファイル、実行ファイル、そしてデフォルトで生成される依存関係ファイルを削除している。
コマンドの先頭に-(ハイフン)を付けることで、削除対象のファイルが存在しない場合でもエラーで停止せずに処理を続行できる。

13. .PHONYターゲット
.PHONYは、実際のファイルを生成しない擬似的なターゲットであることを宣言する特殊なターゲットである。
これにより、たとえ「clean」や「all」という名前のファイルが存在していても、makeコマンドは常にそのルールを実行する。
.PHONYを使用しない場合、同名のファイルが存在すると、makeはそのターゲットが「最新である」と判断してルールを実行しない可能性がある。



Makefileのシンタックス

Makefileをより効果的に記述するための、詳細なシンタックス要素について説明する。

変数の定義と展開

Makefileでは、様々な方法で変数を定義し展開することができる。変数を適切に使用することで、保守性の高いMakefileを作成できる。

変数定義の種類
即時展開変数 (:=)
変数定義時に右辺を即座に評価する。
CC := gcc
CFLAGS := -Wall $(OPTIMIZATION)
この時点でOPTIMIZATIONが未定義であれば、CFLAGSには-Wallのみが設定される。

遅延展開変数 (=)
変数が使用される時点で右辺を評価する。これがMakefileのデフォルトの動作である。
CC = gcc
CFLAGS = -Wall $(OPTIMIZATION)
CFLAGSが使用される時点でOPTIMIZATIONが評価されるため、後からOPTIMIZATIONを定義しても有効になる。

条件付き代入 (?=)
変数が未定義の場合のみ値を設定する。
CC ?= gcc
これにより、環境変数やコマンドライン引数で既にCCが定義されている場合は、その値が優先される。

追加代入 (+=)
既存の変数に値を追加する。
CFLAGS = -Wall
CFLAGS += -O2
結果として、CFLAGSは「-Wall -O2」となる。


自動変数

Makefileには、ターゲットや依存関係を参照するための特殊な自動変数が用意されている。

$@
現在のターゲット名を表す。
$(TARGET): $(OBJS)
$(CC) -o $@ $^
この例では、$@はSampleという実行ファイル名に展開される。

$^
すべての依存ファイルをスペース区切りで表す。
重複する依存ファイルがある場合、重複は削除される。

$<
最初の依存ファイルを表す。
パターンルールで頻繁に使用される。
%.o: %.cpp
$(CC) $(CFLAGS) -c $<

$?
ターゲットより新しい依存ファイルのリストを表す。
差分ビルドを実装する際に有用である。

$*
パターンルールにおいて、%にマッチした部分(ステム)を表す。


パターンルール

パターンルールは、同じ種類のファイル変換を一般化して記述する方法である。これにより、複数のソースファイルに対して同じコンパイルルールを適用できる。

# パターンルールの基本形
%.o: %.cpp
	$(CC) $(CFLAGS) $(INCDIR) -c $< -o $@


この例では、任意の.cppファイルから対応する.oファイルを生成するルールを定義している。%は任意の文字列にマッチするワイルドカードとして機能する。例えば、main.cppがあれば、このルールによりmain.oが生成される。

パターンルールを使用することで、ソースファイルが増えても、Makefileを変更する必要がなくなる。SRCSに新しいファイルを追加するだけで、自動的に適切なオブジェクトファイルが生成される。

暗黙のルール

makeコマンドには、よく使われるファイル変換のための暗黙のルール(built-in rules)が組み込まれている。
例えば、.cファイルから.oファイルへの変換ルールは、明示的に記述しなくてもmakeが自動的に処理できる。

# 暗黙のルールの例
# 以下のルールは明示的に書かなくても動作する
# %.o: %.c
#     $(CC) $(CFLAGS) -c $< -o $@


ただし、プロジェクト固有のコンパイルオプションやインクルードパスを指定する必要がある場合は、明示的にルールを記述する方が望ましい。暗黙のルールに依存すると、Makefileの意図が不明確になり、予期しない動作を引き起こす可能性があるためである。

依存関係の自動生成

大規模なプロジェクトでは、ヘッダーファイルの依存関係を手動で管理することは困難である。gccの-MMオプションを使用することで、依存関係を自動的に生成できる。

# 依存関係ファイルの生成
%.d: %.cpp
	$(CC) -MM $(CFLAGS) $(INCDIR) $< > $@

# 依存関係ファイルをインクルード
-include $(OBJS:.o=.d)


この方法により、ヘッダーファイルが変更された場合、それを使用するソースファイルが自動的に再コンパイルされる。-includeの前のハイフンは、依存関係ファイルが存在しない場合でもエラーにならないようにするためのものである。

条件分岐

Makefileでは、条件に応じて異なる処理を行うことができる。これにより、デバッグビルドとリリースビルドを切り替えたり、異なるプラットフォームに対応したりできる。

 # ビルドタイプの指定(デフォルトはdebug)
 BUILD_TYPE ?= debug
 
 # 条件分岐
 ifeq ($(BUILD_TYPE),debug)
     CFLAGS = -Wall -g -DDEBUG
 else ifeq ($(BUILD_TYPE),release)
     CFLAGS = -Wall -O2 -DNDEBUG
 else
     $(error Unknown BUILD_TYPE: $(BUILD_TYPE))
 endif


この例では、makeコマンド実行時に「make BUILD_TYPE=release」のように指定することで、ビルドタイプを切り替えることができる。

Makefileの関数

Makefileには、文字列操作やファイル操作のための組み込み関数が用意されている。

よく使用される関数
$(wildcard pattern)
指定したパターンにマッチするファイルのリストを返す。
SRCS = $(wildcard *.cpp)
この例では、カレントディレクトリのすべての.cppファイルをSRCSに設定する。

$(patsubst pattern,replacement,text)
textから、patternにマッチする部分をreplacementで置換する。
OBJS = $(patsubst %.cpp,%.o,$(SRCS))
これは$(SRCS:.cpp=.o)と同じ意味である。

$(foreach var,list,text)
listの各要素に対してtextを評価し、結果を連結する。
DIRS = src test lib
SRCS = $(foreach dir,$(DIRS),$(wildcard $(dir)/*.cpp))
この例では、複数のディレクトリから.cppファイルを収集する。

$(shell command)
シェルコマンドを実行し、その出力を返す。
GIT_HASH = $(shell git rev-parse --short HEAD)
CFLAGS += -DGIT_VERSION=\"$(GIT_HASH)\"
この例では、Gitのコミットハッシュをコンパイル時に埋め込む。

$(addprefix prefix,names)
namesの各要素の前にprefixを追加する。
OBJS = $(addprefix obj/,$(notdir $(SRCS:.cpp=.o)))

$(dir names)と$(notdir names)
dirはパスからディレクトリ部分を、notdirはファイル名部分を抽出する。
$(dir src/main.cpp) → src/
$(notdir src/main.cpp) → main.cpp


Makefileの例

上記の要素を組み合わせた、より実践的なMakefileの例を示す。

 # コンパイラとオプション
 CC       := g++
 CFLAGS   := -Wall -std=c++11
 LDFLAGS  := -L/usr/local/lib
 LIBS     := -lpthread
 
 # ディレクトリ構成
 SRC_DIR  := src
 OBJ_DIR  := obj
 INC_DIR  := include
 BIN_DIR  := bin
 
 # ビルドタイプ
 BUILD_TYPE ?= debug
 ifeq ($(BUILD_TYPE),debug)
     CFLAGS += -g -DDEBUG
 else ifeq ($(BUILD_TYPE),release)
     CFLAGS += -O2 -DNDEBUG
 else
     $(error Unknown BUILD_TYPE: $(BUILD_TYPE))
 endif
 
 # ソースとオブジェクトファイル
 SRCS    := $(wildcard $(SRC_DIR)/*.cpp)
 OBJS    := $(patsubst $(SRC_DIR)/%.cpp,$(OBJ_DIR)/%.o,$(SRCS))
 DEPS    := $(OBJS:.o=.d)
 TARGET  := $(BIN_DIR)/myapp
 
 # デフォルトターゲット
 all: $(TARGET)
 
 # 実行ファイルの生成
 $(TARGET): $(OBJS) | $(BIN_DIR)
 	$(CC) -o $@ $^ $(LDFLAGS) $(LIBS)
 	@echo "Build complete: $@"
 
 # オブジェクトファイルの生成
 $(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp | $(OBJ_DIR)
 	$(CC) $(CFLAGS) -I$(INC_DIR) -MMD -MP -c $< -o $@
 
 # ディレクトリの作成
 $(OBJ_DIR) $(BIN_DIR):
 	mkdir -p $@
 
 # クリーンアップ
 clean:
 	rm -rf $(OBJ_DIR) $(BIN_DIR)
 
 # 依存関係ファイルのインクルード
 -include $(DEPS)
 
 # 擬似ターゲット
 .PHONY: all clean


この例では、以下の高度な機能を使用している。

  • ワイルドカードとpatsubstによるファイルの自動検出
  • 条件分岐によるビルドタイプの切り替え
  • 依存関係の自動生成 (-MMD -MPオプション)
  • Order-only prerequisites (|記号) によるディレクトリの作成
  • @記号によるコマンドのエコー抑制


Order-only prerequisites (|の後に記述される依存関係) は、ターゲットの生成には必要だが、タイムスタンプの比較には使用されない特殊な依存関係である。
ディレクトリが既に存在する場合、そのタイムスタンプが更新されても再コンパイルが発生しないため、効率的である。