summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEdouard CHIN <chin.edouard@gmail.com>2026-01-15 17:09:35 +0100
committergit <svn-admin@ruby-lang.org>2026-01-26 08:48:22 +0000
commitace5b10de44acc7ce7717d4109beaeba9a130700 (patch)
treec2a5f41b1764fe4035600ee6a4feb0dc01386294
parentb3cca1e201238def12f57a9ad21da8ef65ad3563 (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.rb73
-rw-r--r--spec/bundler/runtime/self_management_spec.rb18
-rw-r--r--test/rubygems/test_gem_bundler_version_finder.rb25
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