ひとり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というライブラリの中で定義されていることがわかります*1activerecordは馴染み深いですが、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とかしてそうなのでいきなりinvokegrepします。すると

# 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に迫ってみたいと考えています。

*1:確かにgem install rails すると一緒に入りますよね

*2:多分ファイル検索のキャッシュでしょう。毎回ファイル検索をすると遅いのでパスを配列を保存するのは、有効そうですし。

*3:多分ファイル生成したときにログが出ますよね?多分アレだと思う