diff options
| author | Edouard CHIN <chin.edouard@gmail.com> | 2026-01-15 17:09:35 +0100 |
|---|---|---|
| committer | git <svn-admin@ruby-lang.org> | 2026-01-26 08:48:22 +0000 |
| commit | ace5b10de44acc7ce7717d4109beaeba9a130700 (patch) | |
| tree | c2a5f41b1764fe4035600ee6a4feb0dc01386294 | |
| parent | b3cca1e201238def12f57a9ad21da8ef65ad3563 (diff) | |
[ruby/rubygems] Fix Bundler that re-exec $0 when a `version` is present in the config:
- ### Problem
If you have a `version` in your config file (this feature was
introduced in #6817), then running any `bundle` command will
make Bundler re-exec and ultimately run the `bundle` binstub twice.
### Details
When the `bundle` binstub gets executed, a `require "bundler"` is
evaluated. RubyGems tries to require the `bundler.rb` file from
the right `bundler` gem (in the event where you have multiple
bundler versions in your system).
RubyGems will prioritize a bundler version based on a few
heurisitics.
https://github.com/ruby/rubygems/blob/b50c40c92abb00bb172f1579356cc73c242b1849/lib/rubygems/bundler_version_finder.rb#L19-L21
This prioritize logic doesn't take into account the bundler version
a user has specific in this config. So what happens is:
1. User execute the `bundle` binstub
2. `require 'bundler'` is evaluated.
3. RubyGems prioritize activating the bundler version specified
in the Gemfile.lock
4. The CLI starts, and [Auto switch kicks in](https://github.com/ruby/rubygems/blob/b50c40c92abb00bb172f1579356cc73c242b1849/bundler/lib/bundler/cli.rb#L81). Bundler detects that
user specifed a version in its config and the current Bundler
version doesn't match.
5. Bundler exit and re-exec with the right bundler version.
### Solution
This patch introduce two fixes. First, it reads the bundler config
file and check for the local config first and then the global
config. This is because the local has precedence over global.
Second, the prioritization takes into account the version in config
and let RubyGems activate the right version in order to prevent
re-exec moments later.
Finally, I also want to fix this problem because its a step toward
fixing https://github.com/ruby/rubygems/issues/8106. I'll open
a follow up patch to explain.
https://github.com/ruby/rubygems/commit/d6e0f43133
| -rw-r--r-- | lib/rubygems/bundler_version_finder.rb | 73 | ||||
| -rw-r--r-- | spec/bundler/runtime/self_management_spec.rb | 18 | ||||
| -rw-r--r-- | test/rubygems/test_gem_bundler_version_finder.rb | 25 |
3 files changed, 84 insertions, 32 deletions
diff --git a/lib/rubygems/bundler_version_finder.rb b/lib/rubygems/bundler_version_finder.rb index c930c2e19c..d792358da7 100644 --- a/lib/rubygems/bundler_version_finder.rb +++ b/lib/rubygems/bundler_version_finder.rb @@ -10,6 +10,8 @@ module Gem::BundlerVersionFinder v ||= bundle_update_bundler_version return if v == true + v ||= bundle_config_version + v ||= lockfile_version return unless v @@ -49,21 +51,7 @@ module Gem::BundlerVersionFinder private_class_method :lockfile_version def self.lockfile_contents - gemfile = ENV["BUNDLE_GEMFILE"] - gemfile = nil if gemfile&.empty? - - unless gemfile - begin - Gem::Util.traverse_parents(Dir.pwd) do |directory| - next unless gemfile = Gem::GEM_DEP_FILES.find {|f| File.file?(f) } - - gemfile = File.join directory, gemfile - break - end - rescue Errno::ENOENT - return - end - end + gemfile = gemfile_path return unless gemfile @@ -82,19 +70,24 @@ module Gem::BundlerVersionFinder private_class_method :lockfile_contents def self.bundle_config_version - config_file = bundler_config_file - return unless config_file && File.file?(config_file) + version = nil - contents = File.read(config_file) - contents =~ /^BUNDLE_VERSION:\s*["']?([^"'\s]+)["']?\s*$/ + [bundler_local_config_file, bundler_global_config_file].each do |config_file| + next unless config_file && File.file?(config_file) - $1 + contents = File.read(config_file) + contents =~ /^BUNDLE_VERSION:\s*["']?([^"'\s]+)["']?\s*$/ + + version = $1 + break if version + end + + version end private_class_method :bundle_config_version - def self.bundler_config_file - # see Bundler::Settings#global_config_file and local_config_file - # global + def self.bundler_global_config_file + # see Bundler::Settings#global_config_file if ENV["BUNDLE_CONFIG"] && !ENV["BUNDLE_CONFIG"].empty? ENV["BUNDLE_CONFIG"] elsif ENV["BUNDLE_USER_CONFIG"] && !ENV["BUNDLE_USER_CONFIG"].empty? @@ -103,10 +96,36 @@ module Gem::BundlerVersionFinder ENV["BUNDLE_USER_HOME"] + "config" elsif Gem.user_home && !Gem.user_home.empty? Gem.user_home + ".bundle/config" - else - # local - "config" end end - private_class_method :bundler_config_file + private_class_method :bundler_global_config_file + + def self.bundler_local_config_file + gemfile = gemfile_path + return unless gemfile + + File.join(File.dirname(gemfile), ".bundle", "config") + end + private_class_method :bundler_local_config_file + + def self.gemfile_path + gemfile = ENV["BUNDLE_GEMFILE"] + gemfile = nil if gemfile&.empty? + + unless gemfile + begin + Gem::Util.traverse_parents(Dir.pwd) do |directory| + next unless gemfile = Gem::GEM_DEP_FILES.find {|f| File.file?(f) } + + gemfile = File.join directory, gemfile + break + end + rescue Errno::ENOENT + return + end + end + + gemfile + end + private_class_method :gemfile_path end diff --git a/spec/bundler/runtime/self_management_spec.rb b/spec/bundler/runtime/self_management_spec.rb index fbffd2dca2..34382df268 100644 --- a/spec/bundler/runtime/self_management_spec.rb +++ b/spec/bundler/runtime/self_management_spec.rb @@ -171,6 +171,24 @@ RSpec.describe "Self management" do expect(out).to eq(previous_minor) end + it "requires the right bundler version from the config and run bundle CLI without re-exec" do + unless Bundler.rubygems.provides?(">= 4.1.0.dev") + skip "This spec can only run when Gem::BundlerVersionFinder.bundler_versions reads bundler configs" + end + + lockfile_bundled_with(current_version) + + bundle "config set --local version #{previous_minor}" + bundle "config set --local path.system true" + bundle "install" + + script = bundled_app("script.rb") + create_file(script, "p 'executed once'") + + bundle "-v", env: { "RUBYOPT" => "-r#{script}" } + expect(out).to eq(%("executed once"\n9.3.0)) + end + it "does not try to install when using bundle config version global" do lockfile_bundled_with(previous_minor) diff --git a/test/rubygems/test_gem_bundler_version_finder.rb b/test/rubygems/test_gem_bundler_version_finder.rb index a773d6249b..88ee9c6759 100644 --- a/test/rubygems/test_gem_bundler_version_finder.rb +++ b/test/rubygems/test_gem_bundler_version_finder.rb @@ -66,7 +66,7 @@ class TestGemBundlerVersionFinder < Gem::TestCase f.write(config_content) f.flush - bvf.stub(:bundler_config_file, f.path) do + bvf.stub(:bundler_global_config_file, f.path) do assert_nil bvf.bundler_version end end @@ -81,7 +81,7 @@ class TestGemBundlerVersionFinder < Gem::TestCase f.write(config_with_single_quoted_version) f.flush - bvf.stub(:bundler_config_file, f.path) do + bvf.stub(:bundler_global_config_file, f.path) do assert_nil bvf.bundler_version end end @@ -98,18 +98,33 @@ class TestGemBundlerVersionFinder < Gem::TestCase f.write(config_content) f.flush - bvf.stub(:bundler_config_file, f.path) do + bvf.stub(:bundler_global_config_file, f.path) do assert_equal v("1.1.1.1"), bvf.bundler_version end end end def test_bundler_version_with_bundle_config_non_existent_file - bvf.stub(:bundler_config_file, "/non/existent/path") do + bvf.stub(:bundler_global_config_file, "/non/existent/path") do assert_nil bvf.bundler_version end end + def test_bundler_version_set_on_local_config + config_content = <<~CONFIG + BUNDLE_VERSION: "1.2.3" + CONFIG + + Tempfile.create("bundle_config") do |f| + f.write(config_content) + f.flush + + bvf.stub(:bundler_local_config_file, f.path) do + assert_equal v("1.2.3"), bvf.bundler_version + end + end + end + def test_bundler_version_with_bundle_config_without_version config_without_version = <<~CONFIG BUNDLE_JOBS: "8" @@ -120,7 +135,7 @@ class TestGemBundlerVersionFinder < Gem::TestCase f.write(config_without_version) f.flush - bvf.stub(:bundler_config_file, f.path) do + bvf.stub(:bundler_global_config_file, f.path) do assert_nil bvf.bundler_version end end |
