このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

Makefileの書き方:プログラミング言語Make

「Makefileの書き方、その勘どころ」にて:

まだ、関数を使ってソースやターゲットを生成する方法とかパターン規則の説明をしてないので、続きを書くと思います。調べているうちに、GNU Makeの構文(の一部)はある種のプログラミング言語だという気がしてきました;そのことも書きたい気がしてます。

というわけで続きを書きます。

実は、関数呼び出しを使うときは、代入に「=」を使うより「:=」のほうが適切かつ効率的なときが多いのですが、その話は次の機会にします。

これの説明が中心になります。

内容:

  1. 前置き
  2. 変数の種類と変数定義
  3. ソースコードの後のほうを参照すること
  4. Makeは上から下へと実行していくのだ
  5. MakeとLispは似ている
  6. 実例

●前置き

以下、Make一般ではなくてGNU Makeの話です。GNU Makeより古いMakeにも備わっていた伝統的機能の説明はしません。

GNU MakeのMakefile記述構文は、ある種のプログラミング言語とみなせます。そのことを認識しないと、シッカリしたMakefileは書けないと思います。そこであえて、Makefile記述構文を「プログラミング言語Make」、あるいは「Make言語」と呼びます。プログラミング言語Makeのソースファイルを、実際の名前がどうであれMakefileと呼びます*1

最後の実例では、Erlangのビルドに使う処理を出しますが、Erlangの知識は一切不要です。次のことだけ知っておけば十分。

  • Erlangソースファイルの拡張子は「.erl」
  • Erlangヘッダーファイルの拡張子は「.hrl」
  • Erlangの実行可能ファイルの拡張子は「.beam」

Cで考えたい人は、「.erl→.c」「.hrl→.h」「.beam→.o(または.obj)」と置換すればOKです*2

●変数の種類と変数定義

GNU Makeの変数は2種類(2つのフレーバー)あって、再帰的変数単純変数と呼ばれています。でも、再帰的変数と単純変数を構文的に区別することはできません。「=」で定義されるか、「:=」で定義されるかの違いです。

これ以上、Makeの概念と用語法で説明してもラチがあかないと思うので、思い切って次のように考えましょう。

  1. 単純変数とは、普通の変数のこと。
  2. 再帰的変数とは、実は引数なしの関数のこと。
  3. 「:=」は、普通の代入。
  4. 「=」は、実は関数定義

例えば、


INCLUDE_DIR := ../include
HEADERS = $(wildcard $(INCLUDE_DIR)/*.hrl) $(wildcard *.hrl)
JavaScriptで書けば、


var INCLUDE_DIR = "../include";
function HEADERS() {
return wildcard(INCLUDE_DIR + "/*.hrl") + " " + wildcard("*.hrl");
}
となります。Lispなら、

(setq INCLUDE_DIR "../include")
(defun HEADERS ()
(concat
(wildcard (concat INCLUDE_DIR "/*.hrl"))
" "
(wildcard "*.hrl")))
ですかね。

以前のMakeには、「=」しかなかったので、すべての変数は引数なし関数のような扱いだったのです。習慣と互換性から、変数定義には今でもたいてい「=」が使われていますが、多くの場合は「:=」のほうが適切かつ効率的です。例えば、右辺が定数($を含まない)なら、「:=」でかまいません(つうか、そのほうがベター)。

ソースコードの後のほうを参照すること

前節で、JavaScriptLispによって、Makefileに対応するコードを示しました。JavaScriptLispでは動作原理が異なるところがあるので、その点に触れておきましょう。


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 = 3

print_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) + 1

print_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 = 3

print_x:
@echo "x='$(x)'"


$ make -f t9.mk
x='3 * 2 + 1'

●MakeとLispは似ている

Make構文の一部は、次のようなプログラミング言語になっています。

  1. データ型は文字列だけ。
  2. 文字列を、空白で区切った語(ワード)のリストとして扱うことができる。
  3. 変数を持ち、「:=」により代入ができる。
  4. いくつかの組み込み関数を持つ。
  5. 引数なしの関数を「=」で定義できる。([追記]引数も使えます。コメント欄参照、コッチも参照。[/追記]
  6. 繰り返し制御構造は、組み込み関数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 # !HEADERS

COMMON_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

*1:makefileとかmake fileとか呼んだほうがいいかも。

*2:Erlangの.beamファイルは、.soや.dllにより近いですけどね。

*3:[追記]参照された値は空文字列となりますが、origin関数を使えば、変数が未定義なのか、定義されているが値が空文字列に設定されているかの区別がつきます。[/追記]

*4:って、「Makefileなんて、もう一生書かねーよ、バーロー」と思っていた僕が言うのもなんですが。