Capistranoのコードリーディングをしてみました。今回はCapistranoが利用しているRakeの基本部分の紹介をします。
概要
コードの概要をまずはざっくり- Rake::Applicationを継承したのがCapistranoなので基本的にRakeタスクを走らせるのと同じ感じで処理が進む
- Rakefile => Capfileだったり、環境設定のタスクを定義したりするのが違うところ
 
 - 以下の処理を実行している
- パラメータ初期化
 - タスクファイルの読み込み
 - タスクの実行
 
 - 環境のセットアップ自体がタスクになっている。例えばconfig/deploy/staging.rbを読み込むとstagingというタスクが作成される。引数の順にタスクが実行される仕様なので、環境による変数設定のタスク => メインタスクの実行という流れで処理が行われる。
cap staging deployコマンドを叩くのはstagingとdeployというタスクを順に実行していることになる
 
コードリーディング
まず、Capistrano::Application.new.runが走ります。#!/usr/bin/env ruby
require "capistrano/all"
Capistrano::Application.new.run
Capistrano::ApplicationはRake::Applicationを継承しています。
module Capistrano
  class Application < Rake::Application
    def initialize
      super
      @rakefiles = %w{capfile Capfile capfile.rb Capfile.rb} << capfile
    end
...
runメソッドでは継承元のRake::Applicationを呼び出します。
def run
  Rake.application = self
  super
end
runメソッドはinit/load_rakefile/top_levelの3つに処理が分かれています。
def run
  standard_exception_handling do
    init
    load_rakefile
    top_level
  end
end
initメソッド
standard_exception_handlingはエラーハンドリングをラップするメソッドになります。# Provide standard exception handling for the given block.
def standard_exception_handling # :nodoc:
  yield
rescue SystemExit
  # Exit silently with current status
  raise
rescue OptionParser::InvalidOption => ex
  $stderr.puts ex.message
  exit(false)
rescue Exception => ex
  # Exit with error message
  display_error_message(ex)
  exit_because_of_exception(ex)
end
initは@top_level_tasksにcapコマンドの引数の環境名、タスク名を格納する処理です。
# Initialize the command line parameters and app name.
def init(app_name="rake")
  standard_exception_handling do
    @name = app_name
    args = handle_options
    collect_command_line_tasks(args)
  end
end
def collect_command_line_tasks(args) # :nodoc:
  @top_level_tasks = []
  args.each do |arg|
    if arg =~ /^(\w+)=(.*)$/m
      ENV[$1] = $2
    else
      @top_level_tasks << arg unless arg =~ /^-/
    end
  end
  @top_level_tasks.push(default_task_name) if @top_level_tasks.empty?
end
handle_options、standard_rake_options経由で呼び出されるsort_optionsメソッドはオーバーライドされていて、capistrano独自のオプションを扱えるようになっています。
def sort_options(options)
  not_applicable_to_capistrano = %w(quiet silent verbose)
  options.reject! do |(switch, *)|
    switch =~ /--#{Regexp.union(not_applicable_to_capistrano)}/
  end
  super.push(version, dry_run, roles, hostfilter, print_config_variables)
end
load_rakefileメソッド
load_rakefileはraw_load_rakefileを呼び出します。rakefile(=Capfile)を検索して、Rake.load_rakefileを呼び出して、rakefileをロードします。def raw_load_rakefile # :nodoc:
  rakefile, location = find_rakefile_location
  if (! options.ignore_system) &&
      (options.load_system || rakefile.nil?) &&
      system_dir && File.directory?(system_dir)
    print_rakefile_directory(location)
    glob("#{system_dir}/*.rake") do |name|
      add_import name
    end
  else
    fail "No Rakefile found (looking for: #{@rakefiles.join(', ')})" if
      rakefile.nil?
    @rakefile = rakefile
    Dir.chdir(location)
    print_rakefile_directory(location)
    Rake.load_rakefile(File.expand_path(@rakefile)) if
      @rakefile && @rakefile != ""
    options.rakelib.each do |rlib|
      glob("#{rlib}/*.rake") do |name|
        add_import name
      end
    end
  end
  load_imports
end
find_rakefile_locationはCapfile(厳密には@rakefilesのファイル名)が見つかるまで上の階層に登っていき、Capfileがあるディレクトリとファイル名を返します。
def find_rakefile_location # :nodoc:
  here = Dir.pwd
  until (fn = have_rakefile)
    Dir.chdir("..")
    return nil if Dir.pwd == here || options.nosearch
    here = Dir.pwd
  end
  [fn, here]
ensure
  Dir.chdir(Rake.original_dir)
end
load_rakefileの実体はKernel#loadの呼び出しです。
def load_rakefile(path)
  load(path)
end
options.rakelib直下のrakeファイルを読み出していますが、実体はadd_importメソッドで@pending_importsに追加しています。importメソッドはCapfileでのlib/capistrano/tasks/*.rakeの読み出しに利用されていて、add_importメソッドを呼び出しています。
def import(*fns) # :doc:
  fns.each do |fn|
    Rake.application.add_import(fn)
  end
end
def add_import(fn) # :nodoc:
  @pending_imports << fn
end
raw_load_rakefileの最後にload_importsメソッドを呼び出していますが、ここで@pending_importsで加えたtaskファイルを読み出しています。loader.loadはDefautLoader#loadで、実体はRake#load_rakefile、つまりKernel#loadを呼び出します。
def load_imports # :nodoc:
  while fn = @pending_imports.shift
    next if @imported.member?(fn)
    fn_task = lookup(fn) and fn_task.invoke
    ext = File.extname(fn)
    loader = @loaders[ext] || @default_loader
    loader.load(fn)
    if fn_task = lookup(fn) and fn_task.needed?
      fn_task.reenable
      fn_task.invoke
      loader.load(fn)
    end
    @imported << fn
  end
end
loadによるRakeタスクの定義
CapistranoというよりはRakeタスクの定義の説明になります。require “rake” => require “rake/dsl_definition"で呼び出し元(Rake::Application.new.runの実行コンテキスト)のselfに対してRake::DSLモジュールのメソッドが定義されます。
# Extend the main object with the DSL commands. This allows top-level
# calls to task, etc. to work from a Rakefile without polluting the
# object inheritance tree.
self.extend Rake::DSL
namespaceメソッドでは引数を適切な文字列に変換し、Rake.application.in_namespaceを呼び出します。
def namespace(name=nil, &block) # :doc:
  name = name.to_s if name.kind_of?(Symbol)
  name = name.to_str if name.respond_to?(:to_str)
  unless name.kind_of?(String) || name.nil?
    raise ArgumentError, "Expected a String or Symbol for a namespace name"
  end
  Rake.application.in_namespace(name, &block)
end
scopeを定義するとScopeクラスというLinkedListクラスのインスタンスが@scopeに設定されます。namespaceのブロックを抜けると@scope.tailが呼び出され、親のscopeに戻ります。
def in_namespace(name)
  name ||= generate_name
  @scope = Scope.new(name, @scope)
  ns = NameSpace.new(self, @scope)
  yield(ns)
  ns
ensure
  @scope = @scope.tail
end
namespaceをネストするとScopeクラスのLinkedListが連結されていきます。直近でnamespace呼び出ししたものがLinkedListの先頭になるので、タスク名をnamespaceを考慮して書き出すときはLinkedListの逆順に出力すると、最初に定義した順にnamespaceが並びます。
def path
  map(&:to_s).reverse.join(":")
end
# Path for the scope + the named path.
def path_with_task_name(task_name)
  "#{path}:#{task_name}"
end
taskはdefine_taskメソッドを呼び出します。
def task(*args, &block) # :doc:
  Rake::Task.define_task(*args, &block)
end
def define_task(*args, &block)
  Rake.application.define_task(self, *args, &block)
end
Rake::TaskManagerにdefine_taskが定義されており、Rake::TaskManagerはRake::Applicationにincludeされています。
def define_task(task_class, *args, &block) # :nodoc:
  task_name, arg_names, deps = resolve_args(args)
  original_scope = @scope
  if String === task_name and
     not task_class.ancestors.include? Rake::FileTask
    task_name, *definition_scope = *(task_name.split(":").reverse)
    @scope = Scope.make(*(definition_scope + @scope.to_a))
  end
  task_name = task_class.scope_name(@scope, task_name)
  deps = [deps] unless deps.respond_to?(:to_ary)
  deps = deps.map { |d| Rake.from_pathname(d).to_s }
  task = intern(task_class, task_name)
  task.set_arg_names(arg_names) unless arg_names.empty?
  if Rake::TaskManager.record_task_metadata
    add_location(task)
    task.add_description(get_description(task))
  end
  task.enhance(deps, &block)
ensure
  @scope = original_scope
end
internメソッドで@tasksのハッシュに、タスク名をキーにRake::Taskクラスのインスタンスを格納します。
def intern(task_class, task_name)
  @tasks[task_name.to_s] ||= task_class.new(task_name, self)
end
enhanceメソッドで引数として与えられたブロックを@actionsに登録します。
# Enhance a task with prerequisites or actions.  Returns self.
def enhance(deps=nil, &block)
  @prerequisites |= deps if deps
  @actions << block if block_given?
  self
end
これによってタスク名 => タスク => アクション(ブロック)の紐付けがされます。
環境(stage)のロード
capistrano/setupをrequireすると、以下のコードによって環境登録用のタスクが登録されます。stages.each do |stage|
  Rake::Task.define_task(stage) do
    set(:stage, stage.to_sym)
    invoke "load:defaults"
    Rake.application["load:defaults"].extend(Capistrano::ImmutableTask)
    env.variables.untrusted! do
      load deploy_config_path
      load stage_config_path.join("#{stage}.rb")
    end
    configure_scm
    I18n.locale = fetch(:locale, :en)
    configure_backend
  end
end
loadメソッドによってconfig/deploy.rbとconfig/deploy/xxx.rbが読み出されます。
def stages
  names = Dir[stage_definitions].map { |f| File.basename(f, ".rb") }
  assert_valid_stage_names(names)
  names
end
def stage_definitions
  stage_config_path.join("*.rb")
end
def stage_config_path
  Pathname.new fetch(:stage_config_path, "config/deploy")
end
top_level
最後にtop_levelメソッドを呼び出して、タスクを実行します。options.show_tasks、options.show_prereqsともにfalseyな値の場合、top_level_tasks.eachが呼び出されます。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
def top_level_tasks
  if tasks_without_stage_dependency.include?(@top_level_tasks.first)
    @top_level_tasks
  else
    @top_level_tasks.unshift(ensure_stage.to_s)
  end
end
tasks_without_stage_dependencyはstages + default_tasksになります。
cap staging helloと叩くと、@top_level_tasks.firstはstagingになるので、@top_level_tasksが返されます。
これらの引数文字列(cap stagin helloだとstagingとhelloが引数)を元にinvoke_taskが実行されます。parse_task_stringは引数とタスク名に分解します。
def invoke_task(task_string) # :nodoc:
  name, args = parse_task_string(task_string)
  t = self[name]
  t.invoke(*args)
end
self[]はタスク名からRake::Taskクラスを取得します。self.lookupはRake::TaskManagerで格納した@tasksのハッシュからタスク名をキーにRake::Taskクラスを取得しています。
def [](task_name, scopes=nil)
  task_name = task_name.to_s
  self.lookup(task_name, scopes) or
    enhance_with_matching_rule(task_name) or
    synthesize_file_task(task_name) or
    fail "Don't know how to build task '#{task_name}' (see --tasks)"
end
Rake::Task#invoke => Rake::Task#invoke_with_call_chain => Rake::Task#executeを呼び出します。
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
executeメソッドではタスクの@actions(ブロックの配列)が引数付きで呼び出されます。
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 { |act| act.call(self, args) }
end
全体の流れはこんな感じ。
次回はon、execute周りのDSLの解説をする予定です!