ひとりRails読書会(1)
なんてことないんですが、ひとりでRailsのコードを読んでいくことで、「Rubyの暗黒面-eval heaven-」に到達するのが目的です。ワクワク。バージョンは2.1.1です。
今回は、不思議なおまじない
# rails hoge
したときに何が起こっているかについて追っていきます!
まず、どこのrailsを呼び出しているか見てみましょう。
# which rails /var/lib/gems/1.8/bin/rails
なるへそ。gem以下のbinを呼んでいると。該当ファイルを読んでみましょう。
#!/usr/bin/ruby1.8 # # This file was generated by RubyGems. # # The application 'rails' is installed as part of a gem, and # this file is here to facilitate running it. # require 'rubygems' version = ">= 0" # gems と rails のバージョンをチェックする if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then version = $1 ARGV.shift end # 使用するバージョンのrailsをrequire。ちなみにloadは既にライブラリが読み込まれていても読み込みます。 gem 'rails', version load 'rails'
あらま、基本的な引数チェックしかしてませんね。本体はきっとgemの下だろう、ということでfindして探してみると/var/lib/gems/1.8/gems/rails-2.1.1/bin/railsがありますね!読みながらコメントつけてってみました:
# まぁ、バージョンチェックしてるんでしょうね。 require File.dirname(__FILE__) + '/../lib/ruby_version_check' # Module Signalを使って、INTのシグナルハンドラを登録。ブロックをシグナルハンドラとして登録できるとは、素敵ですね。 Signal.trap("INT") { puts; exit } # --versionオプションの解釈。 # バージョン情報を別ファイルで管理することで、DRYなコードに仕上がっていますね。 require File.dirname(__FILE__) + '/../lib/rails/version' if %w(--version -v).include? ARGV.first # バージョンを表示しておしまい。 puts "Rails #{Rails::VERSION::STRING}" exit(0) end # freeze オプションがついていたら freeze フラグを立てる。多分あとできいてくる...はず。 freeze = ARGV.any? { |option| %w(--freeze -f).include?(option) } app_path = ARGV.first require File.dirname(__FILE__) + '/../lib/rails_generator' require 'rails_generator/scripts/generate' # これは別ファイルを読まないとわからん。ここを追ってみよう Rails::Generator::Base.use_application_sources! Rails::Generator::Scripts::Generate.new.run(ARGV, :generator => 'app') # freeze オプションが指定されていたらrakeするらしい。興味ないのでパス Dir.chdir(app_path) { `rake rails:freeze:gems`; puts "froze" } if freeze
どうみてもrails_generatorが本体ですね。findかけたところ、/var/lib/gems/1.8/gems/rails-2.1.1/lib/rails_generatorに関連ファイルがごろごろ転がっていることが分かりました。base.rbを読めばとっかかりがつかめそうなので読んでみると、コメントがいっぱいです。
module Rails # Rails::GeneratorはRailsのコード生成プラットフォーム! # これにより、model、controllerみたいなコンポーネントをホイホイ追加/削除できるよ! # てきなことがつらつらと。 # 実例を見たい場合はrails_gemerator/generatorsをみてくれ!
ほう。実例ってのが何の実例なのかいまいちわからんが、まぁ見てみるか。とりあえず、概要をつかむために伝家の宝刀treeコマンド!
tozawa@oza:/var/lib/gems/1.8/gems/rails-2.1.1/lib/rails_generator$ tree generators/ generators/ |-- applications | `-- app | |-- USAGE | `-- app_generator.rb `-- components |-- controller | |-- USAGE | |-- controller_generator.rb | `-- templates | |-- controller.rb | |-- functional_test.rb | |-- helper.rb | `-- view.html.erb |-- integration_test | |-- USAGE | |-- integration_test_generator.rb | `-- templates | `-- integration_test.rb |-- mailer | |-- USAGE | |-- mailer_generator.rb | `-- templates | |-- fixture.erb | |-- fixture.rhtml | |-- mailer.rb | |-- unit_test.rb | |-- view.erb | `-- view.rhtml |-- migration | |-- USAGE | |-- migration_generator.rb | `-- templates | `-- migration.rb |-- model | |-- USAGE | |-- model_generator.rb | `-- templates | |-- fixtures.yml | |-- migration.rb | |-- model.rb | `-- unit_test.rb |-- observer | |-- USAGE | |-- observer_generator.rb | `-- templates | |-- observer.rb | `-- unit_test.rb |-- plugin | |-- USAGE | |-- plugin_generator.rb | `-- templates | |-- MIT-LICENSE | |-- README | |-- Rakefile | |-- USAGE | |-- generator.rb | |-- init.rb | |-- install.rb | |-- plugin.rb | |-- tasks.rake | |-- uninstall.rb | `-- unit_test.rb |-- resource | |-- USAGE | |-- resource_generator.rb | `-- templates | |-- controller.rb | |-- functional_test.rb | `-- helper.rb |-- scaffold | |-- USAGE | |-- scaffold_generator.rb | `-- templates | |-- controller.rb | |-- functional_test.rb | |-- helper.rb | |-- layout.html.erb | |-- style.css | |-- view_edit.html.erb | |-- view_index.html.erb | |-- view_new.html.erb | `-- view_show.html.erb `-- session_migration |-- USAGE |-- session_migration_generator.rb `-- templates `-- migration.rb
おおっと、見慣れた単語がでてきましたね。悪名高きscaffoldとか、controller、model、などなど。この段階では、applications側とcomponents側の違いがイマイチ分からない感じなので、applications/app側をみてみます。まずはUSAGEから。
説明: railsコマンドは、ユーザが指定したディレクトリに新規プロジェクトを作成します。 例: rails ~/Code/Ruby/weblog 続きは新規アプリケーションの中にあるREADMEをよんでね!
入門書を読むと出てくるコマンドですが、こんなところで説明がされています。/var/lib/以下をrails初心者は見るのだろうか...w それと、ディレクトリ名に大文字が入っていますが、この辺ドキュメント作成者の趣味が分かっておもしろいです。というわけで本体を追っかけてみましょう。app_generator.rb です。
Rails::Generator::Base.use_application_sources! を探して読む
class AppGenerator < Rails::Generator::Base # 色々書いてある。 end
まてまて、今探しているRails::Generator::Base.use_application_sources!がないぞ。親クラスで定義されているに違いない。base.rbをみてみよう。
module Rails module Generator module Base # しかし、みつからなかった! end end end
えー!仕方がないので、find . | xargs grep use_appli で探してみる。rails-2.1.1/lib/rails_generator/lookup.rbにあることがわかりました。
module Rails # hogehoge module Generator # hugahuge module Lookup # foo foo module ClassMethods # Reset the source list. def reset_sources write_inheritable_attribute(:sources, []) invalidate_cache! end # Use application generators (app, ?). def use_application_sources! reset_sources sources << PathSource.new(:builtin, "#{File.dirname(__FILE__)}/generators/applications") end ...
ありましたね!とりあえずreset_sourcesの動きを見てみます。んー、write_inheritable_attributeってなんなんでしょう。railsの中で定義されている様子はないので、gemのライブラリをfindで全部探索しますw
すると、activesupportというライブラリの中で定義されていることがわかります*1。activerecordは馴染み深いですが、activesupportってなんなんでしょう。ぐぐると、
active supportはrailsを便利にするさまざまなユーティリティクラスと標準ライブラリの拡張を集めたものです
参考:http://wiki.fdiary.net/rails/?ActiveSupport
などと出てきます!あれか、黒魔術の塊はおまえか!後でよんでやる!とおもいつつ、とりあえずwrite_inheritable_attributeの部分だけ抜き出します。
def write_inheritable_attribute(key, value) if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES) @inheritable_attributes = {} end inheritable_attributes[key] = value end
あー、sourceという名前の配列に空配列を入れているのですね。ちなみに、invalidate_cacheはrails側で定義されており、
# $RAILS_ROOT/lib/lookup.rb # Clear the cache whenever the source list changes. def invalidate_cache! @cache = nil end
となっています。これはわかりやすい。cacheメンバが何をやっているかはよく分かりませんが*2、とりあえずほっておきましょう。つまり、reset_sourcesは前に生成したファイルなどのキャッシュを消しているようです。なぜかはよくわかりませんが、ないと問題が生じるのでしょう、多分。次は sources << PathSource.new(:builtin, "#{File.dirname(__FILE__)}/generators/applications")を追います。PathSourceはlib/rails_generatar/lookup.rbで定義されています。
# Sources enumerate (yield from #each) generator specs which describe # where to find and how to create generators. Enumerable is mixed in so, # for example, source.collect will retrieve every generator. # Sources may be assigned a label to distinguish them. class Source include Enumerable attr_reader :label def initialize(label) @label = label end # The each method must be implemented in subclasses. # The base implementation raises an error. def each raise NotImplementedError end # Return a convenient sorted list of all generator names. def names map { |spec| spec.name }.sort end end # PathSource looks for generators in a filesystem directory. class PathSource < Source attr_reader :path def initialize(label, path) super label @path = path end # Yield each eligible subdirectory. def each Dir["#{path}/[a-z]*"].each do |dir| if File.directory?(dir) yield Spec.new(File.basename(dir), dir, label) end end end end
initializeのとこだけ見てみましょう。ラベルに:builtin、pathにはlib/rails_generator/generators/applicationsが渡されています。pathはメンバ変数@pathに渡され、外部参照できるようになっています。きっとこのメンバ変数を使ってファイルを生成するのでしょう。
Rails::Generator::Scripts::Generate.new.run(ARGV, :generator => 'app')を追う
さて、railsコマンドの挙動追跡も大詰めです。lib/rails_generator/scripts.rbをみてみると、件の関数があります。コメントをつけながらちゃんと読んでみます。
class Base include Options default_options :collision => :ask, :quiet => false # ジェネレータスクリプトを起動する。 def run(args = [], runtime_options = {}) begin parse!(args.dup, runtime_options) rescue OptionParser::InvalidOption => e # ジェネレータからみてユーザの処理は不正なときにここにくる end # ジェネレータの名前は必須。今回の場合はappが渡されている。 unless options[:generator] usage if args.empty? options[:generator] ||= args.shift end # ジェネレータインスタンスを見つけてコマンドを起動する。 Rails::Generator::Base.instance(options[:generator], args, options).command(options[:command]).invoke! rescue => e puts e puts " #{e.backtrace.join("\n ")}\n" if options[:backtrace] raise SystemExit end
ここにきて、$RIALS_ROOT/lib/rails_generator/generators/applications/appのメソッドが呼ばれていることがわかりました。
ついに本体!Rails::Generator::Base.instance(options[:generator], args,options).command(options[:command]).invoke!を追う
なんかここまでくるとdelegationとかしてそうなのでいきなりinvokeをgrepします。すると
# lib/rails_generator/commands.rb class Base < DelegateClass(Rails::Generator::Base) # Replay action manifest. RewindBase subclass rewinds manifest. def invoke! manifest.replay(self) end
なんてものが見つかって、ああ、やはり。と言う感じになります。本体はmanifestってわけです。manifestは「現れる」みたいな感じの意味があるので、ファイルが登場!っといったところでしょうか。manifestはlib/rails_generator/generators/以下のディレクトリの末端の数分だけ存在しますが、今回はapplications/appだけおっかけます。さすがに疲れてきたしw
# lib/rails_generator/generators/applications/ap def manifest # Use /usr/bin/env if no special shebang was specified script_options = { :chmod => 0755, :shebang => options[:shebang] == DEFAULT_SHEBANG ? nil : options[:shebang] } dispatcher_options = { :chmod => 0755, :shebang => options[:shebang] } # duplicate CGI::Session#generate_unique_id md5 = Digest::MD5.new now = Time.now md5 << now.to_s md5 << String(now.usec) md5 << String(rand(0)) md5 << String($$) md5 << @app_name # Do our best to generate a secure secret key for CookieStore secret = Rails::SecretKeyGenerator.new(@app_name).generate_secret record do |m| # Root directory and all subdirectories. m.directory '' BASEDIRS.each { |path| m.directory path } # Rootディレクトリのファイル生成 m.file "fresh_rakefile", "Rakefile" m.file "README", "README" # Applicationディレクトリのファイル生成 m.template "helpers/application.rb", "app/controllers/application.rb", :assigns => { :app_name => @app_name, :app_secret => md5.hexdigest } m.template "helpers/application_helper.rb", "app/helpers/application_helper.rb" m.template "helpers/test_helper.rb", "test/test_helper.rb" # database.yml and routes.rbを生成 m.template "configs/databases/#{options[:db]}.yml", "config/database.yml", :assigns => { :app_name => @app_name, :socket => options[:db] == "mysql" ? mysql_socket_location : nil } m.template "configs/routes.rb", "config/routes.rb" # Initializersを生成 m.template "configs/initializers/inflections.rb", "config/initializers/inflections.rb" m.template "configs/initializers/mime_types.rb", "config/initializers/mime_types.rb" m.template "configs/initializers/new_rails_defaults.rb", "config/initializers/new_rails_defaults.rb" # Environmentsを生成 m.file "environments/boot.rb", "config/boot.rb" m.template "environments/environment.rb", "config/environment.rb", :assigns => { :freeze => options[:freeze], :app_name => @app_name, :app_secret => secret } m.file "environments/production.rb", "config/environments/production.rb" m.file "environments/development.rb", "config/environments/development.rb" m.file "environments/test.rb", "config/environments/test.rb" %w( about console dbconsole destroy generate performance/benchmarker performance/profiler performance/request process/reaper process/spawner process/inspector runner server plugin ).each do |file| m.file "bin/#{file}", "script/#{file}", script_options end # Dispatches m.file "dispatches/dispatch.rb", "public/dispatch.rb", dispatcher_options m.file "dispatches/dispatch.rb", "public/dispatch.cgi", dispatcher_options m.file "dispatches/dispatch.fcgi", "public/dispatch.fcgi", dispatcher_options # HTML files %w(404 422 500 index).each do |file| m.template "html/#{file}.html", "public/#{file}.html" end m.template "html/favicon.ico", "public/favicon.ico" m.template "html/robots.txt", "public/robots.txt" m.file "html/images/rails.png", "public/images/rails.png" # Javascripts m.file "html/javascripts/prototype.js", "public/javascripts/prototype.js" m.file "html/javascripts/effects.js", "public/javascripts/effects.js" m.file "html/javascripts/dragdrop.js", "public/javascripts/dragdrop.js" m.file "html/javascripts/controls.js", "public/javascripts/controls.js" m.file "html/javascripts/application.js", "public/javascripts/application.js" # Docs m.file "doc/README_FOR_APP", "doc/README_FOR_APP" # Logs %w(server production development test).each { |file| m.file "configs/empty.log", "log/#{file}.log", :chmod => 0666 } end end
生成の直前にrecordメソッドを呼び出していますが、これはbase.rbで定義されている簡単なメソッドで、これを用いることで簡単にレコード*3を生成できるらしいです。コメントによると。
いやぁ、これでなんとなく動きがつかめましたね。
残された疑問
なぜRails::Generator::Lookup::ClassMethodsに所属しているはずのメソッドがRails::Generator::Base.use_application_sources!で呼び出せるんでしょうか。
まとめ
今回は、railsコマンドの詳細をおってみました。単なるコマンドかとおもいきや、意外と複雑であることがわかりました。多分コード量を抑えるためにこういう構成になっているのだろうと推測します。今後は疑問の部分に迫ってみて、その後でeval heavenと推測されるactivesupportに迫ってみたいと考えています。