[rubygems/rubygems] Simulate Bundler 4 in a better way

Overriding the version constant feels too magic and creates a set of
problems. For example, Bundler will lock the simulated version, and that
can cause issues when the lockfile is used under an environment not
simulating Bundler 4 (it will try to auto-install and auto-switch to a
version that does not exist).

On top of that, it can only be configured with an ENV variable which is
not too flexible.

This commit takes a different approach of using a setting, which is
configurable through ENV or `bundle config`, and pass the simulated
version to `Bundler::FeatureFlag`. The real version is still the one set
by `VERSION`, but anything that `Bundler::FeatureFlag` controls will use
the logic of the "simulated version".

In particular, all feature flags and deprecation messages will respect
the simulated version, and this is exactly the set of functionality that
we want users to be able to easily try before releasing it.

8129402193
This commit is contained in:
David Rodríguez 2025-06-18 22:08:32 +02:00 committed by Hiroshi SHIBATA
parent 938ab128a4
commit 90085f62fb
12 changed files with 36 additions and 29 deletions

View file

@ -567,7 +567,7 @@ module Bundler
end end
def feature_flag def feature_flag
@feature_flag ||= FeatureFlag.new(VERSION) @feature_flag ||= FeatureFlag.new(settings[:simulate_version] || VERSION)
end end
def reset! def reset!

View file

@ -6,7 +6,6 @@ module Bundler
BUNDLER_KEYS = %w[ BUNDLER_KEYS = %w[
BUNDLE_BIN_PATH BUNDLE_BIN_PATH
BUNDLE_GEMFILE BUNDLE_GEMFILE
BUNDLER_4_MODE
BUNDLER_VERSION BUNDLER_VERSION
BUNDLER_SETUP BUNDLER_SETUP
GEM_HOME GEM_HOME

View file

@ -50,6 +50,8 @@ module Bundler
@major_version >= target_major_version @major_version >= target_major_version
end end
attr_reader :bundler_version
def initialize(bundler_version) def initialize(bundler_version)
@bundler_version = Gem::Version.create(bundler_version) @bundler_version = Gem::Version.create(bundler_version)
@major_version = @bundler_version.segments.first @major_version = @bundler_version.segments.first

View file

@ -98,7 +98,6 @@ module Bundler
def autoswitching_applies? def autoswitching_applies?
ENV["BUNDLER_VERSION"].nil? && ENV["BUNDLER_VERSION"].nil? &&
ENV["BUNDLER_4_MODE"].nil? &&
ruby_can_restart_with_same_arguments? && ruby_can_restart_with_same_arguments? &&
lockfile_version lockfile_version
end end

View file

@ -1,7 +1,7 @@
# frozen_string_literal: false # frozen_string_literal: false
module Bundler module Bundler
VERSION = (ENV["BUNDLER_4_MODE"] == "true" ? "4.0.0" : "2.7.0.dev").freeze VERSION = "2.7.0.dev".freeze
def self.bundler_major_version def self.bundler_major_version
@bundler_major_version ||= gem_version.segments.first @bundler_major_version ||= gem_version.segments.first

View file

@ -333,7 +333,7 @@ that would suck --ehhh=oh geez it looks like i might have broken bundler somehow
C C
expect(Bundler.ui).not_to receive(:warn) expect(Bundler.ui).not_to receive(:warn)
expect(settings.all).to be_empty expect(settings.all).to eq(simulated_version ? ["simulate_version"] : [])
end end
it "converts older keys with dashes" do it "converts older keys with dashes" do

View file

@ -453,7 +453,7 @@ E
it "does not make bundler crash and ignores the configuration" do it "does not make bundler crash and ignores the configuration" do
bundle "config list --parseable" bundle "config list --parseable"
expect(out).to be_empty expect(out).to eq(simulated_version ? "simulate_version=#{simulated_version}" : "")
expect(err).to be_empty expect(err).to be_empty
ruby(<<~RUBY) ruby(<<~RUBY)
@ -476,26 +476,38 @@ E
describe "subcommands" do describe "subcommands" do
it "list" do it "list" do
bundle "config list", env: { "BUNDLE_FOO" => "bar" } bundle "config list", env: { "BUNDLE_FOO" => "bar" }
expect(out).to eq "Settings are listed in order of priority. The top value will be used.\nfoo\nSet via BUNDLE_FOO: \"bar\"" expected = "Settings are listed in order of priority. The top value will be used.\nfoo\nSet via BUNDLE_FOO: \"bar\""
expected += "\n\nsimulate_version\nSet via BUNDLE_SIMULATE_VERSION: \"#{simulated_version}\"" if simulated_version
expect(out).to eq(expected)
bundle "config list", env: { "BUNDLE_FOO" => "bar" }, parseable: true bundle "config list", env: { "BUNDLE_FOO" => "bar" }, parseable: true
expect(out).to eq "foo=bar" expected = "foo=bar"
expected += "\nsimulate_version=#{simulated_version}" if simulated_version
expect(out).to eq(expected)
end end
it "list with credentials" do it "list with credentials" do
bundle "config list", env: { "BUNDLE_GEMS__MYSERVER__COM" => "user:password" } bundle "config list", env: { "BUNDLE_GEMS__MYSERVER__COM" => "user:password" }
expect(out).to eq "Settings are listed in order of priority. The top value will be used.\ngems.myserver.com\nSet via BUNDLE_GEMS__MYSERVER__COM: \"user:[REDACTED]\"" expected = "Settings are listed in order of priority. The top value will be used.\ngems.myserver.com\nSet via BUNDLE_GEMS__MYSERVER__COM: \"user:[REDACTED]\""
expected += "\n\nsimulate_version\nSet via BUNDLE_SIMULATE_VERSION: \"#{simulated_version}\"" if simulated_version
expect(out).to eq(expected)
bundle "config list", parseable: true, env: { "BUNDLE_GEMS__MYSERVER__COM" => "user:password" } bundle "config list", parseable: true, env: { "BUNDLE_GEMS__MYSERVER__COM" => "user:password" }
expect(out).to eq "gems.myserver.com=user:password" expected = "gems.myserver.com=user:password"
expected += "\nsimulate_version=#{simulated_version}" if simulated_version
expect(out).to eq(expected)
end end
it "list with API token credentials" do it "list with API token credentials" do
bundle "config list", env: { "BUNDLE_GEMS__MYSERVER__COM" => "api_token:x-oauth-basic" } bundle "config list", env: { "BUNDLE_GEMS__MYSERVER__COM" => "api_token:x-oauth-basic" }
expect(out).to eq "Settings are listed in order of priority. The top value will be used.\ngems.myserver.com\nSet via BUNDLE_GEMS__MYSERVER__COM: \"[REDACTED]:x-oauth-basic\"" expected = "Settings are listed in order of priority. The top value will be used.\ngems.myserver.com\nSet via BUNDLE_GEMS__MYSERVER__COM: \"[REDACTED]:x-oauth-basic\""
expected += "\n\nsimulate_version\nSet via BUNDLE_SIMULATE_VERSION: \"#{simulated_version}\"" if simulated_version
expect(out).to eq(expected)
bundle "config list", parseable: true, env: { "BUNDLE_GEMS__MYSERVER__COM" => "api_token:x-oauth-basic" } bundle "config list", parseable: true, env: { "BUNDLE_GEMS__MYSERVER__COM" => "api_token:x-oauth-basic" }
expect(out).to eq "gems.myserver.com=api_token:x-oauth-basic" expected = "gems.myserver.com=api_token:x-oauth-basic"
expected += "\nsimulate_version=#{simulated_version}" if simulated_version
expect(out).to eq(expected)
end end
it "get" do it "get" do

View file

@ -109,7 +109,7 @@ RSpec.describe "the lockfile format" do
#{version} #{version}
L L
install_gemfile <<-G, verbose: true, env: { "BUNDLER_4_MODE" => nil } install_gemfile <<-G, verbose: true
source "https://gem.repo4" source "https://gem.repo4"
gem "myrack" gem "myrack"

View file

@ -131,14 +131,8 @@ RSpec.describe "bundle install with complex dependencies", realworld: true do
end end
G G
if Bundler.feature_flag.bundler_4_mode? bundle "lock", env: { "DEBUG_RESOLVER" => "1" }
bundle "lock", env: { "DEBUG_RESOLVER" => "1" }, raise_on_error: false
expect(out).to include("backtracking").exactly(26).times expect(out).to include("Solution found after 10 attempts")
else
bundle "lock", env: { "DEBUG_RESOLVER" => "1" }
expect(out).to include("Solution found after 10 attempts")
end
end end
end end

View file

@ -10,7 +10,7 @@ RSpec.describe "Self management" do
"9.4.0" "9.4.0"
end end
around do |example| before do
build_repo4 do build_repo4 do
build_bundler previous_minor build_bundler previous_minor
@ -26,8 +26,6 @@ RSpec.describe "Self management" do
G G
pristine_system_gems "bundler-#{current_version}" pristine_system_gems "bundler-#{current_version}"
with_env_vars("BUNDLER_4_MODE" => nil, &example)
end end
it "installs locked version when using system path and uses it" do it "installs locked version when using system path and uses it" do

View file

@ -9,5 +9,9 @@ module Spec
def rubylib def rubylib
ENV["RUBYLIB"].to_s.split(File::PATH_SEPARATOR) ENV["RUBYLIB"].to_s.split(File::PATH_SEPARATOR)
end end
def simulated_version
ENV["BUNDLE_SIMULATE_VERSION"]
end
end end
end end

View file

@ -1,9 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class RequirementChecker < Proc class RequirementChecker < Proc
def self.against(present, major_only: false) def self.against(provided, major_only: false)
present = present.split(".")[0] if major_only provided = Gem::Version.new(provided.segments.first) if major_only
provided = Gem::Version.new(present)
new do |required| new do |required|
requirement = Gem::Requirement.new(required) requirement = Gem::Requirement.new(required)
@ -28,8 +27,8 @@ end
RSpec.configure do |config| RSpec.configure do |config|
config.filter_run_excluding realworld: true config.filter_run_excluding realworld: true
config.filter_run_excluding bundler: RequirementChecker.against(Bundler::VERSION, major_only: true) config.filter_run_excluding bundler: RequirementChecker.against(Bundler.feature_flag.bundler_version, major_only: true)
config.filter_run_excluding rubygems: RequirementChecker.against(Gem::VERSION) config.filter_run_excluding rubygems: RequirementChecker.against(Gem.rubygems_version)
config.filter_run_excluding ruby_repo: !ENV["GEM_COMMAND"].nil? config.filter_run_excluding ruby_repo: !ENV["GEM_COMMAND"].nil?
config.filter_run_excluding no_color_tty: Gem.win_platform? || !ENV["GITHUB_ACTION"].nil? config.filter_run_excluding no_color_tty: Gem.win_platform? || !ENV["GITHUB_ACTION"].nil?
config.filter_run_excluding permissions: Gem.win_platform? config.filter_run_excluding permissions: Gem.win_platform?