Read more

How to discard a surrounding Bundler environment

Arne Hartherz
April 17, 2013Software engineer at makandra GmbH

tl;dr: Ruby's Bundler environment is passed on to system calls, which may not be what you may want as it changes gem and binary lookup. Use Bundler.with_original_env to restore the environment's state before Bundler was launched. Do this whenever you want to execute shell commands inside other bundles.

Example outline

Illustration UI/UX Design

UI/UX Design by makandra brand

We make sure that your target audience has the best possible experience with your digital product. You get:

  • Design tailored to your audience
  • Proven processes customized to your needs
  • An expert team of experienced designers
Read more Show archive.org snapshot

Consider this setup:

my_project/Gemfile     # says: gem 'rails', '~> 3.0.0'
my_project/foo/Gemfile # says: gem 'rails', '~> 3.2.0'

And, just to confirm this, these are the installed Rails versions for each of the bundles:

~/my_project$ bundle show rails
.../gems/rails-3.0.20
~/my_project$ cd foo && bundle show rails
.../gems/rails-3.2.13

Now you will usually just use bundle exec to run stuff, which is good as it makes sure you are using the gems from the bundle. Here is how it would look like:

~/my_project$ bundle exec ruby -e "require %(rails) ; puts Rails.version"
3.0.20
~/my_project$ cd foo && bundle exec ruby -e "require %(rails) ; puts Rails.version"
3.2.13

So far, so good.
FYI, we're using %(...) instead of '...' here because we don't want to escape quotes for the example below.

Bundler + shell execution from Ruby = fun

Now let's see what happens when we do the above by executing the second command via Ruby:

~/my_project$ bundle exec ruby -e 'system %(
  cd foo && bundle exec ruby -e "require %(rails) ; puts Rails.version"
)'
3.0.20

Well, that is not what we want. That should have been 3.2.13 since we're saying bundle exec from the 3.2 bundle.

Why it happens and how to fix it

To change some gem and binary lookup, Bundler defines these environment variables:

  • BUNDLE_BIN_PATH
  • BUNDLE_GEMFILE
  • RUBYOPT

Basically, that environment is being passed on the child calls, so when you say bundle exec and then inside make system calls.

So Bundler's environment still prevails when running shell commands from Ruby. On the one hand, this is good as you'd expect your bundle to deliver all you need. However, it will come bite you when you try to run code that has its own bundle.

If you need to do exactly that, Bundler supplies a method that will give you the environment state from before Bundler added its variables, which is just what we need: Bundler.with_original_env.

~/my_project$ bundle exec ruby -e 'Bundler.with_original_env { system %(
  cd foo && bundle exec ruby -e "require %(rails) ; puts Rails.version"
) }'
3.2.13

If you want a completely Bundler-free environment, you can use Bundler.with_unbundled_env on modern Bundler (2.1+). If you started with an environment that already held env variables related to Bundler, this will restore the original environment with such keys discarded as well. On older Bundlers, this method was called Bundler.with_clean_env.

Posted by Arne Hartherz to makandra dev (2013-04-17 17:46)