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
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 が適切に解決してくれる