[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
def feature_flag
@feature_flag ||= FeatureFlag.new(VERSION)
@feature_flag ||= FeatureFlag.new(settings[:simulate_version] || VERSION)
end
def reset!

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
# frozen_string_literal: false
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
@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
expect(Bundler.ui).not_to receive(:warn)
expect(settings.all).to be_empty
expect(settings.all).to eq(simulated_version ? ["simulate_version"] : [])
end
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
bundle "config list --parseable"
expect(out).to be_empty
expect(out).to eq(simulated_version ? "simulate_version=#{simulated_version}" : "")
expect(err).to be_empty
ruby(<<~RUBY)
@ -476,26 +476,38 @@ E
describe "subcommands" do
it "list" do
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
expect(out).to eq "foo=bar"
expected = "foo=bar"
expected += "\nsimulate_version=#{simulated_version}" if simulated_version
expect(out).to eq(expected)
end
it "list with credentials" do
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" }
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
it "list with API token credentials" do
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" }
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
it "get" do

View file

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

View file

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

View file

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

View file

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

View file

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