bundle execはなぜ必要なのか


この記事は「なんか分かんないけどbundle execつけないと動かないからつけてるけど、つけなきゃいけない理由が知りたい」という方向けに説明を試みるものです。

TL;DR

一言で言うと、bundle exec をつけるとGemfile.lockに書いてあるとおりにrequireするようになります。
Gemfile.lockに記載されているgemは、お互いの依存関係を満たすようにバージョンが選ばれているので、Gem:: ConflictErrorを防ぐことができます。

もうちょっと詳しく

Bundlerがないと何が起きるのか

具体例で説明します。以下のような依存関係を持つgem, "GemA", "GemB"があるとしましょう。両方ともインストール済みとします。

gem_a.rb
module GemA
  Version = "1.0.0"
end
gem_b.rb
require 'gem_a'

module GemB
  Version = '1.0.0'

  def self.say
    puts "I'm using GemA ver. #{::GemA::Version}!"
  end
end
# gem list

*** LOCAL GEMS ***

gem_a (1.0.0)
gem_b (1.0.0)

もちろんどちらもrequireできます。

irb(main):001:0> require 'gem_a'
=> true
irb(main):002:0> GemA::Version
=> "1.0.0"
irb(main):003:0> require 'gem_b'
=> true
irb(main):004:0> GemB::Version
=> "1.0.0"
irb(main):005:0> GemB::say
I'm using GemA ver. 1.0.0!
=> nil

ここで、GemAを2.0.0にアップデートします。

# gem update gem_a
Updating installed gems
Updating gem_a
Fetching: gem_a-2.0.0.gem (100%)
Successfully installed  gem_a-2.0.0.gem
Gems updated: gem_a
# gem list

*** LOCAL GEMS ***

gem_a (2.0.0, 1.0.0)
gem_b (1.0.0)

アップデートできました。irbでさっきと同じことをしてみます。

irb(main):001:0> require 'gem_a'
=> true
irb(main):002:0> GemA::Version
=> "2.0.0"

ちゃんとアップデートされています。GemBも使ってみましょう。

irb(main):003:0> require 'gem_b'
Traceback (most recent call last):
        8: from /usr/local/bin/irb:11:in `<main>'
        7: from (irb):3
        6: from /usr/local/Cellar/ruby/2.5.1/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:39:in `require'
        5: from /usr/local/Cellar/ruby/2.5.1/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:128:in `rescue in require'
        4: from /usr/local/Cellar/ruby/2.5.1/lib/ruby/2.5.0/rubygems.rb:217:in `try_activate'
        3: from /usr/local/Cellar/ruby/2.5.1/lib/ruby/2.5.0/rubygems.rb:224:in `rescue in try_activate'
        2: from /usr/local/Cellar/ruby/2.5.1/lib/ruby/2.5.0/rubygems/specification.rb:1438:in `activate'
        1: from /usr/local/Cellar/ruby/2.5.1/lib/ruby/2.5.0/rubygems/specification.rb:2325:in `raise_if_conflicts'
Gem::ConflictError (Unable to activate gem_b-1.0.0, because gem_a-2.0.0 conflicts with gem_a (= 1.0.0))

おおっと…困りました。GemBが壊れてしまいました。既にGemA (2.0.0)をrequireしているので、GemBに必要なGemA (1.0.0)を読み込むことができず、エラーになってしまいました。1

どうなると嬉しいのか

GemBを使うプロジェクトでは、アップデートしたGemA (2.0.0)を使わず、GemA (1.0.0)がそのまま使えればよかったのです。

irb(main):001:0> require 'gem_a'
=> true
irb(main):002:0> GemA::Version
=> "1.0.0"
irb(main):003:0> require 'gem_b'
=> true
irb(main):004:0> GemB::Version
=> "1.0.0"

一方で、GemBを使わないプロジェクトでは、最新のGemA (2.0.0)を使いたいですね。

irb(main):001:0> require 'gem_a'
=> true
irb(main):002:0> GemA::Version
=> "2.0.0"

これを実現するのがBundlerであり、bundle execというコマンドです。

Bundlerがあるとどうなるのか

GemAとGemBを使いたいプロジェクトに、こんな感じのGemfileを用意します。

gem 'gem_a'
gem 'gem_b'

そして、bundleを実行すると、Gemfile.lockが生成されます。

# bundle 
Fetching gem metadata from https://rubygems.org/.
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...
Using bundler 1.16.2
Using gem_a 1.0.0
Using gem_b 1.0.0
Bundle complete! 2 Gemfile dependencies, 3 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

# cat Gemfile.lock
GEM
  remote: https://rubygems.org/
  specs:
    gem_a (1.0.0)
    gem_b (1.0.0)
      gem_a (= 1.0.0)

PLATFORMS
  ruby

DEPENDENCIES
  gem_a
  gem_b

BUNDLED WITH
   1.16.2

この状態でbundle exec irbしてみましょう。

irb(main):001:0> require 'gem_a'
=> true
irb(main):002:0> GemA::Version
=> "1.0.0"
irb(main):003:0> require 'gem_b'
=> true
irb(main):004:0> GemB::Version
=> "1.0.0"
irb(main):005:0> GemB::say
I'm using GemA ver. 1.0.0!
=> nil

ちゃんと動きます!

普通にirbを起動すると、新しいバージョンのGemA (2.0.0)もちゃんと使えます。

irb(main):001:0> require 'gem_a'
=> true
irb(main):002:0> GemA::Version
=> "2.0.0"

Gemfileからgem_bを取り除いてbundleすれば、GemA (2.0.0)が使えます。

gem 'gem_a'
# bundle
Using bundler 1.16.2
Using gem_a 2.0.0
Bundle complete! 1 Gemfile dependency, 2 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

# cat Gemfile.lock
GEM
  remote: https://rubygems.org/
  specs:
    gem_a (2.0.0)

PLATFORMS
  ruby

DEPENDENCIES
  gem_a

BUNDLED WITH
   1.16.2

Bundlerってすばらしいですね。

まとめ

Bundlerがあれば、いろんなバージョンのgemをたくさんインストールしても、コンフリクトしないようにrequireすることができます。

(余談ですが、上記のような動きなので、基本的にはbundle --path=vendor/bundleとかしないでグローバルインストールしてもちゃんとプロジェクトごとの独立性は保たれるはずです)


  1. 「なんでGemBはGemA (2.0.0)を使わずにGemA (1.0.0)を読み込もうとすることができるの?それってBundlerの仕事なんじゃないの?」と思う方は、おそらくGemとBundlerの機能を混同しています。gemの依存関係はgemごとに定義してあります (gemの中に.gemspecという定義ファイルがあります)。このため、gemを1つ使うだけなら普通にrequireすれば、依存しているgemも一緒に適切なバージョンが読み込まれます。一方で、Bundlerは「複数のgemを同時に使うとき、どう依存関係を解決すればGem::ConflictErrorを避けられるか?」という問題を解決するためのものです。