kei-p3’s blog

kei-pによる技術共有と思考整理

Rakeを徹底解剖 - その4 "タスクの実行"

過去3回のソースリーディングに続き、今回はついにタスクの実行について調べていきます。

def run
  standard_exception_handling do
    init
    load_rakefile
    top_level ## 今回のキモになる部分
  end
end

なお、まだその1, その2、その3を読んでない方はこちらから。

kei-p3.hatenablog.com

kei-p3.hatenablog.com

kei-p3.hatenablog.com

タスクの実行

それでは、まず 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

最初の ifelsif に関しては、オプションによって それぞれ -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もどきでも作ってみようと思う。