Makefile の書き方 (C 言語)

始めに

ここでは, Makefile の中でも GNU make に限って説明する.

makeってなに?

ソースファイルを分割して大規模なプログラムを作成していると, コマンドでコンパイルするのが面倒である. また, 一部のソースファイルを書き換えだけなのに全部をコンパイルし直すのは時間の無駄である.

そんな問題を解決するのが make である. Makefile と呼ばれるテキストファイルに必要なファイルと各ファイルのコンパイルのコマンド, ファイル間の依存関係を記す. そして, “make” というコマンドを実行するだけで, 自動的にコマンドを実行してコンパイルしてくれる. これだけではスクリプトと大差がないのだが, make は Makefile に記された依存関係に基づいて更新されたファイルの内関連のあるものだけを更新することで, コンパイル時間を短くする.

make は特定のプログラミング言語に依存したものではない. C 言語のソースファイルのコンパイルにも使えるし, Verilog-HDL のシミュレーションにも使えるし, Tex のコンパイルにも使える.

make 色々

実は make には色々種類がある. 主なものをあげると以下の通り:

Microsoft nmake (Windows)
Borland make (Windows)
GNU make (windows, UNIX 系)
Solaris make (Solaris)

make で Hello World!

hello.c

1
2
3
4
5
6
7
/* hello.c */
#include <stdio.h>

int main(int argc, char *argv[]){
  printf("Hello C World\n");
  return 0;
}

同じディレクトリに “GNUmakefile” というファイルを作成し, 以下の内容を記述してください

GNUmakefile

# Makefile for hello.c
hello: hello.c
	gcc -Wall -O2 -o hello hello.c

三行目の先頭は空白文字ではなくてタブ文字 (Tab) なので注意してください! 一行目はコメントである.

コマンドから make を実行すると以下のようにコンパイルしてくれる:

[cactus:~/code_c/mkfile/helloworld]% make
gcc -Wall -O2 -o hello hello.c

この後もう一度 make を実行してみよう:

[cactus:~/code_c/mkfile/helloworld]% make
make: `hello' is up to date.

hello.c が更新されていない (hello.c より hello のほうが日付が新しい) のでコンパイルされない. こんな感じで, 必要な部分のみコンパイルしてくれる.

GNUmakefile のファイル名を指定する場合は:

[cactus:~/code_c/mkfile/helloworld]% make -f GNUmakefile
make: `hello' is up to date.

とする. ファイル名を指定しない場合は, “GNUmakefile”, “makefile”, “Makefile” の順に検索する.

Makefile の基本文法: 依存関係行

Makefile の基本的な構文は依存関係を表す依存関係行である. 依存関係行はこんな感じである:

ターゲット名: 依存ファイル名 1 依存ファイル名 2 依存ファイル名 3
              コマンド行 1
              コマンド行 2
              コマンド行 3

ターゲット名は一般的に生成されるファイルのファイル名にする (そうでない場合については後述する).

ターゲット名の後い ”:” を書いて, その後にスペース区切りで依存するファイルのファイル名を記述する. これらのファイルのうちどれか一つでも更新されるとコマンドが実行される.

ターゲット名を指定して make を実行する場合は:

make ターゲット名

とする. ターゲットを省略すると, Makefile の中で先頭のターゲットが実行される.

ターゲット名から始まる行の次の行から実行するコマンドを記述する. コマンドを記述する場合は必ず先頭にタブ文字を入れる必要がある.

例として, C 言語の分割コンパイルをしてみよう. 分割コンパイル用に以下のファイルを用意する.

hello.c

1
2
3
4
5
6
7
8
9
/* hello.c */
#include <stdio.h>

void print(void);

int main(int argc, char *argv[]){
  print();
  return 0;
}

print.c

1
2
3
4
5
6
7
/* print.c */
#include <stdio.h>

void print(void){
  printf("This is a test1.\n");
  printf("This is a test2.\n");
}

Makefile はこんな感じで

Makefile

# Makefile
hello: hello.c print.c
	gcc -Wall -O2 -o hello hello.c print.c

hello.c または print.c のいずれかを更新するとコンパイルしくれる. しかし, このままでは更新されてないファイルもコンパイルし直されてしまうので, 効率よくなるために, すこし変更しよう.

Makefile2

# Makefile2
hello: hello.o print.o
	gcc -Wall -O2 -o hello hello.o print.o

hello.o: hello.c
	gcc -c hello.c

print.o: print.c
	gcc -c print.c

こうして make -f Makefile2 を実行すると,:

[cactus:~/code_c/mkfile/divprint]% make -f Makefile2
gcc -c hello.c
gcc -c print.c
gcc -Wall -O2 -o hello hello.o print.o

となる. ここで, print.c を更新し, make -f Makefile2 を実行すると,:

[cactus:~/code_c/mkfile/divprint]% make -f Makefile2
gcc -c print.c
gcc -Wall -O2 -o hello hello.o print.o

となって, print.o だけが更新されている.

依存関係行の応用その 1

依存関係行を使った応用について説明する. プログラムをコンパイルすると中間ファイルなどができていちいち削除するのが面倒である. そこで, Makefile に以下の行をつけることにする

Makefile

# Makefile

hello: hello.o print.o
	gcc -Wall -O2 -o hello hello.o print.o

hello.o: hello.c
	gcc -c hello.c

print.o: print.c
	gcc -c print.c

clean:
	rm -f hello hello.o print.o

こうしてコマンドで以下のように実行する:

[cactus:~/code_c/mkfile/applied1]% make clean
rm -f hello hello.o print.o

不要なファイルをすべて削除してくれる. “clean” は依存するファイルがなく, clean というファイルを生成するわけでもなく, コマンドを実行である. このようなターゲットを “phony target” と呼ぶ. phony ターゲットを使用する場合, ターゲット名と同じ名前のファイルがあると変なことになる:

[cactus:~/code_c/mkfile/applied1]% touch clean
[cactus:~/code_c/mkfile/applied1]% make clean
make: `clean' is up to date.

これをさけるためには Makefile を以下のように書き換える:

.PHONY: clean
clean:
      rm -f hello hello.o print.o

こうすると clean というファイルが存在していても問題ない.

依存関係行の応用その 2

もう一つの応用は, 複数のプログラムを作成するときに役に立つ. ここでは以下のソースファイルを追加する.

som.c

1
2
3
4
5
6
7
8
9
/* som.c */
#include <stdio.h>

int main(int argc, char *argv[]){
  printf("som test 1\n");
  printf("som test 2\n");

  return 0;
}

そして Makefile を以下のようにする.

Makefile

これで hello と som を作ろうとすると,:

[cactus:~/code_c/mkfile/applied2]% make hello
gcc -c hello.c
gcc -c print.c
gcc -Wall -O2 -o hello hello.o print.o
[cactus:~/code_c/mkfile/applied2]% make som
gcc -c som.c
gcc -Wall -O2 -o som som.o

となり, 面倒である. そこで, ダミーの依存関係を追加する

Makefile

# Makefile

hello: hello.o print.o
	gcc -Wall -O2 -o hello hello.o print.o

hello.o: hello.c
	gcc -c hello.c

print.o: print.c
	gcc -c print.c

clean:
	rm -f hello hello.o print.o

先端に追加した “all” がミソである. これで make を実行すると,:

[cactus:~/code_c/mkfile/applied2]% make
gcc -c hello.c
gcc -c print.c
gcc -Wall -O2 -o hello hello.o print.o
gcc -c som.c
gcc -Wall -O2 -o som som.o

となって, 2つのプログラムを一度に作成することができた.

依存関係行の応用その 3

C 言語ではコンパイルしないけどソースファイルにインクルードされるヘッダファイルが存在する. ヘッダファイルが更新されたときにソースファイルをコンパイルし直すにはどうしたらよいのだろうか?

この問題を解決するには, 同じターゲット名の依存関係行を追加する. 例えば以下のようなファイルを用意する.

str.h

1
2
/* str.h */
char str[] = "This is a test";

main.c

1
2
3
4
5
6
7
8
/* main.c */
#include <stdio.h>
#include "str.h"

int main(int argc, char *argv[]){
  printf("%s\n", str);
  return 0;
}

Makefile

# Makefile
.PHONY: all
all: main

main: main.o
	gcc -o main main.o

main.o: main.c
	gcc -c main.c

.PHONY: clean
clean:
	rm -rf main main.o

make を実行すると,:

[cactus:~/code_c/mkfile/applied3]% make
gcc -c main.c
gcc -o main main.o

となる. ここで “str.h” を書き換える:

/* str.h */
char str[] = "This is another test";

そして, make を実行すると,:

[cactus:~/code_c/mkfile/applied3]% make
make: Nothing to be done for `all'.

といって更新してくれない. そこで, 以下のように “makefile” を書き換える.

Makefile2

# Makefile2
.PHONY: all
all: main

main: main.o
	gcc -o main main.o

main.o: main.c
	gcc -c main.c

main.o: str.h

.PHONY: clean
clean:
	rm -rf main main.o

“main.o: str.h” という行がポイントである. そして, make を実行すると,:

[cactus:~/code_c/mkfile/applied3]% make -f Makefile2
gcc -c main.c
gcc -o main main.o

となり, ちゃんと更新される.

マクロ

ここから少し難しくなる. これまでは Makefile にファイル名やコマンド名を直接書いていた. しかし, マクロを使うと直接書かなくてもすみ, 他への流用などが用意となる. マクロを定義するには以下の用にする:

マクロ名 = 文字列

マクロを参照するには,:

$(マクロ名)
または
${マクロ名}

とする. 実際に使ってみるとこんな感じである.

Makefile

# Makefile

objs = hello.o print.o

hello: $(objs)
	gcc -Wall -O2 -o hello $(objs)

hello.o: hello.c
	gcc -c hello.c

print.o: print.c
	gcc -c print.c

.PHONY: clean
clean:
	rm -f hello $(objs)

ここでは, オブジェクトファイル名を “objs” というマクロとして定義している. “$(objs)” は “hello.o print.o” に置換される.

GNU make では, 定義済みマクロとして以下のものがある.

_images/macro1.png

上記のプログラムの引数用のマクロもある

_images/macro2.png

これらのマクロは再定義可能である. 例えば, こんな感じである.

Makefile2

# Makefile2

OBJS = hello.o print.o
CC = gcc


hello: $(OBJS)
	$(CC) -Wall -O2 -o hello $(OBJS)

hello.o: hello.c
	$(CC) -c hello.c

print.o: print.c
	$(CC) -c print.c

.PHONY: clean
clean:
	$(RM) hello $(objs)

ここでは “CC” というマクロを “gcc” という文字列で再定義している. また, “RM” というマクロをそのまま使用している.

内部マクロ

前述のマクロは単純に文字列に置換するだけだったが, 内部マクロはもう少し複雑になる. 例えば, こんな感じの内部マクロがある.

Makefile

# Makefile2

OBJS = hello.o print.o
CC = gcc

hello: $(OBJS)
	$(CC) -Wall -O2 -o $@ $(OBJS)

hello.o: hello.c
	$(CC) -c $<

print.o: print.c
	$(CC) -c $<

.PHONY: clean
clean:
	$(RM) hello $(objs)

ここでは, “$@” という内部マクロを使用している. これはターゲット名を表すものである. そのため上記の記述は,:

hello: $(OBJS)
       $(CC) -o hello $(OBJS)

と解釈される.

また, 以下のものもある.:

hello.o: hello.c
         $(CC) -c $<

ここでは “$<” という内部マクロを使用している. これは依存ファイルの先頭のファイル名を表すものである. そのため上記の記述は,:

hello.o: hello.c
         $(CC) -c hello.c

と解釈される. 依存ファイル名のリストを表す “$^” という内部マクロもある.

内部マクロをまとめるとこんな感じである.

_images/inmacro.png

マクロと内部マクロを駆使すると, Makefile はこんな感じになる.

Makefile2

# Makefile2

PROGRAM = hello
OBJS = hello.o print.o
CC = gcc
CFLAGS = -Wall -O2

$(PROGRAM): $(OBJS)
	$(CC) -o $(PROGRAM) $^

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

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

.PHONY: clean
clean:
	$(RM) $(PROGRAM) $(OBJS)

サフィックスルール

サフィックスルールとは, ファイル名の拡張子 (サフィックス) ごとにルールを定義するものである. 例えばこんな感じである:

SUFFIXES: .o .c

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

”.SUFFIXE” は依存関係行と同じ形であるが, 意味が違う. サフィックスルールを適用する拡張子のリストを書く.

”.c.o” がサフィックスルールとなっており, 拡張子が ”.o” のファイルは拡張子 ”.c” 変えたファイルに依存していることを表す. 変換はコマンドで表されている.

例えば, ターゲット名が “hoge.o” ならば make はこのサフィックスルールより “hoge.c” に依存していると判断して, コマンドを実行し “hoge.o” を生成する.

サフィックスルールを用いると, こんな感じで書ける.

Makefile3

# Makefile3

# プログラム名とオブジェクトファイル名
PROGRAM = hello
OBJS = hello.o print.o

# 定義済みマクロの再定義
CC = gcc
CFLAGS = -Wall -O2

# サフィックスルール適用対象の拡張子の定義
.SUFFIXES: .c .o

# プライマリターゲット
$(PROGRAM): $(OBJS)
	$(CC) -o $(PROGRAM) $^

# サフィックスルール
.c.o:
	$(CC) $(CFLAGS) -c $<

# ファイル削除用ターゲット
.PHONY: clean
clean:
	$(RM) $(PROGRAM) $(OBJS)

ここまでくると, あとは “PROGRAM” や “OBJS” を書き換えるだけでいくらでも流用ができる. ちなみに, ヘッダファイルの依存関係だけは自分で記述しなければならない. 例えばこんな感じである.

Makefile_other

# Makefile_other

# プログラム名とオブジェクトファイル名
PROGRAM = main
OBJS = main.o

# 定義済みマクロの再定義
CC = gcc
CFLAGS = -Wall -O2

# サフィックスルール適用対象の拡張子の定義
.SUFFIXES: .c .o

# プライマリターゲット
$(PROGRAM): $(OBJS)
	$(CC) -o $(PROGRAM) $^

# サフィックスルール
.c.o:
	$(CC) $(CFLAGS) -c $<

# ファイル削除用ターゲット
.PHONY: clean
clean:
	$(RM) $(PROGRAM) $(OBJS)

# ヘッダファイルの依存関係
main.o: str.h

分割 Makefile

プログラムが複雑になって, ディレクトリごとにソースコードを分けるなどしていくと, 一つの Makefile で管理するのは面倒になってくる. そんな時には, Makefile を分割することができる. 例えば, subdir というサブディレクトリの中に別の Makefile があるとした場合, カレントディレクトリの Makefile で:

subsystem:
      cd subdir && $(MAKE)
または

subsystem:
      $(MAKE) -C subdir

とする.

C 言語のヘッダーファイルの依存関係の自動解決

C 言語でプログラミングしている際に, ソースファイルが増えるとヘッダファイルの依存関係をいちいち記述するのは面倒である. いろいろな解決方法があるみたいであるが, とりあえずこんなん考えてみた.

Makefile_header

# Makefile_header

# プログラム名とオブジェクトファイル名
PROGRAM = main
OBJS = main.o

# 定義済みマクロの再定義
CC = gcc
CFLAGS = -Wall -O2

# サフィックスルール適用対象の拡張子の定義
.SUFFIXES: .c .o

# プライマリターゲット
.PHONY: all
all: depend $(PROGRAM)

# プログラムの生成ルール
$(PROGRAM): $(OBJS)
	$(CC) -o $(PROGRAM) $^

# サフィックスルール
.c.o:
	$(CC) $(CFLAGS) -c $<

# ファイル削除用ターゲット
.PHONY: clean
clean:
	$(RM) $(PROGRAM) $(OBJS) depend.inc

# ヘッダファイルの依存関係
.PHONY: depend
depend: $(OBJS:.o=.c)
	-@ $(RM) depend.inc
	-@ for i in $^; do cpp -MM $$i | sed "s/\ [_a-zA-Z0-9][_a-zA-Z0-9]*\.c//g" >> depend.inc; done

-include depend.inc

gcc のプリプロセッサである cpp と sed を組み合わせている. cpp は 指定したソースファイルの依存関係を make の形式で出力してくれるオプションを持っている. それを使って, 全ソースファイルの依存関係を “depend.inc” に出力し, それをインクルードしている. “make depend” とコマンドを実行すれば OK である. また, “all: depend $(PROGRAM)” とすることで, make する際に毎回 “depend.inc” を作成するようにしている.

その他

GNU make には他にも色々な機能がある. また, make を発展させた, autoconf, automake, libtool, などもある. これは OS 間の差異を吸収するためのツールである.