こんにちは、あらい(@araitta)です。低依存好きです。
makeというコマンドがあります。
自分はちょいとしたコマンドをMakefileに書いておくことが多いのですが、この前「Makefileの読み方が分からん」と言われる事例がありました。 そこで今日はmakeを知らない人向けに*1、 makeがどんなものか、どんな使い方ができるか紹介してみようと思います。
make is 何
makeはビルドツールの1つです。
例えばC言語で開発する時、 *.c
のファイルを書いて cc
コマンドで実行バイナリを作ります。
$ cat > hello.c #include <stdio.h> int main() { puts("Hello, world!"); return 0; } ^D $ cc hello.c -o hello $ ls -l total 32 -rwxr-xr-x 1 cw-arai wheel 8432 Dec 26 17:51 hello -rw-r--r-- 1 cw-arai wheel 77 Dec 26 17:47 hello.c $ ./hello Hello, world!
ファイルが hello.c
1ヶならまだマシだけど、5ヶくらいになるとコマンド手打ちはめんどいですね。
というか -o hello
みたいなオプションを覚えてられません。
コマンドをshell scriptにしても良いけど、 foo.c
と bar.c
で同じビルドコマンド使うならルールを共通化してDRYにしたいです。
このような課題を解決するのが「ビルドツール」≒作成手順を自動化するプログラムであり、make
コマンドはその代表的なものです。
makeは古くからあるコマンド*2で、
ほぼ全てのUNIX・Linux系OSに最初から入っている*3のが特長です。
基本的な使い方
makeでは「どんなルールで何を作るか」という指示を Makefile
というファイルに書き、make
はこれに従ってコマンドを実行してくれます。
まずは Makefile
の書き方を見てみましょう。これはshell scriptに似ているけれど、ちょっと違う記法で書きます。
# シャープでコメントが書ける # shell scriptと違って = の前後にスペースがあって良い 変数 = 変数の中身 作りたいファイル: 材料になるファイル1 材料2 材料3 ファイルを作るコマンド $(変数)
ポイントは下記3つのものの関係を定義するところです。この関係を ルール と呼びます。
- 作りたいファイル。これをmakeの用語で ターゲット と呼びます。
- それを作る元になるファイル。なくてもいいし複数あってもいい。makeの用語では 依存関係 または 依存 と呼びます。
- 作るコマンド。普通は依存のファイルを使ってターゲットファイルを作るコマンドになります。 記法として、 コマンドのインデントは必ずタブ文字でなければならない という規則があるので注意しましょう。
先ほどのC言語の例をMakefileにしてみると、このようになります。
# helloを作るMakefile hello: hello.c cc hello.c -o hello
雰囲気が掴めたでしょうか。
ただこれでは「hello」の繰り返しがちょっと冗長な気がしますね。
make
には様々な省略記法(自動的に定義される変数)があり、これと同じ意味をこのように書けます。
# $@ はターゲットを示す自動変数 # $< は依存関係を示す自動変数 hello: hello.c cc $< -o $@
make
コマンドは、カレントディレクトリに Makefile
という名前のファイルがあると自動的に読み取ってコマンドを実行します。
$ vi Makefile $ ls -l total 16 -rw-r--r-- 1 cw-arai wheel 30 Dec 26 18:47 Makefile -rw-r--r-- 1 cw-arai wheel 77 Dec 26 17:47 hello.c $ make cc hello.c -o hello $ ls -l total 40 -rw-r--r-- 1 cw-arai wheel 30 Dec 26 18:47 Makefile -rwxr-xr-x 1 cw-arai wheel 8432 Dec 26 18:47 hello -rw-r--r-- 1 cw-arai wheel 77 Dec 26 17:47 hello.c $ ./hello Hello, world!
make
コマンドを実行すると cc hello.c -o hello
がエコーバックされ、コマンドが実行されファイルが生成されました。
期待通りですね。
make
の省略記法はもっとたくさんあり、より少ない記述でルールを作ることもできるのですが、ここでは省略します。
便利な特徴
基本的な使い方が分かったところで一歩次に進みましょう。
ファイルの更新状態判定
makeはターゲットと依存ファイルの更新時刻を自動的に判定します。
ターゲットのファイルが存在して、かつ依存ファイルより新しい時、ビルドする必要がないと判断してコマンド実行をスキップしてくれます。
先ほどの例の続きでもう一度 make
コマンドを実行してみましょう。
$ make make: `hello' is up to date.
helloファイルは作成済みなので cc
コマンドは実行されずにスキップされました。
一瞬で終わるコマンドならともかく、時間のかかるコンパイルなどでは非常に嬉しいですね。
ここでのポイントは「ファイルの更新時刻」が判断の元になることです。
なので hello.c
の時間を更新してやるともう一度 cc
コマンドが実行されることになります。
$ touch hello.c $ make cc hello.c -o hello
複数のルール
1つのMakefileには複数のルールを定義できます。
# helloとhowareyouの定義を書いたMakefile hello: hello.c cc $< -o $@ howareyou: howareyou.c cc $< -o $@
make
コマンドは第1引数にターゲット名を受け取ります。
上記の定義を使って howareyou
を作りたい場合、 $ make howareyou
というコマンドを実行すればOKです。
ターゲット名が省略された場合は、最初に定義されたターゲット(この場合は hello
)が対象となります。
ルールの連鎖
ビルドの内容によっては中間生成物が発生する場合もあります。
C言語での開発でいうと複数の *.o
ファイルを作ってリンクするようなケースですね。
このようなケースでは、最終的に作りたいターゲットの依存ファイルを別のターゲットとすることで
ルールを連鎖させられます。
すなわち、Aを作るにはBが必要、Bを作るにはCが必要、Cを作るにはDが... と定義できます。 ここでAを作るルールを実行すると、makeは依存関係を解決してD→C→B→Aを作る処理を実行してくれます。
C言語からちょっと趣向を変えて、 「JSONファイルをダウンロードしてCSVファイルを作らないといけない」 という状況を考えてみましょう。
JSONを返すAPIの例としてお天気Webサービスを参照させていただきます。 これを使うと、私の住んでいる東京の天気は次の要領で取得できます。
$ curl http://weather.livedoor.com/forecast/webservice/json/v1?city=130010
ここで取得できるJSONデータから、直近の天気をCSVにしたいとしましょう。 これはjqでこのようなクエリを書くと実現できます。
$ curl 'http://weather.livedoor.com/forecast/webservice/json/v1?city=130010' | jq -r '.forecasts[]|[.dateLabel,.date,.telop]|@csv' % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 9419 0 9419 0 0 212k 0 --:--:-- --:--:-- --:--:-- 213k "今日","2019-03-06","曇のち雨" "明日","2019-03-07","曇時々雨" "明後日","2019-03-08","晴時々曇"
このワンライナーコマンドは実行するたびにAPIにアクセスしてしまいます。 アクセス回数を抑えるため、curlの結果を中間ファイルとして取っておくことにしましょう。
ということで、ここでmakeの出番です。
API_URL = "http://weather.livedoor.com/forecast/webservice/json/v1" # 東京を示すコード AREA_TOKYO = 130010 TARGET_AREA = $(AREA_TOKYO) JSON_FILE = $(TARGET_AREA).json CSV_FILE = $(TARGET_AREA).csv $(CSV_FILE) : $(JSON_FILE) cat $< | jq -r '.forecasts[]|[.dateLabel,.date,.telop]|@csv' > $@ $(JSON_FILE): curl $(API_URL)?city=$(TARGET_AREA) > $@
複雑になりました。変数が5つ、ルールが2つ定義されています。意味はなんとなくわかるでしょうか? CSV_FILEの依存としてJSON_FILEが定義されているところに注目してください。 これを実行します。
$ make curl "http://weather.livedoor.com/forecast/webservice/json/v1"?city=130010 > 130010.json % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 9419 0 9419 0 0 215k 0 --:--:-- --:--:-- --:--:-- 219k cat 130010.json | jq -r '.forecasts[]|[.dateLabel,.date,.telop]|@csv' > 130010.csv $ ls -l total 40 -rw-r--r-- 1 cw-arai wheel 114 Mar 6 20:40 130010.csv -rw-r--r-- 1 cw-arai wheel 9419 Mar 6 20:40 130010.json -rw-r--r-- 1 cw-arai wheel 337 Mar 6 20:40 Makefile $ cat 130010.csv "今日","2019-03-06","曇のち雨" "明日","2019-03-07","曇時々雨" "明後日","2019-03-08","晴時々曇"
APIを叩いてCSVファイルが出来上がりました。 APIの結果が中間ファイルとしてJSONファイルをローカルに保存しているので、CSVファイルを消して再実行するとAPIにアクセスせずにCSVを再生成できます。
$ rm *.csv $ make cat 130010.json | jq -r '.forecasts[]|[.dateLabel,.date,.telop]|@csv' > 130010.csv
パラメタの注入
前掲のMakefileではAREA_TOKYOとTARGET_AREAの変数定義を分けていました。 なぜか? というと、実行時にパラメタを与えて動作を変えることができるからです。 次の構文で実行してみます。
# 270000は大阪を示すコード値 $ make TARGET_AREA=270000 curl "http://weather.livedoor.com/forecast/webservice/json/v1"?city=270000 > 270000.json % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 6305 0 6305 0 0 152k 0 --:--:-- --:--:-- --:--:-- 153k cat 270000.json | jq -r '.forecasts[]|[.dateLabel,.date,.telop]|@csv' > 270000.csv $ ls -l total 32 -rw-r--r-- 1 cw-arai wheel 105 Mar 6 21:00 270000.csv -rw-r--r-- 1 cw-arai wheel 6305 Mar 6 21:00 270000.json -rw-r--r-- 1 cw-arai wheel 370 Mar 6 20:58 Makefile $ cat 270000.csv "今日","2019-03-06","雨" "明日","2019-03-07","曇時々雨" "明後日","2019-03-08","晴時々曇"
ルールを変えずに大阪のCSVファイルを生成することができました。
make TARGET_AREA=...
というコマンドも「TARGET_AREAが...のものを作る」と自然に読めて理解しやすいです。
このように差し替え可能な部分は適切な変数としておくことで、より分かりやすく柔軟で便利なルールになります。
まとめ
ちょいと長くなりましたので、続きは応用編として後日に回そうと思います。 簡単なまとめです。
- makeは汎用的に使えるビルドツールの1つです。
- *nix系OSであれば最初から使えます。低依存です。
- makeというコマンド名がイカしてます。
ではまた。
*1:対象読者としては、shell scriptはちょっと分かるけどmakeは知らない。くらいの人を想定