diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index d7c61b3066e6..15b92f3e18d2 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -319,7 +319,7 @@ def install method_option "source", type: :array, banner: "Update a specific source (and all gems associated with it)" method_option "force", type: :boolean, aliases: "--redownload", banner: "Force reinstalling every gem, even if already installed" method_option "ruby", type: :boolean, banner: "Update ruby specified in Gemfile.lock" - method_option "bundler", type: :string, lazy_default: "> 0.a", banner: "Update the locked version of bundler" + method_option "bundler", type: :string, lazy_default: ">= #{Bundler::VERSION}", banner: "Update the locked version of bundler" method_option "patch", type: :boolean, banner: "Prefer updating only to next patch version" method_option "minor", type: :boolean, banner: "Prefer updating only to next minor version" method_option "major", type: :boolean, banner: "Prefer updating to next major version (default)" @@ -647,7 +647,7 @@ def inject(*) method_option "pre", type: :boolean, banner: "If updating, always choose the highest allowed version, regardless of prerelease status" method_option "strict", type: :boolean, banner: "If updating, do not allow any gem to be updated past latest --patch | --minor | --major" method_option "conservative", type: :boolean, banner: "If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated" - method_option "bundler", type: :string, lazy_default: "> 0.a", banner: "Update the locked version of bundler" + method_option "bundler", type: :string, lazy_default: ">= #{Bundler::VERSION}", banner: "Update the locked version of bundler" method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable." def lock require_relative "cli/lock" diff --git a/lib/bundler/cli/update.rb b/lib/bundler/cli/update.rb index c722474f64d7..fe740be07076 100644 --- a/lib/bundler/cli/update.rb +++ b/lib/bundler/cli/update.rb @@ -13,7 +13,7 @@ def run update_bundler = options[:bundler] - Bundler.self_manager.update_bundler_and_restart_with_it_if_needed(update_bundler) if update_bundler + Bundler.self_manager.update_bundler_and_restart_with_it_if_needed(update_bundler, pre: options[:pre]) if update_bundler sources = Array(options[:source]) groups = Array(options[:group]).map(&:to_sym) diff --git a/lib/bundler/man/bundle-lock.1 b/lib/bundler/man/bundle-lock.1 index f060fb1a3ad2..3f0f623bce96 100644 --- a/lib/bundler/man/bundle-lock.1 +++ b/lib/bundler/man/bundle-lock.1 @@ -13,7 +13,7 @@ Lock the gems specified in Gemfile\. Ignores the existing lockfile\. Resolve then updates lockfile\. Taking a list of gems or updating all gems if no list is given\. .TP \fB\-\-bundler[=BUNDLER]\fR -Update the locked version of bundler to the given version or the latest version if no version is given\. +Update the locked version of bundler\. BUNDLER can be a version such as \fB4\.0\.20\fR, or a requirement such as \fB"> 0\.a"\fR\. With no argument, update to the latest released version, which never selects a prerelease\. .TP \fB\-\-local\fR Do not attempt to connect to \fBrubygems\.org\fR\. Instead, Bundler will use the gems already present in Rubygems' cache or in \fBvendor/cache\fR\. Note that if a appropriate platform\-specific gem exists on \fBrubygems\.org\fR it will not be found\. diff --git a/lib/bundler/man/bundle-lock.1.ronn b/lib/bundler/man/bundle-lock.1.ronn index df46ac39fc51..ecf477f475f6 100644 --- a/lib/bundler/man/bundle-lock.1.ronn +++ b/lib/bundler/man/bundle-lock.1.ronn @@ -33,8 +33,9 @@ Lock the gems specified in Gemfile. of gems or updating all gems if no list is given. * `--bundler[=BUNDLER]`: - Update the locked version of bundler to the given version or the latest - version if no version is given. + Update the locked version of bundler. BUNDLER can be a version such as + `4.0.20`, or a requirement such as `"> 0.a"`. With no argument, update to the + latest released version, which never selects a prerelease. * `--local`: Do not attempt to connect to `rubygems.org`. Instead, Bundler will use the diff --git a/lib/bundler/man/bundle-update.1 b/lib/bundler/man/bundle-update.1 index 9d5ea89c4d71..2ff8401be300 100644 --- a/lib/bundler/man/bundle-update.1 +++ b/lib/bundler/man/bundle-update.1 @@ -27,7 +27,7 @@ Do not attempt to fetch gems remotely and use the gem cache instead\. Update the locked version of Ruby to the current version of Ruby\. .TP \fB\-\-bundler[=BUNDLER]\fR -Update the locked version of bundler to the invoked bundler version\. +Update the locked version of bundler\. BUNDLER can be a version such as \fB4\.0\.20\fR, or a requirement such as \fB"> 0\.a"\fR\. With no argument, update to the latest released version, which never selects a prerelease\. .TP \fB\-\-force\fR, \fB\-\-redownload\fR Force reinstalling every gem, even if already installed\. diff --git a/lib/bundler/man/bundle-update.1.ronn b/lib/bundler/man/bundle-update.1.ronn index 3ca4dc730a2f..bb1913622c71 100644 --- a/lib/bundler/man/bundle-update.1.ronn +++ b/lib/bundler/man/bundle-update.1.ronn @@ -53,7 +53,9 @@ gem. Update the locked version of Ruby to the current version of Ruby. * `--bundler[=BUNDLER]`: - Update the locked version of bundler to the invoked bundler version. + Update the locked version of bundler. BUNDLER can be a version such as + `4.0.20`, or a requirement such as `"> 0.a"`. With no argument, update to the + latest released version, which never selects a prerelease. * `--force`, `--redownload`: Force reinstalling every gem, even if already installed. diff --git a/lib/bundler/self_manager.rb b/lib/bundler/self_manager.rb index 4e6156ffa2f5..aa948841ba5a 100644 --- a/lib/bundler/self_manager.rb +++ b/lib/bundler/self_manager.rb @@ -30,8 +30,8 @@ def install_locked_bundler_and_restart_with_it_if_needed install_and_restart_with(restart_version) end - def update_bundler_and_restart_with_it_if_needed(target) - spec = resolve_update_version_from(target) + def update_bundler_and_restart_with_it_if_needed(target, pre: false) + spec = resolve_update_version_from(target, pre: pre) return unless spec version = spec.version @@ -108,9 +108,9 @@ def autoswitching_applies? lockfile_version end - def resolve_update_version_from(target) + def resolve_update_version_from(target, pre: false) requirement = Gem::Requirement.new(target) - update_candidate = find_latest_matching_spec(requirement) + update_candidate = find_latest_matching_spec(requirement, pre: pre) if update_candidate.nil? raise InvalidOption, "The `bundle update --bundler` target version (#{target}) does not exist" @@ -137,18 +137,24 @@ def remote_specs end end - def find_latest_matching_spec(requirement) + def find_latest_matching_spec(requirement, pre: false) Bundler.configure - local_result = find_latest_matching_spec_from_collection(local_specs, requirement) + # A bare `bundle update --bundler` must stay on releases, like `gem update + # --system`, so only `--pre` or a prerelease requirement opts into one. + allow_prerelease = pre || requirement.prerelease? + + local_result = find_latest_matching_spec_from_collection(local_specs, requirement, allow_prerelease) return local_result if local_result && requirement.specific? - remote_result = find_latest_matching_spec_from_collection(remote_specs, requirement) + remote_result = find_latest_matching_spec_from_collection(remote_specs, requirement, allow_prerelease) return remote_result if local_result.nil? - [local_result, remote_result].max + [local_result, remote_result].compact.max end - def find_latest_matching_spec_from_collection(specs, requirement) + def find_latest_matching_spec_from_collection(specs, requirement, allow_prerelease) + specs = specs.reject {|spec| spec.version.prerelease? } unless allow_prerelease + specs.sort.reverse_each.find {|spec| requirement.satisfied_by?(spec.version) } end diff --git a/spec/commands/lock_spec.rb b/spec/commands/lock_spec.rb index 97a3989d4b83..c02ab1ef205b 100644 --- a/spec/commands/lock_spec.rb +++ b/spec/commands/lock_spec.rb @@ -886,6 +886,26 @@ expect(lockfile).to end_with("BUNDLED WITH\n 99\n") end + it "does not update the bundler version in the lockfile to a prerelease version, unless the target version allows prereleases" do + build_repo4 do + build_gem "bundler", "55" + build_gem "bundler", "56.0.0.beta1" + end + + system_gems "bundler-55", gem_repo: gem_repo4 + + install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + source "https://gem.repo4" + G + lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, '\11.0.0\2') + + bundle "lock --update --bundler --verbose", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + expect(lockfile).to end_with("BUNDLED WITH\n 55\n") + + bundle "lock --update --bundler '> 0.a' --verbose", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + expect(lockfile).to end_with("BUNDLED WITH\n 56.0.0.beta1\n") + end + it "supports adding new platforms when there's no previous lockfile" do gemfile_with_rails_weakling_and_foo_from_repo4 diff --git a/spec/commands/update_scenarios_spec.rb b/spec/commands/update_scenarios_spec.rb index f1d9635bb22b..5428b6169ee1 100644 --- a/spec/commands/update_scenarios_spec.rb +++ b/spec/commands/update_scenarios_spec.rb @@ -466,7 +466,7 @@ bundle :update, bundler: true, verbose: true expect(out).to include("Updating bundler to 999.0.0") - expect(out).to include("Running `bundle update --bundler \"> 0.a\" --verbose` with bundler 999.0.0") + expect(out).to include("Running `bundle update --bundler \">= 999.0.0\" --verbose` with bundler 999.0.0") expect(out).not_to include("Installing Bundler 2.99.9 and restarting using that version.") expect(lockfile).to eq <<~L @@ -578,6 +578,180 @@ expect(out).to include("myrack (1.0)") end + it "does not update the bundler version in the lockfile to a prerelease version", :ruby_repo do + pristine_system_gems "bundler-9.9.9" + + build_repo4 do + build_gem "myrack", "1.0" + + build_bundler "9.9.9" + build_bundler "999.0.0.beta1" + end + + checksums = checksums_section do |c| + c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(gem_repo4, "bundler", "9.9.9") + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + bundle :update, bundler: true, verbose: true + + expect(out).to include("Using bundler 9.9.9") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 9.9.9 + L + end + + it "updates the bundler version in the lockfile to a prerelease version when the target version allows prereleases", :ruby_repo do + bundle_config "path.system true" + + pristine_system_gems "bundler-9.0.0" + + build_repo4 do + build_gem "myrack", "1.0" + + build_bundler "999.0.0.beta1" + end + + checksums = checksums_section do |c| + c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(gem_repo4, "bundler", "999.0.0.beta1") + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + bundle "update --bundler '> 0.a' --verbose" + + expect(out).to include("Updating bundler to 999.0.0.beta1") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 999.0.0.beta1 + L + end + + it "goes back to a released version given explicitly when the lockfile is locked to a prerelease", :ruby_repo do + bundle_config "path.system true" + + pristine_system_gems "bundler-9.0.0.beta1" + + build_repo4 do + build_gem "myrack", "1.0" + + build_bundler "9.0.0" + end + + checksums = checksums_section do |c| + c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(gem_repo4, "bundler", "9.0.0") + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + # Auto switching puts the beta back in charge on every command, so an + # explicit target is the only way out of a lockfile that names one. + expect(lockfile).to match(/BUNDLED WITH\n\s+9\.0\.0\.beta1\n/) + + bundle "update --bundler 9.0.0 --verbose" + + expect(out).to include("Updating bundler to 9.0.0") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 9.0.0 + L + + bundle "--version" + expect(out).to include("9.0.0") + end + + it "updates the bundler version in the lockfile to a prerelease version when --pre is given", :ruby_repo do + bundle_config "path.system true" + + pristine_system_gems "bundler-9.0.0" + + build_repo4 do + build_gem "myrack", "1.0" + + build_bundler "999.0.0.beta1" + end + + checksums = checksums_section do |c| + c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(gem_repo4, "bundler", "999.0.0.beta1") + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + bundle :update, bundler: true, pre: true, verbose: true + + expect(out).to include("Updating bundler to 999.0.0.beta1") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 999.0.0.beta1 + L + end + it "errors if the explicit target version does not exist" do pristine_system_gems "bundler-9.9.9"