いまさら make
Make を体験する
例えば, こんなソースコードがあります:
#include <stdio.h>
int hello(void)
{
printf("Hello, World!\n");
return 0;
}
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 はプログラムを生成するだけでなく, 不要になったファイルの削除, 更には
先程の hello を make を利用して生成してみます. 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 の内容を次のように変更します. 出力するメッセージが変更されました.
#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> <コンポーネント 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
と書いてしまうと, それは $P
と ROGRAM
(こちらはただの文字列) として解釈されます (そしてそれは意図しない結果になります).
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
定義済みマクロ
マクロのうちのいくつかは予め定義されています. これらはよく使われるコマンドやオプション等です. 以下にいくつかの例を挙げます (これらのうちの VPATH
と SHELL
については少しだけ性質が異なります. 詳細は後述します).
マクロ名 | デフォルトの値 (例) | 内容 |
---|---|---|
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 を書き直すと次のようになります.
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 は次のように書き直すことができます.
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 のソースからオブジェクトを作るルールはデフォルトで定義されているので記述を省略できます.
PROGRAM = hello
OBJS = main.o hello.o
${PROGRAM}: ${OBJS}
${CC} -o $@ $^
疑似ターゲット
例えば, 実際に配布されているプロジェクトなどの makefile の冒頭部分には次のように記述されています.
all: ${PROGRAM} ${MANPAGE}
つまり, この例では, 引数なしで make を実行すると all というターゲットを構成するために ${PROGRAM}
と ${MANPAGE}
を生成しようとします. しかし, 実際には all と名付けられたファイルは出現しないでしょう. これを 疑似ターゲット (ダミーターゲット) といいます. ここでの all にターゲットのファイル名としての意味はなく, コンポーネントとして指定されたターゲットを生成するためのトリガとしての役割しか持ちません. Make は実際に all と名付けられたファイルが生成されなくてもエラーを発することはありません.
しかし, 単に上のように記述するだけでは問題がある場合もあります. それはターゲット名と同名のファイルが存在するときに発生します. 次の例を考えます.
clean:
${RM} ${PROGRAM} ${OBJS}
この例では clean と名付けられたファイルが存在すると, ターゲットは最新である (何もすることがない) 旨を表示して処理を抜けます. これは, make の依存関係を調べる機能によるものです. ターゲット clean にはコンポーネントがない —— コンポーネントに対して常に最新であるため make には clean 再生成の必要がないと判断されたのです.
GNU make にはこれに対する解決策が用意されています. それは .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 で生成ファイル削除の処理を記述します.
PROGRAM = hello
OBJS = main.o hello.o
# デフォルト動作
.PHONY: all
all: ${PROGRAM}
${PROGRAM}: ${OBJS}
${CC} -o $@ $^
# 生成ファイル削除
.PHONY: clean
clean:
${RM} ${PROGRAM} ${OBJS}
特別なマクロ
定義済みマクロの節でちらりと紹介した VPATH
と SHELL
について説明します.
VPATH (ファイル検索パスの指定)
例えば, ソースファイルは src などと名付けられたディレクトリに配置されることがしばしばでしょう.
./
├── Makefile
├── README
└── src/
├── hello.c
└── main.c
Make のデフォルトの挙動では, ファイル検索の対象となるパスはカレントディレクトリのみです. つまり, make は src 下にあるファイルの存在に気づくことができません. VPATH
はこれを解決します. Makefile の冒頭に次のように記述します.
VPATH = src
この記述は, ファイルを検索するパスを指定します. この記述により, make はまずカレントディレクトリを検索し, ファイルが見つからなければ src を検索するようになります. VPATH
に複数のディレクトリを指定したい場合は, 次のようにコロン (:
) で区切ります. より左側にあるものが優先して検索されます.
VPATH = src:/usr/src:hoge:${DIRS}
上の例では, まずはカレントディレクトリを, ついで src, /usr/src, hoge, マクロ DIRS
の値 を検索するようになります.
GNU make ではマクロ VPATH
の代わりに vpath
命令を利用できます. vpath
命令では, ファイル名のパターンによって検索パスを切り替えることができます. vpath
命令の書式は次のとおりです.
vpath <pattern> <directories>
ここで <pattern>
の部分には %.c
のようなパターンを (詳細はパターンルールの節を参照), <directories>
にはコロンで区切られたディレクトリのリストを指定します. C のソースファイルの検索パスに src を加えるには次のように記述します.
vpath %.c src
SHELL (実行シェルの指定)
Makefile のコマンド行に記述される内容は, シェルによって解釈され実行されます. マクロ SHELL
にシェルへのパスを指定することで make のコマンド実行に利用されるシェルを指定できます.
しかし, 互換性の観点からマクロ SHELL
には常に値 /bin/sh
を指定するべきです. これは通常 Bourne シェルです. どうしてもそれ以外のシェルを利用したい場合は, そのシェルをコマンド行から呼び出すのが良いでしょう.
おまけ: ヘッダファイルの依存関係を洗い出す (C 言語)
おまけです. ヘッダファイルで宣言されている文字列定数 HELLO_WORLD
を表示するだけのプログラムです.
#include <stdio.h>
#include "hello.h"
int main (void)
{
puts(HELLO_WORLD);
return 0;
}
#pragma once
#ifndef HELLO_H
#define HELLO_WORLD "Hello, World!"
#endif /* HELLO_H */
そして 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 を次のように変更します.
#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 に依存関係を記述すれば正しく更新されます.
main.o: include/hello.h
$ make
$ ./hello
Saluton, Mondo!
しかし, 実際のプロジェクトにおいて上のように依存関係を逐一記述していたのではキリがありません. キリがあっても保守の観点から推奨されません.
ここで, プリプロセッサの機能を利用します. 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
- GNU Make 3.79.1 ドキュメント (2009)
Discussion