Rake って?

Rake ってのは Ruby で書かれた単純なビルド・プログラム。ぶっちゃけいえ
ば make のパクリ。

make って?

『make のパクリ』ってだけじゃピンと来ない? ふーん。そういう人もいるん
だね、世の中。じゃあ、ちょっと説明しとく。実際にブツ見てもらったほうが
早そうだから、いきなり Makefile を見せる:

makefile-1.make

make で使うファイルはフツーは "Makefile" っていう名前にするんだけど。
ここじゃオトナの事情で "makefile-1.make" ってなってる。もし、これが
"Makefile" っていう名前だったら、そのファイルが置かれてるディレクトリ
で "make" ってコマンドを実行すると、"Makefile" の中身が読み込まれるこ
とになってる。今はオトナの事情で "Makefile" じゃないから、わざわざそれ
を教えてやんないといけない。すると:

  $ make -f makefile-1.make
  echo hello
  hello

みたいに表示される。

これだけ見ると、sh に毛が生えた程度なんだけどね。さすがにもうちょっと
ビルドに特化した機能を持ってる。make でピンと来ないくらいだから、『ビ
ルド』もチンプンカンプンだろうね。『ビルド』っていうのは、コンパイルと
かインストールとかドキュメント生成とかそういうことにまつわる事柄ってこ
と。オレも今わかったんだけど、結構曖昧なんだよね。

とはいっても、ビルドの主役はコンパイル。それも C 言語で書かれたソース・
ファイルのコンパイルね。これも例を見てもらったほうが早そう:

makefile-2.make
hello.c
main.c

で、同じように実行してみる:

  $ make -f makefile-2.make
  gcc -Wall   -c -o hello.o hello.c
  gcc -Wall   -c -o main.o main.c
  gcc -o makesample -Wall hello.o main.o

どう? 何かいろいろやってそうでしょ? まぁ、正直、こんなチンケな例じゃ:

  $ gcc -o makesample -Wall hello.c main.c

ってやっちゃったほうが早いんだけどね。そこは、あくまでも例ってヤツだか
ら。

だから、結局のところビルドっていうのは、ソース・ファイルを加工すること
で、make っていうのはその作業を自動化するツールっていうことでいいんじゃ
ない? その加工っていうのはコンパイルが多いんだけど、他にもいろいろあ
ると。

最初の Rake

オレは make 捨てたくなったから rake 使いだしたんだよね。だから、make 
の話はもう終わり。

  # Ant もねぇ、あんまし便利じゃない。やっぱね、XML がね、面倒なのよね。

さて、make が "Makefile" っていう名前のファイルをデフォにしてたみたい
に、rake でも "Rakefile" っていう名前のファイルがデフォになってる。も
ちろん、ここでもオトナの事情は有効なわけです。いきなりその Rakefile を
見てもらう:

rake-1.rake

rake の場合も同じように実行する:

  $ rake -f rake-1.rake
  (in /home/mcd/doc/rake)
  hello, world

くどいようだけど、"Rakefile" っていう名前にしとけば、"rake" ってタイプ
するだけでいい。

ほんじゃ、いよいよ Rakefile の中身の説明に入ってくぜ。1 行目は問題ない
だろ。いわゆる shebang というヤツ。あ、いっとくけど、rake では Ruby の
コメントは有効だから。っていうか、ほとんど Ruby と変わらないと思ってく
れていい。カッチョよくいえば言語内 DSL ってヤツなんだけど、知らなかっ
たらこれは忘れてくれていい。

最初にやんなきゃいけないのは、デフォルトのタスクの指定。まぁ、絶対やん
んなきゃいけないわけじゃないんだけど、これがないと rake を実行するとき
にわざわざタスクを指定しないといけない。そんな面倒はヤだから、ここでやっ
とく。

『タスクってなぁに?』グッドクゥェスチョン! でも、Rakefile 見れば何と
なくわかるでしょ? 上では world っていうタスクが定義されてる。それでもっ
て、その world っていうのがデフォルトのタスクになってる。

で、world タスクの中身はというと、"hello, world" を印字することなわけ。

タスクをわかりやすくするために、タスクをもう 1 つ増やしてみよう:

rake-2.rake

これで、world と universe っていう 2 つのタスクができた。でも、デフォ
のタスクは world のまま。だから:

  $ rake -f rake-2.rake 
  (in /home/mcd/doc/rake)
  hello, world

ってやっても何も変わらないアル。ところがところが、ここで universe を指
定すると、アラ不思議:

  $ rake -f rake-2.rake universe
  (in /home/mcd/doc/rake)
  hello, universe

ホレこのとおり。"hello, universe" と印字されたアル。

これでタスクは理解できたと思う。どんなことかをクチで説明できるよりも、
肌で理解できてればそれで OK よ! しょせんこの世界、コードでしか語れな
いんだから。なんつってな。

アクション!

タスクの中身のことをアクションという。見てのとおり、アクションの中身は 
Ruby のスクリプト。そもそも、Rake 自体が Ruby の言語内 DSL だから、フ
ツーに Ruby のスクリプトが書ける:

rake-3.rake

この柔軟性が Rake のウリでもある。前に『ビルドの定義は曖昧だ』って書い
たけど、そんだけビルドでやんなきゃいけないことは多様だってこと。コンパ
イルだけじゃないし、インストールとか配備 (デプロイ、deployment) とか、
ドキュメントの生成とかもある。そうなると、なかなか『型にハメる』っての
が難しくなってくる。そうなってくると、ビルド・ツールが柔軟でないと使い
にくくなる。そこで Rake は Ruby の柔軟性をできるだけ活かせるようになっ
てるというわけ。

もちろん、柔軟性にはトレードオフがある。Rakefile は Makefile を書くよ
りちょっと手間がかかる。Ruby の知識もあったほうがいいしね。でも、今と
なっては、そうした犠牲を払ってでも柔軟性がほしいということ。アンダスタ
ン? グーッド。

依存関係

ビルドっていうのはソース・ファイルを加工することだと書いたけど。その典
型がコンパイル。コンパイルっていうのは、ソース・ファイルからオブジェク
ト・ファイルを吐き出すよね。これを逆に見れば、オブジェクト・ファイルを
吐き出すためにはソース・ファイルが必要だということになる。つまり、オブ
ジェクト・ファイルはソース・ファイルに依存してるわけだ。

hello の例でいえば、hello を作るには hello.o と main.o がいるわけだ 
(もちろんキミはオトナの事情をわかっているよね?)。で、hello.o を作るに
は hello.c がいるし、main.o を作るには main.c がいるわけだ。

んじゃ、そうした依存関係を Rakefile で実際に書いてみよう:

rake-4.rake

前の Rakefile と比べると変わったところがいくつかあるけど。まず、デフォ
ルト・タスクの指定がシンボルから文字列になったこと。タスクの名前はシン
ボルでも文字列でもいいんだけど、シンボルだと表現できないものもある。た
とえばピリオドを含むファイル名とかね。だから慣例として、ファイルを作る
タスクの場合はタスク名を文字列にする。

で、次にタスクの定義なんだけど:

  task "hello" => ["hello.o", "main.o"]

っていうのがある。この配列の中身が hello を作るのにいるもの、つまり
hello が依存しているものってことになる。この配列の中身は、基本的にはファ
イル名じゃなくってタスク名ね。たまたまタスク名とファイル名がおんなじだ
ということ。

そうすると、hello タスクを実行する前に hello.o と main.o っていうタス
クをやんなきゃなんないってことになる。で、その 2 つが下に書いてある。

hello.o タスクを見ると、単純に hello.c をコンパイルしてるだけ。main.o 
も同様。で、hello.o と main.o っていうファイルが作られる。すると、
hello タスクのとこに戻ってくる。ああ、書き忘れたけど、sh っていうのは、
system 関数とほぼ同じ。ブロック取れたりするみたいだけど。

んで、ブロック引数の "t" はタスク・オブジェクトね。それを文字列の中に
埋め込むともちろん "hello" になる。で、"prerequisites" っていう舌噛ん
じまいそうなのが配列で並べた名前たち。ここでは "hello.o" と "main.o"。
これが文字列の中に埋め込まれると、名前たちが空白でつなげられる。じゃあ、
ちょっと動かしてみよっか:

  $ rake -f rake-4.rake
  (in /home/mcd/doc/rake)
  gcc -c -o hello.o hello.c
  gcc -c -o main.o main.c
  gcc -o hello hello.o main.o

出力をじっくり見れば、何がどう置き換わってるかわかると思う。そんなに難
しくないでしょ?

ファイル・タスク

この前の Rakefile には不満な点がある。それは、rake を呼び出すたんびに
オブジェクト・ファイルを全部作り直しちゃうこと。

  $ rake -f rake-4.rake
  (in /home/mcd/doc/rake)
  gcc -c -o hello.o hello.c
  gcc -c -o main.o main.c
  gcc -o hello hello.o main.o
  $ rake -f rake-4.rake
  (in /home/mcd/doc/rake)
  gcc -c -o hello.o hello.c
  gcc -c -o main.o main.c
  gcc -o hello hello.o main.o

ソース・ファイルいじってないのに、そのオブジェクト・ファイルを作り直し
ちゃう。これだと分割コンパイルしてる意味なくなっちゃう。

もちろん、Rake 作ってる人もそんなことはわかっててね。そういうのにはス
ペシャルなタスクが用意されてる:

rake-5.rake

rake-4.rake と見比べてみると、いくつか違いがある。まず 3 つの task が 
file に変わってる。これがスペサルなタスク、ファイル・タスクと呼ばれる
もの。

で、今度は、hello.o と main.o がそれぞれ hello.c と main.c に依存して
いることをコードに書いてる。でも、hello.c と main.c っていうタスクは見
当たらないよね? こういうとき、Rake はその依存物がファイル名だと見なす
んだねぇ。

じゃ、動かしてみよっか:

  $ rm *.o hello
  $ rake -f rake-5.rake
  (in /home/mcd/doc/rake)
  gcc -c -o hello.o hello.c
  gcc -c -o main.o main.c
  gcc -o hello hello.o main.o
  $ rake -f rake-5.rake
  (in /home/mcd/doc/rake)

ね? 作り直さないでしょ? ここで hello.c をおさわりして (イヤン)、もう
1 回試してみる:

  $ touch hello.c
  $ rake -f rake-5.rake
  (in /home/mcd/doc/rake)
  gcc -c -o hello.o hello.c
  gcc -o hello hello.o main.o

すると、hello.c がコンパイルされ直されて、でも main.c はコンパイルされ
なくて、でも hello は作り直されてる。hello.c はおさわりされたから作り
直されるのは当然。で、それに依存してる hello のほうも作り直される。で
も、main.o はおさわりされてないからシカトされるというわけ。スキンシッ
プ重要! でもセクハラは厳禁!

ファイル・タスクっていうのは、それが依存してるものが変わってなければ何
もしないということ。これさえ押さえときゃ、まず赤点はもらわない。

ルール

マナーからルールへ! というわけで、ルールの話。

もう一度 rake-5.rake を見てもらいましょう:

rake-5.rake

ここで注目すべきは:

  file "hello.o" => "hello.c" do |t|
    sh "gcc -c -o #{t} #{t.prerequisites}"
  end

  file "main.o" => "main.c" do |t|
    sh "gcc -c -o #{t} #{t.prerequisites}"
  end

の部分。これでピンと来たら、あなたも達人の仲間入り!

そう DRY の原則。これをどうにかしたい。そんなときにルールを持ち出すわ
けです:

rake-6.rake

ハイ、消えたぁ (by キンキン)。

あるタスクの依存物がファイル名で、なおかつ、そのファイル名に対応するタ
スクが定義されていないとき、そのファイル名を持つファイルがルールによっ
て作成されます。っていってもよくわかんないか。

『あるタスクの依存物がファイル名で』っていうのは、hello タスクの依存関
係に並んでいる hello.o と main.o のこと。で、『そのファイル名に対応す
るタスクが定義されていない』というのは、hello.o と main.o がタスクとし
て定義されていないということ。実際、今 Rakefile にあるタスクは hello 
だけだよね。で、定義されてなきゃルールが適用されるってわけだ。

で、そのルールというのは、.o という拡張子を持つファイルは、.c という拡
張子を持つファイルから生成されると。で、その生成方法が do...end の中で
書かれてるわけ。とどのつまり、*.c というソース・ファイルから *.o とい
うオブジェクト・ファイルの作り方が書かかれているわけだ。t.source って
いうのがそのソース・ファイルの名前になりそうのはすぐわかるべ。

ルールそのあとに

さて、ルールが理解できたとこで、もうちょっとだけ現実に近いソース・ファ
イルを扱ってみようか。

ここでヘッダ・ファイルに登場してもらう。だから、他のファイルも書き換え
なきゃいけない。その結果が次:

hello2.h
hello2.c
main2.c

見てわかるとおり、hello2.c も main2.c も hello2.h をインクルードしてい
る。言い替えるなら、hello2.c も main2.c も hello2.h に依存してるわけだ。
今度はこれをビルドしてみよう。もちろん、ルールを使ってだ:

rake-7.rake

実質的には下の 2 行が追加された感じ。hello2.o は hello2.c と hello2.h 
とに依存してる。main2.o は main2.c と hello2.h とに依存してる。

実行してみよう:

  $ rake -f rake-7.rake 
  (in /home/mcd/doc/rake)
  gcc -c -o hello2.o hello2.c
  gcc -c -o main2.o main2.c
  gcc -o hello2 hello2.o main2.o

ここで、ルールと依存関係をゴッチャにしないように。あそこで書いたルール
は『.o は .c から生成されて、その生成方法はこうです』ってこと。そのルー
ルとタスクの依存関係は別物。だから、hello2.o のタスクには、実際に依存
するものを書かなきゃいけない。ためしに rake-7.rake を:

  file "hello2.o" => ["hello2.h"]

みたいに書きなおして、hello2.c をおさわりして何が起こるか見てみよう:

  $ touch hello2.c
  $ rake -f rake-7.rake 
  (in /home/mcd/doc/rake)

何も起こらない。前みたく暗黙のタスクだったら、hello2.o と hello2.c と
の依存関係を rake が解決してくれたんだけど、今は hello2.h のために明示
的にタスクを書かなきゃいけなくなった。だから、hello2.o と hello2.c と
の依存関係も書いとかなきゃいけないんだね。その代わり、hello2.o は 
hello2.c から生成されるっていうのは、前もってルールで決めたからもう一
度書く必要はないってわけ。

clean

ババンバ・バン・バン・バン 風呂入れよ! とカトちゃんもいうように、きれ
にすることは大事なことなんだよな。そんなわけで、clean だ。

みんなは良い子だから Ruby をソースからビルドしたことがあると思う。そん
なとき、『いらなくなったオブジェクト・ファイルは make clean で消したほ
うがいいよ』っていわれなかったかなぁ? それとおんなじことを手軽にやっ
ちまおうっていうのが CLEAN なわけだ。早速見てもらおう:

rake-clean.rake

しょっぱなに 'rake/clean' ってのを require してる。で、デフォルト・タ
スクの次に CLEAN.include ウンチャラってのをやってる。見ればやってるこ
とは大体わかるだろう。んじゃ実行してみよう:

  $ rm -f *.o hello
  $ rake -f rake-clean.rake 
  (in /home/mcd/doc/rake)
  gcc -c -o hello.o hello.c
  gcc -c -o main.o main.c
  gcc -o hello hello.o main.o
  $ rake -f rake-clean.rake clean
  (in /home/mcd/doc/rake)
  rm -r hello.o
  rm -r main.o
  rm -r hello

3 番目のコマンド入力に注目。clean っていうタスクを指定してるよね。これ
が clean の効果なわけ。clean を require して、CLEAN に消したいファイル
を include すれば勝手に clean っていうタスクが追加されるわけ。

ちなみに、CLEAN が消すファイル・パターンはデフォルトだと:

  CLEAN = Rae::FileList["**/*~", "**/*.bak", "**/core"]

ってなってるらしい。.bak が消されちゃヤバイときは注意が必要だ。

おまけ

これを書いてて、rake-6.rake と rake-7.rake をいっぺんにビルドできて、
なおかつ clean で全部きれいにしたくなったんだ。それで書いてみたのが:

all.rake

こういうこともできるんだと軽く読み流してほしい。

おわり

そう、これでおしまいなんだ。もちろん、オチもない。あとは各自いろいろと
工夫して使ってほしい。そして、素晴らしい使い方を見つけたら、こっそり教
えてほしい。そんなところ。いざさらば。

2007-01-08
2010-03-09

Copyright © 1906 tko at jitu.org