2017-04-17

Capistranoコードリーディング [基本]

Capistranoのコードリーディングをしてみました。今回はCapistranoが利用しているRakeの基本部分の紹介をします。

概要

コードの概要をまずはざっくり

コードリーディング

まず、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の解説をする予定です!

このエントリーをはてなブックマークに追加