Perform the install triggered by auto_install in a subprocess - #9758
Open
meganemura wants to merge 1 commit into
Open
Perform the install triggered by auto_install in a subprocess#9758meganemura wants to merge 1 commit into
meganemura wants to merge 1 commit into
Conversation
When `auto_install` is enabled and gems are missing, Bundler installs them 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 newest installed version, which is 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, so the process that boots the application never activates gems that were only needed to install. Platforms without `fork` keep installing in process. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was the end-user or developer problem that led to this PR?
With
auto_installenabled,require "bundler/setup"(andbundle exec) can fail with aGem::LoadErrorthe first time gems are missing:The automatic install itself succeeds, so running the same command again works. That makes it look unreproducible, and both suggestions in the error message are dead ends:
opensslis usually a transitive dependency that can't be dropped from the Gemfile, and the bundler in use is already current.Reproduction, on a Ruby whose default
openssldiffers from the pinned version:Expected
OK, got theGem::LoadErrorabove. What does not reproduce it:bundle exec <binstub>, gems missingGem::LoadError(binstubs areKernel.loaded in process)bundle exec ruby -e ..., gems missingrubyisKernel.execed so activation is resetauto_installdisabledBundler::GemNotFound, which is clear and actionablebundle installBundler.setupOnly
auto_install, which chains an install and aBundler.setupin one process, breaks.What is your fix for the problem, implemented in this PR?
Why it happens
lib/bundler/setup.rbcallsBundler.auto_installbeforeBundler.setup, andBundler.auto_installrunsCLI::Installin the same process:openssl.Bundler.setuphas not run yet, so no Gemfile requirement is in effect. RubyGems activates the newest installed version, which is the default gem while the locked one is still missing.reset!only drops Bundler'sDefinition, not RubyGems' activated specs.Bundler.setupthen activates the lockfile's version andRuntime#check_for_activated_spec!raises.bundle execreaches the same code throughlib/bundler/cli.rb(AUTO_INSTALL_CMDS).Bundler normally avoids this class of conflict by vendoring what it needs (
lib/bundler/vendor/:net-http-persistent,uri,securerandom,thor, …).opensslis a C extension and can't be vendored, so it is the hole left in that defense.The fix
Run the install in a forked child, so the process that boots the application never activates gems that were only needed to install.
bundler/inlinealready does exactly this, for exactly this reason:Platforms without
fork(Windows, JRuby) keep installing in process, as inbundler/inline.Differences from
bundler/inline, and whyThe child is wrapped in
with_friendly_errors. The handlerexe/bundlewraps around the parent cannot see a failure in the child:fork's block runs underrb_protectand the child exits without unwinding into the parent's frames. Verified: with an enclosingrescue Exceptionaround theforkcall, the rescue never fires and the child prints the exception itself. Without wrapping, a failed automatic install underbundle execwould degrade from a friendly message and a proper status code to a raw backtrace and exit 1.The child ends with
exit!, after flushing by hand.fork { ... }exits the child normally, which runs theat_exithandlers of the program that is booting — they would run once in the install child and once in the real process.exit!skips them, but it also skips flushing, hence the explicit flush.The parent flushes before forking, so the child cannot inherit and re-emit buffered output.
No
Gem.load_yamlbefore the fork.bundler/inlinecalls it, but here it would defeat the fix: on Ruby 3.2,Gem.load_yamlrequirespsychunconditionally, activating the defaultpsychin the parent, which is the very conflict this PR removes. Measured on Ruby 3.2.4 / RubyGems 3.4.19 with the new spec below:lib/bundler.rbGem::LoadError: already activated psych 5.0.1, but your Gemfile requires psych 999Gem.load_yamlGem::LoadErrorThe
NameErrorthat call is meant to prevent did not occur, becauseBundler.setuphas already loaded the realPsychby the time YAML is loaded.bundler/inlinecan afford the pre-activation because it recovers by re-resolving with the activated version; that is not an option here, sinceBundler.setupmust activate what the lockfile says.No
$VERBOSE = nilin the child. Nothing in this path emits warnings underruby -w, and silencing them would hide warnings from a realbundle installtriggered during application boot.Tests
spec/bundler/bundler_spec.rb: the install runs in a subprocess, and a failed install exits with the child's status code.spec/runtime/setup_gems_spec.rb: an automatic install of a bundle that locks a default gem to another version.opensslcan't be used here because the suite stubs HTTPS, so it uses apsych 999fixture, the same stand-inspec/runtime/inline_spec.rbalready uses. This one reproduces the bug in thesystem-rubygems-bundlerjob, where RubyGems is old enough to requirepsychwhile installing.Make sure the following tasks are checked