🛠️

いまさら make

2020/12/06に公開

Make を体験する

例えば, こんなソースコードがあります:

hello.c
#include <stdio.h>

int hello(void)
{
    printf("Hello, World!\n");
    return 0;
}
main.c
extern int hello(void);

int main(void)
{
    hello();
    return 0;
}

これらのファイルから hello と名付けられたプログラムを生成します. 次のようにコマンドを実行するでしょう.

$ cc -c main.c
$ cc -c hello.c
$ cc -o hello main.o hello.o

しかし, 修正のたびにこれらのコマンドを実行することは面倒です. 実行漏れなどのミスも発生するかもしれません. これを解消する手段が make です. Make はプログラムを生成するだけでなく, 不要になったファイルの削除, 更には \TeX 文書の作成などにも応用できます.

先程の hello を make を利用して生成してみます. Makefile と名付けられたファイルに以下のように記述します.

Makefile
hello: main.o hello.o
	cc -o hello main.o hello.o
	
main.o: main.c
	cc -c main.c

hello.o: hello.c
	cc -c hello.c

そして, コマンド make を実行します.

$ make            # 実際に入力するのはここだけ
cc -c main.c
cc -c hello.c
cc -o hello main.o hello.o

ソースコードや Makefile に記述した内容に問題がなければ, いくつかの処理が実行されて無事にプログラム hello が生成されます.

ここで, hello.c の内容を次のように変更します. 出力するメッセージが変更されました.

hello.c
#include <stdio.h>

int hello(void)
{
    printf("Saluton, Mondo!\n");
    return 0;
}

変更後に make してみます. Make が hello.c の変更を検出して必要な部分のみ処理を実行するのがわかると思います.

$ make
cc -c hello.c
cc -o hello main.o hello.o

このように make は必要な部分だけ処理してくれる賢いツールであることがわかります.

Makefile の書き方

Makefile とは, make にプログラムなどの作り方を指示する, いわゆる設計図です. ファイル名は, 通常 Makefile とします. 小文字で makefile としても良いですが, makefile は重要なファイルです —— ファイル名の先頭を大文字にしておくことで ls コマンドなどで ReadMe などの近くに表示させることができるかもしれません.

基本の書き方

Makefile に記述する基本的なものは次の 3 つです.

  1. 作るもの (ターゲット)
  2. 作るために必要なもの (コンポーネント)
  3. 作り方 (レシピ)

以下のように, コロン (:) を含む ルール行 とタブ文字で始まる コマンド行 を記述します. ルール行はコロンで区切られ, 左側にターゲットを, 右側にそのコンポーネントを記述します. コマンド行にはコンポーネントからターゲットを生成するためのコマンド列を記述します.

<ターゲット>: <コンポーネント 1> <コンポーネント 2> <コンポーネント n>
        <作り方 1>
        <作り方 2>
        <作り方 n>

これを踏まえて先の節の makefile を見てみると, その内容は hello に関する部分, main.o に関する部分, hello.o に関する部分の 3 つに分けられることがわかります.

Make を使ってターゲットを生成するには, 例えば make main.o hello.o のように生成したいターゲットを 0 個以上指定して実行します. ターゲットの指定を省略して実行されると makefile の一番先頭にあるターゲットを生成しようとします[1].

Makefile にはコメントを含めることができます. 文字 # から行末までがコメントとして扱われ, 実行に影響を及ぼさなくなります. しかし, コマンド行に含まれる # はそのままシェルに渡されます.

コメントの例
# この行はコメント
hello: main.o hello.o    # ここもコメント
        echo hogehoge # この部分はシェルに渡される (シェルによって無視されるかも)
        echo fugafuga# こうするとそのまま表示される
        cc -o hello main.o hello.o

また, 行末にバックスラッシュ (\) を置くと改行がエスケープされます. つまり, その行と次の行が単一の行のようにして扱われます.

行末エスケープの例
# 次のようにしてルール行を記述できます.  
hoge: /long/long/path/foo \
 /long/long/path/bar \
 /long/long/path/baz        # やっと行区切り
	echo "ここからコマンド行"

マクロ

マクロ とは, makefile の中で利用できる, 環境やユーザによって定義される変数のようなものです. マクロの名前には英数字と記号を組み合わせたものを使用することができます. 慣習では大文字の英字を使うことが多いようです.

マクロの定義は次のように行います.

マクロの定義
PROGRAM = hello

マクロの値を参照するには次のようにします. どちらも効果は同じです. マクロの名前が 1 文字の場合は $A のように括弧を省略することもできます. 文字 $ そのものを表したい場合は $$ とします.

マクロの参照
${PROGRAM}
$(PROGRAM)

もし, ${PROGRAM} のように書くべきところを誤って $PROGRAM と書いてしまうと, それは $PROGRAM (こちらはただの文字列) として解釈されます (そしてそれは意図しない結果になります).

Makefile の中で環境変数 (シェル変数) を利用することができます. これもまた, マクロと同様にして (例えば ${PATH} のようにして) 参照することができます.

マクロの定義にマクロの値を利用することができます. 奇妙な性質の一つに, マクロ定義の順番が考慮されないことがあります. 次の例では, マクロ ABC の値は hoge.fuga となります.

マクロの定義にマクロを使う
ABC = hoge.${XYZ}
XYZ = fuga

この性質のせいで, make では再帰的なマクロ定義 (FOO = ... ${FOO} など) を行うことができません. GNU make では, = の代わりに := を利用すことでこれを実現することができます.

定義されていないマクロを参照してもエラーになりません[2]. その参照は空文字列に展開されます. 次の例では, マクロ TEST の値はマクロ BAR が定義されていないときに foobaz のようになります.

未定義マクロの参照
TEST = foo${BAR}baz

Make を実行する際に, コマンドライン上でマクロを定義することもできます. 次の例は, make の実行時にマクロ DIR を定義します.

$ make "DIR = ./hoge/ ./fuga/ ./piyo/"

シェルに Bourne シェルや Korn シェル (の互換) を利用している場合は, make の前でマクロを定義することもできます. これは, 実際にはシェルの機能で, (一時的な) 環境変数として定義されています.

$ DIR=./hoge/ make

定義済みマクロ

マクロのうちのいくつかは予め定義されています. これらはよく使われるコマンドやオプション等です. 以下にいくつかの例を挙げます (これらのうちの VPATHSHELL については少しだけ性質が異なります. 詳細は後述します).

マクロ名 デフォルトの値 (例) 内容
CC cc C コンパイラ
CFLAGS (一部では -O) C コンパイラのオプション
CPP $(CC) -E C プリプロセッサ
CXX g++ C++ コンパイラ
CXXFLAGS C++ コンパイラのオプション
RM rm -f ファイルの削除
VPATH ソースファイルを検索するパス
SHELL コマンドを実行するシェル

利用している make のデフォルトのマクロを表示するには make -p と実行します. これには, 後に紹介するデフォルトのサフィックスルールなども含まれます.

内部マクロ (自動変数)

Make には, ターゲットやコンポーネントを参照するための 内部マクロ (自動変数) があります. よく使うものを以下に挙げます.

内部マクロ 説明
$@ ターゲット
$< 最初のコンポーネント
$^ 重複を除去したすべてのコンポーネントのリスト
$? ターゲットより新しいすべてのコンポーネントのリスト

あまり一般的ではありませんが, これらのマクロも ${@} のように括弧を添えて参照することができます.

また, $? 以外の内部マクロに対して ${<D} のようにすると $< のディレクトリ部分のみを, ${<F} のようにすると $< のファイル名部分のみを切り出して利用できます.

マクロの優先順位

今まで紹介してきたマクロや環境変数の優先順位は次のようになります. 上にあるものがより優先的に利用されます.

  • make の引数として渡されたもの
  • makefile 内で定義されたもの
  • シェル変数として定義されたもの
  • あらかじめ定義されているもの

Make に -e (--environment-overrides) のオプションを添えて実行すると優先順位は以下のように変化します.

  • make の引数として渡されたもの
  • シェル変数として定義されたもの
  • makefile 内で定義されたもの
  • あらかじめ定義されているもの

ここまでのまとめ

ここまでの内容を利用して, 最初の makefile を書き直すと次のようになります.

Makefile (マクロを使う)
PROGRAM = hello
OBJS = main.o hello.o

${PROGRAM}: ${OBJS}
	${CC} -o $@ $^

main.o: main.c
	${CC} ${CFLAGS} -c $?

hello.o: hello.c
	${CC} ${CFLAGS} -c $?

サフィックスルール (パターンルール)

サフィックス (suffix) は接尾辞と訳され “語尾に付与されるもの” の意味ですが, ここでは拡張子のことを指します. 例えば, C のソースファイルは .c というサフィックスを持っています.

C のコンパイラは, ソースファイル (.c) をコンパイルしてオブジェクト (.o) を出力します. これを利用して “.o を作るには .c を探せ” と make に指示することができます. これを サフィックスルール といいます. サフィックスルールを利用すると, 先程の makefile は次のように書き直すことができます.

Makefile (サフィックスルールを使う)
PROGRAM = hello
OBJS = main.o

${PROGRAM}: ${OBJS}
	${CC} -o $@ $^

.SUFFIXES: .c .o
.c.o:
	${CC} ${CFLAGS} -c $<

.c.o の部分が .c から .o を作るサフィックスルールの記述です. .SUFFIXES の文はサフィックスルールを適用する拡張子を指定しています. 実際には, make によって定義されているリストに追加されます. Make によって定義されている既存のサフィックスルールが干渉して意図した動作にならない場合は, 以下のようにしてサフィックスルールをリセットすることができます[3].

サフィックスルールのリセット
.SUFFIXES:          # サフィックスルールのリセット
.SUFFIXES: .c .o    # 新しいリストの定義
.c.o:
	${CC} ${CFLAGS} -c $<

GNU make を利用する場合は パターンルール がサフィックスルールの代わりに推奨されています. 以下は上に示したサフィックスルールの依存関係と一致します.

パターンルールを使う
%.o: %.c
	${CC} ${CFLAGS} -c $<

パターンルールの % はシェルの * に相当します. これはパターンのどこにでも置くことができますが, 一度しか使えません. ターゲットの % とコンポーネントの % には同じ文字列がマッチします. 次のようなルールも正しいルールです.

パターンの例
hoge_%: fuga_%    # fuga_* から hoge_* を生成する

pre%.o: %.c       # *.c から pre*.o を生成する

%.nyan: cat.%     # cat.* から *.nyan を生成する

また, サフィックスルールの複雑な例では次のようなものがあります.

シングルサフィックスルール
.sh:
	cat $< >$@
	chmod a+x $@

これはシェルスクリプト (.sh) から実行ファイル (サフィックスなし) を生成するルールです. これをパターンルールを利用して書くと次のようになります.

パターンルール版
%: %.sh
	cat $< >$@
	chmod a+x $@

こうしてみるとパターンルールのほうがよりわかりやすく記述できるようです. 古い make に対応する必要がなければパターンルールを利用したほうがいいかもしれません.

定義済みのルール

マクロと同様に, よく使われるルールは make によって予め定義されています. これらはパターンルールやサフィックスルールとして定義されています. make -p と実行することで定義済みのすべてのルールを表示することができます.

ここまでのまとめ

ここまでの内容を利用して, 最初の makefile を書き直すと次のようになります. C のソースからオブジェクトを作るルールはデフォルトで定義されているので記述を省略できます.

Makefile (定義済みのルールを使う)
PROGRAM = hello
OBJS = main.o hello.o

${PROGRAM}: ${OBJS}
	${CC} -o $@ $^

疑似ターゲット

例えば, 実際に配布されているプロジェクトなどの makefile の冒頭部分には次のように記述されています.

Makefile の冒頭部分例
all: ${PROGRAM} ${MANPAGE}

つまり, この例では, 引数なしで make を実行すると all というターゲットを構成するために ${PROGRAM}${MANPAGE} を生成しようとします. しかし, 実際には all と名付けられたファイルは出現しないでしょう. これを 疑似ターゲット (ダミーターゲット) といいます. ここでの all にターゲットのファイル名としての意味はなく, コンポーネントとして指定されたターゲットを生成するためのトリガとしての役割しか持ちません. Make は実際に all と名付けられたファイルが生成されなくてもエラーを発することはありません.

しかし, 単に上のように記述するだけでは問題がある場合もあります. それはターゲット名と同名のファイルが存在するときに発生します. 次の例を考えます.

問題のある clean
clean:
	${RM} ${PROGRAM} ${OBJS}

この例では clean と名付けられたファイルが存在すると, ターゲットは最新である (何もすることがない) 旨を表示して処理を抜けます. これは, make の依存関係を調べる機能によるものです. ターゲット clean にはコンポーネントがない —— コンポーネントに対して常に最新であるため make には clean 再生成の必要がないと判断されたのです.

GNU make にはこれに対する解決策が用意されています. それは .PHONY を利用するものです.

.PHONY を利用する
.PHONY: clean
clean:
	${RM} ${PROGRAM} ${OBJS}

.PHONY: clean の部分は, ターゲット clean が疑似ターゲットであることを明示しています —— コンポーネントからターゲットを構築するという make の原則的ルールを無視して常に再構築することを指示します. これにより, make はファイル clean が存在していても clean に対応した処理を実行しようとします.

代表的な疑似ターゲット

多くの makefile で利用されているような, 代表的な疑似ターゲットの一覧を以下に示します.

疑似ターゲット名 内容
all 最初のターゲットとして配置され アプリケーションの構築処理を全て実行する
install 生成された実行可能ファイルをインストールする
clean make によって生成されたファイルを削除する
info GNU info を生成する

個人的には, 少なくとも all と clean の疑似ターゲットを用意しておくと必要最低限の親切感があると思います.

ここまでのまとめ

疑似ターゲットを利用して, デフォルト (all) でプログラム生成, clean で生成ファイル削除の処理を記述します.

Makefile (疑似ターゲットを使う)
PROGRAM = hello
OBJS = main.o hello.o

# デフォルト動作
.PHONY: all
all: ${PROGRAM}

${PROGRAM}: ${OBJS}
	${CC} -o $@ $^

# 生成ファイル削除
.PHONY: clean
clean:
	${RM} ${PROGRAM} ${OBJS}

特別なマクロ

定義済みマクロの節でちらりと紹介した VPATHSHELL について説明します.

VPATH (ファイル検索パスの指定)

例えば, ソースファイルは src などと名付けられたディレクトリに配置されることがしばしばでしょう.

ディレクトリ構成図
./
├── Makefile
├── README
└── src/
    ├── hello.c
    └── main.c

Make のデフォルトの挙動では, ファイル検索の対象となるパスはカレントディレクトリのみです. つまり, make は src 下にあるファイルの存在に気づくことができません. VPATH はこれを解決します. Makefile の冒頭に次のように記述します.

VPATH を使う
VPATH = src

この記述は, ファイルを検索するパスを指定します. この記述により, make はまずカレントディレクトリを検索し, ファイルが見つからなければ src を検索するようになります. VPATH に複数のディレクトリを指定したい場合は, 次のようにコロン (:) で区切ります. より左側にあるものが優先して検索されます.

VPATH に複数のディレクトリを指定
VPATH = src:/usr/src:hoge:${DIRS}

上の例では, まずはカレントディレクトリを, ついで src, /usr/src, hoge, マクロ DIRS の値 を検索するようになります.

GNU make ではマクロ VPATH の代わりに vpath 命令を利用できます. vpath 命令では, ファイル名のパターンによって検索パスを切り替えることができます. vpath 命令の書式は次のとおりです.

vpath 命令の書式
vpath <pattern> <directories>

ここで <pattern> の部分には %.c のようなパターンを (詳細はパターンルールの節を参照), <directories> にはコロンで区切られたディレクトリのリストを指定します. C のソースファイルの検索パスに src を加えるには次のように記述します.

C ソースの検索パスに src を追加
vpath %.c src

SHELL (実行シェルの指定)

Makefile のコマンド行に記述される内容は, シェルによって解釈され実行されます. マクロ SHELL にシェルへのパスを指定することで make のコマンド実行に利用されるシェルを指定できます.

しかし, 互換性の観点からマクロ SHELL には常に値 /bin/sh を指定するべきです. これは通常 Bourne シェルです. どうしてもそれ以外のシェルを利用したい場合は, そのシェルをコマンド行から呼び出すのが良いでしょう.


おまけ: ヘッダファイルの依存関係を洗い出す (C 言語)

おまけです. ヘッダファイルで宣言されている文字列定数 HELLO_WORLD を表示するだけのプログラムです.

src/main.c
#include <stdio.h>
#include "hello.h"

int main (void)
{
    puts(HELLO_WORLD);
    return 0;
}
include/hello.h
#pragma once
#ifndef HELLO_H

#define HELLO_WORLD "Hello, World!"

#endif /* HELLO_H */

そして makefile も.

Makefile
SHELL   = /bin/sh
VPATH   = src
CFLAGS  = -I include
OBJS    = main.o
PROGRAM = hello

.PHONY: all
all: ${PROGRAM}

${PROGRAM}: ${OBJS}
	${CC} -o $@ $^

.PHONY: clean
clean:
	${RM} ${OBJS} ${PROGRAM}

ここで make を実行すると, プログラム hello が生成されます.

$ make
$ ./hello
Hello, World!

この後に include/hello.h を次のように変更します.

include/hello.h (変更後)
#pragma once
#ifndef HELLO_H

#define HELLO_WORLD "Saluton, Mondo!" /* ここが変わった */

#endif /* HELLO_H */

そして make を実行すると, …… 何も更新されません.

$ make
make: Nothing to be done for 'all'.

なぜでしょうか. それは C の #include による依存関係を make は知らないからです. ですから makefile に依存関係を記述すれば正しく更新されます.

Makefile (追加分のみ)
main.o: include/hello.h
$ make
$ ./hello 
Saluton, Mondo!

しかし, 実際のプロジェクトにおいて上のように依存関係を逐一記述していたのではキリがありません. キリがあっても保守の観点から推奨されません.

ここで, プリプロセッサの機能を利用します. Makefile を次のように記述します.

Makefile (ヘッダファイルの依存関係対応版)
SHELL   = /bin/sh
VPATH   = src
CFLAGS  = -I include
OBJS    = main.o
PROGRAM = hello
DEPEND  = depend.inc    # 依存関係を記述するファイル

.PHONY: all
all: ${DEPEND} ${PROGRAM}

${PROGRAM}: ${OBJS}
	${CC} -o $@ $^

.PHONY: clean
clean:
	${RM} ${OBJS} ${PROGRAM} ${DEPEND}

# 依存関係を調べる
.PHONY: ${DEPEND}    # 以下の処理は常に実行される
${DEPEND}: ${OBJS:.o=.c}
	-@ ${RM} ${DEPEND}
	-@ for i in $^; do ${CPP} ${CFLAGS} -M $$i >>$@; done

-include ${DEPEND}

${DEPEND} に関する部分が追加されました. この処理を詳しく見ていきます.

.PHONY: ${DEPEND} によって, ${DEPEND} はそのコンポーネントの更新の有無にかかわらず必ず再生成されるようになります.

まずはルール行です. コンポーネントはマクロ OBJS の各要素のサフィックス (.o) を .c に置き換えたものです. つまり, ソースファイルそのものを指しています. ${DEPEND} はそれぞれのソースファイルに依存しています.

次にコマンド行です. コマンド行の先頭にある -@-@ に分けられます. - は処理の失敗を無視することを表します. これがついていない場合には, コマンドが 0 以外の値を返すと make が停止します. @ を添えるとコマンドは非表示で実行されます.

まず ${DEPEND} を削除します. 初めて make を実行する場合は ${DEPEND} は存在しないでしょうから失敗します. しかし - が添えられているので make はそれを無視します.

次にコンポーネントのそれぞれから依存関係を洗い出します. C のプリプロセッサ ${CPP} にオプション -M を添えて実行すると, そのソースコードの依存しているヘッダファイルの一覧を makefile の書式で標準出力に出力します. それをリダイレクトで ${DEPEND} に追加していきます. その内容に興味があれば ${DEPEND} の中身を見てみると良いでしょう.

最後に include 命令によって ${DEPEND} を makefile に取り込みます. この命令はちょうど C の #include 命令のようなものです. これにも - が添えられているので, 失敗は無視されます.

この処理をターゲット all のコンポーネントに設定して呼び出すことで, ${DEPEND} の更新が都度行われるようにします. ${DEPEND} には ${OBJS} とそれに関連するヘッダファイルの依存関係が記述されているので, make がヘッダファイルの更新を検出できるようになります.


参考

  • make 改訂版 (1997) ISBN4-900900-6-05
  • GNU Make 第 3 版 (2005) ISBN4-87311-269-9
脚注
  1. GNU make では make の実行時に -B (--always-make) のオプションを添えることで無条件に全ターゲットを make させることができます. ↩︎

  2. GNU make では make の実行時に --warn-undefined-variables のオプションを添えることで警告を発することができます. ↩︎

  3. GNU make では make の実行時に -r (--no-builtin-rules) のオプションを添えることでも同様の効果を得ることができます. ↩︎

Discussion