Skip to content

Commit

Permalink
Merge pull request #5182 from rubygems/bundle-update-bundler
Browse files Browse the repository at this point in the history
Make `bundle update --bundler` actually lock to the latest bundler version (even if not yet installed)

(cherry picked from commit 474a5e8)
  • Loading branch information
deivid-rodriguez committed Jan 12, 2022
1 parent 8b86bed commit ab1e3e5
Show file tree
Hide file tree
Showing 14 changed files with 306 additions and 73 deletions.
13 changes: 3 additions & 10 deletions bundler/lib/bundler/cli.rb
Expand Up @@ -809,17 +809,10 @@ def warn_on_outdated_bundler

current = Gem::Version.new(VERSION)
return if current >= latest
latest_installed = Bundler.rubygems.find_name("bundler").map(&:version).max

installation = "To install the latest version, run `gem install bundler#{" --pre" if latest.prerelease?}`"
if latest_installed && latest_installed > current
suggestion = "To update to the most recent installed version (#{latest_installed}), run `bundle update --bundler`"
suggestion = "#{installation}\n#{suggestion}" if latest_installed < latest
else
suggestion = installation
end

Bundler.ui.warn "The latest bundler is #{latest}, but you are currently running #{current}.\n#{suggestion}"
Bundler.ui.warn \
"The latest bundler is #{latest}, but you are currently running #{current}.\n" \
"To update to the most recent version, run `bundle update --bundler`"
rescue RuntimeError
nil
end
Expand Down
8 changes: 6 additions & 2 deletions bundler/lib/bundler/cli/update.rb
Expand Up @@ -11,12 +11,16 @@ def initialize(options, gems)
def run
Bundler.ui.level = "warn" if options[:quiet]

update_bundler = options[:bundler]

Bundler.self_manager.update_bundler_and_restart_with_it_if_needed(update_bundler) if update_bundler

Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.feature_flag.plugins?

sources = Array(options[:source])
groups = Array(options[:group]).map(&:to_sym)

full_update = gems.empty? && sources.empty? && groups.empty? && !options[:ruby] && !options[:bundler]
full_update = gems.empty? && sources.empty? && groups.empty? && !options[:ruby] && !update_bundler

if full_update && !options[:all]
if Bundler.feature_flag.update_requires_all_flag?
Expand Down Expand Up @@ -49,7 +53,7 @@ def run

Bundler.definition(:gems => gems, :sources => sources, :ruby => options[:ruby],
:conservative => conservative,
:bundler => options[:bundler])
:bundler => update_bundler)
end

Bundler::CLI::Common.configure_gem_version_promoter(Bundler.definition, options)
Expand Down
8 changes: 0 additions & 8 deletions bundler/lib/bundler/definition.rb
Expand Up @@ -309,14 +309,6 @@ def lock(file, preserve_unknown_sections = false)
end
end

def locked_bundler_version
if @locked_bundler_version && @locked_bundler_version < Gem::Version.new(Bundler::VERSION)
new_version = Bundler::VERSION
end

new_version || @locked_bundler_version || Bundler::VERSION
end

def locked_ruby_version
return unless ruby_version
if @unlock[:ruby] || !@locked_ruby_version
Expand Down
2 changes: 1 addition & 1 deletion bundler/lib/bundler/lockfile_generator.rb
Expand Up @@ -71,7 +71,7 @@ def add_locked_ruby_version
end

def add_bundled_with
add_section("BUNDLED WITH", definition.locked_bundler_version.to_s)
add_section("BUNDLED WITH", Bundler::VERSION)
end

def add_section(name, value)
Expand Down
132 changes: 103 additions & 29 deletions bundler/lib/bundler/self_manager.rb
Expand Up @@ -9,46 +9,58 @@ class SelfManager
def restart_with_locked_bundler_if_needed
return unless needs_switching? && installed?

restart_with_locked_bundler
restart_with(lockfile_version)
end

def install_locked_bundler_and_restart_with_it_if_needed
return unless needs_switching?

install_and_restart_with_locked_bundler
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."

install_and_restart_with(lockfile_version)
end

def update_bundler_and_restart_with_it_if_needed(target)
return unless autoswitching_applies?

spec = resolve_update_version_from(target)
return unless spec

version = spec.version

Bundler.ui.info "Updating bundler to #{version}."

install(spec)

restart_with(version)
end

private

def install_and_restart_with_locked_bundler
bundler_dep = Gem::Dependency.new("bundler", lockfile_version)
spec = fetch_spec_for(bundler_dep)
return if spec.nil?
def install_and_restart_with(version)
requirement = Gem::Requirement.new(version)
spec = find_latest_matching_spec(requirement)

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."
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

spec.source.install(spec)
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_locked_bundler
restart_with(version)
end

def fetch_spec_for(bundler_dep)
source = Bundler::Source::Rubygems.new("remotes" => "https://rubygems.org")
source.remote!
source.add_dependency_names("bundler")
spec = source.specs.search(bundler_dep).first
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}"
end
spec
def install(spec)
spec.source.install(spec)
end

def restart_with_locked_bundler
def restart_with(version)
configured_gem_home = ENV["GEM_HOME"]
configured_gem_path = ENV["GEM_PATH"]

Expand All @@ -57,20 +69,79 @@ def restart_with_locked_bundler

Bundler.with_original_env do
Kernel.exec(
{ "GEM_HOME" => configured_gem_home, "GEM_PATH" => configured_gem_path, "BUNDLER_VERSION" => lockfile_version },
{ "GEM_HOME" => configured_gem_home, "GEM_PATH" => configured_gem_path, "BUNDLER_VERSION" => version.to_s },
*cmd
)
end
end

def needs_switching?
autoswitching_applies? &&
released?(lockfile_version) &&
!running?(lockfile_version) &&
!updating?
end

def autoswitching_applies?
ENV["BUNDLER_VERSION"].nil? &&
Bundler.rubygems.supports_bundler_trampolining? &&
SharedHelpers.in_bundle? &&
lockfile_version &&
!lockfile_version.end_with?(".dev") &&
lockfile_version != current_version &&
!updating?
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
end
end

def find_latest_matching_spec(requirement)
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 updating?
Expand All @@ -80,15 +151,18 @@ def updating?
def installed?
Bundler.configure

Bundler.rubygems.find_bundler(lockfile_version)
Bundler.rubygems.find_bundler(lockfile_version.to_s)
end

def current_version
@current_version ||= Bundler::VERSION
@current_version ||= Gem::Version.new(Bundler::VERSION)
end

def lockfile_version
@lockfile_version ||= Bundler::LockfileParser.bundled_with
return @lockfile_version if defined?(@lockfile_version)

parsed_version = Bundler::LockfileParser.bundled_with
@lockfile_version = parsed_version ? Gem::Version.new(parsed_version) : nil
end
end
end
4 changes: 2 additions & 2 deletions bundler/spec/bundler/cli_spec.rb
Expand Up @@ -170,7 +170,7 @@ def out_with_macos_man_workaround
bundle "fail", :env => { "BUNDLER_VERSION" => bundler_version }, :raise_on_error => false
expect(err).to start_with(<<-EOS.strip)
The latest bundler is #{latest_version}, but you are currently running #{bundler_version}.
To install the latest version, run `gem install bundler`
To update to the most recent version, run `bundle update --bundler`
EOS
end

Expand All @@ -195,7 +195,7 @@ def out_with_macos_man_workaround
bundle "fail", :env => { "BUNDLER_VERSION" => bundler_version }, :raise_on_error => false
expect(err).to start_with(<<-EOS.strip)
The latest bundler is #{latest_version}, but you are currently running #{bundler_version}.
To install the latest version, run `gem install bundler --pre`
To update to the most recent version, run `bundle update --bundler`
EOS
end
end
Expand Down
8 changes: 4 additions & 4 deletions bundler/spec/commands/binstubs_spec.rb
Expand Up @@ -182,7 +182,7 @@
end

context "and the version is older and the same major" do
let(:system_bundler_version) { "2.3.1" }
let(:system_bundler_version) { "2.3.3" }

before do
lockfile lockfile.gsub(/BUNDLED WITH\n .*$/m, "BUNDLED WITH\n 2.3.0")
Expand All @@ -191,16 +191,16 @@
it "installs and runs the exact version of bundler", :rubygems => ">= 3.3.0.dev" do
sys_exec "bin/bundle install --verbose", :artifice => "vcr"
expect(exitstatus).not_to eq(42)
expect(out).to include("Bundler 2.3.1 is running, but your lockfile was generated with 2.3.0. Installing Bundler 2.3.0 and restarting using that version.")
expect(out).to include("Bundler 2.3.3 is running, but your lockfile was generated with 2.3.0. Installing Bundler 2.3.0 and restarting using that version.")
expect(out).to include("Using bundler 2.3.0")
expect(err).not_to include("Activating bundler (~> 2.3.0) failed:")
end

it "runs the available version of bundler", :rubygems => "< 3.3.0.dev" do
sys_exec "bin/bundle install --verbose"
expect(exitstatus).not_to eq(42)
expect(out).not_to include("Bundler 2.3.1 is running, but your lockfile was generated with 2.3.0. Installing Bundler 2.3.0 and restarting using that version.")
expect(out).to include("Using bundler 2.3.1")
expect(out).not_to include("Bundler 2.3.3 is running, but your lockfile was generated with 2.3.0. Installing Bundler 2.3.0 and restarting using that version.")
expect(out).to include("Using bundler 2.3.3")
expect(err).not_to include("Activating bundler (~> 2.3.0) failed:")
end
end
Expand Down

0 comments on commit ab1e3e5

Please sign in to comment.