bundle中に別のbundleを呼ぶと予期しない結果になる対策

bundle execでrubyファイルを実行し、別のGemfileのあるディレクトリに移動してbundle系のコマンドを実行すると、
一回目のbundlerに対してbundleが実行されてしまい、
別のbundleを呼び出せないという問題が起きました。
これはbundlerが設定する環境変数が原因でした。

まとめ

  • bundle exec中に、別のbundlerを実行するとおかしくなる
    • 主にsystemやspawn等を使った場合
    • 最初のbundle execと同じものであれば問題は起きない
  • bundlerが設定をしている環境変数が問題
    • 別のbundlerを呼ぼうとして元のbundlerが呼ばれている
  • Bundler.with_clean_envで回避可能
    • Bundler.clean_systemでも可

問題

以下のような構成かつtestフォルダにいる状態で、
bundle install --path vendor/bundle
bundle exec test.rbを実行すると、
test2内でbundlerを呼んだ時にエラーになります

.
├─ test
│  │  Gemfile (gemのhello_world_gemを使用)
│  └─ test.rb
|
└─ test2
   └─ Gemfile (gemのhello-worldを使用)
system 'hello_world_gem'
Dir.chdir("../test2") do
  # error
  system 'bundle install --path vendor/bundle'
  system 'bundle exec hello-world'
end

エラー文言

oh hai thar
Using hello_world_gem 0.0.3
Using bundler 1.7.3
Your bundle is complete!
It was installed into ./vendor/bundle
bundler: command not found: hello-world
Install missing gem executables with `bundle install`

原因

bundlerが実行時に、BUNDLE_GEMFILEやRUBYOPTといった環境変数をいじっており、
そこで指定されているbundlerから実行されていると見なされるため、
bundle系のコマンドが全て元のbundlerに対して実行されてしまうのが原因です。
実際、test2に移動後のbundle install時のログが、testフォルダでbundle installした時のものと同じ出力になっています。

この仕組みのおかげで同じbundleの場合はそのまま実行できるため、
1行目のhello_world_gemの呼び出しにbundle execが不要になっています。
このように必ずしも害となるものではありませんが、違うbundlerを呼び出せないのは不便です。

解決策

Bundler.with_clean_envを利用することで、この問題を回避できます。
このメソッドは、実行時の環境変数にBUNDLE_という名前の環境変数を全て削除して渡されたブロックを実行します。
そのため、これに渡したブロックの中で別のbundlerを呼ぶことで、予期した動作になります。
また、コマンドが一つだけの場合はBundler.clean_systemメソッドも使えます。

system 'hello_world_gem'
Dir.chdir("../test2") do
  Bundler.with_clean_env do
    # ok
    system 'bundle install --path vendor/bundle'
    system 'bundle exec hello-world'
  end
end

補足

他にGEM_HOMEという環境変数を設定していますが、
これはbundler起動時に上書きされるらしく、with_clear_envで消さなくても良さそうです。
https://github.com/bundler/bundler/blob/a400c5ed9eca82598a76f9818a1bf7129515a2a0/lib/bundler.rb#L416