ruby/lib/bundler/self_manager.rb
David Rodríguez 90085f62fb [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
2025-06-26 08:06:48 +09:00

196 lines
5.9 KiB
Ruby

# frozen_string_literal: true
module Bundler
#
# This class handles installing and switching to the version of bundler needed
# by an application.
#
class SelfManager
def restart_with_locked_bundler_if_needed
restart_version = find_restart_version
return unless restart_version && installed?(restart_version)
restart_with(restart_version)
end
def install_locked_bundler_and_restart_with_it_if_needed
restart_version = find_restart_version
return unless restart_version
if restart_version == lockfile_version
Bundler.ui.info \
"Bundler #{current_version} is running, but your lockfile was generated with #{lockfile_version}. " \
"Installing Bundler #{lockfile_version} and restarting using that version."
else
Bundler.ui.info \
"Bundler #{current_version} is running, but your configuration was #{restart_version}. " \
"Installing Bundler #{restart_version} and restarting using that version."
end
install_and_restart_with(restart_version)
end
def update_bundler_and_restart_with_it_if_needed(target)
spec = resolve_update_version_from(target)
return unless spec
version = spec.version
Bundler.ui.info "Updating bundler to #{version}."
install(spec) unless installed?(version)
restart_with(version)
end
private
def install_and_restart_with(version)
requirement = Gem::Requirement.new(version)
spec = find_latest_matching_spec(requirement)
if spec.nil?
Bundler.ui.warn "Your lockfile is locked to a version of bundler (#{lockfile_version}) that doesn't exist at https://rubygems.org/. Going on using #{current_version}"
return
end
install(spec)
rescue StandardError => e
Bundler.ui.trace e
Bundler.ui.warn "There was an error installing the locked bundler version (#{lockfile_version}), rerun with the `--verbose` flag for more details. Going on using bundler #{current_version}."
else
restart_with(version)
end
def install(spec)
spec.source.install(spec)
end
def restart_with(version)
configured_gem_home = ENV["GEM_HOME"]
configured_orig_gem_home = ENV["BUNDLER_ORIG_GEM_HOME"]
configured_gem_path = ENV["GEM_PATH"]
configured_orig_gem_path = ENV["BUNDLER_ORIG_GEM_PATH"]
argv0 = File.exist?($PROGRAM_NAME) ? $PROGRAM_NAME : Process.argv0
cmd = [argv0, *ARGV]
cmd.unshift(Gem.ruby) unless File.executable?(argv0)
Bundler.with_original_env do
Kernel.exec(
{
"GEM_HOME" => configured_gem_home,
"BUNDLER_ORIG_GEM_HOME" => configured_orig_gem_home,
"GEM_PATH" => configured_gem_path,
"BUNDLER_ORIG_GEM_PATH" => configured_orig_gem_path,
"BUNDLER_VERSION" => version.to_s,
},
*cmd
)
end
end
def needs_switching?(restart_version)
autoswitching_applies? &&
released?(restart_version) &&
!running?(restart_version)
end
def autoswitching_applies?
ENV["BUNDLER_VERSION"].nil? &&
ruby_can_restart_with_same_arguments? &&
lockfile_version
end
def resolve_update_version_from(target)
requirement = Gem::Requirement.new(target)
update_candidate = find_latest_matching_spec(requirement)
if update_candidate.nil?
raise InvalidOption, "The `bundle update --bundler` target version (#{target}) does not exist"
end
resolved_version = update_candidate.version
needs_update = requirement.specific? ? !running?(resolved_version) : running_older_than?(resolved_version)
return unless needs_update
update_candidate
end
def local_specs
@local_specs ||= Bundler::Source::Rubygems.new("allow_local" => true).specs.select {|spec| spec.name == "bundler" }
end
def remote_specs
@remote_specs ||= begin
source = Bundler::Source::Rubygems.new("remotes" => "https://rubygems.org")
source.remote!
source.add_dependency_names("bundler")
source.specs.select(&:matches_current_metadata?)
end
end
def find_latest_matching_spec(requirement)
Bundler.configure
local_result = find_latest_matching_spec_from_collection(local_specs, requirement)
return local_result if local_result && requirement.specific?
remote_result = find_latest_matching_spec_from_collection(remote_specs, requirement)
return remote_result if local_result.nil?
[local_result, remote_result].max
end
def find_latest_matching_spec_from_collection(specs, requirement)
specs.sort.reverse_each.find {|spec| requirement.satisfied_by?(spec.version) }
end
def running?(version)
version == current_version
end
def running_older_than?(version)
current_version < version
end
def released?(version)
!version.to_s.end_with?(".dev")
end
def ruby_can_restart_with_same_arguments?
$PROGRAM_NAME != "-e"
end
def installed?(restart_version)
Bundler.configure
Bundler.rubygems.find_bundler(restart_version.to_s)
end
def current_version
@current_version ||= Bundler.gem_version
end
def lockfile_version
return @lockfile_version if defined?(@lockfile_version)
parsed_version = Bundler::LockfileParser.bundled_with
@lockfile_version = parsed_version ? Gem::Version.new(parsed_version) : nil
rescue ArgumentError
@lockfile_version = nil
end
def find_restart_version
return unless SharedHelpers.in_bundle?
configured_version = Bundler.settings[:version]
return if configured_version == "system"
restart_version = configured_version == "lockfile" ? lockfile_version : Gem::Version.new(configured_version)
return unless needs_switching?(restart_version)
restart_version
end
end
end