UUUMエンジニアブログ

UUUMのエンジニアによる技術ブログです

いまさら人に聞けないmake入門

おはこんばんちは!! 尾藤 a.k.a. BTO です。

最近の若い人で Makefile を書く人が増えているそうじゃないですか。 そしたらもう、ウホ、これはオサーンの出番ってなるわけですよ。 僕みたいな老害はこんな場面でしか幅を利かせられないってことで、ええ、やりましたよ make 入門を社内勉強会で。

makeとは

最近はタスクツールとしても使われることの多い make ですが、元々はビルドツールで、ライブラリや実行ファイルのビルドに使われるツールです。 UNIX環境で使われることが多く、調べてみると最初のリリースは 1976年4月で、 Stuart Feldman というベル研の方によって実装されたそうです。 make が登場する以前は make という shell script が使われていたみたいで、make はそれの置き換えの位置付けのようです。

ご存知の通り make は Makefile というファイルにルールを記述していき、その内容に従って処理を実行していきます。

様々な実装

make は歴史が長く、何度も rewrite されているので、様々な実装が存在します。基本的な処理は同じですが、実装によって処理が多少異なります。 主な実装は次の3つぐらいかなと思います。

  • System V make
  • BSD make
  • GNU make

System V の make は恐らくオリジナルの実装に一番近いんじゃないでしょうか。僕は使ったこともないので詳細はわかりません。 BSD系UNIXを使っている方は BSD make を使う機会が多いんじゃないかと思います。

最近のUNIX環境では、GNU make ほぼ一択じゃないでしょうか。機能的にも一番上だし、UNIX環境だとほぼ使えるし。かくいう僕もほとんと GNU make しか使ったことがありません。ちなみに BSD系と言われる Mac OS X の make も GNU make ですね。

% /usr/bin/make -v
GNU Make 3.81
Copyright (C) 2006  Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.

This program built for i386-apple-darwin11.3.0

サンプル

今回の勉強会にあたって、簡単なサンプルプログラムを用意しました。 画面上に "foo" と表示するだけのプログラムです。

// foo.c
#include <stdio.h>
#include "foo.h"
void foo() { printf("foo\n"); }
// foo.h
void foo();
// main.c
#include "foo.h"
int main() { foo(); }

ルール

Makefile には処理をさせたいルールを記述していきます。 ルールは次のようなフォーマットになります

target [target...]: [dependency...]
<Tab>command1
<Tab>command2

target にはこのルールで生成したいファイル名を指定します。 dependency には、このターゲットの生成に依存するファイルを記述します。 その後の行に、そのターゲットを生成するためのコマンドを記述していきます。

注意しないといけないのが、コマンド行の先頭が必ず Tab で始まらないといけないところです。 ここはよくハマるポイントなので、注意しましょう。

やってみる

では、実際に Makefile を作ってやってみましょう!!

foo: foo.o main.o
  cc -o foo foo.o main.o

foo.o: foo.c foo.h
  cc -c -O0 -o foo.o foo.c

main.o: main.c foo.h
  cc -c -O0 -o main.o main.c

clean:
  rm -f foo *.o

このような Makefile で実際に実行してみます。

% make foo
cc -c -O0 -o foo.o foo.c
cc -c -O0 -o main.o main.c
cc -o foo foo.o main.o
% ./foo
foo

うまくコンパイルできました。 実際の流れは次のようになります。

  • foo ターゲットが指定される
  • 依存ファイル foo.o main.o が存在しない
  • foo.o main.o をターゲットにする
  • foo.o を生成するコマンドを実行
  • main.o を生成するコマンドを実行
  • foo を生成するコマンドを実行

それぞれのルールには、それぞれのファイルを生成するためのコマンドだけが書かれています。そうするだけで、make がターゲットと依存ファイルを再帰的に辿っていって、必要なコマンドを実行してくれます。

ファイルを一部だけ更新してみる

例えば foo.c だけを更新してみるとどうなるでしょうか。

% touch foo.c
% make
cc -c -O0 -o foo.o foo.c
cc -o foo foo.o main.o

foo.c がコンパイルされ、main.c はコンパイルされませんでした。 make はターゲットと依存ファイルのタイムスタンプを比較して、依存ファイルが新しい場合にだけ、コマンドを実行してくれます。 こうすることで、必要な処理だけが実行されるようになるため、効率よくビルドすることができるようになります。

変数

Makefile では変数が使えます。 変数は variable = value... の書式で代入し、 $(variable) で参照できます。 変数は複数の値を入れられますが、正確にはスペース区切りの文字列が格納されているだけです。 配列ではないので、添字で参照するようなことはできません。

make の変数は遅延評価されます。 変数は定義したときには評価されず、参照するときに毎回評価されます。 変数を即時評価したときは := を使います。 遅延評価で困ることはあまりありませんが、知らないと変な挙動に惑わされることになるので、ちゃんと覚えておきましょう。

また変数の値は、 make varibale=value のようにして、コマンドラインで上書きできます。 後から動的に変更したいところを予め変数にしておくと、コマンドラインで簡単に値が変更できて便利です。

変数を使って Makefile を書き換えると、こんな感じになります。

CC = cc
CFLAGS = -O0
objects = foo.o main.o
headers = foo.h
output = foo

$(output): $(objects)
  $(CC) -o foo $(objects)

foo.o: foo.c $(headers)
  $(CC) -c $(CFLAGS) -o foo.o foo.c

main.o: main.c $(headers)
  $(CC) -c $(CFLAGS) -o main.o main.c

clean:
  rm -f $(output) $(objects)

コマンドラインから変数を書き換えて実行してみます。

% make -f Makefile.variables CFLAGS=-O1 CC=gcc
gcc -c -O1 -o foo.o foo.c
gcc -c -O1 -o main.o main.c
gcc -o foo foo.o main.o

最適化オプションを -O1 に、ccgcc に変更して実行してみました。 ちゃんとコマンドが変更されているのが確認できます。

自動変数

変数には自動で値が設定される自動変数があります。 自動変数を使うと、実行するコマンドがより汎用的に記述できるようになります。

自動変数はいろいろありますが、次の3つぐらいを覚えれば大丈夫でしょう。

  • $@: ターゲット
  • $<: 最初の依存ファイル
  • $?: 依存ファイル全て

自動変数を使った Makefile は次のようになります。

CC = cc
CFLAGS = -O0
objects = foo.o main.o
headers = foo.h
output = foo

$(output): $(objects)
  $(CC) -o $@ $?

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

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

clean:
  rm -f $(output) $(objects)

foo.o main.o を生成するコマンドが全く同じ内容になりました。 foo を生成するためのコマンドも、ターゲットや依存ファイルに依存しない汎用的な記述になっています。 実際に実行してみましょう。

% make -f Makefile.auto
cc -c -O0 -o foo.o foo.c
cc -c -O0 -o main.o main.c
cc -o foo foo.o main.o

問題なく動作しています。

Suffix Rule(古い)

foo.o main.o を生成しているルールでやっていることは同じです。 しかし、今のままではファイルが増えるたびに似たようなルールを追加していかなくてはなりません。 このような問題を解決するために Suffix Rule という機能が追加されました。

Suffix Rule は今となっては次に紹介する Pattern Rule に置き換わってしまいましたが、未だに使われていることもあるので紹介します。 新規で Makefile を書く場合は Pattern Rule を使うようにしましょう。

今回の Makefile だと、次のような記述に変更します。

CC = cc
CFLAGS = -O0
objects = foo.o main.o
headers = foo.h
output = foo

$(output): $(objects)
  $(CC) -o $@ $?

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

clean:
  rm -f $(output) $(objects)

.SUFFIXES という特殊ターゲットを使って、対象となる suffix を設定します。 Suffix Rule のターゲットには、 依存ファイルのsuffix ターゲットのsuffix の順番でスペースを入れずに続け文字で指定します。 実際に実行してみましょう。

% make -f Makefile.suffix
cc -c -O0 -o foo.o foo.c
cc -c -O0 -o main.o main.c
cc -o foo foo.o main.o

問題なく実行できました。

実は Suffix Rule には重要な問題点があります。 Suffix Rule では依存ファイルを指定することができません。 Makefile をみればわかりますが、Suffix Rule の場合は foo.h への依存関係がありません。 そのため foo.h が更新されてもコンパイルは実行されません。

% touch foo.h
% make -f Makefile.suffix
make: `foo' is up to date.

これは Pattern Rule を使うと解決します。

Pattern Rule

ルールのターゲットや依存ファイルに % を入れることができます。 % は任意の文字列にマッチします。 この % を含んだルールのことを Pattern Rule と呼びます。

先程の Suffix Rule は Pattern Rule で置き換えることが可能ですし、もっと柔軟な指定ができるようになります。 Suffix Rule ではできなかった依存ファイルの指定もできます。 なので、Suffix Rule ではなくて Pattern Rule を使ってルールを記述するようにしましょう。

Pattern Rule を使った Makefile は次のようになります。

CC = cc
CFLAGS = -O0
objects = foo.o main.o
headers = foo.h
output = foo

$(output): $(objects)
  $(CC) -o $@ $?

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

clean:
  rm -f $(output) $(objects)

foo.omain.o が Pattern Rule によって一つのルールにまとめられました。 また Suffix Rule の時に問題になった foo.h への依存関係もちゃんと記述できています。 実際に実行してみましょう。

% make -f Makefile.pattern
cc -c -O0 -o foo.o foo.c
cc -c -O0 -o main.o main.c
cc -o foo foo.o main.o
% touch foo.h
% make -f Makefile.pattern
cc -c -O0 -o foo.o foo.c
cc -c -O0 -o main.o main.c
cc -o foo foo.o main.o

問題なく実行できています。 foo.h の更新にも対応しています。

Phony Target

Makefile の中にビルドしたファイルを削除する clean ターゲットが設定されています。 もし clean というファイルが存在した場合の挙動はどうなるでしょうか。

% make clean
rm -f foo *.o
% touch clean
% make clean
make: `clean' is up to date.

make はターゲットファイルを生成するというのが基本的な考え方なので、ターゲットファイルが存在する場合は当然処理を実行してくれません。 しかし、 clean はファイルを削除するためにタスクとして定義しているものなので、ファイルの存在の有無に関わらず常に実行して欲しいものです。

このようなファイルを生成しないとターゲットを指定するのが Phony Target です。 .PHONY という特殊なターゲットで Phony Target を指定します。 Phony Target に指定されたターゲットは、ファイルの存在の有無に関わらず、常に処理が実行されるようになります。

Phony Target を指定した Makefile がこちらです。

CC = cc
CFLAGS = -O0
objects = foo.o main.o
headers = foo.h
output = foo

$(output): $(objects)
  $(CC) -o $@ $?

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

.PHONY: clean
clean:
  rm -f $(output) $(objects)

実行してみます。

% make -f Makefile.phony clean
rm -f foo foo.o main.o
% touch clean
% make -f Makefile.phony clean
rm -f foo foo.o main.o

clean というファイルが存在しても、ちゃんと処理が実行されました。

clean だとファイルが存在することはほとんどないので、大丈夫だと思いますが、 test というディレクトリを作成して make test が実行できないという経験をしたことがある人は多いんじゃないでしょうか。 Phony Target をちゃんと指定しておけば、そういうトラブルが起きることもなくなります。

まとめ

富士山山頂から見たご来光はきれいでした!!!!

f:id:masatobito:20170912184525j:plain


www.wantedly.com