Bundler.setupとrequire

Bundlerの仕組みを十分に理解しているとは言い難かったので、深堀りしてみたい。

Bundler.setup

http://bundler.io/bundler_setup.html

Configure the load path so all dependencies in your Gemfile can be required

まず最初にやるべきは依存関係にある全てのgemのロードパスを解決すること。Bundler.setupはまさにこの役割を担う。 少しコードを追って見る。

https://github.com/bundler/bundler/blob/v1.9.2/lib/bundler.rb#L114-L139

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

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

    ...

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

となっており、Runtimeのインスタンスを生成し、setupメソッドを実行していることがわかる。 さらに bundler/runtime.rbを見てみる。

https://github.com/bundler/bundler/blob/v1.9.2/lib/bundler/runtime.rb#L7-L47

    def setup(*groups)

      # Has to happen first
      clean_load_path

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

      setup_environment

      # Activate the specs
      specs.each do |spec|
        ...

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

      self
    end

specをロードするパスを、$LOAD_PATH に追加していることが分かる。 spec が具体的にどんなインスタンスなのかは、例えば 以下のようにすればよい。

[5] pry(main)> Bundler.definition.specs_for([:development]).first

Gem::Specification.new do |s|
  s.name = "rake"
  s.version = Gem::Version.new("10.4.2")
  s.installed_by_version = Gem::Version.new("0")
  s.date = Time.utc(2015, 4, 7)
  s.executables = ["rake"]
  s.files = ["bin/rake",
   ...
   ...
   "rake/version.rb",
   "rake/win32.rb"]
  s.require_paths = ["lib"]
  s.rubygems_version = "2.4.5"
  s.specification_version = 4
  s.summary = "This rake is bundled with Ruby"
end

Bundler.require

http://bundler.io/groups.html

Require the default gems, plus the gems in a group named the same as the current Rails environment

英語そのままだが、defaultのgemsをrequireし、現在のRails環境と同じ名前のグループ gems を require する。 コードを少し追いかけてみる。

https://github.com/bundler/bundler/blob/v1.9.2/lib/bundler.rb#L133-L135

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

      ...
      ...
    end

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

細かい説明を飛ばして結論だけ書くと、@setup は Runtime インスタンスを保持しており、このインスタンスのrequireメソッドを呼び出すことになる。 コードは https://github.com/bundler/bundler/blob/v1.9.2/lib/bundler/runtime.rb#L57 を見ればok。

bundle exec

Bundler.setup と Bundler.require の仕組みは分かったので、次は bundle exec について理解を深めてみたい。 例によって、コードを追いかけてみる。

bin/bundle で bundler/cli が requireされ... https://github.com/bundler/bundler/blob/v1.9.2/bin/bundle#L19-L20

  require 'bundler/cli'
  Bundler::CLI.start(ARGV, :debug => true)

bundler/cli/exec が require される。 https://github.com/bundler/bundler/blob/v1.9.2/lib/bundler/cli.rb#L269-L272

    def exec(*args)
      require 'bundler/cli/exec'
      Exec.new(options, args).run
    end

runの内部では、SharedHelpers.set_bundle_envirionmentが呼び出されており、ここでRUBYOPTがセットされる。 https://github.com/bundler/bundler/blob/v1.9.2/lib/bundler/shared_helpers.rb#L82-L87

    def set_bundle_environment
      # Set PATH
      paths = (ENV["PATH"] || "").split(File::PATH_SEPARATOR)
      paths.unshift "#{Bundler.bundle_path}/bin"
      ENV["PATH"] = paths.uniq.join(File::PATH_SEPARATOR)

      # Set RUBYOPT
      rubyopt = [ENV["RUBYOPT"]].compact
      if rubyopt.empty? || rubyopt.first !~ /-rbundler\/setup/
        rubyopt.unshift %|-rbundler/setup|
        ENV["RUBYOPT"] = rubyopt.join(' ')
      end

      # Set RUBYLIB
      rubylib = (ENV["RUBYLIB"] || "").split(File::PATH_SEPARATOR)
      rubylib.unshift File.expand_path('../..', __FILE__)
      ENV["RUBYLIB"] = rubylib.uniq.join(File::PATH_SEPARATOR)
    end

http://docs.ruby-lang.org/ja/2.2.0/doc/spec=2fenvvars.htmlによれば、RUBYOPT環境変数は「Rubyインタプリタにデフォルトで渡すオプションを指定します。」とのことなので、実行時に "-rbundler/setup" を指定したことになる。

結果として、bundle exec をCLIから実行すると Bundler.setupが(bundler/setup経由で)実行される。

Rails

では、 Railsではbundlerをどう使っているのか? これは config/boot.rb と config/application.rb を見ればよい。

config/boot.rb
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])

によって、$LOAD_PATHを解決し、

config/application.rb
require File.expand_path('../boot', __FILE__)
Bundler.require(*Rails.groups)

必要な gem を require する。 ちなみに、Rails.groups は [:default, :production] となる。(RAILS_ENV=productionのとき)

適切なversionのロード

「versionが異なるgemがインストールされていても、正しくロードできるのか? できるとすると、どういう機構なのか?」と気になったので sinatra で試してみたい。

まず Gemfile を用意する。

source 'https://rubygems.org'
gem 'sinatra', '1.4.5'

bundle install --path=vendor/bundle を実行し、さらに sinatra の version を上げる。

source 'https://rubygems.org'
gem 'sinatra', '1.4.6'

bundle install を再実行すると、vendor/bundle は以下のようになった。

$ ll vendor/bundle/ruby/2.1.0/gems/
total 0
drwxr-xr-x   8 kotaroito kotaroito  272  4  7 22:59 .
drwxr-xr-x   9 kotaroito kotaroito  306  4  7 22:49 ..
drwxr-xr-x  13 kotaroito kotaroito  442  4  7 22:49 rack-1.6.0
drwxr-xr-x   8 kotaroito kotaroito  272  4  7 22:49 rack-protection-1.5.3
drwxr-xr-x  23 kotaroito kotaroito  782  4  7 22:49 sinatra-1.4.5
drwxr-xr-x  23 kotaroito kotaroito  782  4  7 22:59 sinatra-1.4.6
drwxr-xr-x  13 kotaroito kotaroito  442  4  7 22:49 tilt-1.4.1
drwxr-xr-x  13 kotaroito kotaroito  442  4  7 22:59 tilt-2.0.1

この時、$LOAD_PATH は どうなっているか?

$ bundle exec ruby -e 'puts $LOAD_PATH'
/Users/kotaroito/sandbox/ruby/sinatra/vendor/bundle/ruby/2.1.0/gems/sinatra-1.4.6/lib
/Users/kotaroito/sandbox/ruby/sinatra/vendor/bundle/ruby/2.1.0/gems/tilt-2.0.1/lib
/Users/kotaroito/sandbox/ruby/sinatra/vendor/bundle/ruby/2.1.0/gems/rack-protection-1.5.3/lib
/Users/kotaroito/sandbox/ruby/sinatra/vendor/bundle/ruby/2.1.0/gems/rack-1.6.0/lib
/Users/kotaroito/.rbenv/versions/2.1.3/lib/ruby/gems/2.1.0/gems/bundler-1.7.3/lib/gems/bundler-1.7.3/lib
/Users/kotaroito/.rbenv/versions/2.1.3/lib/ruby/gems/2.1.0/gems/bundler-1.7.3/lib
/usr/local/Cellar/rbenv-gem-rehash/1.0.0
...

sinatra-1.4.6だけが設定されているので、require 'sinatra'すれば 1.4.6がロードされる。 なぜ、bundle exec の内部で Bundle.requireしなくていいのか疑問だったが、謎が解けた。

まとめ

  • Bundler.setup は $LOAD_PATH の解決をし、Bundler.require は Gemfile の group に設定された gem を requireする
  • bundle exec は 内部で RUBYOPT に -rbundler/setup を設定している
  • Rails は config/boot.rb で Bundler.setup、config/application.rb で Bundler.require してる
  • version違いで複数インストールされていても、Bundler.setup が適切に解決してくれる

See Also