In Ruby under Bundler, why does shelling out from one bundled process to another project's `bundle exec` load the wrong Gemfile, and how does `Bundler.with_unbundled_env` fix it?
answer
- the child inherits the parent's bundle
- BUNDLE_GEMFILE and RUBYOPT leak down
- with_unbundled_env strips BUNDLE_ keys
- with_original_env keeps your own settings
- with_clean_env was removed
basics
~10 sA process running under Bundler exports BUNDLE_GEMFILE and -rbundler/setup in RUBYOPT, and every child inherits them, so the other project's bundle exec reuses the parent's Gemfile. Bundler.with_unbundled_env runs a block with those variables removed.
solid answer
~40 s`bundle exec` and `Bundler.setup` leave marks in `ENV`: `BUNDLE_GEMFILE` names the parent's Gemfile, `RUBYOPT` carries `-rbundler/setup`, `RUBYLIB` and `PATH` point at Bundler and the bundle's executables. `system`, backticks and `Process.spawn` pass `ENV` to the child, so a Ruby script in another project starts inside the *parent's* bundle, and its own `bundle exec` finds the inherited `BUNDLE_GEMFILE` instead of searching for its Gemfile. `Bundler.with_unbundled_env { ... }` runs the block with the environment as it was before Bundler was activated, minus every `BUNDLE_*` key and Bundler's `RUBYOPT` and `RUBYLIB` entries, then restores `ENV`. `Bundler.with_original_env` restores the pre-Bundler environment but keeps any `BUNDLE_*` values you had set yourself. `Bundler.unbundled_system` and `unbundled_exec` wrap `system` and `exec`. The Bundler 1 names `with_clean_env` and `clean_env` now raise and point to these.
code
ruby · 8 lines# Rakefile task in project A, run with bundle exec
task :build_billing do
Bundler.with_unbundled_env do
Dir.chdir("../billing") do
system("bundle", "exec", "rake", "build", exception: true)
end
end
endgo deeper
Recall that processes started from a bundled program inherit its Bundler environment, and that Bundler.with_unbundled_env runs a block without it.
Explain which variables carry the bundle into children, BUNDLE_GEMFILE, RUBYOPT, RUBYLIB and PATH, and why an inherited BUNDLE_GEMFILE beats the Gemfile search.
Diagnose cross-project tooling that loads the wrong gems, choose between unbundled and original environments deliberately, and use BUNDLE_GEMFILE on purpose for multi-Gemfile test matrices.
Design build and release tooling that spans many repositories so each step runs in its own bundle, isolating environments rather than relying on each script to clean up.
## How the bundle leaks into child processes When a command starts through `bundle exec`, Bundler records the bundle in **environment variables** so every Ruby process the command spawns stays inside it: | Variable | Value under Bundler | |---|---| | `BUNDLE_GEMFILE` | absolute path of the parent's Gemfile | | `BUNDLE_LOCKFILE`, `BUNDLE_BIN_PATH`, `BUNDLER_VERSION`, `BUNDLER_SETUP` | details of the same bundle | | `RUBYOPT` | includes `-r.../bundler/setup` | | `RUBYLIB` | Bundler's own `lib` directory first | | `PATH` | the bundle's executable directory first | That is exactly right for `bundle exec rake` spawning `ruby worker.rb` in the same project. It is exactly wrong when a tool running in project A shells out to project B: ```ruby # a release script in project A, started with bundle exec Dir.chdir("../billing") { system("bundle exec rake build") } ``` The child inherits `BUNDLE_GEMFILE=/path/to/A/Gemfile`. Bundler normally searches upward from the working directory for a Gemfile, but an explicit `BUNDLE_GEMFILE` wins, so project B's `rake` runs with project A's gems. The symptoms: missing gems, the wrong version of a shared gem, or a baffling "already activated" error. ## The fix: `Bundler.with_unbundled_env` ```ruby Bundler.with_unbundled_env do Dir.chdir("../billing") { system("bundle exec rake build") } end ``` Inside the block, `ENV` is replaced with a cleaned copy of the environment from **before Bundler was activated**: 1. Start from the original environment. Bundler saved the pre-`bundle exec` values of `PATH`, `RUBYOPT`, `RUBYLIB`, `GEM_HOME`, `GEM_PATH` and the `BUNDLE_*` keys it changed. 2. Delete **every** key starting with `BUNDLE_`, plus `BUNDLER_SETUP`. 3. Remove `-rbundler/setup` from `RUBYOPT` and Bundler's directory from `RUBYLIB`. After the block, `ENV` is restored. The child now behaves as if typed in a fresh shell: its own `bundle exec` finds project B's Gemfile. ## The related helpers - **`Bundler.with_original_env`** - the pre-Bundler environment *without* step 2, so `BUNDLE_*` values you exported yourself (say `BUNDLE_PATH` for CI) survive. Use it when the child should inherit your own settings but not the parent's bundle. - **`Bundler.unbundled_system(cmd)`** and **`Bundler.unbundled_exec(cmd)`** - shorthands for calling `system` or `exec` inside `with_unbundled_env`; `original_system` and `original_exec` do the same with `with_original_env`. - **`Bundler.unbundled_env`** returns the cleaned hash without switching `ENV`, useful for passing an explicit environment to a spawn call. The Bundler 1 names are gone: `Bundler.with_clean_env` and `Bundler.clean_env` raise, saying they were removed in favour of `with_unbundled_env` and `unbundled_env`. ## Diagnosing a leaked bundle When a child process loads unexpected gems, check what it inherited before changing any code: 1. Print the Bundler-related variables in the child, for example `ENV.select { |k, _| k.start_with?("BUNDLE") }` together with `ENV["RUBYOPT"]`. 2. Compare `BUNDLE_GEMFILE` with the directory the child runs in; a mismatch is the leak. 3. In the parent, `Bundler.original_env` shows what the environment looked like before Bundler touched it, which tells you whether a `BUNDLE_*` value came from Bundler or from your own shell. The same inheritance explains a subtler symptom: a child Ruby process that is not a Bundler project at all, such as a system tool written in Ruby, fails because `-rbundler/setup` in `RUBYOPT` forces it into the parent's bundle, where its own gems are missing. The bundle-exec man page gives this case, with a Homebrew command as its example, alongside shelling out to a different bundle. ## Choosing a Gemfile on purpose `BUNDLE_GEMFILE` is also the supported way to run one project against several dependency sets: ```bash BUNDLE_GEMFILE=gemfiles/rack_3_1.gemfile bundle install BUNDLE_GEMFILE=gemfiles/rack_3_1.gemfile bundle exec rake test ``` The lockfile follows the Gemfile name (`gemfiles/rack_3_1.gemfile.lock`), and the Gemfile's directory becomes the project root for relative paths. `bundle exec --gemfile=...` and `bundle install --gemfile=...` do the same for a single command. ## Traps - Calling `bundle exec` from a Git hook or editor task that itself runs under another bundle. - Using `with_original_env` and expecting a clean slate; a `BUNDLE_GEMFILE` you exported earlier survives it. - Forgetting `Dir.chdir` or `chdir:`: even with a clean environment, Bundler searches for the Gemfile from the child's working directory.
- When should you choose `Bundler.with_original_env` over `with_unbundled_env`?When the child should keep `BUNDLE_*` settings that existed before Bundler started, for example a `BUNDLE_PATH` or credentials exported by CI. `with_original_env` restores the pre-Bundler environment as is; `with_unbundled_env` additionally deletes every `BUNDLE_` key. If one of those was a `BUNDLE_GEMFILE` you exported yourself, only the unbundled form drops it.
- A gem's test suite runs against three Rack versions. How does `BUNDLE_GEMFILE` help?Keep one Gemfile per dependency set, such as `gemfiles/rack_3_1.gemfile`, and run `BUNDLE_GEMFILE=gemfiles/rack_3_1.gemfile bundle exec rake test` for each. Bundler uses that Gemfile and a lockfile named after it, and the Gemfile's directory becomes the root for relative paths, so each combination resolves and locks separately.
saying these in an interview costs you the question
- A child process always starts outside the parent's bundle
- bundle exec in another directory always finds that directory's Gemfile
- with_clean_env is the current name for with_unbundled_env
- with_unbundled_env permanently strips Bundler variables from ENV
- with_original_env and with_unbundled_env produce identical environments