Bundler入門 - setupとrequire を読み解く

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

```ruby
  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

```ruby
    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.10.6/lib/bundler/shared_helpers.rb#L82-L87

```ruby
    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 に 指定したことになり、以下のコードに辿り着きます。

https://github.com/bundler/bundler/blob/v1.10.6/lib/bundler/setup.rb#L3-L19


```ruby
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 を設定します。


少しコードを追いかけてみます。

https://github.com/bundler/bundler/blob/v1.10.6/lib/bundler.rb#L119-L139


```ruby
    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を覗いてみると、、

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

```ruby
    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 が具体的にどんなインスタンスなのかは、例えば 以下のようにすればよいです。

```bash
[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 が担います。


https://github.com/bundler/bundler/blob/v1.10.6/lib/bundler.rb#L119-L135

```ruby
    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](http://qiita.com/advent-calendar/2015/coubic) に向けて、加筆・再編したものです。