RakeRakeRubyによるビルドツールRakeの覚え書き。興に乗ったので詳しく書いてみた。Weirich氏が亡くなられました。URLRakeとは?Rakeは、MakeをRubyで実装したものを超越したビルドツールである。Antが有名で、よく使われている。Rakeは、これらのいいとこ取りをした上で、特有のフィーチャーを追加した新しいビルドツールであり、複雑なビルドを柔軟に書きこなすことができる。その秘密は内部DSLという仕組みにあり、このおかげでビルドの記述にRubyの強力な文法をそのまま使うことができる。この自由度の高さは、ビルドの記述に独自の言語の使用を選択したMakeとAntには無い強みだ。その代わりと言ってはなんだが、Rubyにある程度習熟していないと扱えないツールである。インストールRuby入れて、rubygems入れて、下記のコマンドを実行。--remote「さっぱりわからん!」という人は、とりあえずRubyの学習から始めていただきたい。重ねて言うが、Rubyでプログラムを書けない人はRakeを扱えない。単純なRakefileとりあえず簡単な例を見てみよう。中身がわからずとも、雰囲気を感じ取ってもらえればよい。Rakeを使用するためには、まずRakefileという名称のファイルを作成し、その中にビルド定義を記述する。rakeはRakefileを解析し、その内容に従ってビルドを進めるのである。下記の例は、C言語で書かれたソースコードをビルドする、極単純な例である。message.c"end何をしたいのかは、見た目からなんとなく読み取れるのではないだろうか。ビルドツールで定義するのは、実施するタスク(作業)と、タスク間の依存関係である。上記の例では、以下のようなことを定義している。タスクを指定しなければ、デフォルトで"hello"を作成する。ファイル"hello"を作成するには、ファイル"hello.o"とファイル"message.o"が必要である。message.oのコマンドで作成できる。ファイル"hello.o"を作成するには、ファイル"hello.c"が必要である。hello.cのコマンドで作成できる。ファイル"message.o"を作成するには、ファイル"message.c"が必要である。message.cのコマンドで作成できる。ここで定義された依存関係を追いかけて、必要なタスクを芋づる式に引きずり出して順次実行していくのである。"hello"を作るために"hello.o"が必要だと書いてあるので、先にそいつの作成から始めるわけだ。Rakefileを置いたディレクトリで、rakeコマンドを実行するのだ。message.o$単にrakeとだけ打つと、カレントディレクトリ中のrakefileまたはRakefileファイルを探し出して読み込み、そこに記述されたデフォルトタスクを実行する。ビルド定義ファイルは先頭を大文字にする習慣がある(そうしておくと、lsしたときに先頭付近に来るのでわかりやすい)ので、倣っておいたほうがよい。このドキュメントも、Rakefileで通すことにする。差分ビルドもしてくれる。message.cだけ変更してみると・・・。message.o$hello.oのビルドが含まれていない点に注目。変更したファイルに関係する箇所のみビルドし直している。以降は、Rakefileの中身をもう少し詳細に掘り下げる。高度な使い方についても触れる。タスクRakefileはタスクとタスク間の依存関係を記述するファイルである。よって、まず覚えるべきはタスクの定義方法であろう。タスクは、下記のように定義する。タスク名という書式でタスクを定義する。タスク名は文字列でもシンボルでも良い。タスクに依存関係を書きたければ、=>の先に依存するタスクを書く。依存先が複数ある場合は、配列として渡せばよい。distタスクを実行させてみる。実行するタスクは、rakeの引数として与えればよい。タスクの実行順序を見るために-tオプションを付加する。distの順で実施されている。基本的にタスク名は自由につけることができるが、"default"タスクは予約されている。実行タスクが指定されない場合、rakeは"default"と名付けられたタスクを実行するのである。先ほどのRakefileに"default"タスクを追加してみる。"compile"このファイルに対して、タスクを指定せずにrakeを実行してみる。default$真っ先に"default"タスクが解決されているのがわかるだろうか?そして、"default"タスクが依存している"dist"タスクが解決され、あとは同じ流れだ。ここまでのところ、タスクを定義するは良いが、なにもさせていない。タスクは実際に作業させてナンボである。タスクの作業内容(アクションという)はブロックとして与える。下記に例を示すが、あくまで例なので、簡単にメッセージを表示するだけにしておく。--"endRubyでは、ブロックの定義にはdo-endを使う方法と、{}を使う方法がある。taskに与える場合は必ずdo-endを用いること。その理由は後ほど説明する。今度はメッセージ表示させているので、-tを付けずとも動きが分かる。--$各タスクが、指定したアクションを、しかるべき順番で実施しているのがわかる。ファイルタスクビルドにおいて実行されるタスクは多岐に及ぶ。ディレクトリを作る、ファイルをFTPで送り込む、設定ファイルの検証をする、などなどである。頻繁に行われるのは、「なにかしらのファイルを作成する」というタスクであろう。ビルドタスクとしては代表的な「コンパイル」は、ソースコードからオブジェクトファイルや実行可能ファイルを作成することが目的である。Rakeでは、ファイル生成に特化したタスクが用意されている。これをファイルタスクという。定義の仕方はtaskと大差ない。ここでは、冒頭で用いた単純なRakefileを再び例として挙げる。ファイルパスという形で定義されているものが、ファイルタスクである。ここで与えるファイルパスは、パスであると同時にタスクの名称でもある。タスクではなく、あえてファイルタスクを用いる理由、それは差分ビルドの実現にある。タスクとして定義された場合、そのタスクは必ず実行される。言えば、いまそれをする必要が無くとも、定義されている以上実施してしまう。ファイルタスクの場合、必要なときだけタスクを実施するのである。具体的には、定義されたファイルを作ろうとする際、まず目的ファイルが作成済みかどうかを調べる。作成済みであれば、続けて依存先のファイルの更新日時を調べ、それが目的ファイルよりも新しくなっている場合のみ、変更があったと見なしてビルド作業を実施するのだ。これは大変な時間の節約になる。この例はすでに挙げた。ところで、このRakefileは実際に使えるが、欠点がある。message.c"end同じファイル名を何度も書いている。これはDRY原則に反する、醜い書き方だ。同じ事はなるべく一度しか書かないようにすべきだ。そもそも、作成したいファイルや依存するファイルはすでにファイルタスクに定義しているのだから、タスクに直接問い合わせればよいのだ。そこで、ブロック引数を追加する。#{t.prerequisites[0]}"endブロック引数tには、ファイルタスクそのものが渡される。これでtから必要な情報を取り出して、コマンドに渡すことができる。ファイルタスク、正確にはRake::FileTaskクラスにはいくつかのメソッドが定義されているが、よく使いそうなのは、次のものである。nameタスクの名称。ファイルタスクの場合、ファイルパス。prerequisites依存先であるタスク名の配列。"message.o"]である。いちおう、実行してみよう。message.o$期待通りの動作だ!ちなみに、ブロック引数はtaskでも使用可能である。実際のところタスクでブロック引数を必要とするケースは少ないので、ファイルタスクの説明として記載した。hello.cからhello.oを作るのも、message.cからmessage.oを作るのも、まったく同じ構造をしている。これらもまとめて定義したいところだ。これを実現するには、ルールの記述を覚えなければならない。ルールさて、このRakefileを見ていただきたい。#{t.prerequisites[0]}"endソースファイルとオブジェクトファイルの関係を一つ一つ定義している。これはまだ良い、所詮は例にすぎず、ソースファイルの数もたった2個と少ないから、こうして書いていられる。ソースファイルの数が100やら1000になったらどうするのか?とてもじゃないが、全てのファイルについてファイルタスクを定義することなどできない。そこで、個々のファイルの関連を見るのではなく、全体で共通するルールを見いだすことを考える。XXX.cである」というルールの存在が見て取れる。Rakeでは、上記のようなルールを定義できるようになっている。#{t.source}"endruleで始まっているタスクが、ルールである。この例では、「拡張子が'o'であるファイルを作るために、拡張子'c'のファイルが必要」といったことを定義している。sourceメソッドを使うことで、依存するファイルパスを参照することができる(prerequisitesではない!)。ここでは使用していないが、t.nameも有効である。この例では、"hello"を作ろうとして"hello.o"が必要なことがわかる。"hello.o"が存在しないので作りたいが、"hello.o"のファイルタスクが無い!?しかし、*.oの作り方を定義したルールがある。これによると、"hello.c"があれば"hello.o"を作れそうだ。いった流れで処理が進む。message.oも同様。ここで説明したのはruleの最も簡単な使い方である。実際に開発で使おうと思えば、正規表現やProcを併用した高度な扱い方を知っておかなければならない。ディレクトリ作成ビルド中に、ディレクトリを作成したくなることはよくある。作成したパッケージの置き場所を別に作りたい、といった場合である。思いつくのは、そうしたタスクを作ることである。true}endこうしておけば、パッケージを作成する前に必ずディレクトリが作成される。Rakeにはこれを行うための機能が備わっているので、こちらを使った方がスマートだ。ディレクトリパスという形で定義する。しておくと、そのディレクトリが必要とされた時点で勝手にディレクトリが作られる。./srcdist$Rakefile中で特にディレクトリを作れと指示していないが、必要になった(依存先になっている)ため勝手に作っている。深くネストしたディレクトリも一回で作ってくれる。次のようなdirectoryを設定しても・・・。"./dist/ver1.0/src"これは次のように記述したに等しい。ruleと似た書き方をするが、違っている点がある。directoryにはブロックを追加できない。次のような書き方はできない。t.nameendただし、後からファイルタスクとして追加することで、期待した定義を行うことが可能。t.nameendこの場合、ディレクトリ作成後、そこにhoge.txtをコピーする、という動作になる。つまるところ、初期化処理である。mkdirといったファイル操作メソッドは、FileUtilsモジュールのものである。Rakeでは、これにいくらかの拡張を施した上でトップレベルにincludeしてある。よって、モジュール名を指定せずとも関数のように呼び出せる。ファイルに対して何かしたい場合は、FileUtilsのドキュメントを参照して適切なメソッドを探すと良い。タスクの説明すぐれたエンジニアは、自分の作ったスクリプトや設定ファイルにわかりやすい説明を残すものである。Rakeでは、自分で定義したタスクの説明文を定義することができる。書き方は実に簡単である。-Tで見ることができる。Initialize$ファイルリストAntにfilesetというタスクがあるのをご存じだろうか?「srcディレクトリ以下にある全てのファイル、でも.svnと.bakは除く」といった複雑なファイル群の定義を柔軟に行うための機構で、非常に便利である。Rakeにもこれ相当の機能が用意されている。FileListである。#{t.source}"endこのRakefileは、ソースファイルがhello.cとmessage.cしかない(ヘッダファイルはあるかもしれないが・・・)という前提で書かれている。この書き方では、将来ソースファイルが一つ増えるごとにRakefileも修正しなければならない。そこで、コンパイル対象を「このディレクトリ以下にある全ての.cファイル」としたい。FileListを使えば、こうした定義が可能だ。FileListは次のように定義する。FileList["**/*.c"]FileListには、扱いたいファイルのパターンを与える。アスタリスク(*)は、「全ての」といった意味合いがある。UNIXやコマンドプロンプトのコンソールでの扱いと同じだ。"hello"ファイルタスクの依存先は*.cではなく*.oである。よって、用があるのは*.oの方だ。そこで、拡張子のみを変換した別のFileListが必要になるのだが、そのためのメソッドもちゃんとある。SRCS.ext('o')ちなみに、いきなりFileList["**/*.o"]といった定義をしても駄目だ。*.oはこれから作るのだから、まだ存在していない。それはさておき、これをタスクの依存先とすれば、問題解決である。#{t.source}"end今後はFileListが勝手に*.cファイルを見つけてコンパイル対象にしてくれる。複雑なパターンを設定したい場合は、後からどんどん追加できる。includeおよびexcludeメソッドを使うと良い。FileListにFileListを追加することもできる。除外パターンも書ける。イテレータを使えば、include/exclude共に一度に定義することもできる。FileList.newは、FileList[]の別名。こちらの記法でないとブロックを渡せない。f.exclude("**/.svn")endFileListの中身は、eachメソッドで取り出すことができる。次のように書けばFileListが捕捉したファイルの一覧を表示できる。endendクリーニングRakeは差分ビルドできるよう作られているが、これまで作成したファイルを一度全て削除して、まっさらな状態からビルドしたくなることがままある。依存関係というのは、ときにファイルの更新日時だけでは量れない複雑さを持つことがあるのだ。よって、ビルドに使っているのがMakeだろうがAntだろうが、生成したファイル群を削除するためのタスクを用意しておくものである。Rakeでは、このありがちなタスクをすでに用意している。次のように書くと良い。#{t.source}"endCLEANとCLOBBERに注目。rake/cleanをrequireすることで、自動的に追加されるFileListである。これらに登録したファイルがクリーン実行時の削除対象となる。-Tでタスクの説明を見てみると、cleanとclobberが追加されているのが分かる。file.$実際に実行してみる。hello$これで生成されたファイルは綺麗サッパリ消えた。cleanは、CLEANファイルリストに定義されているファイルを削除する。clobberは、CLEANとCLOBBERに定義されているファイルを削除する。clobberのほうがより徹底的に掃除を行う。両者の使い分けは、タスクの説明にある通りである。cleanは一時ファイルの削除に使用し、clobberは生成された全てのファイルの削除に使用する。先ほどの例ではCLEANにはリンクするときに使用するだけの一時ファイルであるオブジェクトファイルを追加し、CLOBBERには最終生成ファイルである実行可能ファイルを含めているのだ。CLEANとCLOBBERの中身は自由に設定できるのでこの規則に従わなくとも良いが、反抗してみせたところで何も嬉しいことはないので、倣っておいたほうがよい。パッケージ作成ソースファイルにしろ、バイナリファイルにしろ、それを配布する際は1ファイルにまとめてしまうものだ。それはzipかもしれないし、tar.gzかもしれない。これまたありがちなタスクである。clean同様、Rakeにはパッケージを作成するためのタスクが用意されている。Rake::PackageTaskだ。使い方は、cleanによく似ている。trueend作りたいパッケージの定義を書くだけでよい。newの第一引数はパッケージ名、第二引数はバージョン番号である。定義できるフィールドには、下記のものがある。package_dirパッケージの配置先ディレクトリ。作業ディレクトリと言っても良い。このディレクトリにパッケージング対象ファイルが集められ、作成したパッケージが配置される。デフォルト値は"pkg"。package_filesパッケージに含めるファイルを設定するFileList。need_zipzip形式のパッケージを作りたければtrueにする。need_tartgz形式のパッケージを作りたければtrueにする。need_tar_gztar.gz形式のパッケージを作りたければtrueにする。need_tar_bz2tar.bz2形式のパッケージを作りたければtrueにする。nameパッケージ名。versionバージョン番号。上記の例では、./pkgディレクトリ中に、lib/ディレクトリ以下全てのファイルをまとめてパッケージを作ることになる。ファイル形式はtar.gzおよびtar.bz2である。ファイル名はパッケージ名-バージョン番号.拡張子となる。この場合、mylibs-1.0.0.tar.gzと、mylibs-1.0.0.tar.bz2である。need_XXXフィールドは複数同時にtrueに設定でき、trueにした形式のパッケージが全て作成される。-Tすると、例によってタスクが定義されているのがわかる。files$packageタスクを呼び出せば、パッケージが生成される。mylibs-1.0.0.tar.gz$clobber_packageタスクは、パッケージおよびパッケージ生成用の一時ファイルを全て削除する。このタスクはclobberタスクの依存先に追加登録される。clobberタスクを実行すると、clobber_packageタスクも一緒に実行される。repackageは、パッケージを作り直す(clobber_packageして、packageする)。-Tには出てこないが、ファイルタスクとしてパッケージのパスが追加されている。次のようにrakeを実行すれば、tar.bz2だけ作成される(もっとも、追加されるのはneed_XXXをtrueとしたパッケージのみである)。./pkg/mylibs-1.0.0.tar.bz2PackageTaskは必要な数だけ定義できるので、複数バージョンのパッケージを作れるようにしたければ、それだけの数PackageTaskを定義すればよい。タスクの動的生成Rakeの柔軟性を象徴するテクニックの一つとして、タスクの動的生成がある。無意味だが単純な例を出そう。次のようなRakefileを書いてみる。no.5$ループ文を使って、任意数のタスクを作った。task_3$これはMakeやAntでは容易にマネできない機能である。taskはあくまでメソッドに過ぎないから、つまりRakeが内部DSLを採用しているからこそできる芸当だ。これができると何が嬉しいのか、という話になるのだが、とりあえず設定ファイルから動的にタスクを生成する、という技がそこそこ役に立っている。ビルドした実行ファイルを適切なサーバに配備しなければならないが、これがテスト用、本番用など複数あるとする。いう場合、配備先サーバの情報は設定ファイルとして外に出してしまいたくなる。そこで、設定ファイルlogin.yamlを作ってみた。フォーマットはCSVでもXMLでもよかったのだが、ここではYAMLを使用している。192.168.0.11このファイルを読み込んで、配備タスクを生成するようRakefileを書く。本当はFTPなりSCPなりを使ってファイル転送する処理を書く。test1設定ファイルで定義したサーバ分、ファイル配備タスクができている。タスクは実際に実行できる。marvin/xxxxxx@192.168.0.10$配備先サーバが増えた場合でも、login.yamlファイルに追記するだけでよい。test2$テストタスク近年のソフトウェア開発におけるベストプラクティスの一つとして、テストの自動化がある。Rubyによる開発でもこれは奨励されており、単体テスト用ライブラリTest::Unitが標準添付されている。Rakeには、これを使用したユニットテストを実行するためのタスクが用意されている。Test::Unitの使い方はこのドキュメントの範囲を超えるので言及しないが、とりあえず簡単なテストクラスおよびテスト対象ファイルを用意する。文字ごとの出現回数を集計する複雑な(笑)メソッド。count_char.rbのテストクラス。endend最小であれば、Rakefileの記述内容はこれだけでよい。-Tしてみると、タスクが増えている。tests$もちろん、実行できる。errorsエラー無し。デフォルトではtest/test*.rbのパターンに合致する全てのテストクラスのテストが実施される。特定のテストクラスを選んで実行したい場合は、実行時にオプションを渡す。errors$この例のRakefileの定義が単純極まりないのは、ファイル名やファイル配置をTestTaskのデフォルト値に合わせているからである。自分の環境がそれに合わない場合は、定義を追加してカスタマイズしなければならない。trueendTestTask.newの第一引数はタスク名である。デフォルトではtestになっている。ブロック内では追加でプロパティ設定を行っている。設定可能な値は以下である。libs$LOAD_PATHに追加するディレクトリを設定する。テスト実行時に参照しなければならないライブラリがあるならば、それを配置しているディレクトリのパスをここに渡す。デフォルトではlibである。loader使用するテストローダを設定する。設定できる値は次の三種類である。:rakeRakeのテストローダ。:testrbRuby標準のテストローダ。:directコマンドラインのローダ。基本的にデフォルトのまま使えばよい。nameこのタスクの名前。TestTask.newの引数と同様の意味を持つ。optionsテストローダに渡すオプションを定義できる。patternテストファイルを示すパターン。デフォルトではtest/test*.rb。ruby_optsテスト実行時にRubyインタープリタに渡すオプションの配列。verboseテスト実行時の出力を詳細化(冗長化)させたい場合はtrueにする。デフォルトはfalse。上で書いた、カスタマイズしたRakefileを実行すると、次のようになる。errorsタスク名も変更されているし、出力もいくらか詳細になっているのがわかると思う。複数のRakefileの連携ビルド定義が巨大になってくると、役割ごとに定義ファイルを分割したくなってくる。Rakeで複数の定義ファイルを組み合わせるためにはどうすればよいか。簡単な方法は、通常のRubyスクリプト同様、requireまたはloadを使用することである。Egyptian!"endこれで片付くならばそれでよい。いうのも、片付かないケースがあるのだ。それは、読み込みたいファイルを動的に生成したいケースである。requireにしろloadにしろ、ベースとなるRakefileで定義されるよりも先に解決される。rakeコマンドを実行したときには、読み込み対象ファイルは完全な形で存在していなければならない。rakeのタスクで読み込むファイルを動的に作る、といった格好いい処理を実装できない。Documentでは、依存性のインポートがこのパターンとして説明されており、これを実現するための手段が書かれている。そこで使われているのは、requireでもloadでもなく、importである。importは、他の2つとは次の点で異なる。ファイルが読み込まれるのは、ベースのRakefileのタスク定義を解決した後である。importするファイルは、ファイルタスクの依存性解決に組み込める。下記は、これを説明するための例である。Documentから頂戴してきた。C言語のソースコード群から依存性を抽出し、インポートしている。Makeで使われてきた依存性抽出ツールmakedependと、RakeにあるMakefileインポート機能の組み合わせである。".depends.mf"このRakefileでは、依存性をrakeのファイルタスクで生成しているので、requireは使えない。その代わりにimportを使用している。importしようとすると、その対象がファイルタスクとして定義されているので、まずはそちらを処理する。これは先に挙げたimportの特徴の両方を活用している。ちなみに、複数ファイルの連携というと、次のような書き方が真っ先に思いつく。echo"endMakeでは一般的な書き方だが、これでもOK。複数のRakefileを使いつつ、かつそれぞれのRakefileで設定を共有させたくない場合は、この方法を選択することになる。なんといってもプロセスから違うので、ベースとなるRakefileの設定は一切引き継がれない。名前空間Rakefileが大きくなり、受け持つべき作業が増えてくると、タスクの数も多くなり、タスク名が枯渇するかもしれない。...と、ネーミングルールだけで重複を回避するのはスマートではない。いう場合、名前空間を利用すべきである。endという書き方で名前空間を定義できる。clientとserverで同じ名前のタスクを定義しているが、異なる名前空間上にあるので、別タスクとして認識される。descも、ちゃんと名前空間の違いを理解した表示になる。components$名前空間内のタスクは、名前空間名:タスク名いう形で指定する。components...名前空間をネストさせることも可能。components$ただし、ここで一つ注意しておきたいことがある。名前空間が使えるとなると、次のような定義をしたくなるかもしれない。endend同じ意味合いの変数なのに、名前空間ごとに別々の名前を与えるのは面倒だ。異なる名前空間にしてあるのだから、同じ変数名で良いだろうし、そうするべきである。複数のビルドに別々のパラメータを持たせる方法としては、Rakefileを複数用意して別プロセスとして呼び出す方法を複数のRakefileの連携で説明したが、それとは異なるアプローチである。気づいたであろうか、これまでソースファイルリストなどは定数として定義してきたが、この例では変数としてある。Rubyでは、変数と定数とでスコープが異なる。上記の例のようにブロック内で定義した場合、変数であればブロックローカルになる。client名前空間のsrcsと、server名前空間のsrcsは異なる変数である。定数は宣言されたモジュールまたはクラスに定義されたことになる。よって、もしsrcsをSRCSとして定義した場合、client名前空間とserver名前空間のSRCSは同じ定数として扱われる。なると、定数を上書きすることになるので、警告が出る。client/src/Client.java$とりあえず動きはするが、Rubyの文法を考えれば反則である。Rakeでは、名前空間といってもあくまでnamespaceメソッドにブロックを与えるだけである点に注意。