Pylone Blog - 2011年12月

再帰実行しないmakefile

この記事ではmakeの再帰呼びだしに頼らないmakefileを書くためのtipsを紹介します。 社内勉強会向けに作成した資料をベースとして、公開用に再構成しました。

ある程度の規模のプロジェクトのビルドを効率化したり、 LinuxカーネルやAndroid、OpenJDKなどで使われている(kbuild)ビルド機構を理解するのに役立つでしょう。 記述はGNU Makeの拡張機能が使えることを前提としていますが、BSD系のmakeでも類似の技法は機能するはずです。

Recursive Make Considered Harmful

過去にはよく使われたmakefileの書きかたとして、makefileのルールから別のmakeを (recursiveに) 呼びだす、というものがありました。 そのようなmakeの使い方は

  • なぜかビルドにとても時間がかかる
  • ときどきファイルが正しく再生成されない

といった問題を起こすのでやめるべきといわれて10年以上たちますが、いまだにそのような構成のプロジェクトはよく見られます。 makefileを適当に動くものを雛形にして作るなら、できるだけ内容を理解して適切なコピー元を選びましょう。

サンプルプロジェクト

以降では例としてソースファイルの構成が

- (toplevel)
  - src/
      main.c
  - parser/
      parser.grammar

ビルド手順は

  1. parser.grammarからパーサジェネレータ(lemon)でparser.cとparser.hを生成
  2. parser.cをコンパイルしてparser.oを生成
  3. parser.hとmain.cからmain.oを生成
  4. main.oとparser.oをリンクして実行ファイルappを生成

という場合を想定したmakefile群の書き方を考えます。

分割しない場合

今回の例のサイズであれば、単一のmakefileに全ての依存関係を書く:

all: app

src/main.o: src/main.c parser/parser.h

parser/parser.o: parser/parser.c
parser/parser.c parser/parser.h: parser/parser.grammar
        lemon $<

app: src/main.o parser/parser.o
        $(CC) $(LDFLAGS) -o $@ $+

ことも十分可能です。

しかし、現実的なサイズのプロジェクトを単一のmakefileでビルドするのは難しいでしょう。

再帰バージョン

旧い方法に従うと、

  1. サブディレクトリのためのmakefile群 (parser/Makefile, src/Makefile) を用意する
  2. トップレベルのMakefileのルールとして、サブディレクトリでmakeを実行する

ということになります。

トップレベルのMakefile

parser/、src/それぞれのディレクトリでmakeコマンドを起動することになります。

#Makefile
SUBDIRS := parser src
all: $(SUBDIRS) app

$(SUBDIRS):
        $(MAKE) -C $@

app: src/main.o parser/parser.o
        $(CC) $(LDFLAGS) -o $@ $+

.PHONY: all $(SUBDIRS)
サブディレクトリのmakefile

関連するファイルの生成規則だけを含むように分割すると

#parser/Makefile
parser.o: parser.c
parser.h parser.c: parser.grammar
        lemon $<
#src/Makefile
main.o: main.c ../parser/parser.h

となるでしょう。

再帰的makeの問題点

とりあえず使える程度の動作はしますが、

  • 必要がなくても下位makeプロセスが起動されるので遅い
  • makeに平行処理を許した (たとえば make -j3) 場合はビルドに失敗することがある

といった問題を解決するのは困難です。

非再帰バージョン (include directive)

GNU Makeの、makefile内に別のファイルを読み込む拡張機能(include directive)を使えばmakefileの一部を別のファイルへと分割可能です。

この例の場合、src/、parser/以下の依存関係を

#src/build.mk
src/main.o: src/main.o parser/parser.h

および

#parser/build.mk
parser/parser.o: parser/parser.c
parser/parser.h parser/parser.c: parser/parser.grammar
        lemon $<

として抜きだして、トップレベルのMakefileからそれぞれを読みこむ

#Makefile
all: main

main: src/main.o parser/parser.o
        $(CC) $(LDFLAGS) -o $@ $+

include src/build.mk
include parser/build.mk

ようにできます。

非再帰バージョン (下位へのパラメータ渡し)

前の例ではincludeされるファイル内で明示的にディレクトリ名 (src/、parser/) を都度書いていますが、もしこれを修正することになるとかなりの手間がかかります。 makeの変数を使ってincludeする側から渡すようにして、ディレクトリ名に依存してしまう要素を減らしましょう。

makeの変数RELに対象ディレクトリ名が格納されるようにすれば、(ディレクトリ名を$(REL)に置きかえることで) src/build.mkを

$(REL)main.o: $(REL)main.c parser/parser.h

parser/build.mk を

$(REL)parser.o: $(REL)parser.c
$(REL)parser.c $(REL)parser.h: $(REL)parser.grammar
        lemon $<

のように書きなおすことができます。

この場合、トップレベルのmakefile側でincludeする前にRELを設定しておく

all: app

REL := src/
include $(REL)build.mk

REL := parser/
include $(REL)build.mk

app: src/main.o parser/parser.o
        $(CC) $(LDFLAGS) -o $@ $+

ことになります。 なお、includeされるファイル毎に値を変えたいRELはsimply-expanded な (:=を用いた) 変数にしないと、最後の定義によって全ての$(REL)の値が上書きされてしまうことに注意してください。

非再帰バージョン (上位へのパラメータ渡し)

関連するオブジェクトファイルが多くなる場合、すべてを手作業で列挙するのは危険なので、includeされる側でファイルのリストを構築した方がよいでしょう。

トップレベルのmakefileを(simply-expandedな)変数OBJSを利用するように:

all: app
OBJS :=

REL := src/
include $(REL)build.mk

REL := parser/
include $(REL)build.mk

app: $(OBJS)
        $(CC) $(LDFLAGS) -o $@ $+

書いておくと、OBJSはmakefile全体で共有されるため、 src/build.mk、parser/build.mk内でOBJSへ加えた変更がトップレベルの "app" のルール処理に反映されます。

src/build.mk、parser/build.mk 内では、

# src/build.mk
OBJS += $(REL)main.o
# parser/build.mk
OBJS += $(REL)parser.o

のようにして、必要なオブジェクトファイルを順に追加していけばよいでしょう。

ただし、includeされる側で行なう操作はリストへの追加だけにするべきです。リスト自体を上書きしてしまう (「+=」ではなく「:=」を使う)

OBJS := $(REL)foo.o

と、グローバルなOBJSの内容は破壊されます。 下位のmakefileの作成者が信用できないなら、OBJSへの操作を (includeされる側には委ねないで) 上位側に移した方がよいかもしれません。

非再帰バージョン (自動include)

下位ディレクトリが多数存在し、それぞれに対する個々のincludeをトップレベルに書きたくない場合、GNU Make自身に等価な記述を生成させることができます。

下位のmakefileの名前をbuild.mkとしたとき、build.mkが存在する下位ディレクトリのリストは

$(dir $(wildcard */build.mk))

で得られます。これと

  • 引数を与えると
          REL := [引数のディレクトリ名]
          include $(REL)build.mk
    を生成するmakeのユーザー定義関数
  • 組み込み関数foreachによるリスト要素への関数適用

を組み合わせれば、下位ディレクトリのbuild.mkを自動的にincludeするmakefileを

all: app

SUBDIRS := $(dir $(wildcard */build.mk))
OBJS :=

define include_fragment
 REL:= $(1)
 include $(1)build.mk
endef

$(foreach i, $(SUBDIRS), \
        $(eval $(call include_fragment, $(i))))

app: $(OBJS)
        $(CC) $(LDFLAGS) -o $@ $+

のように書くことができます。

こうしておけば、下位ディレクトリを追加した際にはbuild.mkを置くだけでトップレベルのmakefileから自動的に読みこまれます。 あとからmakefileを解読するのは難しくなってしまいますが、目的によっては便利に使えるのではないでしょうか。

ターゲットごとに変数値を設定

includeを使ってmakefileを合成する場合、makeの変数は全体で共有されます。 しかし、一部のターゲットについてだけCFLAGS、LDFLAGSといった変数の値を変更したい場合もあるでしょう。

幸いGNU Makeには

ターゲット : 変数定義

とすることで、ターゲットごとの変数値を設定する機能 (target-specific variable value) が存在します。

たとえば main.o の生成規則を

$(REL)main.o: $(REL)main.c parser/parser.h

として定義していたとき、

$(REL)main.o: CFLAGS += -I parser/
$(REL)main.o: $(REL)main.c parser/parser.h

のように定義を追加することで、このルールを実行する時だけ CFLAGS に「-I parser/」が追加されます。

この機能はincludeディレクティブによって展開された時点の変数の値を保存しておくのにも使えるので、makefile自体のデバッグにも役立ちます。たとえば

$(REL)parser.o: LOCAL_REL := $(REL)
$(REL)parser.o: $(REL)parser.c
        @echo "LOCAL_REL=" $(LOCAL_REL)
        $(CC) $(CFLAGS) -c -o $@ $<

とすれば、上書きされてしまう前にRELを保存しておいて確認することもできるでしょう。

まとめ

新しく作るmakefileでは「$(MAKE) -C」を止めて「include」を使いましょう。

makefileを書くとき、makeの出力に大量の「Entering directory」「Leaving directory」(残念な環境では「入りますディレクトリ」) といったメッセージが含まれるようなプロジェクトを参考にするのは避けるべきです。

年末年始休業のお知らせ

誠に勝手ではございますが、株式会社パイロンは2011年12月27日から2012年1月5日の間を休業とさせていただきます。ご迷惑をおかけいたしますが、よろしくお願いいたします。