4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【ビルドシステム入門#1】Makefileをちゃんと理解する ― パターンルール・自動変数・依存関係追跡

4
Posted at

はじめに

make コマンドは日常的に使っているけれど、Makefileの中身はなんとなくコピペで乗り越えてきた——そういうエンジニアは多いと思います。本記事ではMakefileの「なんとなく」を「ちゃんと」に変えることを目的に、実際に動くサンプルを使いながら基本を整理します。

本記事はシリーズの第1回です。

  • 第1回:Makefileをちゃんと理解する(本記事)
  • 第2回:./configure の正体——autoconf/automake入門
  • 第3回:CMake入門——Autotoolsとの比較で理解する

対象読者

  • make は使えるがMakefileの書き方に自信がない方
  • .c.o:%.o: %.c の違いがあやふやな方
  • ヘッダファイルを変更したのに再ビルドされなかった経験がある方

サンプルプロジェクトの構成

本記事では以下の構成のCプロジェクトを題材にします。

sample/
├── include/
│   └── greet.h
└── src/
    ├── main.c
    └── greet.c

ソースコード

/* include/greet.h */
#ifndef GREET_H
#define GREET_H

void greet(const char *name);

#endif /* GREET_H */
/* src/greet.c */
#include <stdio.h>
#include "greet.h"

void greet(const char *name)
{
    printf("Hello, %s!\n", name);
}
/* src/main.c */
#include "greet.h"

int main(void)
{
    greet("World");
    greet("Qiita");
    return 0;
}

Makefileの基本構文

Makefileはひとつ以上のルールから成り立っています。ルールの基本形はこうです。

ターゲット: 依存ファイル ...
	レシピ(コマンド)
  • ターゲット:生成したいファイル名(または処理名)
  • 依存ファイル:ターゲットを作るために必要なファイル
  • レシピ:実行するコマンド(行頭はタブ文字。スペース不可

makeは「依存ファイルがターゲットより新しければレシピを実行する」という仕組みで動きます。これが差分ビルドの基本原理です。


変数

Makefileでは変数を使って記述を共通化できます。

CC     = gcc
CFLAGS = -Wall -Wextra

main.o: src/main.c
	$(CC) $(CFLAGS) -c src/main.c -o main.o

変数の参照は $(変数名) または ${変数名} で行います。よく使う組み込み変数を以下に示します。

変数 デフォルト値 意味
CC cc Cコンパイラ
CXX g++ C++コンパイラ
CFLAGS (空) Cコンパイルオプション
LDFLAGS (空) リンクオプション
CPPFLAGS (空) プリプロセッサオプション(-Iなど)

自動変数

レシピの中では自動変数を使うことで、ターゲット名や依存ファイル名をハードコードせずに書けます。

自動変数 意味
$@ ターゲットファイル名
$< 最初の依存ファイル名
$^ すべての依存ファイル名(重複除去)
hello: src/main.o src/greet.o
	$(CC) -o $@ $^
#           ↑      ↑
#         hello   src/main.o src/greet.o

暗黙ルール:.c.o: とパターンルール %.o: %.c

.c ファイルから .o ファイルを作るルールは毎回同じです。これをファイルごとに書いていたら大変なので、makeには暗黙ルールの仕組みがあります。

サフィックスルール(旧式)

.c.o:
	$(CC) $(CFLAGS) -c $< -o $@

.c.o: は「.c ファイルから .o ファイルを作る」という意味です。歴史的な記法で、古いMakefileでよく見かけます。現在は非推奨です。

パターンルール(現代的な書き方)

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

% がワイルドカードとして機能し、foo.c → foo.obar.c → bar.o のように任意のファイルに対して適用されます。パターンルールのほうが柔軟で明確なため、現在はこちらを使うのが標準です。


組み込み暗黙ルール

実はMakefileに何も書かなくても、make foo.o と実行するだけで foo.c からコンパイルしてくれることがあります。これはmakeが組み込みの暗黙ルールを持っているためです。ただし明示的に書いたほうが意図が明確になるため、Makefileには書くことをお勧めします。

ヘッダファイルの依存関係追跡

素朴なMakefileには落とし穴があります。

# 問題のあるMakefile
src/main.o: src/main.c
	$(CC) -c src/main.c -o src/main.o

このルールでは greet.h が変更されても main.o は再ビルドされません。依存関係に greet.h が書かれていないためです。

古い解決策:make depend

かつては以下のようなターゲットを用意して、手動で依存関係を生成していました。

depend:
	$(CC) -MM src/*.c >> Makefile

gcc -MM を実行すると依存関係が出力されます。

src/main.o: src/main.c include/greet.h
src/greet.o: src/greet.c include/greet.h

しかしこの方法は「make depend を実行し忘れる」という問題があり、現在は使われません。

現代的な解決策:-MMD -MP オプション

gccにはコンパイルと同時に依存関係ファイル(.dファイル)を自動生成するオプションがあります。

オプション 効果
-MMD .d ファイルを生成する(システムヘッダを除く)
-MP ヘッダファイルが削除されたときのエラーを防ぐ
DEPFLAGS = -MMD -MP

%.o: %.c
	$(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@

# .dファイルをMakefileとして読み込む(-はエラーを無視する意味)
-include $(DEPS)

src/main.c をコンパイルすると src/main.d が自動生成され、中身はこうなります。

src/main.o: src/main.c include/greet.h
include/greet.h:

これを -include で読み込むことで、ヘッダの変更を自動的に検知できます。


.PHONY

cleanall のようなターゲットは実際のファイルを生成しません。もし clean という名前のファイルが存在すると、makeは「cleanは最新だ」と判断してレシピを実行しません。これを防ぐのが .PHONY です。

.PHONY: all clean

all: hello

clean:
	rm -f $(TARGET) $(OBJS) $(DEPS)

.PHONY に登録されたターゲットは、同名ファイルの有無に関わらず常にレシピが実行されます。


完成したMakefile

以上の要素をまとめた、実際に動作するMakefileです。

# ターゲット名
TARGET  = hello

# コンパイラとフラグ
CC       = gcc
CFLAGS   = -Wall -Wextra
CPPFLAGS = -I./include
DEPFLAGS = -MMD -MP

# ソースファイルとオブジェクトファイル
SRCS    = src/main.c src/greet.c
OBJS    = $(SRCS:.c=.o)
DEPS    = $(OBJS:.o=.d)

# デフォルトターゲット
.PHONY: all
all: $(TARGET)

# リンク
$(TARGET): $(OBJS)
	$(CC) -o $@ $^

# コンパイル(パターンルール)
%.o: %.c
	$(CC) $(CFLAGS) $(CPPFLAGS) $(DEPFLAGS) -c $< -o $@

# 依存関係ファイルを読み込む
-include $(DEPS)

# クリーン
.PHONY: clean
clean:
	rm -f $(TARGET) $(OBJS) $(DEPS)

実行結果

$ make
gcc -Wall -Wextra -I./include -MMD -MP -c src/main.c -o src/main.o
gcc -Wall -Wextra -I./include -MMD -MP -c src/greet.c -o src/greet.o
gcc -o hello src/main.o src/greet.o

$ ./hello
Hello, World!
Hello, Qiita!

2回目以降は変更されたファイルだけが再コンパイルされます。

$ touch include/greet.h  # ヘッダを変更したとする
$ make
gcc -Wall -Wextra -I./include -MMD -MP -c src/main.c -o src/main.o
gcc -Wall -Wextra -I./include -MMD -MP -c src/greet.c -o src/greet.o
gcc -o hello src/main.o src/greet.o
# greet.hに依存する両ファイルが再ビルドされる

まとめ

項目 ポイント
パターンルール %.o: %.c を使う。.c.o: は旧式
自動変数 $@(ターゲット)$<(最初の依存)$^(全依存)
依存関係追跡 -MMD -MP.d ファイルを自動生成し -include で読み込む
.PHONY ファイルを生成しないターゲットには必ず付ける

次回は ./configure の正体——autoconfautomake が何をしているのかを、同じサンプルプロジェクトを使って説明します。


参考

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?