2010-03-06
generatorの仕組みを解読する
Rails3.0 | |
Rails3.0.0betaが出ていて仕事でFW作ることになったのでRailsのgeneratorの仕組みを参考にしたい。
環境
bash-3.2$ rvm use Now using ruby 1.9.1 p378 bash-3.2$ rails -v Rails 3.0.0.beta
流れを追うコマンド
bash-3.2$ rails pochi -d mysql create create README create .gitignore create Rakefile create config.ru create Gemfile create app create app/controllers/application_controller.rb create app/helpers/application_helper.rb create app/models create app/views/layouts create config create config/routes.rb create config/application.rb create config/environment.rb ... create test/integration create test/unit create tmp create tmp/sessions create tmp/sockets create tmp/cache create tmp/pids create vendor/plugins create vendor/plugins/.gitkeep
railsコマンドの中身
railsのバージョンだけ指定して後はGem.bin_pathを呼んでいることが分かる。
require 'rubygems' version = ">= 0" if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then version = $1 ARGV.shift end gem 'rails', version load Gem.bin_path('railties', 'rails', version)
Gem#bin_path
Gem.bin_path(name, exec_name, version)
今日はRailsのgeneratorに着目するのであまり気にしない。
gemから実行時のフルパスを取得する。
さっきのrailsのコマンドをたたいた場合、結局以下が呼ばれてることになる(はず)
load "#{railties_home}/bin/rails"
Railitiesのrailsコマンド
if File.exists?(Dir.getwd + '/script/rails') exec(Dir.getwd + '/script/rails', *ARGV) else railties_path = File.expand_path('../../lib', __FILE__) $:.unshift(railties_path) if File.directory?(railties_path) && !$:.include?(railties_path) require 'rails/ruby_version_check' Signal.trap("INT") { puts; exit } require 'rails/commands/application' end
簡単な流れは以下。
1. 実行ディレクトリ以下に"/script/rails"があればそれを実行 2. それがない場合(要はRailsアプリを新規作成する場合) 2-1. railtiesパスがLOAD_PATHになければ追加 2-2. rubyのバージョンをチェック 2-3. rails/commands/applicationを呼ぶ
ruby_version_check.rbは以下。(1.8.7以上を有効にしてる)
min_release = "1.8.7" ruby_release = "#{RUBY_VERSION} (#{RUBY_RELEASE_DATE})" if ruby_release < min_release abort <<-end_message Rails requires Ruby version #{min_release} or later. You're running #{ruby_release}; please upgrade to continue. end_message end
#{railties_path}/lib/rails/commands/application
require 'rails/version' if %w(--version -v).include? ARGV.first puts "Rails #{Rails::VERSION::STRING}" exit(0) end ARGV << "--help" if ARGV.empty? require 'rubygems' if ARGV.include?("--dev") require 'rails/generators' require 'generators/rails/app/app_generator' Rails::Generators::AppGenerator.start
以下のことしてます。
1. バージョンオプションはいってる場合はversion出しておしまい 2. 引数がなかった場合は"--help"オプションを付け足す 3. "--dev"があった場合はrubygems読み込み(ライブラリ単体で開発テストしたいときは必要なのかも) 4. 必要はライブラリ読み込んでRails::Gnerators::AppGeneratot.start
ここまでは、なんとなく予想範囲な実装コードだ。
Rails::Generators::AppGenerator.start
このstartメソッドはThorというgemから呼ばれている。
クラス階層はこんな感じ。
----------- Thor::Base ----------- Thor::Group ----------- Rails::Generators::Base ----------- Rails::Generators::AppGenerator -----------
ちょっとややこしいけどstartメソッドはThor::Baseメソッドが最初に呼ばれている。
中身は以下。
def start(given_args=ARGV, config={})
self.debugging = given_args.include?("--debug")
config[:shell] ||= Thor::Base.shell.new
yield(given_args.dup)
rescue Thor::Error => e
debugging ? (raise e) : config[:shell].error(e.message)
exit(1) if exit_on_failure?
end
コード読むと以下のようなことしてるはず。
1. debbugモードなのかどうかをオプションから判断 2. confi[:shell]にThor::Base.shellオブジェクトを入れる 3. yield(Thor::Group.startに処理を渡す)
じゃあ次に見るのはThor::Group.start
def start(original_args=ARGV, config={})
super do |given_args|
if Thor::HELP_MAPPINGS.include?(given_args.first)
help(config[:shell])
return
end
args, opts = Thor::Options.split(given_args)
new(args, opts, config).invoke
end
end
やってるのは以下のこと。
1. Thor::HELP_MAPPINGS(%w(-h -? --help -D))が入っていればshelllからメッセージを出力 2. オプション解析 3. newしてinvokeを呼ぶ
2,3をちょっと詳しく見ていく。
2のオプション解析部分はthor/lib/thor/parser/arguments.rbにある。
def self.split(args)
arguments = []
args.each do |item|
break if item =~ /^-/
arguments << item
end
return arguments, args[Range.new(arguments.size, -1)]
end
最初のアプリケーション名をargumentsにいれてそれ以外はオプション配列にうめこんでいる。
次に3を見る。
def initialize(args=[], options={}, config={})
args = Thor::Arguments.parse(self.class.arguments, args)
args.each { |key, value| send("#{key}=", value) }
parse_options = self.class.class_options
if options.is_a?(Array)
task_options = config.delete(:task_options) # hook for start
parse_options = parse_options.merge(task_options) if task_options
array_options, hash_options = options, {}
else
array_options, hash_options = [], options
end
opts = Thor::Options.new(parse_options, hash_options)
self.options = opts.parse(array_options)
opts.check_unknown! if self.class.check_unknown_options?
end
僕のRuby力が足らなくてself.class.argumentsを読むのがやたら難しかったんだけど、
ここでいうself.classはRails::Generators:::Appgeneratorをさす。ただそこにargumentsメソッドはなく呼ばれるのはThor::Base.argumentsを呼んでいる。
def arguments
@arguments ||= from_superclass(:arguments, [])
end
def from_superclass(method, default=nil)
if self == baseclass || !superclass.respond_to?(method, true)
default
else
value = superclass.send(method)
value.dup if value
end
end
from_superclassはsuperclassにメソッド呼び出しできるか聞きにいき、いた場合はそれを呼んだ結果を、それ以外は引数(true/false)を返すようにしている。
で結局self.class.argumentsを呼ぶとArray[Thor::Argument]みたいなのが帰ってくる。
[#<Thor::Argument:0x72f3c8 @name="app_path", @description=nil, @required=true, @type=:string, @default=nil, @banner="APP_PATH">]
でそれをもとにThor::Arguments.parse(self.class.arguments, args)を呼ぶ。
def initialize(arguments=[])
@assigns, @non_assigned_required = {}, []
@switches = arguments
arguments.each do |argument|
if argument.default
@assigns[argument.human_name] = argument.default
elsif argument.required?
@non_assigned_required << argument
end
end
end
def parse(args)
@pile = args.dup
@switches.each do |argument|
break unless peek
@non_assigned_required.delete(argument)
@assigns[argument.human_name] = send(:"parse_#{argument.type}", argument.human_name)
end
check_requirement!
@assigns
end
ここでオプションを返してくれる。({app_name => "hoge"}みたいな)
もいっかい3のnew部分をまとめておく。
1. アプリケーション名をセットする 2. その他Railsオプションを解析する 3. Thor::Optionsに2のものを詰めてparse
んー、めちゃくちゃややこしいですね。
Thorは後でしっかり読む必要があるかも。
newまでで何ができるかというと、例えば以下のコマンドをたたきます。
rails pochi -d mysql -q
そうするとnewした際は以下のオブジェクトができています。
#<Rails::Generators::AppGenerator:0x728764 @behavior=:invoke, @_invocations={}, @_initializer=[["pochi"], ["-d", "mysql", "-q"], {:shell=>#<Thor::Shell::Color:0x728924 @base=nil, @padding=0>}], @app_path="pochi", @options={"ruby"=>"/Users/kuro/.rvm/rubies/ruby-1.9.1-p378/bin/ruby", "database"=>"mysql", "quiet"=>true}>
それではinvokeを呼んだみたいと思います。
invokeはThor::Baseのクラスメソッドであるthor/lib/thor/invocation.rbにあるものが呼ばれる。
def invoke(name=nil, *args)
args.unshift(nil) if Array === args.first || NilClass === args.first
task, args, opts, config = args
object, task = _prepare_for_invocation(name, task)
klass, instance = _initialize_klass_with_initializer(object, args, opts, config)
method_args = []
current = @_invocations[klass]
iterator = proc do |_, task|
unless current.include?(task.name)
current << task.name
task.run(instance, method_args)
end
end
if task
args ||= []
method_args = args[Range.new(klass.arguments.size, -1)] || []
iterator.call(nil, task)
else
klass.all_tasks.map(&iterator)
end
end
1. 事前処理 2. プロック処理を用意 3. Rails generatorオブジェクトが持つタスクを全てiteratorに処理させる
この3のとこで実際にRailsアプリケーションのひな形ができます。
(Thor::Taskオブジェクトがいっぱいつまってる)
思ったより長かった。Thorをしっかり読まないとよくわかんないことがわかった。
- 428 http://pipes.yahoo.com/pipes/pipe.info?_id=tDfBdGWF3RGl9XNm1L3fcQ
- 21 http://pipes.yahoo.com/pipes/pipe.info?_id=12e453e301454b799b3ac6642aa089b5
- 14 http://pipes.yahoo.com/pipes/pipe.info?_id=5c957097ed152660234169b605fb3fa7
- 5 http://search.minakoe.jp/rsss/rsss.asp?qry=ruby&multi=1
- 4 http://pipes.yahoo.com/pipes/pipe.info?_id=VPw6npu13RGKo15vBRNMsA
- 3 http://reader.livedoor.com/reader/
- 2 http://www.google.co.jp/m/search?oe=UTF-8&client=safari&q=rails3.0&hl=ja&start=10&sa=N
- 2 http://www.google.co.jp/reader/view/
- 1 http://b.hatena.ne.jp/entry/d.hatena.ne.jp/POCHI_BLACK/20100226
- 1 http://d.hatena.ne.jp/diarylist?of=100&mode=rss&type=public