diff --git a/bundler/lib/bundler/man/bundle-config.1 b/bundler/lib/bundler/man/bundle-config.1 index ed66ba9a4817..336030d9e0e6 100644 --- a/bundler/lib/bundler/man/bundle-config.1 +++ b/bundler/lib/bundler/man/bundle-config.1 @@ -142,6 +142,12 @@ When set, no funding requests will be printed\. \fBignore_messages\fR (\fBBUNDLE_IGNORE_MESSAGES\fR) When set, no post install messages will be printed\. To silence a single gem, use dot notation like \fBignore_messages\.httparty true\fR\. .TP +\fBignore_ruby_upper_bounds\fR (\fBBUNDLE_IGNORE_RUBY_UPPER_BOUNDS\fR) +When set, Bundler ignores upper bound constraints (\fB<\fR and \fB<=\fR) on \fBrequired_ruby_version\fR in gem metadata\. Useful when gems haven't updated their gemspecs for newer Ruby versions but still work fine\. +.TP +\fBignore_rubygems_upper_bounds\fR (\fBBUNDLE_IGNORE_RUBYGEMS_UPPER_BOUNDS\fR) +When set, Bundler ignores upper bound constraints (\fB<\fR and \fB<=\fR) on \fBrequired_rubygems_version\fR in gem metadata\. Useful when gems haven't updated their gemspecs for newer RubyGems versions but still work fine\. +.TP \fBinit_gems_rb\fR (\fBBUNDLE_INIT_GEMS_RB\fR) Generate a \fBgems\.rb\fR instead of a \fBGemfile\fR when running \fBbundle init\fR\. .TP diff --git a/bundler/lib/bundler/man/bundle-config.1.ronn b/bundler/lib/bundler/man/bundle-config.1.ronn index b70293cfedda..7774a31d1bb7 100644 --- a/bundler/lib/bundler/man/bundle-config.1.ronn +++ b/bundler/lib/bundler/man/bundle-config.1.ronn @@ -189,6 +189,14 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). * `ignore_messages` (`BUNDLE_IGNORE_MESSAGES`): When set, no post install messages will be printed. To silence a single gem, use dot notation like `ignore_messages.httparty true`. +* `ignore_ruby_upper_bounds` (`BUNDLE_IGNORE_RUBY_UPPER_BOUNDS`): + When set, Bundler ignores upper bound constraints (`<` and `<=`) on + `required_ruby_version` in gem metadata. Useful when gems haven't updated + their gemspecs for newer Ruby versions but still work fine. +* `ignore_rubygems_upper_bounds` (`BUNDLE_IGNORE_RUBYGEMS_UPPER_BOUNDS`): + When set, Bundler ignores upper bound constraints (`<` and `<=`) on + `required_rubygems_version` in gem metadata. Useful when gems haven't + updated their gemspecs for newer RubyGems versions but still work fine. * `init_gems_rb` (`BUNDLE_INIT_GEMS_RB`): Generate a `gems.rb` instead of a `Gemfile` when running `bundle init`. * `jobs` (`BUNDLE_JOBS`): diff --git a/bundler/lib/bundler/match_metadata.rb b/bundler/lib/bundler/match_metadata.rb index 6fd2994a85f2..de692e3b07f8 100644 --- a/bundler/lib/bundler/match_metadata.rb +++ b/bundler/lib/bundler/match_metadata.rb @@ -7,11 +7,21 @@ def matches_current_metadata? end def matches_current_ruby? - @required_ruby_version.satisfied_by?(Gem.ruby_version) + requirement = if Bundler.settings[:ignore_ruby_upper_bounds] + remove_upper_bounds(@required_ruby_version) + else + @required_ruby_version + end + requirement.satisfied_by?(Gem.ruby_version) end def matches_current_rubygems? - @required_rubygems_version.satisfied_by?(Gem.rubygems_version) + requirement = if Bundler.settings[:ignore_rubygems_upper_bounds] + remove_upper_bounds(@required_rubygems_version) + else + @required_rubygems_version + end + requirement.satisfied_by?(Gem.rubygems_version) end def expanded_dependencies @@ -24,7 +34,24 @@ def expanded_dependencies def metadata_dependency(name, requirement) return if requirement.nil? || requirement.none? + if (name == "Ruby" && Bundler.settings[:ignore_ruby_upper_bounds]) || + (name == "RubyGems" && Bundler.settings[:ignore_rubygems_upper_bounds]) + requirement = remove_upper_bounds(requirement) + return if requirement.none? + end + Gem::Dependency.new("#{name}\0", requirement) end + + private + + def remove_upper_bounds(requirement) + filtered = requirement.requirements.reject {|op, _| ["<", "<="].include?(op) } + if filtered.empty? + Gem::Requirement.default + else + Gem::Requirement.new(filtered.map {|op, v| "#{op} #{v}" }) + end + end end end diff --git a/bundler/lib/bundler/settings.rb b/bundler/lib/bundler/settings.rb index 120a3202afd5..a3c6bbbe8c4d 100644 --- a/bundler/lib/bundler/settings.rb +++ b/bundler/lib/bundler/settings.rb @@ -27,6 +27,8 @@ class Settings git.allow_insecure global_gem_cache ignore_messages + ignore_ruby_upper_bounds + ignore_rubygems_upper_bounds init_gems_rb inline lockfile_checksums diff --git a/spec/bundler/match_metadata_spec.rb b/spec/bundler/match_metadata_spec.rb new file mode 100644 index 000000000000..548d9925e975 --- /dev/null +++ b/spec/bundler/match_metadata_spec.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::MatchMetadata do + let(:klass) do + Class.new do + include Bundler::MatchMetadata + + attr_accessor :required_ruby_version, :required_rubygems_version, :dependencies + + def initialize(ruby_version, rubygems_version) + @required_ruby_version = ruby_version + @required_rubygems_version = rubygems_version + @dependencies = [] + end + + alias_method :runtime_dependencies, :dependencies + end + end + + describe "#matches_current_ruby?" do + context "when the ruby version has an upper bound that excludes the current ruby" do + let(:spec) { klass.new(Gem::Requirement.new(">= 3.0", "< 3.1"), Gem::Requirement.default) } + + it "returns false by default" do + expect(spec.matches_current_ruby?).to be false + end + + it "returns true when ignore_ruby_upper_bounds is set" do + bundle "config set ignore_ruby_upper_bounds true" + expect(spec.matches_current_ruby?).to be true + end + end + + context "when the ruby version has only a lower bound" do + let(:spec) { klass.new(Gem::Requirement.new(">= 2.0"), Gem::Requirement.default) } + + it "returns true regardless of setting" do + expect(spec.matches_current_ruby?).to be true + + bundle "config set ignore_ruby_upper_bounds true" + expect(spec.matches_current_ruby?).to be true + end + end + + context "when the ruby version has only an upper bound" do + let(:spec) { klass.new(Gem::Requirement.new("< 3.1"), Gem::Requirement.default) } + + it "returns false by default" do + expect(spec.matches_current_ruby?).to be false + end + + it "returns true when ignore_ruby_upper_bounds is set (upper bound removed, defaults to >= 0)" do + bundle "config set ignore_ruby_upper_bounds true" + expect(spec.matches_current_ruby?).to be true + end + end + end + + describe "#matches_current_rubygems?" do + context "when the rubygems version has an upper bound that excludes the current rubygems" do + let(:spec) { klass.new(Gem::Requirement.default, Gem::Requirement.new(">= 3.0", "< 3.1")) } + + it "returns false by default" do + expect(spec.matches_current_rubygems?).to be false + end + + it "returns true when ignore_rubygems_upper_bounds is set" do + bundle "config set ignore_rubygems_upper_bounds true" + expect(spec.matches_current_rubygems?).to be true + end + end + + context "when the rubygems version has only a lower bound" do + let(:spec) { klass.new(Gem::Requirement.default, Gem::Requirement.new(">= 2.0")) } + + it "returns true regardless of setting" do + expect(spec.matches_current_rubygems?).to be true + + bundle "config set ignore_rubygems_upper_bounds true" + expect(spec.matches_current_rubygems?).to be true + end + end + end + + describe "#metadata_dependency" do + context "when ignore_ruby_upper_bounds is set" do + before { bundle "config set ignore_ruby_upper_bounds true" } + + it "removes upper bounds from Ruby dependency" do + spec = klass.new(Gem::Requirement.new(">= 3.0", "< 3.1"), Gem::Requirement.default) + dep = spec.send(:metadata_dependency, "Ruby", spec.required_ruby_version) + expect(dep.requirement).to eq(Gem::Requirement.new(">= 3.0")) + end + + it "returns nil when only upper bounds exist" do + spec = klass.new(Gem::Requirement.new("< 3.1"), Gem::Requirement.default) + dep = spec.send(:metadata_dependency, "Ruby", spec.required_ruby_version) + expect(dep).to be_nil + end + + it "does not affect RubyGems dependency" do + spec = klass.new(Gem::Requirement.default, Gem::Requirement.new(">= 3.0", "< 4.0")) + dep = spec.send(:metadata_dependency, "RubyGems", spec.required_rubygems_version) + expect(dep.requirement).to eq(Gem::Requirement.new(">= 3.0", "< 4.0")) + end + end + + context "when ignore_rubygems_upper_bounds is set" do + before { bundle "config set ignore_rubygems_upper_bounds true" } + + it "removes upper bounds from RubyGems dependency" do + spec = klass.new(Gem::Requirement.default, Gem::Requirement.new(">= 3.0", "< 4.0")) + dep = spec.send(:metadata_dependency, "RubyGems", spec.required_rubygems_version) + expect(dep.requirement).to eq(Gem::Requirement.new(">= 3.0")) + end + + it "does not affect Ruby dependency" do + spec = klass.new(Gem::Requirement.new(">= 3.0", "< 3.1"), Gem::Requirement.default) + dep = spec.send(:metadata_dependency, "Ruby", spec.required_ruby_version) + expect(dep.requirement).to eq(Gem::Requirement.new(">= 3.0", "< 3.1")) + end + end + end +end