kubell Creator's Note

ビジネスチャット「Chatwork」のエンジニアのブログです。

ビジネスチャット「Chatwork」のエンジニアのブログです。

読者になる

makeの話(基本編)

こんにちは、あらい(@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.cbar.c で同じビルドコマンド使うならルールを共通化してDRYにしたいです。

このような課題を解決するのが「ビルドツール」≒作成手順を自動化するプログラムであり、make コマンドはその代表的なものです。 makeは古くからあるコマンド*2で、 ほぼ全てのUNIX・Linux系OSに最初から入っている*3のが特長です。

基本的な使い方

makeでは「どんなルールで何を作るか」という指示を Makefile というファイルに書き、make はこれに従ってコマンドを実行してくれます。 まずは Makefile の書き方を見てみましょう。これはshell scriptに似ているけれど、ちょっと違う記法で書きます。

# シャープでコメントが書ける

# shell scriptと違って = の前後にスペースがあって良い
変数  =  変数の中身

作りたいファイル: 材料になるファイル1 材料2 材料3
        ファイルを作るコマンド $(変数)

ポイントは下記3つのものの関係を定義するところです。この関係を ルール と呼びます。

  1. 作りたいファイル。これをmakeの用語で ターゲット と呼びます。
  2. それを作る元になるファイル。なくてもいいし複数あってもいい。makeの用語では 依存関係 または 依存 と呼びます。
  3. 作るコマンド。普通は依存のファイルを使ってターゲットファイルを作るコマンドになります。 記法として、 コマンドのインデントは必ずタブ文字でなければならない という規則があるので注意しましょう。

先ほどの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は知らない。くらいの人を想定

*2:Wikipediaによれば1977年に作られたそうです

*3:POSIXという規格に含まれています