Skip to content

Commit d3d2b3d

Browse files
meganemuraclaude
andcommitted
Perform the install triggered by auto_install in a subprocess
With `auto_install`, Bundler installs missing gems in the same process that then runs `Bundler.setup`. Installing requires `openssl` for HTTPS remotes, and no Gemfile requirement is in effect yet, so RubyGems activates the default gem while the locked one is still missing. Activation can't be undone, so the `Bundler.setup` that follows raises a `Gem::LoadError` when the lockfile pins `openssl` to another version. Do the install in a forked child, like `bundler/inline` already does for the same reason. Platforms without `fork` keep installing in process. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent e5c8f5f commit d3d2b3d

3 files changed

Lines changed: 92 additions & 1 deletion

File tree

lib/bundler.rb

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def auto_install
187187
rescue GemNotFound, GitError
188188
ui.info "Automatically installing missing gems."
189189
reset!
190-
CLI::Install.new({}).run
190+
auto_install_missing_gems
191191
reset!
192192
end
193193
end
@@ -605,6 +605,44 @@ def self_manager
605605

606606
private
607607

608+
# When possible we do the install in a subprocess because to install gems
609+
# we need to require some default gems like `openssl` (for HTTPS remotes)
610+
# which may later conflict with the Gemfile requirements. `bundler/inline`
611+
# re-resolves when that happens. We can't: the `Bundler.setup` that follows
612+
# must activate what the lockfile says.
613+
def auto_install_missing_gems
614+
do_install = -> { CLI::Install.new({}).run }
615+
616+
if Process.respond_to?(:fork)
617+
[$stdout, $stderr].each(&:flush) # don't let the fork inherit buffered output
618+
619+
_, status = Process.waitpid2(Process.fork do
620+
exit_status = 1
621+
622+
begin
623+
# Errors here never reach the parent's handler. Report them in the
624+
# child, and let the parent exit with the status. Required inside the
625+
# fork so the CLI's vendored Thor stays out of the parent.
626+
require_relative "bundler/friendly_errors"
627+
628+
with_friendly_errors(&do_install)
629+
exit_status = 0
630+
rescue SystemExit => e
631+
exit_status = e.status
632+
ensure
633+
# Skip `at_exit` handlers, they belong to the booting program.
634+
# `exit!` doesn't flush, so flush by hand.
635+
[$stdout, $stderr].each(&:flush)
636+
exit!(exit_status)
637+
end
638+
end)
639+
640+
exit(status.exitstatus || status.to_i) unless status.success?
641+
else
642+
do_install.call
643+
end
644+
end
645+
608646
def unbundle_env(env)
609647
if env.key?("BUNDLER_ORIG_MANPATH")
610648
env["MANPATH"] = env["BUNDLER_ORIG_MANPATH"]

spec/bundler/bundler_spec.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,36 @@
207207
end
208208
end
209209

210+
describe "#auto_install" do
211+
let(:install) { double("install") }
212+
213+
before do
214+
skip "requires Process.fork" unless Process.respond_to?(:fork)
215+
216+
definition = double("definition")
217+
allow(definition).to receive(:specs).and_raise(Bundler::GemNotFound)
218+
allow(Bundler).to receive(:definition).and_return(definition)
219+
allow(Bundler::CLI::Install).to receive(:new).with({}).and_return(install)
220+
end
221+
222+
it "installs in a subprocess, so that gems activated to install don't conflict with the Gemfile" do
223+
installer_pid = tmp("auto_install_pid")
224+
allow(install).to receive(:run) { File.write(installer_pid, Process.pid) }
225+
226+
Bundler.settings.temporary(auto_install: true) { Bundler.auto_install }
227+
228+
expect(installer_pid.read.to_i).not_to eq(Process.pid)
229+
end
230+
231+
it "exits with the status code of a failed install" do
232+
allow(install).to receive(:run).and_raise(Bundler::InstallError)
233+
234+
expect do
235+
Bundler.settings.temporary(auto_install: true) { Bundler.auto_install }
236+
end.to raise_error(SystemExit) {|error| expect(error.status).to eq(5) }
237+
end
238+
end
239+
210240
describe "#mkdir_p" do
211241
it "creates a folder at the given path" do
212242
install_gemfile <<-G

spec/runtime/setup_gems_spec.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,29 @@ def require(path)
792792
expect(out).to include("Installing myrack 1.0.0")
793793
end
794794

795+
it "performs an automatic bundle install of a default gem locked to another version" do
796+
build_repo4 do
797+
build_gem "psych", "999"
798+
build_gem "myrack", "1.0.0"
799+
end
800+
801+
gemfile <<-G
802+
source "https://gem.repo4"
803+
gem "psych"
804+
gem "myrack"
805+
G
806+
807+
bundle_config "auto_install 1"
808+
809+
ruby <<-RUBY, artifice: "compact_index"
810+
require 'bundler/setup'
811+
puts Gem.loaded_specs["psych"].version
812+
RUBY
813+
expect(err).to be_empty
814+
expect(out).to include("Installing psych 999")
815+
expect(out).to include("999")
816+
end
817+
795818
context "in a read-only filesystem" do
796819
before do
797820
gemfile <<-G

0 commit comments

Comments
 (0)