From 0bc96a3bcad2f1f3cdac7669854047b219a122ce Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Fri, 6 Feb 2026 17:15:15 -0500 Subject: [PATCH 1/4] Add ignore_ruby_upper_bounds setting to skip < and <= Ruby constraints Opt-in setting (BUNDLE_IGNORE_RUBY_UPPER_BOUNDS=true) that filters out upper-bound Ruby version requirements from gem metadata. Useful when gems haven't updated their metadata for newer Ruby versions. Co-Authored-By: Claude Opus 4.6 --- bundler/lib/bundler/match_metadata.rb | 6 ++++++ bundler/lib/bundler/settings.rb | 1 + 2 files changed, 7 insertions(+) diff --git a/bundler/lib/bundler/match_metadata.rb b/bundler/lib/bundler/match_metadata.rb index 6fd2994a85f2..863c2a250dfd 100644 --- a/bundler/lib/bundler/match_metadata.rb +++ b/bundler/lib/bundler/match_metadata.rb @@ -24,6 +24,12 @@ def expanded_dependencies def metadata_dependency(name, requirement) return if requirement.nil? || requirement.none? + if name == "Ruby" && Bundler.settings[:ignore_ruby_upper_bounds] + reqs = requirement.requirements.reject { |op, _| op == "<" || op == "<=" } + return if reqs.empty? + requirement = Gem::Requirement.new(reqs.map { |op, v| "#{op} #{v}" }) + end + Gem::Dependency.new("#{name}\0", requirement) end end diff --git a/bundler/lib/bundler/settings.rb b/bundler/lib/bundler/settings.rb index 120a3202afd5..8ae5c1b53b5c 100644 --- a/bundler/lib/bundler/settings.rb +++ b/bundler/lib/bundler/settings.rb @@ -27,6 +27,7 @@ class Settings git.allow_insecure global_gem_cache ignore_messages + ignore_ruby_upper_bounds init_gems_rb inline lockfile_checksums From a06adbf357bb92914828d10323bd460e028a4826 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 2 Apr 2026 17:54:11 +0900 Subject: [PATCH 2/4] Apply ignore_ruby_upper_bounds to matches_current_ruby? and extract remove_upper_bounds The cherry-picked commit from tobi only applied the upper bounds filtering to metadata_dependency (resolver path). This extends it to matches_current_ruby? so that the install-time compatibility check in ensure_specs_are_compatible! also respects the setting. The inline filtering logic is extracted into a shared remove_upper_bounds method. Co-Authored-By: Claude Opus 4.6 (1M context) --- bundler/lib/bundler/match_metadata.rb | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/bundler/lib/bundler/match_metadata.rb b/bundler/lib/bundler/match_metadata.rb index 863c2a250dfd..6e96827757ab 100644 --- a/bundler/lib/bundler/match_metadata.rb +++ b/bundler/lib/bundler/match_metadata.rb @@ -7,7 +7,12 @@ 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? @@ -25,12 +30,22 @@ def metadata_dependency(name, requirement) return if requirement.nil? || requirement.none? if name == "Ruby" && Bundler.settings[:ignore_ruby_upper_bounds] - reqs = requirement.requirements.reject { |op, _| op == "<" || op == "<=" } - return if reqs.empty? - requirement = Gem::Requirement.new(reqs.map { |op, v| "#{op} #{v}" }) + 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, _| op == "<" || op == "<=" } + if filtered.empty? + Gem::Requirement.default + else + Gem::Requirement.new(filtered.map {|op, v| "#{op} #{v}" }) + end + end end end From b75e643d9b82db9bd5928a372670b1da8875c44b Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 2 Apr 2026 17:55:54 +0900 Subject: [PATCH 3/4] Add tests for ignore_ruby_upper_bounds setting in MatchMetadata Test that the setting correctly removes upper bound constraints from both matches_current_ruby? and metadata_dependency, while leaving RubyGems constraints unaffected. Co-Authored-By: Claude Opus 4.6 (1M context) --- spec/bundler/match_metadata_spec.rb | 82 +++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 spec/bundler/match_metadata_spec.rb diff --git a/spec/bundler/match_metadata_spec.rb b/spec/bundler/match_metadata_spec.rb new file mode 100644 index 000000000000..968948402f17 --- /dev/null +++ b/spec/bundler/match_metadata_spec.rb @@ -0,0 +1,82 @@ +# 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 "#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 + end +end From 7f19b831bce0e4802e467ff7153ec2ec5595bf1f Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 2 Apr 2026 18:14:00 +0900 Subject: [PATCH 4/4] Add ignore_rubygems_upper_bounds setting Same approach as ignore_ruby_upper_bounds but for required_rubygems_version constraints. The metadata_dependency method now checks both settings independently, and matches_current_rubygems? also respects the setting for install-time compatibility checks. Co-Authored-By: Claude Opus 4.6 (1M context) --- bundler/lib/bundler/man/bundle-config.1 | 6 +++ bundler/lib/bundler/man/bundle-config.1.ronn | 8 ++++ bundler/lib/bundler/match_metadata.rb | 12 ++++-- bundler/lib/bundler/settings.rb | 1 + spec/bundler/match_metadata_spec.rb | 42 ++++++++++++++++++++ 5 files changed, 66 insertions(+), 3 deletions(-) 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 6e96827757ab..de692e3b07f8 100644 --- a/bundler/lib/bundler/match_metadata.rb +++ b/bundler/lib/bundler/match_metadata.rb @@ -16,7 +16,12 @@ def matches_current_ruby? 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 @@ -29,7 +34,8 @@ def expanded_dependencies def metadata_dependency(name, requirement) return if requirement.nil? || requirement.none? - if name == "Ruby" && Bundler.settings[:ignore_ruby_upper_bounds] + 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 @@ -40,7 +46,7 @@ def metadata_dependency(name, requirement) private def remove_upper_bounds(requirement) - filtered = requirement.requirements.reject {|op, _| op == "<" || op == "<=" } + filtered = requirement.requirements.reject {|op, _| ["<", "<="].include?(op) } if filtered.empty? Gem::Requirement.default else diff --git a/bundler/lib/bundler/settings.rb b/bundler/lib/bundler/settings.rb index 8ae5c1b53b5c..a3c6bbbe8c4d 100644 --- a/bundler/lib/bundler/settings.rb +++ b/bundler/lib/bundler/settings.rb @@ -28,6 +28,7 @@ class Settings 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 index 968948402f17..548d9925e975 100644 --- a/spec/bundler/match_metadata_spec.rb +++ b/spec/bundler/match_metadata_spec.rb @@ -56,6 +56,32 @@ def initialize(ruby_version, rubygems_version) 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" } @@ -78,5 +104,21 @@ def initialize(ruby_version, 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