Rakeを徹底解剖 - その4 "タスクの実行"
過去3回のソースリーディングに続き、今回はついにタスクの実行について調べていきます。
def run
standard_exception_handling do
init
load_rakefile
top_level ## 今回のキモになる部分
end
end
なお、まだその1, その2、その3を読んでない方はこちらから。
タスクの実行
それでは、まず top_level の中を見てみましょう。
## rake/application.rb
def top_level
run_with_threads do
if options.show_tasks
display_tasks_and_comments
elsif options.show_prereqs
display_prerequisites
else
top_level_tasks.each { |task_name| invoke_task(task_name) }
end
end
end
最初の if と elsif に関しては、オプションによって それぞれ -T、-p を指定したときのもので、どちらも実行をせずにタスクの詳細を表示するオプションになります。
今回は実行を見ていくので、else に指定されたケースを見ていくことになります。
なので、invoke_taskについてみていきましょう。
## rake/application.rb
def invoke_task(task_string) # :nodoc:
name, args = parse_task_string(task_string)
t = self[name]
t.invoke(*args)
end
def parse_task_string(string) # :nodoc:
/^([^\[]+)(?:\[(.*)\])$/ =~ string.to_s
name = $1
remaining_args = $2
return string, [] unless name
return name, [] if remaining_args.empty?
args = []
begin
/((?:[^\\,]|\\.)*?)\s*(?:,\s*(.*))?$/ =~ remaining_args
remaining_args = $2
args << $1.gsub(/\\(.)/, '\1')
end while remaining_args
return name, args
end
rake は、 task[arg1,arg2] というようにタスク名のあとに、引数を設定することができます。
parse_task_string は、まさに task[arg1,arg2] という文字列からタスク名と引数にパースしている箇所になります。
余談ですが、筆者はあまりこのタスクに引数を与える使い方をしてません。
どうようのことは、ENV を介してやるようにしています。
なんとも、扱いづらいんですよね。 文字列上に引数が設定されてるんで、shell でちょうどその task の引数の位置に移動できなかったりするんで。
一方で、 -T 実行時に引数が必要なことが明示できないというデメリットがあるのですが、 desc にその旨を書くということで対処してます。
さて、ソースリーディングに戻ります。
パースされた、文字列は args として展開され、Task#invokeへ渡されます。
## rake/task.rb # Invoke the task if it is needed. Prerequisites are invoked first. def invoke(*args) task_args = TaskArguments.new(arg_names, args) invoke_with_call_chain(task_args, InvocationChain::EMPTY) end
引数は、TaskArguments.new(arg_names, args) によって、 Hashのような構造体へと置き換えられます。
そして、 タスクの実行部分invoke_with_call_chainへと引数として渡されていきます。
## rake/task.rb
def invoke_with_call_chain(task_args, invocation_chain) # :nodoc:
new_chain = InvocationChain.append(self, invocation_chain)
@lock.synchronize do
if application.options.trace
application.trace "** Invoke #{name} #{format_trace_flags}"
end
return if @already_invoked
@already_invoked = true
invoke_prerequisites(task_args, new_chain)
execute(task_args) if needed?
end
rescue Exception => ex
add_chain_to(ex, new_chain)
raise ex
end
InvocationChain.append(self, invocation_chain)については、 実行するタスクを保持しているのだが、読み解いていった結果、appendするときに、タスクの循環依存が起きてないか確認してる以外実行自体にはどうやら関係なさそう?
次の@lock.synchronizeについては、おそらく並列処理によるスレッド実行のためのものと思われる。
今回は、タスクの単純な実行だけに焦点をあてるので割愛。
application.options.trace や、 二重実行を防ぐ、@already_invoked のチェックを終えると、invoke_prerequisites が実行される。
これは単純に、dependeciesとして登録されたタスクを実行するものである。
## rake/task.rb
def invoke_prerequisites(task_args, invocation_chain) # :nodoc:
if application.options.always_multitask
invoke_prerequisites_concurrently(task_args, invocation_chain)
else
prerequisite_tasks.each { |p|
prereq_args = task_args.new_scope(p.arg_names)
p.invoke_with_call_chain(prereq_args, invocation_chain)
}
end
end
そして、最後に呼ばれる execute。
## rake/task.rb
def execute(args=nil)
args ||= EMPTY_TASK_ARGS
if application.options.dryrun
application.trace "** Execute (dry run) #{name}"
return
end
application.trace "** Execute #{name}" if application.options.trace
application.enhance_with_matching_rule(name) if @actions.empty?
@actions.each do |act|
case act.arity
when 1
act.call(self)
else
act.call(self, args)
end
end
end
application.enhance_with_matching_rule(name) については、 rule という記述で定義されたタスクで活躍する。
今回の単純なタスクのケースにはあまり関係ないので、割愛。
すると、残すは、@actionsの部分だが、中身はタスクを定義したときに渡しているブロック文である。
act.call(self, args) を呼ぶことで、ブロックにタスクと引数(TaskArguments)が渡ってくる。
少し気になったところとして、なんで actions なんだというところだが、どうやら下記のような動作をするらしい。
task :hoge do puts "hoge1" end task :hoge do puts "hoge2" end
$ bundle exec rake hoge hoge1 hoge2
てっきり、Rubyのメソッド定義のようにあとで書いたものでオーバライドされるのかと思ってたら、どうやら登録された順に追加実行されるようだ。 動作の補足しづらくなるし、正直あまりお勧めしないし。というかこの書き方が起きないように防ぐべきなきがする。
まとめ
今回もあわせて、全4回にわけて rake のソースリーディングを行ってきたのだが、やってみた感想としては、一見単純そうなものでも沢山のクラスで構成されていた。
特に文字列解析や、オプションによる分岐が必要になる箇所は、厚めにクラス分けが行われており、読んでいて感心した。
特に、CLIのツールを0から読み解くのは初めてだったこともあり、コマンドパーサの解析や、オプションによる処理分岐、ファイル読み込み、実行処理といった流れや分け方はかなり勉強になった。
ソースリーディングとしては、以上だが時間に余裕があったら、ここで得た知識をもとにrakeもどきでも作ってみようと思う。