プログラミング/make
makeはソースコードから実行形式のプログラムやライブラリを自動的にビルドするビルド自動化ツールです。 統合開発環境や言語固有のコンパイラー機能でもビルドプロセスを管理できますが、make はプログラムのビルド以外にも、あるファイルが変更されると自動的に他のファイルから更新されなければならないようなプロジェクトの管理にも使用することができます。 特にUnixやUnix系OSではmakeが広く使われています。
はじめに[編集]
makeは何度も書き直され、同じファイルフォーマットと基本的なアルゴリズム原理を使用し、独自の非標準的な拡張を行ったスクラッチから書き起こされた亜種も多数存在します。
- BSD-make
- GNU-make
の2つが多く使われているので、本書では BSD-make(しばしば bmake)とGNU-make(しばしば gmake)に共通する構文・機能ついて説明します(余裕があれば違いについても説明します)。
分割コンパイル[編集]
make の説明に入る前に、分割コンパイルについてザックリ説明します。
プログラミング言語には、コンパイラーによってソースコードから実行形式のプログラムをコンパイルするコンパイル型言語と、ソースコードをインタープリターが逐次実行するインタープリタ-型の2つに分類されます。
分割コンパイルは、コンパイラー型のプログラミング言語で複数のソースコードから1つの実行形式のプログラムを作る方式のことで、1つ1つのソースコードを小さくしたり関連した機能だけをまとめる目的で行なわれます(分割統治法の1つだと考えられます)。
C言語の分割コンパイルの例[編集]
実際に3つのファイルに分割したC言語で書かれたプログラムの例を見てみましょう。
- Main.c
// Main.c: #include </stdio.h> #include "imath.h" int main(void) { for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) print("ipow(%d, %d) = %d\n", i, j, ipow(i, j)); }
- imath.h
// imath.h: extern int ipow(int x, unsigned y);
- imath.c
// imath.c: #include "imath.h" int ipow(int x, unsigned y) { if (y == 0) return 1; if (y == 1) return x; return x * ipow(x, y - 1); }
- コマンドラインでのビルド作業の様子
% mkdir imath % cd imath/ % cat > Main.c // Main.c: #include <stdio.h> #include "imath.h" int main(void) { for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) print("ipow(%d, %d) = %d\n", i, j, ipow(i, j)); } % cat > imath.h // imath.h: extern int ipow(int x, unsigned y); % cat > imath.c // imath.c: #include "imath.h" int ipow(int x, unsigned y) { if (y == 0) return 1; if (y == 1) return x; return x * ipow(x, y - 1); } % cc -c imath.c % cc -c Main.c % cc -o imath Main.o imath.o % ./imath ipow(0, 0) = 1 ipow(0, 1) = 0 ipow(0, 2) = 0 ipow(1, 0) = 1 ipow(1, 1) = 1 ipow(1, 2) = 1 ipow(2, 0) = 1 ipow(2, 1) = 2 ipow(2, 2) = 4
- imath.c をコンパイルし imath.o を得ています。
- Main.c をコンパイルし Main.o を得ています。
- Main.o と imath.o (とランタイムライブラリー)をリンクして実行形式 imath を得ています。
このプログラムは、0の0乗を扱っています。 0**0を1としていますが宗派によっては 0**0を0とする場合もあります。
imath.c をその仕様に変更してみましょう。
- imath.c
// imath.c: #include "imath.h" int ipow(int x, unsigned y) { if (x == 0) return x; if (y == 0) return 1; if (y == 1) return x; return x * ipow(x, y - 1); }
実行してみましょう。
% ./imath ipow(0, 0) = 1 ipow(0, 1) = 0 ipow(0, 2) = 0 ipow(1, 0) = 1 ipow(1, 1) = 1 ipow(1, 2) = 1 ipow(2, 0) = 1 ipow(2, 1) = 2 ipow(2, 2) = 4
あれ、変わりません。ああ、書換えてからコンパイルをしてませんね。
% cc -c imath.c % ./imath ipow(0, 0) = 1 ipow(0, 1) = 0 ipow(0, 2) = 0 ipow(1, 0) = 1 ipow(1, 1) = 1 ipow(1, 2) = 1 ipow(2, 0) = 1 ipow(2, 1) = 2 ipow(2, 2) = 4
?、まだ変わりません。リンクも必要ですね。
% cc -c imath.c % cc -o imath Main.o imath.o % ./imath ipow(0, 0) = 0 ipow(0, 1) = 0 ipow(0, 2) = 0 ipow(1, 0) = 1 ipow(1, 1) = 1 ipow(1, 2) = 1 ipow(2, 0) = 1 ipow(2, 1) = 2 ipow(2, 2) = 4
- やっと変更が反映されました。
- このように、ソースコードの変更によって部分的なビルドが必要になります。
- Main.oは変更の影響を受けないので再コンパイルしていませんね。
この例は単純なので、全てのソースコードの再コンパイルの要否を容易に把握できますが、小さなツールのソースコードでもファイルは十数から数十に及ぶので把握が困難になりがちです。 このような時のために Makefile を用意します。
make を使ったビルド[編集]
Makefile[編集]
imath をビルドするための素朴な Makefile を書いてみます。
- Makefile
# Makefile: imath: Main.o imath.o cc -o imath Main.o imath.o Main.o: Main.c imath.h cc -c Main.c imath.o: imath.c imath.h cc -c imath.c
使ってみます。
% make imath `imath' is up to date. % rm *.o % make imake make: don't know how to make imake. Stop make: stopped in /usr/home/user1/imath % make imath cc -c Main.c cc -c imath.c cc -o imath Main.o imath.o % ./imath ipow(0, 0) = 0 ipow(0, 1) = 0 ipow(0, 2) = 0 ipow(1, 0) = 1 ipow(1, 1) = 1 ipow(1, 2) = 1 ipow(2, 0) = 1 ipow(2, 1) = 2 ipow(2, 2) = 4 % make `imath' is up to date. % _
- make は
make ターゲット名
の形式で呼出します - 依存関係のある Main.o imath.o のタイムスタンプがターゲット imath のタイムスタンプより古いので何もしません
- Main.o と imath.o を消してみます。
- 再び、make
- 綴を imake と間違えました
- make は停止しました
- 正しい名前で make を起動
- 依存関係
imath: Main.o imath.o
から- 依存関係
Main.o: Main.c imath.h
が適応されアクションcc -c Main.c
が実行されます - 依存関係
imath.o: imath.c imath.h
が適応されアクションcc -c imath.c
が実行されます
- 依存関係
Main.o imath.o
の用意ができたのでcc -o imath Main.o imath.o
が実行されます
- 依存関係
Makefileの基本構文[編集]
- ターゲット
- 依存関係の
:
の左側、ファイルを書くとソースのタイムスタンプをもとにアクションに要否を make が判断します - ソース
- 依存関係の
:
の右側、複数可(空白区切り) - アクション
- 依存関係の次からの行、行頭に水平タブが1つ以上必要です。
imath: Main.o imath.o cc -o imath Main.o imath.o
- ターゲット
- imath
- ソース
- Main.o imath.o
- アクション
- cc -o imath Main.o imath.o
- となります。
コメント[編集]
Makefile にはコメントを書くことができます。
行の中に # が出てきたら、そこから行末までがコメントです。
組込みルール[編集]
make には既にルールが組込まれていて、例えば、.c
から .o
を得るルールが組込まれているので
# Makefile: imath: Main.o imath.o cc -o imath Main.o imath.o Main.o: Main.c imath.h # cc -c Main.c imath.o: imath.c imath.h # cc -c imath.c
としてもアクションをコメントにしても
% rm *.o % make cc -c -o Main.o Main.c cc -c -o imath.o imath.c cc -o imath Main.o imath.o
- と組込みルールが適用されます。
cc -c -o Main.o Main.c
- と組込みルールでは生成されるファイルを明示しているところが違います。
ディフォルトターゲット[編集]
Makefileで最初に出てくるターゲットがmakeを引数を渡されなかったときにビルドされるターゲットで、allを使うのが一般的です
# Makefile: all: imath imath: Main.o imath.o cc -o imath Main.o imath.o Main.o: Main.c imath.h imath.o: imath.c imath.h
ファイル以外のターゲット[編集]
ターゲットは、all の様にファイルでないものでも構いません。その場合はタイムスタンプの比較はできないので必ず実行されます。 clean がよく定義されます。
# Makefile: all: imath clean: rm imath Main.o imath.o imath: Main.o imath.o cc -o imath Main.o imath.o Main.o: Main.c imath.h imath.o: imath.c imath.h
.PHONYを使ったダミーターゲットの記述[編集]
ファイル以外をターゲットにするのは便利ですが、一つ問題があります
$ touch clean $ make clean make: Nothing to be done for 'clean'.
- clean と言う名前のファイルが有ると、make は「既に clean が存在し、依存関係もないので特にすることはない」と判断してしまいます。
# Makefile: .PHONY: all clean all: imath clean: rm imath Main.o imath.o imath: Main.o imath.o cc -o imath Main.o imath.o Main.o: Main.c imath.h imath.o: imath.c imath.h
- ファイルでないターゲットを .PHONY: の右に列挙します。
$ make clean rm imath Main.o imath.o
変数[編集]
Makefile で何度も参照される事柄は、変数に定義して複数箇所で参照できます。
# Makefile: .PHONY: all clean EXEC=imath SRCS=Main.c imath.c OBJS=Main.o imath.o all: $(EXEC) clean: rm $(EXEC) $(OBJS) $(EXEC): $(OBJS) cc -o $(EXEC) $(OBJS) Main.o: Main.c imath.h imath.o: imath.c imath.h
[TODO:ディフォルトターゲット・暗黙のルール・組込みルール・変数・マクロ・条件実行…bmakeとgmakeの双方で動くMakefileを書くコツ]