まだ、関数を使ってソースやターゲットを生成する方法とかパターン規則の説明をしてないので、続きを書くと思います。調べているうちに、GNU Makeの構文(の一部)はある種のプログラミング言語だという気がしてきました;そのことも書きたい気がしてます。
というわけで続きを書きます。
実は、関数呼び出しを使うときは、代入に「=」を使うより「:=」のほうが適切かつ効率的なときが多いのですが、その話は次の機会にします。
これの説明が中心になります。
内容:
●前置き
以下、Make一般ではなくてGNU Makeの話です。GNU Makeより古いMakeにも備わっていた伝統的機能の説明はしません。
GNU MakeのMakefile記述構文は、ある種のプログラミング言語とみなせます。そのことを認識しないと、シッカリしたMakefileは書けないと思います。そこであえて、Makefile記述構文を「プログラミング言語Make」、あるいは「Make言語」と呼びます。プログラミング言語Makeのソースファイルを、実際の名前がどうであれMakefileと呼びます*1。
最後の実例では、Erlangのビルドに使う処理を出しますが、Erlangの知識は一切不要です。次のことだけ知っておけば十分。
Cで考えたい人は、「.erl→.c」「.hrl→.h」「.beam→.o(または.obj)」と置換すればOKです*2。
●変数の種類と変数定義
GNU Makeの変数は2種類(2つのフレーバー)あって、再帰的変数と単純変数と呼ばれています。でも、再帰的変数と単純変数を構文的に区別することはできません。「=」で定義されるか、「:=」で定義されるかの違いです。
これ以上、Makeの概念と用語法で説明してもラチがあかないと思うので、思い切って次のように考えましょう。
- 単純変数とは、普通の変数のこと。
- 再帰的変数とは、実は引数なしの関数のこと。
- 「:=」は、普通の代入。
- 「=」は、実は関数定義。
例えば、
をJavaScriptで書けば、
INCLUDE_DIR := ../include
HEADERS = $(wildcard $(INCLUDE_DIR)/*.hrl) $(wildcard *.hrl)
となります。Lispなら、
var INCLUDE_DIR = "../include";
function HEADERS() {
return wildcard(INCLUDE_DIR + "/*.hrl") + " " + wildcard("*.hrl");
}
ですかね。
(setq INCLUDE_DIR "../include")
(defun HEADERS ()
(concat
(wildcard (concat INCLUDE_DIR "/*.hrl"))
" "
(wildcard "*.hrl")))
以前のMakeには、「=」しかなかったので、すべての変数は引数なし関数のような扱いだったのです。習慣と互換性から、変数定義には今でもたいてい「=」が使われていますが、多くの場合は「:=」のほうが適切かつ効率的です。例えば、右辺が定数($を含まない)なら、「:=」でかまいません(つうか、そのほうがベター)。
●ソースコードの後のほうを参照すること
前節で、JavaScriptとLispによって、Makefileに対応するコードを示しました。JavaScriptとLispでは動作原理が異なるところがあるので、その点に触れておきましょう。
var x = f() + 1;
function f() {
return g() * 2;
}
function g() {
return 3;
}
alert("x=" + x); // OK! x=7
このJavaScriptコードは、問題なく実行できます。JavaScriptはいったんソースファイルを全部見て変数/関数の宣言を処理してから実行に入ります。つまり、次のようにソースを書き換えていると思っていいでしょう。
/* == 宣言部 == */
var x;
function f() {
return g() * 2;
}
function g() {
return 3;
}/* == 実行部 == */
x = f() + 1;
alert("x=" + x);
一方Lispは、上から下へとそのまま実行していくので、まだ定義されてない関数が出現するとエラーになります。
(setq x (+ (f) 1)) ; error 関数fはこの時点で定義されてない.
(defun f () (* (g) 2))
(defun g () 3)
(princ (format "x=%d\n" x))
●Makeは上から下へと実行していくのだ
さてMakeですが、JavaScriptよりLispに似た動作をします。
# file: t7.mk
x := $(f) + 1
f = $(g) * 2
g = 3print_x:
@echo "x='$(x)'"
$ make -f t7.mk
x=' + 1'
Makeでは、すべての変数が空文字列で初期化されている*3のでエラーにはなりませんが、期待した結果ではありません。LispでもMakeでも、変数xへの代入文を最後に持ってくればOKです。
(defun f () (* (g) 2))
(defun g () 3)
(setq x (+ (f) 1))
(princ (format "x=%d\n" x))
# file: t8.mk
f = $(g) * 2
g = 3
x := $(f) + 1print_x:
@echo "x='$(x)'"
$ make -f t8.mk
x='3 * 2 + 1'
あるいは、xを変数ではなくて関数にしてしまうのも手です。関数定義内で未定義関数を使っても平気ですから。
(defun x () (+ (f) 1))
(defun f () (* (g) 2))
(defun g () 3)
(princ (format "x=%d\n" x()))
# file: t9.mk
x = $(f) + 1
f = $(g) * 2
g = 3print_x:
@echo "x='$(x)'"
$ make -f t9.mk
x='3 * 2 + 1'
●MakeとLispは似ている
Make構文の一部は、次のようなプログラミング言語になっています。
- データ型は文字列だけ。
- 文字列を、空白で区切った語(ワード)のリストとして扱うことができる。
- 変数を持ち、「:=」により代入ができる。
- いくつかの組み込み関数を持つ。
引数なしの関数を「=」で定義できる。([追記]引数も使えます。コメント欄参照、コッチも参照。[/追記])- 繰り返し制御構造は、組み込み関数foreachでサポートされる。
まー、ちっちゃな関数型言語といってもいいと思いますよ。リスト処理モドキもできるし。雰囲気はLispに似てます;ストールマンの趣味のような気がするな。
でも、ちょっと奇妙なところともありますよ。JavaScriptでもLispでも、変数と引数なし関数はまったくの別物です。しかしMakeでは、変数と引数なし関数の区別は曖昧で、「=」で定義された引数なし関数(Make用語では再帰的変数)が、単なる変数(Make用語では単純変数)に化けたりします。
# file: t10.mk
INCLUDE_DIR := ../include
HEADERS = $(wildcard $(INCLUDE_DIR)/*.hrl)
HEADERS := $(HEADERS) $(wildcard *.hrl)print_headers:
@echo "HEADERS='$(HEADERS)'"
これは何の問題もなく動きます。実はLispでも同じように書けます。
(setq INCLUDE_DIR "../include")
(defun HEADERS ()
(wildcard (concat INCLUDE_DIR "/*.hrl")))
(setq HEADERS (concat (HEADERS) " " (wildcard "*.hrl")))
しかし、変数参照と関数呼び出しの構文が違うので、変数HEADERSと関数HEADERSの区別は厳密にできます。Makeでは、$(HEADERS) という構文しかありません。まー、いいや、許せる範囲。
●実例
「作業ディレクトリにあるすべてのErlangソースを、変数SOURCESにセットする」という問題を考えましょう。
SOURCES:=$(wildcard *.erl)
でほぼOKなんだけど、一時ファイルまでSOURCESに入るとイヤですよね。一時ファイル名はtmpで始まるという約束にしておけば、filter-out関数でふるいにかけられます。SOURCES:=$(filter-out tmp%,$(wildcard *.erl))
;ここで%は、正規表現「(.*)」に相当するパターン・メタ文字です。
tmpによるネーミング規則から外れるゴミファイルがあるときはどうしましょう。not_sources.mkというファイルに、ゴミを並べておくことにします。
SOURCES:=$(filter-out tmp%,$(wildcard *.erl))
-include not_sources.mk
ifdef NOT_SOURCES
SOURCES:=$(filter-out $(NOT_SOURCES),$(SOURCES))
endif # NOT_SOURCES
もしnot_sources.mkがあれば読み込み、not_sources.mk内で定義されている変数NOT_SOURCESに列挙されたゴミをフィルターアウトします。
さて、作業ディレクトリがゴミばっかりで、ほんとのソースは手で列挙するしか方法がないこともあるでしょう。そのときは、ソースファイル達を露骨に(explicitly :-))書き並べたsources.mkを作ることにします。
-include sources.mk
ifndef SOURCES
SOURCES:=$(filter-out tmp%,$(wildcard *.erl))
-include not_sources.mk
ifdef NOT_SOURCES
SOURCES:=$(filter-out $(NOT_SOURCES),$(SOURCES))
endif # NOT_SOURCES
endif # !SOURCES
その他、なにやらかにやら設定すると、こんなふうになります。
INCLUDE_DIR:=../include
BIN_DIR:=../ebin-include sources.mk
ifndef SOURCES
SOURCES:=$(filter-out tmp%,$(wildcard *.erl))
-include not_sources.mk
ifdef NOT_SOURCES
SOURCES:=$(filter-out $(NOT_SOURCES),$(SOURCES))
endif # NOT_SOURCES
endif # !SOURCES-include headers.mk
ifndef HEADERS
HEADERS:=$(wildcard $(INCLUDE_DIR)/*.hrl) $(wildcard *.hrl)
endif # !HEADERSCOMMON_HEADERS:=$(filter common%,$(HEADERS))
OBJECTS:=$(patsubst %.erl,$(BIN_DIR)/%.beam,$(SOURCES))print_vars:
@echo "SOURCES='$(SOURCES)'"
@echo "HEADERS='$(HEADERS)'"
@echo "COMMON_HEADERS='$(COMMON_HEADERS)'"
@echo "OBJECTS='$(OBJECTS)'"
さー、あなたもMakeプログラミングしてみませんか*4。