kubell Creator's Note

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

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

読者になる

makeはいいぞ(実践編)

こんにちは、あらいです。年の瀬ですが3月に書いた記事の続きです。

前回は基礎として、

  • makeは「作りたいファイル、材料になるファイル、作るコマンド」をルールとして定義できること
  • ファイルの更新時間をみてコマンドを実行すること
  • 複数のルールを連鎖できること

などを書きました。 今回は、web開発の現場でmakeにどのような使い道があるかを書きます。

タスクランナーとしての用法

コマンドがファイルを作らない場合

前回の例で、コマンドが最終的に作りたいはずのファイルを作らなかったケースを考えます。

# helloを作るべきところ、typoしてhaloを作るコマンドになっている
hello: hello.c
  cc $< -o halo

make コマンドを実行してみます。

$ make
cc hello.c -o halo
$ make
cc hello.c -o halo
$ make
cc hello.c -o halo

ターゲットのhelloがないと判定され、何度でも cc が動いてしまいます。 (ゆえに、基本的には自動変数 $@ などの使用が推奨されます)

それってオレオレサブコマンドにできるのでは?

この仕組みを使って 実際にはファイルを作ったりしないけど面倒臭いコマンドに名前を付けると便利なのでは? という発想が生まれます。

hello: hello.c
  cc $< -o $@

# 生成されたバイナリを消したい
clean:
        rm -f hello

こう定義することで、 $ make clean というコマンドでrmコマンドを実行することができます。

$ make clean
rm -f hello

今時のCLIツールっぽくサブコマンドが作れそうですね。

フォニーターゲットのお作法

オレオレコマンド用法はmakeの使われ方として一般的であり、正式には フォニーターゲット (Phony Target:偽りのターゲット)と呼ばれます。

ここで1つ問題があります。 もし偶然フォニーターゲットと同名のファイルが存在してしまうと、 ファイルの更新状態判定が誤作動してしまうのです。

$ touch clean # "clean" というファイルを作る
$ make clean
make: `clean' is up to date.

cleanターゲットの処理を実行してほしいのに、ローカルの clean ファイルが邪魔をしています。 これを防ぐため、makeには .PHONY という特殊なターゲットと仕組みが用意してあります。

hello: hello.c
  cc $< -o $@

clean:
        rm -f hello

# cleanは実際にファイルがあろうがなかろうが関係ないことを示す。
# 関係だけ記述して、コマンドは省略する。
.PHONY: clean

これによりcleanターゲットが動作するようになります。

$ ls -l
total 40
-rw-r--r--  1 cw-arai  wheel    66 Dec 26 19:42 Makefile
-rw-r--r--  1 cw-arai  wheel     0 Dec 26 19:40 clean
-rwxr-xr-x  1 cw-arai  wheel  8432 Dec 26 19:44 hello
-rw-r--r--  1 cw-arai  wheel    75 Dec 26 19:18 hello.c
$ make clean
rm -f hello

PHP開発者のためのMakefileの実例

前置きが長くなりました。私が開発で使っているのはC言語ではなくPHPです。

PHPはコンパイルしてバイナリを作る必要はありませんが、 モダンなPHP開発において $ composer install のようなビルドプロセスはもはや当たり前です。 ここではPHPの開発プロジェクトでのタスクを make で自動化してみましょう。

想定する状況

こんな状況を想定します。

  • composerで各種ライブラリを管理している
    • composerの実体としてpharファイルを使う
  • phpdotenvを使って環境変数を読み込む
    • 実際にアプリケーションが読むファイル .env はgitignoreされ、リポジトリに含まれない
    • 開発用のファイルが .env.example としてコミットされている

ここでは作業開始時に

  • composer自体のインストール
  • composer install コマンドの実行
  • .env.exampleを.envとしてコピーする

が必要です。 また、次のことを適宜やる必要があります。

  • composer.jsonが更新されたらcomposer install を再実行する
  • .env.exampleが更新されたら.envにコピーし直す

これをmakeで自動化します。

具体例

次のようなMakefileを書き、composer.jsonと同じくプロジェクトのルートに置きます。

# PHP開発用のMakefileサンプル

COMPOSER_CMD := ./composer.phar

build: .env vendor
.PHONY: build

# composerのインストール
./composer.phar:
  curl 'https://getcomposer.org/installer' | php

# composer installコマンド
vendor: composer.json $(COMPOSER_CMD)
  $(COMPOSER_CMD) install

# .envの初期化
.env: .env.example
  cp $< $@

使用方法

Makefileの置いてあるプロジェクトのルートで make を実行します。

cw-arai@cw-arai-MBP:/tmp/example-project [0]
$ make
cp .env.example .env
curl 'https://getcomposer.org/installer' | php
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  267k  100  267k    0     0   125k      0  0:00:02  0:00:02 --:--:--  125k
All settings correct for using Composer
Downloading...

Composer (version 1.9.1) successfully installed to: /private/tmp/example-project/composer.phar
Use it: php composer.phar

./composer.phar install
Loading composer repositories with package information
Installing dependencies (including require-dev) from lock file
Package operations: 50 installs, 0 updates, 0 removals
  - Installing aura/di (2.2.4): Loading from cache
  - Installing mtdowling/jmespath.php (2.4.0): Loading from cache

()

  - Installing phpunit/phpunit (4.8.36): Loading from cache
Generating autoload files
cw-arai@cw-arai-MBP:/tmp/example-project [0]
$ 

Makefileの最初のターゲット「build」が実行され、.envのコピー、composer自体のインストール、composer installコマンドが実行されました。

この状態でもう一度 make をしても、何もやることないよ、と判定してくれます。

cw-arai@cw-arai-MBP:/tmp/example-project [0]
$ make
make: Nothing to be done for `build'.

git pullしたら .env.examplecomposer.json が更新されてました。もう一度 make します。

cw-arai@cw-arai-MBP:/tmp/example-project [0]
$ make
cp .env.example .env
./composer.phar install
Loading composer repositories with package information
Installing dependencies (including require-dev) from lock file
Package operations: 1 install, 7 updates, 2 removals
  - Removing symfony/stopwatch (v3.4.8)
  - Removing fabpot/php-cs-fixer (v1.13.3)

()

.envのコピー、composer installだけが実行されました。

ちなみに別にインストールされたcomposerの実行ファイルを使いたい場合は COMPOSER_CMD を上書きして make COMPOSER_CMD=/path/to/composer のようにできます。

cw-arai@cw-arai-MBP:/tmp/example-project [0]
$ rm -rf vendor/ composer.phar  # composer.pharもない状態とする

cw-arai@cw-arai-MBP:/tmp/example-project [0]
$ which composer
/Users/cw-arai/work/bin/composer  # ここにインストールされてるものを使いたい

cw-arai@cw-arai-MBP:/tmp/example-project [0]
$ make COMPOSER_CMD=/Users/cw-arai/work/bin/composer
/Users/cw-arai/work/bin/composer install
Loading composer repositories with package information
Installing dependencies (including require-dev) from lock file
Package operations: 49 installs, 0 updates, 0 removals
  - Installing aura/di (2.2.4): Loading from cache
  - Installing mtdowling/jmespath.php (2.4.0): Loading 

()

というわけで、何も考えずとりあえず make しておけば作業環境が整うようになりました。

他のタスクランナーとの比較

composer自体にはカスタムコマンドを定義する機能があります。 これを使うと前述のcleanに相当するタスクをcomposer.jsonに定義することができます。 これとタスクランナーとしてのmakeを比較してみます。

makeのいいところ

  1. ファイルの更新時刻の判定の応用範囲が広い。ファイルがなければ作る、更新があったら新しくする、ということが簡単。
  2. Unix系環境にはだいたい入っており、パッケージ管理システムと独立しているので、composer自体などエコシステム自体のインストールができる。 つまりbrewやcomposerのインストール手順を自動化できる。
  3. 散らばったスクリプトを1つにまとめられる。

最後のやつを補足します。 ちょっと複雑なコマンド、忘れがちなコマンドを書いたshell scriptがリポジトリの中に散らばっていませんか? 深いディレクトリの中にある.shは忘れられがちですが、Makefileだと複数の目的のスクリプトを1つのファイルにまとめられます。 見つけやすいしメンテもしやすくなります。 またコメントを書けるので、README.mdを読んでコマンドをコピペするのではなく、 実行可能なドキュメントとすることができます。これが私の一番気に入ってるところです。

逆にcomposerのいいところ

  • platform中立で、非Unix系環境(Windows)でも動きそうです*1。 makeの利点はインストール不要で最初から入っていること、およびshellとの親和性にあるので、そうでない環境の考慮が必要だと嬉しみが半減します。
  • 言語親和性。例えばcomposer scriptでは pre-dependencies-solving など細かいタイミングで実行制御が可能です*2。 またシェルコマンドだけでなく、PHPの関数・メソッドの単位で実行することができます。これはmakeでは実現できません。

ケースバイケースですが、composer scriptsではこういった特徴を活かすことができます。

小技

最後に小技をいくつか紹介します。

helpの自動生成

次のようなターゲットをMakefileに追加します。

#
### ヘルプを表示する
#
# 行頭が###で始まる行を説明行、記号以外で始まるターゲットをタスクとして
# コマンド一覧とヘルプメッセージを表示する
#
.PHONY: help
help:
   @cat Makefile | awk -F: '/^###/{desc=substr($$0,4)} /^[a-zA-Z_-]+:/{print $$1,"\t",desc; desc=""}'

コメントにある通り、Makefileのコメントから make help の結果を自動的に作ってくれます。 実行可能なドキュメント化がさらに捗るのでオススメです。

shebangを利用したオリジナルコマンド化

Laravelの artisan をカッコイイと思ったことはありませんか? プロジェクトでオリジナルなコマンドがあるとちょっとワクワクします。 それ、makeでできます。

  1. #!/usr/bin/make -f をMakefileの1行目に追加
  2. $ chmod +x Makefile
  3. $ mv Makefile martisan

これでオリジナルの martisan コマンドが完成です。 $ ./martisan build のような要領で実行できます。 $ ./martisan deploy でデプロイされるような仕組みも作れそうですね。

まとめ

makeは古く枯れたツールですが今なお有効に使うことができます。 コンテナやInfrastructure as Codeの進歩によってコマンドで操作できることは増え続けており、 shellと親和性の高いmakeは自動化&ドキュメント化にうってつけのツールだと言えるでしょう。

また、言語を超えた大統一ビルドツールのポテンシャルを持っていると思います。PHPならComposer、Scalaならsbt、frontendならnpmといったエコシステムの違いを乗り越え、「リポジトリをcloneしてとりあえず make したら何かができる」「その詳細は実行可能なドキュメントとして書いてある」というDeveloper Experienceを実現できます。

MacやLinuxなどのUnix系環境では今すぐ使い始めることができるので、ぜひ試してみてください。 良いお年を。

*1:手元にWindowsマシンがないので確認できてませんが

*2:https://getcomposer.org/doc/articles/scripts.md#event-names