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