BundlerはRubyを使う上では欠かせないものですが、ブラックボックスな人は意外と多いのかなと思います。(自分もそうでした)
ブラックボックスで済んでしまうのはツールとしての完成度が高いことの裏返しなので一概に悪いことではありませんが、仕組みを知っておくと、トラブルシューティングの時に役立つ(かもしれない)ので、コードを読みながら少し掘り下げてみたいと思います。
bundle exec
$ bundle exec command
おまじないのように使う bundle exec ですが、gem が require される仕組みを追い掛けてみます。
まず、bin/bundle で bundler/cli が requireされ...
https://github.com/bundler/bundler/blob/v1.10.6/bin/bundle#L19-L20
require 'bundler/cli'
Bundler::CLI.start(ARGV, :debug => true)
bundler/cli/exec が require されます。
https://github.com/bundler/bundler/blob/v1.10.6/lib/bundler/cli.rb#L277-L280
def exec(*args)
require 'bundler/cli/exec'
Exec.new(options, args).run
end
runの内部では、SharedHelpers.set_bundle_envirionmentが呼び出されており、ここでRUBYOPTがセットされます。
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インタプリタにデフォルトで渡すオプションを指定します。」とのこと。
すなわち、bundle exec を実行すると、デフォルトで "-rbundler/setup" を RUBYOPT に 指定したことになり、以下のコードに辿り着きます。
if Bundler::SharedHelpers.in_bundle?
require 'bundler'
if STDOUT.tty? || ENV['BUNDLER_FORCE_TTY']
begin
Bundler.setup
rescue Bundler::BundlerError => e
...
exit e.status_code
end
else
Bundler.setup
end
Bundler.setup とは?
http://bundler.io/v1.10/bundler_setup.html に書かれている通り、Bundler.setup は Gemfile の依存関係を require できるよう load path を設定します。
少しコードを追いかけてみます。
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
...
def load
@load ||= Runtime.new(root, definition)
end
となっており、Runtimeのインスタンスを生成し、setupメソッドを実行していることが分かります。
さらに bundler/runtime.rbを覗いてみると、、
def setup(*groups)
groups.map! { |g| g.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|
...
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
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 とは?
ここまでで load path がセットされる仕組みは分かったので、次は require です。この役割は Bundler.require が担います。
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/master/lib/bundler/runtime.rb#L57 を見るとよいです。
まとめ
- Bundler.setup は $LOAD_PATH の解決をし、Bundler.require は Gemfile の group に設定された gem を requireする
- bundle exec は 内部で RUBYOPT に -rbundler/setup を設定している
See Also
- http://kotaroito.hatenablog.com/entry/2015/02/09/163805
- http://kotaroito.hatenablog.com/entry/2014/09/30/083727
本投稿は http://kotaroito.hatenablog.com/entry/2014/11/12/104146 を Coubic Advent Calendar 2015 に向けて、加筆・再編したものです。