railsを起動するとgemはどのように読み込まれてんのか

railsを起動するとgemはどのように読み込まれてんのか

rails new new_application

をした後、

bundle install

のようにrailsアプリを動かすのに必要なgemをインストールするわけだけど、 インストールした後、

rails server

で起動した後にどのように gemをインストールされるかを追ってみる。

1. bin/rails

railsコマンドが実行されると、config/applicationが呼ばれる。

#!/usr/bin/env ruby
APP_PATH = File.expand_path('../../config/application', __FILE__)
require_relative '../config/boot'
require 'rails/commands'

2. config/application

一行目で、config/bootが呼ばれる。 そのあとにBundler.requireが呼ばれている。

require File.expand_path('../boot', __FILE__)

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
・・・省略

3. config/boot

config/bootでは主にbundler/setupを呼んでいる。

ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)

require 'bundler/setup' # Set up gems listed in the Gemfile.

4. bundler-1.11.2/lib/bundler/setup.rb

Gemfileの確認を行い、OKならBundler.setupを実行。

if Bundler::SharedHelpers.in_bundle?
  require "bundler"

  if STDOUT.tty? || ENV["BUNDLER_FORCE_TTY"]
    begin
      Bundler.setup
    rescue Bundler::BundlerError => e
      puts "\e[31m#{e.message}\e[0m"
      puts e.backtrace.join("\n") if ENV["DEBUG"]
      if e.is_a?(Bundler::GemNotFound)
        puts "\e[33mRun `bundle install` to install missing gems.\e[0m"
      end
      exit e.status_code
    end
  else
    Bundler.setup
  end

  # Add bundler to the load path after disabling system gems
  bundler_lib = File.expand_path("../..", __FILE__)
  $LOAD_PATH.unshift(bundler_lib) unless $LOAD_PATH.include?(bundler_lib)

  Bundler.ui = nil
end

5. bundler-1.11.2/lib/bundler.rb

load.setupを実行。

    def setup(*groups)
      # Just return if all groups are already loaded
      return @setup if defined?(@setup)

      definition.validate_ruby!

      if groups.empty?
        # Load all groups, but only once
        @setup = load.setup
      else
        load.setup(*groups)
      end
    end

load = Runtimeインスタンスですね。

    def load
      @load ||= Runtime.new(root, definition)
    end

6. bundler-1.11.2/lib/bundler/runtime.rb

specってのが、GemfileやGemfile.lockをパースしてオブジェクトにした
各gemの情報(依存関係も含む)になってる。
例えば、railsが依存しているsdocはjsonとrdocに依存しているけれど、 jsonやrdocまで$LOAD_PATHが通る感じ。

    sdoc (0.4.1)
      json (~> 1.7, >= 1.7.7)
      rdoc (~> 4.0)
    def setup(*groups)
      groups.map!(&:to_sym)

      # Has to happen first
      clean_load_path

      specs = groups.any? ? @definition.specs_for(groups) : requested_specs

      setup_environment
      Bundler.rubygems.replace_entrypoints(specs)

      # Activate the specs
      specs.each do |spec|
        unless spec.loaded_from
          raise GemNotFound, "#{spec.full_name} is missing. Run `bundle` to get it."
        end

        if (activated_spec = Bundler.rubygems.loaded_specs(spec.name)) && activated_spec.version != spec.version
          e = Gem::LoadError.new "You have already activated #{activated_spec.name} #{activated_spec.version}, " \
                                 "but your Gemfile requires #{spec.name} #{spec.version}. Prepending " \
                                 "`bundle exec` to your command may solve this."
          e.name = spec.name
          if e.respond_to?(:requirement=)
            e.requirement = Gem::Requirement.new(spec.version.to_s)
          else
            e.version_requirement = Gem::Requirement.new(spec.version.to_s)
          end
          raise e
        end

        Bundler.rubygems.mark_loaded(spec)
        load_paths = spec.load_paths.reject {|path| $LOAD_PATH.include?(path) }
        $LOAD_PATH.unshift(*load_paths)
      end

      setup_manpath

      lock(:preserve_bundled_with => true)

      self
    end

一応、require 'bundler/setup'が、ちゃんと$LOAD_PATHに変化を与えているかを検証してみた。

irb(main):002:0> $LOAD_PATH
=> ["/Users/onody/.rbenv/rbenv.d/exec/gem-rehash", "/Users/onody/.rbenv/versions/2.2.2/lib/ruby/site_ruby/2.2.0", "/Users/onody/.rbenv/versions/2.2.2/lib/ruby/site_ruby/2.2.0/x86_64-darwin15", "/Users/onody/.rbenv/versions/2.2.2/lib/ruby/site_ruby", "/Users/onody/.rbenv/versions/2.2.2/lib/ruby/vendor_ruby/2.2.0", "/Users/onody/.rbenv/versions/2.2.2/lib/ruby/vendor_ruby/2.2.0/x86_64-darwin15", "/Users/onody/.rbenv/versions/2.2.2/lib/ruby/vendor_ruby", "/Users/onody/.rbenv/versions/2.2.2/lib/ruby/2.2.0", "/Users/onody/.rbenv/versions/2.2.2/lib/ruby/2.2.0/x86_64-darwin15"]
irb(main):003:0> require 'bundler/setup'
=> true
irb(main):004:0> $LOAD_PATH
=> ["/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/web-console-2.3.0/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/uglifier-2.7.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/turbolinks-2.5.3/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/spring-1.6.4/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/sdoc-0.4.1/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/sass-rails-5.0.4/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/tilt-2.0.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/sass-3.4.21/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/rdoc-4.2.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/rails-4.2.5.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/sprockets-rails-3.0.4/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/sprockets-3.5.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/extensions/x86_64-darwin-15/2.2.0-static/mysql2-0.4.3", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/mysql2-0.4.3/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/jquery-rails-4.1.0/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/jbuilder-2.4.1/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/multi_json-1.11.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/concurrent-ruby-1.0.1/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/coffee-rails-4.1.1/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/railties-4.2.5.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/thor-0.19.1/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/coffee-script-2.4.1/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/execjs-2.6.0/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/coffee-script-source-1.10.0/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/extensions/x86_64-darwin-15/2.2.0-static/byebug-8.2.2", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/byebug-8.2.2/lib", "/Users/onody/.rbenv/versions/2.2.2/lib/ruby/gems/2.2.0/gems/bundler-1.11.2/lib/gems/bundler-1.11.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/extensions/x86_64-darwin-15/2.2.0-static/binding_of_caller-0.7.2", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/binding_of_caller-0.7.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/extensions/x86_64-darwin-15/2.2.0-static/debug_inspector-0.0.2", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/debug_inspector-0.0.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/bundler/gems/acts_as_bits-26fc7abed0af/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/activerecord-4.2.5.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/arel-6.0.3/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/activemodel-4.2.5.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/actionmailer-4.2.5.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/mail-2.6.3/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/mime-types-2.99.1/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/activejob-4.2.5.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/globalid-0.3.6/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/actionpack-4.2.5.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/rack-test-0.6.3/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/rack-1.6.4/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/actionview-4.2.5.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/rails-html-sanitizer-1.0.3/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/loofah-2.0.3/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/rails-dom-testing-1.0.7/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/rails-deprecated_sanitizer-1.0.3/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/extensions/x86_64-darwin-15/2.2.0-static/nokogiri-1.6.7.2", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/nokogiri-1.6.7.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/mini_portile2-2.0.0/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/erubis-2.7.0/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/builder-3.2.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/activesupport-4.2.5.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/tzinfo-1.2.2/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/thread_safe-0.3.5/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/minitest-5.8.4/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/extensions/x86_64-darwin-15/2.2.0-static/json-1.8.3", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/json-1.8.3/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/i18n-0.7.0/lib", "/Users/onody/Projects/test/rails_test/vendor/bundle/ruby/2.2.0/gems/rake-10.5.0/lib", "/Users/onody/.rbenv/rbenv.d/exec/gem-rehash", "/Users/onody/.rbenv/versions/2.2.2/lib/ruby/gems/2.2.0/gems/bundler-1.11.2/lib", "/Users/onody/.rbenv/versions/2.2.2/lib/ruby/site_ruby/2.2.0", "/Users/onody/.rbenv/versions/2.2.2/lib/ruby/site_ruby/2.2.0/x86_64-darwin15", "/Users/onody/.rbenv/versions/2.2.2/lib/ruby/site_ruby", "/Users/onody/.rbenv/versions/2.2.2/lib/ruby/vendor_ruby/2.2.0", "/Users/onody/.rbenv/versions/2.2.2/lib/ruby/vendor_ruby/2.2.0/x86_64-darwin15", "/Users/onody/.rbenv/versions/2.2.2/lib/ruby/vendor_ruby", "/Users/onody/.rbenv/versions/2.2.2/lib/ruby/2.2.0", "/Users/onody/.rbenv/versions/2.2.2/lib/ruby/2.2.0/x86_64-darwin15"]

ちゃんとなっている。

7. bundler-1.11.2/lib/bundler.rb

3〜6までのステップで、bundler/setupでは
Gemfile,Gemfile.lockを解析して$LOAD_PATHに追加することがわかった。

その後、2の処理に戻ってみると、
Bundler.requireをして、読み込んでいるのだけど、
それを追ってみよう。

    def require(*groups)
      setup(*groups).require(*groups)
    end

ここでは、setup(前に出てきたBundler::Runtime)のrequireメソッドを呼んでいる。 ポイントはgroup。Gemfileで指定したgroupのみをrequireしているところ。

group :test do
  gem 'faker'
  gem 'rspec'
end

8. bundler-1.11.2/lib/bundler/runtime.rb

以下でdependenciesを一つづつrequireしています。 ややこしいのが、 dependenciesがこのrailsが依存しているgem情報で、 specが各gemがさらに依存しているgemの情報です。 これらはGemfile.lockを見るとわかると思います。

def require(*groups)
      groups.map!(&:to_sym)
      groups = [:default] if groups.empty?

      @definition.dependencies.each do |dep|
        # Skip the dependency if it is not in any of the requested
        # groups
        next unless (dep.groups & groups).any? && dep.current_platform?

        required_file = nil

        begin
          # Loop through all the specified autorequires for the
          # dependency. If there are none, use the dependency's name
          # as the autorequire.
          Array(dep.autorequire || dep.name).each do |file|
            # Allow `require: true` as an alias for `require: <name>`
            file = dep.name if file == true
            required_file = file
            begin
              Kernel.require file
            rescue => e
              raise e if e.is_a?(LoadError) # we handle this a little later
              raise Bundler::GemRequireError.new e,
                "There was an error while trying to load the gem '#{file}'."
            end
          end
        rescue LoadError => e
          REQUIRE_ERRORS.find {|r| r =~ e.message }
          raise if dep.autorequire || $1 != required_file

          if dep.autorequire.nil? && dep.name.include?("-")
            begin
              namespaced_file = dep.name.tr("-", "/")
              Kernel.require namespaced_file
            rescue LoadError => e
              REQUIRE_ERRORS.find {|r| r =~ e.message }
              raise if $1 != namespaced_file
            end
          end
        end
      end
    end

まとめ

  • rails起動時は、bundler/setupと、Bundler.requireを実行している。
  • setup => 対象のgemのパスを$LOAD_PATHに通す。ただし、Gemfileで呼んでいるgemの、さらに依存しているgem(spec)もパスを通す。
  • require => Gemfileで呼んでいるgemを読み込む。その先は読まない。引数で指定したgroupのgemのみが読み込まれる。