RubyによるビルドツールRakeの覚え書き。興に乗ったので詳しく書いてみた。
2014/02/20 Rakeの作者、Jim Weirich氏が亡くなられました。安らかに…
Rakeは、MakeをRubyで実装したものを超越したビルドツールである。
世間では、ビルドツールというとMakeやApache Antが有名で、よく使われている。 Rakeは、これらのいいとこ取りをした上で、特有のフィーチャーを追加した新しいビルドツールであり、複雑なビルドを柔軟に書きこなすことができる。その秘密は内部DSLという仕組みにあり、このおかげでビルドの記述にRubyの強力な文法をそのまま使うことができる。この自由度の高さは、ビルドの記述に独自の言語の使用を選択したMakeとAntには無い強みだ。
その代わりと言ってはなんだが、Rubyにある程度習熟していないと扱えないツールである。
Ruby入れて、rubygems入れて、下記のコマンドを実行。
# gem install rake --remote
「さっぱりわからん!」という人は、とりあえずRubyの学習から始めていただきたい。重ねて言うが、Rubyでプログラムを書けない人はRakeを扱えない。
とりあえず簡単な例を見てみよう。中身がわからずとも、雰囲気を感じ取ってもらえればよい。
Rakeを使用するためには、まずRakefileという名称のファイルを作成し、その中にビルド定義を記述する。rakeはRakefileを解析し、その内容に従ってビルドを進めるのである。
下記の例は、C言語で書かれたソースコードをビルドする、極単純な例である。
CC = "gcc"
task :default => "hello"
file "hello" => ["hello.o", "message.o"] do
sh "#{CC} -o hello hello.o message.o"
end
file "hello.o" => "hello.c" do
sh "#{CC} -c hello.c"
end
file "message.o" => "message.c" do
sh "#{CC} -c message.c"
end
何をしたいのかは、見た目からなんとなく読み取れるのではないだろうか。
ビルドツールで定義するのは、実施するタスク(作業)と、タスク間の依存関係である。上記の例では、以下のようなことを定義している。
gcc -o hello hello.o message.oのコマンドで作成できる。 gcc -c hello.cのコマンドで作成できる。 gcc -c message.cのコマンドで作成できる。 ここで定義された依存関係を追いかけて、必要なタスクを芋づる式に引きずり出して順次実行していくのである。例えば、"hello"を作るために"hello.o"が必要だと書いてあるので、先にそいつの作成から始めるわけだ。
実際に走らせてみる。Rakefileを置いたディレクトリで、rakeコマンドを実行するのだ。
$ rake (in /home/idesaku/prog) gcc -c hello.c gcc -c message.c gcc -o hello hello.o message.o $
単にrakeとだけ打つと、カレントディレクトリ中のrakefileまたはRakefileファイルを探し出して読み込み、そこに記述されたデフォルトタスクを実行する。ただ、ビルド定義ファイルは先頭を大文字にする習慣がある(そうしておくと、lsしたときに先頭付近に来るのでわかりやすい)ので、倣っておいたほうがよい。このドキュメントも、Rakefileで通すことにする。
ちゃんと差分ビルドもしてくれる。例えば、message.cだけ変更してみると・・・。
$ rake (in /home/idesaku/prog) gcc -c message.c gcc -o hello hello.o message.o $
hello.oのビルドが含まれていない点に注目。変更したファイルに関係する箇所のみビルドし直している。
以降は、Rakefileの中身をもう少し詳細に掘り下げる。より高度な使い方についても触れる。
Rakefileはタスクとタスク間の依存関係を記述するファイルである。よって、まず覚えるべきはタスクの定義方法であろう。
タスクは、下記のように定義する。
task "dist" => ["init", "compile"] task "init" task "compile"
task タスク名という書式でタスクを定義する。タスク名は文字列でもシンボルでも良い。
タスクに依存関係を書きたければ、=>の先に依存するタスクを書く。依存先が複数ある場合は、配列として渡せばよい。
では、distタスクを実行させてみる。実行するタスクは、rakeの引数として与えればよい。また、タスクの実行順序を見るために-tオプションを付加する。
$ rake -t dist (in /home/idesaku/prog) ** Invoke dist (first_time) ** Invoke init (first_time) ** Execute init ** Invoke compile (first_time) ** Execute compile ** Execute dist $
Execute ...と表示されている場所を見るとわかるが、init, compile, distの順で実施されている。
基本的にタスク名は自由につけることができるが、"default"タスクは予約されている。実行タスクが指定されない場合、rakeは"default"と名付けられたタスクを実行するのである。
先ほどのRakefileに"default"タスクを追加してみる。
task "default" => "dist" task "dist" => ["init", "compile"] task "init" task "compile"
このファイルに対して、タスクを指定せずにrakeを実行してみる。
$ rake -t (in /home/idesaku/prog) ** Invoke default (first_time) ** Invoke dist (first_time) ** Invoke init (first_time) ** Execute init ** Invoke compile (first_time) ** Execute compile ** Execute dist ** Execute default $
真っ先に"default"タスクが解決されているのがわかるだろうか?そして、"default"タスクが依存している"dist"タスクが解決され、あとは同じ流れだ。
さて。ここまでのところ、タスクを定義するは良いが、なにもさせていない。タスクは実際に作業させてナンボである。
タスクの作業内容(アクションという)はブロックとして与える。下記に例を示すが、あくまで例なので、簡単にメッセージを表示するだけにしておく。
task "default" => "dist" task "dist" => ["init", "compile"] do puts "-- make distribution --" end task "init" do puts "-- initialize -- " end task "compile" do puts "-- compile --" end
Rubyでは、ブロックの定義にはdo-endを使う方法と、{}を使う方法がある。しかし、taskに与える場合は必ずdo-endを用いること。その理由は後ほど説明する。
実行してみる。今度はメッセージ表示させているので、-tを付けずとも動きが分かる。
$ rake (in /home/idesaku) -- initialize -- -- compile -- -- make distribution -- $
各タスクが、指定したアクションを、しかるべき順番で実施しているのがわかる。
ビルドにおいて実行されるタスクは多岐に及ぶ。ディレクトリを作る、ファイルをFTPで送り込む、設定ファイルの検証をする、などなどである。しかし、最も頻繁に行われるのは、「なにかしらのファイルを作成する」というタスクであろう。例えば、ビルドタスクとしては代表的な「コンパイル」は、ソースコードからオブジェクトファイルや実行可能ファイルを作成することが目的である。
Rakeでは、ファイル生成に特化したタスクが用意されている。これをファイルタスクという。
定義の仕方はtaskと大差ない。ここでは、冒頭で用いた単純なRakefileを再び例として挙げる。
CC = "gcc"
task :default => "hello"
file "hello" => ["hello.o", "message.o"] do
sh "#{CC} -o hello hello.o message.o"
end
file "hello.o" => "hello.c" do
sh "#{CC} -c hello.c"
end
file "message.o" => "message.c" do
sh "#{CC} -c message.c"
end
file ファイルパスという形で定義されているものが、ファイルタスクである。ここで与えるファイルパスは、パスであると同時にタスクの名称でもある。
タスクではなく、あえてファイルタスクを用いる理由、それは差分ビルドの実現にある。
タスクとして定義された場合、そのタスクは必ず実行される。もっと言えば、いまそれをする必要が無くとも、定義されている以上実施してしまう。しかし、ファイルタスクの場合、必要なときだけタスクを実施するのである。具体的には、定義されたファイルを作ろうとする際、まず目的ファイルが作成済みかどうかを調べる。作成済みであれば、続けて依存先のファイルの更新日時を調べ、それが目的ファイルよりも新しくなっている場合のみ、変更があったと見なしてビルド作業を実施するのだ。これは大変な時間の節約になる。
この例はすでに挙げた。
ところで、このRakefileは実際に使えるが、欠点がある。もう一度見てみよう。
CC = "gcc"
task :default => "hello"
file "hello" => ["hello.o", "message.o"] do
sh "#{CC} -o hello hello.o message.o"
end
file "hello.o" => "hello.c" do
sh "#{CC} -c hello.c"
end
file "message.o" => "message.c" do
sh "#{CC} -c message.c"
end
同じファイル名を何度も書いている。これはDRY原則に反する、醜い書き方だ。保守性も落ちる。同じ事はなるべく一度しか書かないようにすべきだ。
そもそも、作成したいファイルや依存するファイルはすでにファイルタスクに定義しているのだから、タスクに直接問い合わせればよいのだ。そこで、ブロック引数を追加する。
CC = "gcc" task :default => "hello" file "hello" => ["hello.o", "message.o"] do |t| sh "#{CC} -o #{t.name} #{t.prerequisites.join(' ')}" end file "hello.o" => "hello.c" do |t| sh "#{CC} -c #{t.prerequisites[0]}" end file "message.o" => "message.c" do |t| sh "#{CC} -c #{t.prerequisites[0]}" end
ブロック引数tには、ファイルタスクそのものが渡される。これでtから必要な情報を取り出して、コマンドに渡すことができる。
ファイルタスク、正確にはRake::FileTaskクラスにはいくつかのメソッドが定義されているが、よく使いそうなのは、次のものである。
例えば、file "hello"タスクでは、name == "hello"で、prerequisites == ["hello.o", "message.o"]である。
いちおう、実行してみよう。
$ rake (in /home/idesaku/prog) gcc -c hello.c gcc -c message.c gcc -o hello hello.o message.o $
期待通りの動作だ!
ちなみに、ブロック引数はtaskでも使用可能である。が、実際のところタスクでブロック引数を必要とするケースは少ないので、ファイルタスクの説明として記載した。
しかし、まだやるべきことはある。hello.cからhello.oを作るのも、message.cからmessage.oを作るのも、まったく同じ構造をしている。これらもまとめて定義したいところだ。これを実現するには、ルールの記述を覚えなければならない。
さて、このRakefileを見ていただきたい。
CC = "gcc"
task :default => "hello"
file "hello" => ["hello.o", "message.o"] do |t|
sh "#{CC} -o #{t.name} #{t.prerequisites.join(' ')}"
end
file "hello.o" => "hello.c" do |t|
sh "#{CC} -c #{t.prerequisites[0]}"
end
file "message.o" => "message.c" do |t|
sh "#{CC} -c #{t.prerequisites[0]}"
end
ソースファイルとオブジェクトファイルの関係を一つ一つ定義している。これはまだ良い、所詮は例にすぎず、ソースファイルの数もたった2個と少ないから、こうして書いていられる。しかし、ソースファイルの数が100やら1000になったらどうするのか?とてもじゃないが、全てのファイルについてファイルタスクを定義することなどできない。
そこで、個々のファイルの関連を見るのではなく、全体で共通するルールを見いだすことを考える。ここでは、「XXX.oを作るためにXXX.cが必要で、作り方はgcc -c XXX.cである」というルールの存在が見て取れる。
Rakeでは、上記のようなルールを定義できるようになっている。例を示そう。
CC = "gcc"
task :default => "hello"
file "hello" => ["hello.o", "message.o"] do |t|
sh "#{CC} -o #{t.name} #{t.prerequisites.join(' ')}"
end
rule '.o' => '.c' do |t|
sh "#{CC} -c #{t.source}"
end
ruleで始まっているタスクが、ルールである。この例では、「拡張子が'o'であるファイルを作るために、拡張子'c'のファイルが必要」といったことを定義している。sourceメソッドを使うことで、依存するファイルパスを参照することができる(prerequisitesではない!)。ここでは使用していないが、t.nameも有効である。
この例では、
gcc -c hello.c実行。といった流れで処理が進む。message.oも同様。
ここで説明したのはruleの最も簡単な使い方である。おそらく、実際に開発で使おうと思えば、正規表現やProcを併用した高度な扱い方を知っておかなければならない。これは後述する。
ビルド中に、ディレクトリを作成したくなることはよくある。例えば、作成したパッケージの置き場所を別に作りたい、といった場合である。
すぐに思いつくのは、そうしたタスクを作ることである。
SRC_DISTDIR = "./srcdist"
task "init" do
mkdir SRC_DISTDIR
end
task "make_src_package" => "init" do
cp ["hello.c", "message.c", "message.h"], SRC_DISTDIR, {:preserve => true}
end
こうしておけば、パッケージを作成する前に必ずディレクトリが作成される。
しかし、Rakeにはこれを行うための機能が備わっているので、こちらを使った方がスマートだ。
SRC_DISTDIR = "./srcdist"
directory SRC_DISTDIR
task "make_src_package" => SRC_DISTDIR do
cp ["hello.c", "message.c", "message.h"], SRC_DISTDIR, {:preserve => true}
end
directory ディレクトリパスという形で定義する。こうしておくと、そのディレクトリが必要とされた時点で勝手にディレクトリが作られる。
実行してみよう。
$ rake make_src_package (in /home/idesaku/prog) mkdir -p ./srcdist cp -p hello.c message.c message.h ./srcdist $
Rakefile中で特にディレクトリを作れと指示していないが、必要になった(依存先になっている)ため勝手に作っている。
また、深くネストしたディレクトリも一回で作ってくれる。例えば、次のようなdirectoryを設定しても・・・。
directory "./dist/ver1.0/src"
これは次のように記述したに等しい。
file "./dist" do |t| mkdir t.name end file "./dist/ver1.0" => "./dist" do |t| mkdir t.name end file "./dist/ver1.0/src" => "./dist/ver1.0" do |t| mkdir t.name end
directoryは、これまで出てきたtask, file, ruleと似た書き方をするが、違っている点がある。directoryにはブロックを追加できない。例えば、次のような書き方はできない。
SRC_DISTDIR = "./srcdist" directory SRC_DISTDIR do |t| cp "hoge.txt", t.name end
ただし、後からファイルタスクとして追加することで、期待した定義を行うことが可能。
SRC_DISTDIR = "./srcdist" directory SRC_DISTDIR file SRC_DISTDIR do |t| cp "hoge.txt", t.name end
この場合、ディレクトリ作成後、そこにhoge.txtをコピーする、という動作になる。つまるところ、初期化処理である。
余談だが、ここで使用したcp, mkdirといったファイル操作メソッドは、FileUtilsモジュールのものである。Rakeでは、これにいくらかの拡張を施した上でトップレベルにincludeしてある。よって、モジュール名を指定せずとも関数のように呼び出せる。ファイルに対して何かしたい場合は、FileUtilsのドキュメントを参照して適切なメソッドを探すと良い。
すぐれたエンジニアは、自分の作ったスクリプトや設定ファイルにわかりやすい説明を残すものである。Rakeでは、自分で定義したタスクの説明文を定義することができる。
書き方は実に簡単である。taskの直前に、desc 説明文を書けばよい。
task "default" => "dist" desc "Make distribution" task "dist" => ["init", "compile"] do puts "-- make distribution --" end desc "Initialize" task "init" do puts "-- initialize -- " end desc "Compile All Source files" task "compile" do puts "-- compile --" end
記述した説明は、rake -Tで見ることができる。
$ rake -T (in /home/idesaku/prog) rake compile # Compile All Source files rake dist # Make distribution rake init # Initialize $
Antにfilesetというタスクがあるのをご存じだろうか?「srcディレクトリ以下にある全てのファイル、でも.svnと.bakは除く」といった複雑なファイル群の定義を柔軟に行うための機構で、非常に便利である。Rakeにもこれ相当の機能が用意されている。FileListである。
例を出そう。
CC = "gcc"
task :default => "hello"
file "hello" => ["hello.o", "message.o"] do |t|
sh "#{CC} -o #{t.name} #{t.prerequisites.join(' ')}"
end
rule '.o' => '.c' do |t|
sh "#{CC} -c #{t.source}"
end
このRakefileは、ソースファイルがhello.cとmessage.cしかない(ヘッダファイルはあるかもしれないが・・・)という前提で書かれている。この書き方では、将来ソースファイルが一つ増えるごとにRakefileも修正しなければならない。
そこで、コンパイル対象を「このディレクトリ以下にある全ての.cファイル」としたい。FileListを使えば、こうした定義が可能だ。
FileListは次のように定義する。
SRCS = FileList["**/*.c"]
FileListには、扱いたいファイルのパターンを与える。アスタリスク(*)は、「全ての」といった意味合いがある。UNIXやコマンドプロンプトのコンソールでの扱いと同じだ。
ところで、今回のケースではfile "hello"ファイルタスクの依存先は*.cではなく*.oである。よって、用があるのは*.oの方だ。そこで、拡張子のみを変換した別のFileListが必要になるのだが、そのためのメソッドもちゃんとある。
SRCS = FileList["**/*.c"]
OBJS = SRCS.ext('o')
ちなみに、いきなりFileList["**/*.o"]といった定義をしても駄目だ。*.oはこれから作るのだから、まだ存在していない。
それはさておき、これをタスクの依存先とすれば、問題解決である。
CC = "gcc" SRCS = FileList["**/*.c"] OBJS = SRCS.ext('o') task :default => "hello" file "hello" => OBJS do |t| sh "#{CC} -o #{t.name} #{t.prerequisites.join(' ')}" end rule '.o' => '.c' do |t| sh "#{CC} -c #{t.source}" end
今後はFileListが勝手に*.cファイルを見つけてコンパイル対象にしてくれる。
もっと複雑なパターンを設定したい場合は、後からどんどん追加できる。includeおよびexcludeメソッドを使うと良い。
# 生成後に追加
srcs = FileList["src/**/*"]
srcs.include("META/**/*.xml")
# includeだけしたいのであれば、一気に定義することもできる
srcs = FileList["src/**/*", "META/**/*.xml"]
# FileListにFileListを追加することもできる。
srcs.include(FileList["**/*.h"])
# 除外パターンも書ける。
srcs.exclude("**/.svn")
# イテレータを使えば、include/exclude共に一度に定義することもできる。
# FileList.newは、FileList[]の別名。こちらの記法でないとブロックを渡せない。
srcs = FileList.new("src/**/*") do |f|
f.include("META/**/*.xml")
f.include("**/*.h")
f.exclude("**/.svn")
end
FileListの中身は、eachメソッドで取り出すことができる。例えば、次のように書けばFileListが捕捉したファイルの一覧を表示できる。
srcs = FileList.new("src/**/*") do |f|
f.include("META/**/*.xml")
f.exclude("**/.svn")
end
task :default do
srcs.each do |path|
puts path
end
end
Rakeは差分ビルドできるよう作られているが、これまで作成したファイルを一度全て削除して、まっさらな状態からビルドしたくなることがままある。依存関係というのは、ときにファイルの更新日時だけでは量れない複雑さを持つことがあるのだ。
よって、ビルドに使っているのがMakeだろうがAntだろうが、生成したファイル群を削除するためのタスクを用意しておくものである。
Rakeでは、このありがちなタスクをすでに用意している。次のように書くと良い。
require 'rake/clean' CC = "gcc" SRCS = FileList["**/*.c"] OBJS = SRCS.ext('o') CLEAN.include(OBJS) CLOBBER.include("hello") task :default => "hello" file "hello" => OBJS do |t| sh "#{CC} -o #{t.name} #{t.prerequisites.join(' ')}" end rule '.o' => '.c' do |t| sh "#{CC} -c #{t.source}" end
CLEANとCLOBBERに注目。rake/cleanをrequireすることで、自動的に追加されるFileListである。これらに登録したファイルがクリーン実行時の削除対象となる。
rake -Tでタスクの説明を見てみると、cleanとclobberが追加されているのが分かる。
$ rake -T (in /home/idesaku/prog) rake clean # Remove any temporary products. rake clobber # Remove any generated file. $
実際に実行してみる。
$ rake clean (in /home/idesaku/prog) rm -r hello.o rm -r message.o $ rake clobber (in /home/idesaku/prog) rm -r hello.o rm -r message.o rm -r hello $
これで生成されたファイルは綺麗サッパリ消えた。
cleanは、CLEANファイルリストに定義されているファイルを削除する。clobberは、CLEANとCLOBBERに定義されているファイルを削除する。つまり、clobberのほうがより徹底的に掃除を行う。
両者の使い分けは、タスクの説明にある通りである。cleanは一時ファイルの削除に使用し、clobberは生成された全てのファイルの削除に使用する。だから、先ほどの例ではCLEANにはリンクするときに使用するだけの一時ファイルであるオブジェクトファイルを追加し、CLOBBERには最終生成ファイルである実行可能ファイルを含めているのだ。
無論、CLEANとCLOBBERの中身は自由に設定できるのでこの規則に従わなくとも良いが、反抗してみせたところで何も嬉しいことはないので、倣っておいたほうがよい。
ソースファイルにしろ、バイナリファイルにしろ、それを配布する際は1ファイルにまとめてしまうものだ。それはzipかもしれないし、tar.gzかもしれない。これまたありがちなタスクである。
clean同様、Rakeにはパッケージを作成するためのタスクが用意されている。Rake::PackageTaskだ。
使い方は、cleanによく似ている。
require 'rake/packagetask'
Rake::PackageTask.new("mylibs", "1.0.0") do |p|
p.package_dir = "./pkg"
p.package_files.include("lib/**/*")
p.need_tar_gz = true
p.need_tar_bz2 = true
end
作りたいパッケージの定義を書くだけでよい。
newの第一引数はパッケージ名、第二引数はバージョン番号である。定義できるフィールドには、下記のものがある。
FileList。上記の例では、./pkgディレクトリ中に、lib/ディレクトリ以下全てのファイルをまとめてパッケージを作ることになる。ファイル形式はtar.gzおよびtar.bz2である。ファイル名はパッケージ名-バージョン番号.拡張子となる。この場合、mylibs-1.0.0.tar.gzと、mylibs-1.0.0.tar.bz2である。need_XXXフィールドは複数同時にtrueに設定でき、trueにした形式のパッケージが全て作成される。
rake -Tすると、例によってタスクが定義されているのがわかる。
$ rake -T (in /home/idesaku/prog) rake clobber_package # Remove package products rake package # Build all the packages rake repackage # Force a rebuild of the package files $
packageタスクを呼び出せば、パッケージが生成される。
$ rake package (in /home/idesaku/prog) mkdir -p ./pkg mkdir -p ./pkg/mylibs-1.0.0/lib rm -f ./pkg/mylibs-1.0.0/lib/foo.rb ln lib/foo.rb ./pkg/mylibs-1.0.0/lib/foo.rb rm -f ./pkg/mylibs-1.0.0/lib/bar.rb ln lib/bar.rb ./pkg/mylibs-1.0.0/lib/bar.rb cd ./pkg tar zcvf mylibs-1.0.0.tar.gz mylibs-1.0.0 mylibs-1.0.0/ mylibs-1.0.0/lib/ mylibs-1.0.0/lib/foo.rb mylibs-1.0.0/lib/bar.rb cd - cd ./pkg tar jcvf mylibs-1.0.0.tar.bz2 mylibs-1.0.0 mylibs-1.0.0/ mylibs-1.0.0/lib/ mylibs-1.0.0/lib/foo.rb mylibs-1.0.0/lib/bar.rb cd - $ ls ./pkg mylibs-1.0.0 mylibs-1.0.0.tar.bz2 mylibs-1.0.0.tar.gz $
clobber_packageタスクは、パッケージおよびパッケージ生成用の一時ファイルを全て削除する。このタスクはclobberタスクの依存先に追加登録される。つまり、clobberタスクを実行すると、clobber_packageタスクも一緒に実行される。
repackageは、パッケージを作り直す(clobber_packageして、packageする)。
また、rake -Tには出てこないが、ファイルタスクとしてパッケージのパスが追加されている。例えば、次のようにrakeを実行すれば、tar.bz2だけ作成される(もっとも、追加されるのはneed_XXXをtrueとしたパッケージのみである)。
$ rake ./pkg/mylibs-1.0.0.tar.bz2
PackageTaskは必要な数だけ定義できるので、複数バージョンのパッケージを作れるようにしたければ、それだけの数PackageTaskを定義すればよい。
Rakeの柔軟性を象徴するテクニックの一つとして、タスクの動的生成がある。
無意味だが単純な例を出そう。次のようなRakefileを書いてみる。
1.upto(5) { |i|
desc "task no.#{i}"
task "task_#{i}" do |t|
puts "Invoke #{t.name}"
end
}
rake -Tを実行。
$ rake -T (in /home/idesaku) rake task_1 # task no.1 rake task_2 # task no.2 rake task_3 # task no.3 rake task_4 # task no.4 rake task_5 # task no.5 $
ループ文を使って、任意数のタスクを作った。もちろん、どれも実行できる。
$ rake task_3 (in /home/idesaku) Invoke task_3 $
これはMakeやAntでは容易にマネできない機能である。taskはあくまでメソッドに過ぎないから、つまりRakeが内部DSLを採用しているからこそできる芸当だ。
これができると何が嬉しいのか、という話になるのだが、とりあえず設定ファイルから動的にタスクを生成する、という技がそこそこ役に立っている。
ビルドした実行ファイルを適切なサーバに配備しなければならないが、これがテスト用、本番用など複数あるとする。こういう場合、配備先サーバの情報は設定ファイルとして外に出してしまいたくなる。そこで、設定ファイルlogin.yamlを作ってみた。フォーマットはCSVでもXMLでもよかったのだが、ここではYAMLを使用している。
test1:
user: marvin
passwd: xxxxxx
host: 192.168.0.10
release:
user: visar
passwd: yyyyyy
host: 192.168.0.11
このファイルを読み込んで、配備タスクを生成するようRakefileを書く。
require 'yaml'
require 'pp'
# ダミーメソッド。本当はFTPなりSCPなりを使ってファイル転送する処理を書く。
def deploy(param)
puts "Deploy to #{param['user']}/#{param['passwd']}@#{param['host']}"
end
File.open('./login.yaml') { |io|
YAML.load_documents(io) { |obj|
obj.keys.each { |key|
param = obj[key]
desc "Deploy to #{key}"
task "deploy_#{key}" do
deploy(param)
end
}
}
}
task :default
rake -Tしてみよう。
$ rake -T (in /home/idesaku) rake deploy_release # Deploy to release rake deploy_test1 # Deploy to test1
設定ファイルで定義したサーバ分、ファイル配備タスクができている。もちろん、タスクは実際に実行できる。
$ rake deploy_test1 (in /home/idesaku) Deploy to marvin/xxxxxx@192.168.0.10 $
配備先サーバが増えた場合でも、login.yamlファイルに追記するだけでよい。
test1:
user: marvin
passwd: xxxxxx
host: 192.168.0.10
release:
user: visar
passwd: yyyyyy
host: 192.168.0.11
test2:
user: zorac
passwd: zzzzzzzz
host: 192.168.0.12
$ rake -T (in /home/idesaku) rake deploy_release # Deploy to release rake deploy_test1 # Deploy to test1 rake deploy_test2 # Deploy to test2 $
近年のソフトウェア開発におけるベストプラクティスの一つとして、テストの自動化がある。Rubyによる開発でもこれは奨励されており、単体テスト用ライブラリTest::Unitが標準添付されている。Rakeには、これを使用したユニットテストを実行するためのタスクが用意されている。
Test::Unitの使い方はこのドキュメントの範囲を超えるので言及しないが、とりあえず簡単なテストクラスおよびテスト対象ファイルを用意する。
# count_char.rb
# 文字ごとの出現回数を集計する複雑な(笑)メソッド。
def count_char(val)
table = Hash.new(0)
val.split(//).each do |ch|
table[ch] += 1
end
table.sort
end
# test/test_count_char.rb
# count_char.rbのテストクラス。
require 'test/unit'
require 'count_char'
class TestCountChar < Test::Unit::TestCase
def test_count_char
assert_equal([["a", 3], ["b", 1], ["c", 2]], count_char("abacac"))
end
end
最小であれば、Rakefileの記述内容はこれだけでよい。
require 'rake/testtask' Rake::TestTask.new
例によってrake -Tしてみると、タスクが増えている。
$ rake -T (in /home/idesaku) rake test # Run tests $
もちろん、実行できる。
$ rake test (in /home/idesaku) Loaded suite /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.0/lib/rake/rake_test_loader Started . Finished in 0.021736 seconds. 1 tests, 1 assertions, 0 failures, 0 errors
エラー無し。すばらしい。
デフォルトではtest/test*.rbのパターンに合致する全てのテストクラスのテストが実施される。特定のテストクラスを選んで実行したい場合は、実行時にオプションを渡す。
$ rake test TEST=test/test_count_char.rb
(in /home/idesaku)
Loaded suite /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.0/lib/rake/rake_test_loader
Started
.
Finished in 0.013578 seconds.
1 tests, 1 assertions, 0 failures, 0 errors
$
この例のRakefileの定義が単純極まりないのは、ファイル名やファイル配置をTestTaskのデフォルト値に合わせているからである。自分の環境がそれに合わない場合は、定義を追加してカスタマイズしなければならない。
require 'rake/testtask'
Rake::TestTask.new("unittest") do |t|
t.libs << "ext/libs"
t.test_files = FileList["unittest/*_test.rb"]
t.warning = true
t.verbose = true
end
TestTask.newの第一引数はタスク名である。デフォルトではtestになっている。
ブロック内では追加でプロパティ設定を行っている。設定可能な値は以下である。
libである。test/test*.rb。trueにする。デフォルトはfalse。trueにしておけば、ruby -wで実行される。上で書いた、カスタマイズしたRakefileを実行すると、次のようになる。
$ rake -T (in /home/idesaku) rake unittest # Run tests for unittest $ rake unittest (in /home/idesaku) /usr/local/bin/ruby -w -Ilib "/usr/local/lib/ruby/gems/1.8/gems/rake-0.8.0/lib/rake/rake_test_loader.rb" "unittest/count_char_test.rb" Loaded suite /usr/local/lib/ruby/gems/1.8/gems/rake-0.8.0/lib/rake/rake_test_loader Started . Finished in 0.018133 seconds. 1 tests, 1 assertions, 0 failures, 0 errors
タスク名も変更されているし、出力もいくらか詳細になっているのがわかると思う。
ビルド定義が巨大になってくると、役割ごとに定義ファイルを分割したくなってくる。Rakeで複数の定義ファイルを組み合わせるためにはどうすればよいか。
簡単な方法は、通常のRubyスクリプト同様、requireまたはloadを使用することである。
# Rakefile
load "Rakefile.ext"
task :default => :echo
# Rakefile.ext task :echo do puts "Psssst! La-La Moo Moo is an Egyptian!" end
これで片付くならばそれでよい。というのも、片付かないケースがあるのだ。
それは、読み込みたいファイルを動的に生成したいケースである。requireにしろloadにしろ、ベースとなるRakefileで定義されるよりも先に解決される。つまり、rakeコマンドを実行したときには、読み込み対象ファイルは完全な形で存在していなければならない。rakeのタスクで読み込むファイルを動的に作る、といった格好いい処理を実装できない。
Rake Documentでは、依存性のインポートがこのパターンとして説明されており、これを実現するための手段が書かれている。そこで使われているのは、requireでもloadでもなく、importである。
importは、他の2つとは次の点で異なる。
下記は、これを説明するための例である。Rake Documentから頂戴してきた。C言語のソースコード群から依存性を抽出し、インポートしている。Makeで使われてきた依存性抽出ツールmakedependと、RakeにあるMakefileインポート機能の組み合わせである。
require 'rake/loaders/makefile'
file ".depends.mf" => [SRC_LIST] do |t|
sh "makedepend -f- -- #{CFLAGS} -- #{t.prerequisites.join(' ')} > #{t.name}"
end
import ".depends.mf"
このRakefileでは、依存性をrakeのファイルタスクで生成しているので、requireは使えない。その代わりにimportを使用している。importしようとすると、その対象がファイルタスクとして定義されているので、まずはそちらを処理する。これは先に挙げたimportの特徴の両方を活用している。
ちなみに、複数ファイルの連携というと、次のような書き方が真っ先に思いつく。
task :default do sh "rake -f Rakefile.ext echo" end
Makeでは一般的な書き方だが、これでもOK。複数のRakefileを使いつつ、かつそれぞれのRakefileで設定を共有させたくない場合は、この方法を選択することになる。なんといってもプロセスから違うので、ベースとなるRakefileの設定は一切引き継がれない。
Rakefileが大きくなり、受け持つべき作業が増えてくると、タスクの数も多くなり、タスク名が枯渇するかもしれない。例えば、build, compile, deploy, initといった名前はよく使いそうであるが、だからといって、xxx_build, yyy_build, zzz_build, ...と、ネーミングルールだけで重複を回避するのはスマートではない。
こういう場合、名前空間を利用すべきである。
書き方は簡単である。
namespace :client do desc "build client components" task :build do puts "build client components..." end end namespace :server do desc "build server components" task :build do puts "build server components..." end end
namespace 名前空間名 do endという書き方で名前空間を定義できる。clientとserverで同じ名前のタスクを定義しているが、異なる名前空間上にあるので、別タスクとして認識される。
descも、ちゃんと名前空間の違いを理解した表示になる。
$ rake -T (in /home/idesaku/prog) rake client:build # build client components rake server:build # build server components $
名前空間内のタスクは、名前空間名:タスク名いう形で指定する。
$ rake client:build (in /home/idesaku/prog) build client components...
名前空間をネストさせることも可能。
namespace :client do
namespace :web do
desc "build Web client components"
task :build do
puts "build Web client components..."
end
end
namespace :java do
desc "build Java client components"
task :build do
puts "build Java client components..."
end
end
end
namespace :server do
desc "build server components"
task :build do
puts "build server components..."
end
end
$ rake -T (in /home/idesaku/prog3) rake client:java:build # build Java client components rake client:web:build # build Web client components rake server:build # build server components $
ただし、ここで一つ注意しておきたいことがある。
名前空間が使えるとなると、次のような定義をしたくなるかもしれない。
namespace :client do srcs=FileList["client/src/**/*.java"] desc "build client components" task :build => srcs do |t| puts "build client components..." puts "source = #{t.prerequisites.join(' ')}" end end namespace :server do srcs=FileList["server/src/**/*.java"] desc "build server components" task :build => srcs do |t| puts "build server components..." puts "source = #{t.prerequisites.join(' ')}" end end
同じ意味合いの変数なのに、名前空間ごとに別々の名前を与えるのは面倒だ。せっかく異なる名前空間にしてあるのだから、同じ変数名で良いだろうし、そうするべきである。
これは問題なく動く。複数のビルドに別々のパラメータを持たせる方法としては、Rakefileを複数用意して別プロセスとして呼び出す方法を複数のRakefileの連携で説明したが、それとは異なるアプローチである。
ただし、気づいたであろうか、これまでソースファイルリストなどは定数として定義してきたが、この例では変数としてある。
Rubyでは、変数と定数とでスコープが異なる。上記の例のようにブロック内で定義した場合、変数であればブロックローカルになる。つまり、client名前空間のsrcsと、server名前空間のsrcsは異なる変数である。しかし、定数は宣言されたモジュールまたはクラスに定義されたことになる。よって、もしsrcsをSRCSとして定義した場合、client名前空間とserver名前空間のSRCSは同じ定数として扱われる。そうなると、定数を上書きすることになるので、警告が出る。
namespace :client do SRCS=FileList["client/src/**/*.java"] desc "build client components" task :build =>SRCS do |t| puts "build client components..." puts "source = #{t.prerequisites.join(' ')}" end end namespace :server do SRCS=FileList["server/src/**/*.java"] desc "build server components" task :build =>SRCS do |t| puts "build server components..." puts "source = #{t.prerequisites.join(' ')}" end end
$ rake client:build (in /home/idesaku/prog) /home/idesaku/prog/Rakefile:12: warning: already initialized constant SRCS build client components... source = client/src/Client.java $
とりあえず動きはするが、Rubyの文法を考えれば反則である。Rakeでは、名前空間といってもあくまでnamespaceメソッドにブロックを与えるだけである点に注意。