組み込みのためのMakefile入門
IDEのビルドボタンは便利ですが、CI(継続的インテグレーション)での自動ビルド、ビルドの再現性確保、チームでの設定共有を考えると、いずれコマンドラインのビルド、つまりMakefileに行き着きます。本記事では、GNU Make(以下、ツール名は「Make」、コマンドとして実行する場合は「make」と表記します)の原理を1行で押さえたうえで、組み込み(クロスコンパイル)向けの実用的なMakefileを最小構成から組み立て、初心者が必ずはまる2つの罠(タブとヘッダ依存)まで解説します。
- Makeの原理は1行で書ける
- 組み込み向けの最小Makefile
- パターンルールと自動変数
- .PHONY:allとcleanは「ファイルではない」
- 2つ目の罠:ヘッダを変えても再コンパイルされない
- 実務での発展
- まとめ
Makeの原理は1行で書ける
Makefileの本体は「ルール」の集まりで、1つのルールは次の形をしています。
ターゲット: 依存するファイル
そのターゲットを作るコマンド(レシピ)
Makeは、ターゲットと依存ファイルのタイムスタンプを比較し、依存ファイルが新しい、またはターゲットが存在しない場合にレシピを実行します。これが差分ビルド(変更されたファイルだけ再コンパイル)の正体で、Makeの価値のほぼすべてです。そして最初の罠がここにあります。レシピ行の先頭は、スペース(空白)ではなく、通常(既定設定)ではタブ文字である必要があります。設定により変更できますが、まずはタブで覚えてください。スペースでインデントすると「missing separator」エラーになります。エディタの設定でMakefileだけタブを使うようにしておくとよいでしょう。
組み込み向けの最小Makefile
# ツールチェーンと設定をまとめて変数に
# (例:32ビットマイコンコアを対象とした設定。CC・OBJCOPY・SIZE・CFLAGS は、使用するツールチェーンとコアに合わせて変更します。arm-none-eabi は Arm Ltd. の商標を含むツールチェーン名です。Cortex-M4 は Arm Ltd. の登録商標です)
CC = arm-none-eabi-gcc
OBJCOPY = arm-none-eabi-objcopy
SIZE = arm-none-eabi-size
CFLAGS = -mcpu=cortex-m4 -mthumb -O2 -Wall -Wextra # FPUを使用する場合は -mfpu=fpv4-sp-d16 -mfloat-abi=hard なども追加が必要です
DEPFLAGS = -MMD -MP # 依存ファイル生成。コンパイル時のみ使う
LDFLAGS = -T target.ld -Wl,-Map=app.map # target.ld:対象MCU向けのリンカスクリプト
SRCS = main.c uart.c sensor.c
OBJS = $(SRCS:.c=.o)
DEPS = $(OBJS:.o=.d)
all: app.elf app.hex app.bin
app.elf: $(OBJS)
$(CC) $(CFLAGS) $(LDFLAGS) $^ -o $@
$(SIZE) $@
%.o: %.c
$(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
app.hex: app.elf
$(OBJCOPY) -O ihex $< $@
app.bin: app.elf
$(OBJCOPY) -O binary $< $@
clean:
rm -f $(OBJS) $(DEPS) app.elf app.hex app.bin app.map
-include $(DEPS)
.PHONY: all clean
makeと打てばELF・HEX・BINとmapファイルまで生成され、make cleanで全部消えます。生成物の使い分けは「ELF・HEX・BINの違い」、「mapファイルの読み方」は同名記事で解説しています。コピー&ペーストして使う場合は、レシピ行の先頭が前述のとおりタブ文字である点にご注意ください。
※この記事のMakefileは、LinuxやWindowsのWSL・Git BashといったUNIX互換環境での実行を想定しています。Windowsの標準コマンドプロンプトではcleanで使うrmが動作しないため、del /Q などに置き換えてください。
パターンルールと自動変数
上の「%.o: %.c」は、任意の.oを対応する.cから作るパターンルールです。レシピの中の記号は自動変数と呼ばれ、最低限この3つを覚えれば読み書きできます。
- $@:ターゲット名(いま作ろうとしているファイル)。
- $<:依存ファイルの1つ目(パターンルールでは元の.c)。
- $^:依存ファイル全部(リンク行ですべての.oファイルを並べるのに便利)。
.PHONY:allとcleanは「ファイルではない」
allやcleanは生成物の名前ではなく、コマンドの呼び名(偽ターゲット)です。宣言なしで使っていると、たまたまcleanという名前のファイルがディレクトリにできた瞬間、Makeは「cleanは最新」と判断してレシピを実行しなくなります。ファイルを作らないターゲットは.PHONYに列挙するよう習慣づけることをおすすめします。
2つ目の罠:ヘッダを変えても再コンパイルされない
「main.cはconfig.hをインクルードしているのに、config.hを変更してもmain.oが再コンパイルされない」。Makeはソースの中身を読まないため、#includeの依存関係は自動では知り得ないのです。ここで手作業で「main.o: main.c config.h」と書き始めると、ヘッダが増えるたびに破綻します。
定石は、コンパイラに依存関係を出力させることです。DEPFLAGSに置いた-MMD -MPオプションにより、GCCは各.oファイルと同時に依存関係ファイル(.d)を生成します。-MPを付けておくと、ヘッダファイルを削除・リネームした際に、存在しないヘッダを参照してmakeがエラー停止するのを防げます。Makefile末尾の「-include $(DEPS)」がそれを取り込むので、ヘッダを変更すれば、それを使っている.cだけが正しく再コンパイルされます。先頭のハイフンは「.dがまだ無い初回ビルドでもエラーにしない」という意味です。この3行(-MMD -MP、DEPS定義、-include)はセットで定型句として覚えてしまうのが早道です。
実務での発展
- ビルドディレクトリの分離:生成物をbuild/配下にまとめると、cleanが安全になり.gitignoreも1行で済みます。
- MCU依存の集約:CPUフラグやリンカスクリプト名は変数の冒頭に集め、機種展開時はそこだけ差し替える構成にします。
- CIへの接続:コマンド一発でビルドできるようになった時点で、CIに載せる準備は完了です(「組み込みCI/CD入門」参照)。
- 迷ったら公式へ:GNU Makeの仕様は公式マニュアルが最終リファレンスです。条件分岐や関数など、ここで触れていない機能も網羅されています。
まとめ
Makefileは「ターゲット: 依存→レシピ」の1行原理と、タブ・.PHONY・-MMDの3つの定型を押さえれば、組み込みの実用レベルに到達します。IDEと排他ではなく、IDEの裏で何が起きているかを理解する近道でもあるので、小さなプロジェクトで一度自分の手で書いてみるのがおすすめです。

