2014年6月23日月曜日

Rake Part 3: Rules 翻訳

Avdi GrimmLearn Advanced Rake in 7 Episodesを、本人の許可を得て翻訳します。以下、Part 3の翻訳です。



もしあなたが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を使っているなら、連絡して欲しい

関連する投稿:
  1. Rake Part 1: Files and Rules
  2. Rake Part 2: File Lists
  3. Double-Load Guards in Ruby

0 件のコメント:

コメントを投稿