もしあなたがRubyプログラマーなら、Rake (故Jim Weirichによって作られたビルドユーティリティー) をほぼ確実に使ったことがあるだろう。しかし、Rakeがいかにパワフルでフレキシブルなツールになりうるかということは理解していないかもしれない。 実際、私は、Quarto (自作のe-book製作ツールチェーン)の土台としてRakeを使うと決めるまで理解していなかった。
この投稿はRakeについてのシリーズの一部で、基本から始めて高度な使用法に進む。 2013年の8月から9月に購読者に向け公開されたRubyTapasビデオのシリーズのひとつをもとにしている。 各投稿はビデオから始まり、ビデオより文字を好む人の ためにスクリプトが続く。
これらのエピソードを無料で公開するにあたり、より多くの人々が、 この広く行き渡っているが過小評価されているツールのすべての能力を知り愛するようになることを願う。もしあなたがRakeに感謝するのであれば、Jimへの追悼として the Weirich Fundへの寄付を検討して欲しい。
前回までの2回のエピソードでは、MarkdownファイルをHTMLにビルドするためにRakefileを作成しました。今のところRakefileは動作していますが、いくつか重複があります。 HTMLファイルをビルドするためのほとんど同じルールが2つあります。1つは
.md
拡張子を持つソースファイルを探し、 もう1つは.markdown
拡張子をもつファイルを探します。 それらを単一の、より一般的なルールにまとめられるといいですね。
source_files = Rake::FileList.new("**/*.md", "**/*.markdown") do |fl|
fl.exclude("~*")
fl.exclude(/^scratch\//)
fl.exclude do |f|
`git ls-files #{f}`.empty?
end
end
task :default => :html
task :html => source_files.ext(".html")
rule ".html" => ".md" do |t|
sh "pandoc -o #{t.name} #{t.source}"
end
rule ".html" => ".markdown" do |t|
sh "pandoc -o #{t.name} #{t.source}"
end
2番目のルールを取り除くことから始めましょう。この状態でrakeを実行すると失敗します。
source_files = Rake::FileList.new("**/*.md", "**/*.markdown") do |fl|
fl.exclude("~*")
fl.exclude(/^scratch\//)
fl.exclude do |f|
`git ls-files #{f}`.empty?
end
end
task :default => :html
task :html => source_files.ext(".html")
rule ".html" => ".md" do |t|
sh "pandoc -o #{t.name} #{t.source}"
end
$ rake
rake aborted!
Don't know how to build task 'ch4.html'
Tasks: TOP => default => html
(See full trace by running task with --trace)
先に進む前に、このエラーメッセージについて少し話しましょう。 このメッセージは、“タスク‘ch4.html’をビルドする方法がわかりません”と言っています。 これは多くのことを教えてはくれません。ch4.htmlと呼ばれるtaskが話題となっているので、少し紛らわしくもあります。
ch4.html
はビルドしたいfileであり、 タスクではありませんよね?ここから次のことがわかります。Rakeは、ビルドするよう依頼されているすべてのものをタスクとして考えています。単純なタスクとファイルタスクの唯一の違いは、 ファイルタスクの場合、タスク名に一致するファイルがあるか、そして事前条件となるファイルよりそのファイルのほうが新しい(タスクを実行する必要はない)かを Rakeが知っているということです。
このケースでは、なぜRakeがこのファイルをビルドできないのか、私たちは知っています。 どうやってビルドするかを告げるルールを単に削除したからです。 しかし、もしそれを知らなかっとしたらどうでしょう? このメッセージは、どう対処したらよいかについては多くを教えてはくれません。
Rakeが何をしようとしているかをより理解するために、Rakeに
–trace
フラグを渡すことができます。今度は、Rakeはたどるべきパンくずリストを残し、何をしようとしたかを教えてくれます。最初に、デフォルトタスクを呼び出しています。 デフォルトタスクは“html”タスクに依存しているので、次にhtmlタスクが呼ばれます。その次のステップは、
ch4.html
をビルドする方法がわからないためrakeが中断したことをそっけなく通知し、その後にRubyのスタックトレースが続きます。
$ rake --trace
** Invoke default (first_time)
** Invoke html (first_time)
rake aborted!
Don't know how to build task 'ch4.html'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task_manager.rb:49:in `[]'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:53:in `lookup_prerequisite'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:49:in `block in prerequisite_tasks'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:49:in `map'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:49:in `prerequisite_tasks'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:195:in `invoke_prerequisites'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:174:in `block in invoke_with_call_chain'
/usr/lib/ruby/1.9.1/monitor.rb:211:in `mon_synchronize'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:168:in `invoke_with_call_chain'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:197:in `block in invoke_prerequisites'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:195:in `each'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:195:in `invoke_prerequisites'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:174:in `block in invoke_with_call_chain'
/usr/lib/ruby/1.9.1/monitor.rb:211:in `mon_synchronize'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:168:in `invoke_with_call_chain'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:161:in `invoke'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/application.rb:149:in `invoke_task'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/application.rb:106:in `block (2 levels) in top_level'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/application.rb:106:in `each'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/application.rb:106:in `block in top_level'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/application.rb:115:in `run_with_threads'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/application.rb:100:in `top_level'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/application.rb:78:in `block in run'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/application.rb:165:in `standard_exception_handling'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/application.rb:75:in `run'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/bin/rake:33:in `<top (required)>'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/bin/rake:23:in `load'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/bin/rake:23:in `<main>'
Tasks: TOP => default => html
#+END_HTML
Let's ask Rake why it was trying to build =ch4.html= in the first
place. We can do this by running Rake with the =-P= flag, which tells
it to dump a list of prerequisites.
#+BEGIN_EXAMPLE
$ rake -P
rake default
html
rake html
ch1.html
ch2.html
ch3.html
subdir/appendix.html
ch4.html
この出力により、
html
タスクはch4.html
を含むファイルのリストに依存していることが明確になります。このRakefileの何が問題かをまだ知らないふりをしていることを忘れないでください。 Rakeの考え方について多くの知見を集めましたが、今のところ HTMLファイルとMarkdownファイルの関連(この問題を修正するために理解する必要があるなにか)については依然分かっていません。
Rakeの思考プロセスをより深く見るために、Rakefileファイル内で、
Rake.application.options.trace_rules
オプションにtrue
を設定します。 このオプションを有効にすると、その名前が示す通り、Rakefileに定義されたルールについてトレース情報を表示するようRakeに命じます。注:このビデオの作成後、Jim Weirichから、このオプションは`–rules`コマンドラインオプションとしても利用可能であるとの指摘があった。
Rake.application.options.trace_rules = true
source_files = Rake::FileList.new("**/*.md", "**/*.markdown") do |fl|
fl.exclude("~*")
fl.exclude(/^scratch\//)
fl.exclude do |f|
`git ls-files #{f}`.empty?
end
end
task :default => :html
task :html => source_files.ext(".html")
rule ".html" => ".md" do |t|
sh "pandoc -o #{t.name} #{t.source}"
end
再び
rake -trace
を実行すると、今度はタスク呼び出しのパンくずリストに加えて、新たな情報を確認できます。それぞれのファイルのビルドに対して、Rakeが使おうとしたルールと、 そのルールの中でどの.html
ファイルがどの対応する.md
ファイルに依存しているかを教えてくれます。 ch4.html
にたどり着くと、rakeは失敗します。事前条件ファイルch4.md
が見つからなかったと明示されてはいませんが、我々の目の前にあるこの情報により、何が問題かを合理的に推定することができます。
$ rake --trace
** Invoke default (first_time)
** Invoke html (first_time)
Attempting Rule ch1.html => ch1.md
(ch1.html => ch1.md ... EXIST)
Attempting Rule ch2.html => ch2.md
(ch2.html => ch2.md ... EXIST)
Attempting Rule ch3.html => ch3.md
(ch3.html => ch3.md ... EXIST)
Attempting Rule subdir/appendix.html => subdir/appendix.md
(subdir/appendix.html => subdir/appendix.md ... EXIST)
Attempting Rule ch4.html => ch4.md
(ch4.html => ch4.md ... FAIL)
rake aborted!
Don't know how to build task 'ch4.html'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task_manager.rb:49:in `[]'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:53:in `lookup_prerequisite'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:49:in `block in prerequisite_tasks'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:49:in `map'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:49:in `prerequisite_tasks'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:195:in `invoke_prerequisites'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:174:in `block in invoke_with_call_chain'
/usr/lib/ruby/1.9.1/monitor.rb:211:in `mon_synchronize'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:168:in `invoke_with_call_chain'
!/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:197:in `block in invoke_prerequisites'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:195:in `each'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:195:in `invoke_prerequisites'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:174:in `block in invoke_with_call_chain'
/usr/lib/ruby/1.9.1/monitor.rb:211:in `mon_synchronize'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:168:in `invoke_with_call_chain'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/task.rb:161:in `invoke'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/application.rb:149:in `invoke_task'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/application.rb:106:in `block (2 levels) in top_level'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/application.rb:106:in `each'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/application.rb:106:in `block in top_level'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/application.rb:115:in `run_with_threads'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/application.rb:100:in `top_level'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/application.rb:78:in `block in run'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/application.rb:165:in `standard_exception_handling'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/lib/rake/application.rb:75:in `run'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.1.0/bin/rake:33:in `<top (required)>'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/bin/rake:23:in `load'
/home/avdi/.rvm/gems/ruby-1.9.3-p327/bin/rake:23:in `<main>'
Tasks: TOP => default => html
さて、ルールを再び動くようする時です。メソッド
source_for_html
を定義することから始めます。 このメソッドは、HTMLファイルの名前を引数にとり、対応するMarkdownファイルの名前を返します。そのように動作させるには、ソースファイルのリストへのアクセスが必要です。 今のところ、リストはローカル変数で、このメソッド内からはアクセスできないため、定数に変更します。そして、 拡張子を除いた名前が、与えられたHTMLファイル名に一致する最初のソースファイルをソースファイルのリストから探します。 拡張子を除いた名前だけで比較するために、
#ext
メソッドを再び使用しています。 HTML出力ファイル名のリストを得るためにこのメソッドをソースファイルのリストに使用したことを、あなたは覚えているかもしれません。今回は、ファイル拡張子を完全に取り除くために、空の文字列を#ext
に渡しています。
def source_for_html(html_file)
SOURCE_FILES.detect{|f| f.ext('') == html_file.ext('')}
end
あなたが、“ちょっと待てよ!”というのが聞こえます。“前は、
#ext
メッセージをFileList
に送ったけど、ここではそれぞれのファイル名文字列に送っているじゃないか!どうしてそれが動くんだ?”ここから次のことがわかります。Rakeは、
FileList
がサポートしているのと同じメソッドのいくつかをサポートするようRubyのString
クラスを変更しているので、FileListと個々のファイル名が交換可能なものとして同じ操作ができます。HTMLファイル名を与えると、それを生成するのに必要なソースのMarkdownファイルを探すことができるメソッドがあるので、このメソッドを使う
.html
ルールを作る必要があります。ルールの中の
.md
依存関係をラムダに置き換えることにより、これを実現します。ラムダの中で、1つの引数をとり#source_for_html
メソッドに渡します。 Rakeは、.html
ファイルのビルドを試みる時、ターゲットファイルの名前を、事前条件として与えたラムダに引き渡します。 そして、このラムダの戻り値が、存在するファイルに一致するかどうか確認します。もし一致すれば、ルールが一致したと考え、関連するコードの実行に進みます。
rule ".html" => ->(f){source_for_html(f)} do |t|
sh "pandoc -o #{t.name} #{t.source}"
end
まだルールのトレースを有効にしているので、 変更されたルールを使ってRakeがどのように判断しているか、ウインドウに出力されます。
ch4.html
ターゲットにたどり着くと、依存関係がch4.md
ではなく ch4.markdown
であると、正しく決定します。そのファイルを見つけて、 ch4.html
ファイルをビルドします。
$ rake
Attempting Rule ch1.html => ch1.md
(ch1.html => ch1.md ... EXIST)
Attempting Rule ch2.html => ch2.md
(ch2.html => ch2.md ... EXIST)
Attempting Rule ch3.html => ch3.md
(ch3.html => ch3.md ... EXIST)
Attempting Rule subdir/appendix.html => subdir/appendix.md
(subdir/appendix.html => subdir/appendix.md ... EXIST)
Attempting Rule ch4.html => ch4.markdown
(ch4.html => ch4.markdown ... EXIST)
pandoc -o ch4.html ch4.markdown
これで、長い拡張子・短い拡張子のいずれのMarkdownファイルからもHTMLファイルをビルドする一般的なルールができました。しかし、もっと大切なことは、 何をどのようにビルドするかを決定するルールが、Rakeでどのように動作するかについて、より多くの知見を得たことです。 ハッピーハッキング!
ちなみに、これが最終的なRakefileになります。
Rake.application.options.trace_rules = true
SOURCE_FILES = Rake::FileList.new("**/*.md", "**/*.markdown") do |fl|
fl.exclude("~*")
fl.exclude(/^scratch\//)
fl.exclude do |f|
`git ls-files #{f}`.empty?
end
end
task :default => :html
task :html => SOURCE_FILES.ext(".html")
rule ".html" => ->(f){source_for_html(f)} do |t|
sh "pandoc -o #{t.name} #{t.source}"
end
def source_for_html(html_file)
SOURCE_FILES.detect{|f| f.ext('') == html_file.ext('')}
end
Rakeについてのエピソード・文章を楽しんでいただけたことを願っている。もし今日、何かしら学んだのであれば、Jimの遺産である教育プログラムの継続を支援するため、the Weirich Fundに寄付することにより「恩送りすること(訳注:原文は"paying it forward")」を検討して欲しい。 もし、今回のようなビデオをもっと見たければ、RubyTapas をじっくり見てほしい。シリーズが完結するまで、ほぼ毎日公開していくつもりなので、ほどなくご確認を!
P.S. 特に興味深い方法でRakeを使っているなら、連絡して欲しい。
関連する投稿:
0 件のコメント:
コメントを投稿