summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHiroshi SHIBATA <hsbt@ruby-lang.org>2026-05-07 20:07:14 +0900
committergit <svn-admin@ruby-lang.org>2026-05-08 06:43:45 +0000
commit0beb804ef79c3355c19ed272106c42b02780b607 (patch)
treed16262dff90c570f67af779d35e96448de14b85d
parent0d5a5a613fc4c77586166e2a663315b083a0a451 (diff)
[ruby/rubygems] Thread overrides explicitly instead of through a Bundler global
Phase 2.C wired overrides into MatchMetadata via Bundler.overrides, a process-wide accessor read every time a spec answered matches_current_metadata?. That leaked the user's Gemfile overrides into Bundler-internal callers like SelfManager#remote_specs, where overrides have no business: a Gemfile override could let bundle self-update consider Bundler releases that are actually incompatible with the running Ruby/RubyGems. Revert MatchMetadata's matches_current_ruby? and matches_current_rubygems? to evaluate the spec's own metadata, and add explicit matches_current_*_with_overrides? variants. Pass overrides explicitly: Installer#ensure_specs_are_compatible! gets them from @definition, LazySpecification#choose_compatible reads its newly-added @overrides attribute, and SpecSet#valid? reads @overrides set on the SpecSet. Definition propagates @overrides to the SpecSets it constructs and to the LazySpecs they contain. SelfManager and other callers that should keep evaluating real gemspec metadata reach the strict path unchanged. https://github.com/ruby/rubygems/commit/e0ff753bbb Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
-rw-r--r--lib/bundler.rb5
-rw-r--r--lib/bundler/definition.rb12
-rw-r--r--lib/bundler/installer.rb5
-rw-r--r--lib/bundler/lazy_specification.rb5
-rw-r--r--lib/bundler/match_metadata.rb32
-rw-r--r--lib/bundler/match_remote_metadata.rb27
-rw-r--r--lib/bundler/spec_set.rb13
-rw-r--r--spec/bundler/bundler/override_spec.rb34
8 files changed, 96 insertions, 37 deletions
diff --git a/lib/bundler.rb b/lib/bundler.rb
index c686b106c7..12dde90fc5 100644
--- a/lib/bundler.rb
+++ b/lib/bundler.rb
@@ -244,11 +244,6 @@ module Bundler
Bundler.settings[:deployment]
end
- def overrides
- return [] unless defined?(@definition) && @definition
- @definition.overrides
- end
-
def locked_gems
@locked_gems ||=
if defined?(@definition) && @definition
diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb
index aa21b5eb01..2fe28f7656 100644
--- a/lib/bundler/definition.rb
+++ b/lib/bundler/definition.rb
@@ -111,12 +111,12 @@ module Bundler
@locked_bundler_version = @locked_gems.bundler_version
@locked_ruby_version = @locked_gems.ruby_version
@locked_deps = @locked_gems.dependencies
- @originally_locked_specs = SpecSet.new(@locked_gems.specs)
+ @originally_locked_specs = SpecSet.new(@locked_gems.specs).with_overrides(@overrides)
@originally_locked_sources = @locked_gems.sources
@locked_checksums = @locked_gems.checksums
if @unlocking_all
- @locked_specs = SpecSet.new([])
+ @locked_specs = SpecSet.new([]).with_overrides(@overrides)
@locked_sources = []
else
@locked_specs = @originally_locked_specs
@@ -137,7 +137,7 @@ module Bundler
@most_specific_locked_platform = nil
@platforms = []
@locked_deps = {}
- @locked_specs = SpecSet.new([])
+ @locked_specs = SpecSet.new([]).with_overrides(@overrides)
@locked_sources = []
@originally_locked_specs = @locked_specs
@originally_locked_sources = @locked_sources
@@ -506,7 +506,7 @@ module Bundler
def normalize_platforms
resolve.normalize_platforms!(current_dependencies, platforms)
- @resolve = SpecSet.new(resolve.for(current_dependencies, @platforms))
+ @resolve = SpecSet.new(resolve.for(current_dependencies, @platforms)).with_overrides(@overrides)
end
def add_platform(platform)
@@ -762,7 +762,7 @@ module Bundler
local_platform_needed_for_resolvability = @most_specific_non_local_locked_platform && !@platforms.include?(Bundler.local_platform)
@platforms << Bundler.local_platform if local_platform_needed_for_resolvability
- result = SpecSet.new(resolver.start)
+ result = SpecSet.new(resolver.start).with_overrides(@overrides)
@resolved_bundler_version = result.find {|spec| spec.name == "bundler" }&.version
@@ -788,7 +788,7 @@ module Bundler
result.add_originally_invalid_platforms!(platforms, @originally_invalid_platforms)
end
- SpecSet.new(result.for(dependencies, @platforms | [Gem::Platform::RUBY]))
+ SpecSet.new(result.for(dependencies, @platforms | [Gem::Platform::RUBY])).with_overrides(@overrides)
end
def precompute_source_requirements_for_indirect_dependencies?
diff --git a/lib/bundler/installer.rb b/lib/bundler/installer.rb
index 3455f72c21..ef607cabdb 100644
--- a/lib/bundler/installer.rb
+++ b/lib/bundler/installer.rb
@@ -211,12 +211,13 @@ module Bundler
end
def ensure_specs_are_compatible!
+ overrides = @definition.overrides
@definition.specs.each do |spec|
- unless spec.matches_current_ruby?
+ unless spec.matches_current_ruby_with_overrides?(overrides)
raise InstallError, "#{spec.full_name} requires ruby version #{spec.required_ruby_version}, " \
"which is incompatible with the current version, #{Gem.ruby_version}"
end
- unless spec.matches_current_rubygems?
+ unless spec.matches_current_rubygems_with_overrides?(overrides)
raise InstallError, "#{spec.full_name} requires rubygems version #{spec.required_rubygems_version}, " \
"which is incompatible with the current version, #{Gem.rubygems_version}"
end
diff --git a/lib/bundler/lazy_specification.rb b/lib/bundler/lazy_specification.rb
index 46b1e905d3..86b29ee115 100644
--- a/lib/bundler/lazy_specification.rb
+++ b/lib/bundler/lazy_specification.rb
@@ -9,7 +9,7 @@ module Bundler
include ForcePlatform
attr_reader :name, :version, :platform, :materialization
- attr_accessor :source, :remote, :force_ruby_platform, :dependencies, :required_ruby_version, :required_rubygems_version
+ attr_accessor :source, :remote, :force_ruby_platform, :dependencies, :required_ruby_version, :required_rubygems_version, :overrides
#
# For backwards compatibility with existing lockfiles, if the most specific
@@ -234,8 +234,9 @@ module Bundler
# about the mismatch higher up the stack, right before trying to install the
# bad gem.
def choose_compatible(candidates, fallback_to_non_installable: Bundler.frozen_bundle?)
+ override_list = overrides || []
search = candidates.reverse.find do |spec|
- spec.is_a?(StubSpecification) || spec.matches_current_metadata?
+ spec.is_a?(StubSpecification) || spec.matches_current_metadata_with_overrides?(override_list)
end
if search.nil? && fallback_to_non_installable
search = candidates.last
diff --git a/lib/bundler/match_metadata.rb b/lib/bundler/match_metadata.rb
index 75b0e4357c..92aeb2e893 100644
--- a/lib/bundler/match_metadata.rb
+++ b/lib/bundler/match_metadata.rb
@@ -7,11 +7,23 @@ module Bundler
end
def matches_current_ruby?
- effective_required_ruby_version.satisfied_by?(Gem.ruby_version)
+ @required_ruby_version.satisfied_by?(Gem.ruby_version)
end
def matches_current_rubygems?
- effective_required_rubygems_version.satisfied_by?(Gem.rubygems_version)
+ @required_rubygems_version.satisfied_by?(Gem.rubygems_version)
+ end
+
+ def matches_current_metadata_with_overrides?(overrides)
+ matches_current_ruby_with_overrides?(overrides) && matches_current_rubygems_with_overrides?(overrides)
+ end
+
+ def matches_current_ruby_with_overrides?(overrides)
+ effective_required_version(@required_ruby_version, :required_ruby_version, overrides).satisfied_by?(Gem.ruby_version)
+ end
+
+ def matches_current_rubygems_with_overrides?(overrides)
+ effective_required_version(@required_rubygems_version, :required_rubygems_version, overrides).satisfied_by?(Gem.rubygems_version)
end
def expanded_dependencies
@@ -29,18 +41,10 @@ module Bundler
private
- def effective_required_ruby_version
- apply_metadata_override(@required_ruby_version, :required_ruby_version)
- end
-
- def effective_required_rubygems_version
- apply_metadata_override(@required_rubygems_version, :required_rubygems_version)
- end
-
- def apply_metadata_override(requirement, field)
- override = Override.find_for(Bundler.overrides, name, field)
- return requirement unless override
- override.apply_to(requirement)
+ def effective_required_version(requirement, field, overrides)
+ return requirement if overrides.nil? || overrides.empty?
+ override = Override.find_for(overrides, name, field)
+ override ? override.apply_to(requirement) : requirement
end
end
end
diff --git a/lib/bundler/match_remote_metadata.rb b/lib/bundler/match_remote_metadata.rb
index 5e46d52441..601af7e55d 100644
--- a/lib/bundler/match_remote_metadata.rb
+++ b/lib/bundler/match_remote_metadata.rb
@@ -6,19 +6,34 @@ module Bundler
# API didn't include that field, so some marshalled specs in the index have it
# set to +nil+.
def matches_current_ruby?
- @required_ruby_version ||= _remote_specification.required_ruby_version || Gem::Requirement.default
-
+ ensure_required_ruby_version_loaded
super
end
def matches_current_rubygems?
- # A fallback is included because the original version of the specification
- # API didn't include that field, so some marshalled specs in the index have it
- # set to +nil+.
- @required_rubygems_version ||= _remote_specification.required_rubygems_version || Gem::Requirement.default
+ ensure_required_rubygems_version_loaded
+ super
+ end
+
+ def matches_current_ruby_with_overrides?(overrides)
+ ensure_required_ruby_version_loaded
+ super
+ end
+ def matches_current_rubygems_with_overrides?(overrides)
+ ensure_required_rubygems_version_loaded
super
end
+
+ private
+
+ def ensure_required_ruby_version_loaded
+ @required_ruby_version ||= _remote_specification.required_ruby_version || Gem::Requirement.default # rubocop:disable Naming/MemoizedInstanceVariableName
+ end
+
+ def ensure_required_rubygems_version_loaded
+ @required_rubygems_version ||= _remote_specification.required_rubygems_version || Gem::Requirement.default # rubocop:disable Naming/MemoizedInstanceVariableName
+ end
end
module MatchRemoteMetadata
diff --git a/lib/bundler/spec_set.rb b/lib/bundler/spec_set.rb
index e8d990d207..84f20d90d0 100644
--- a/lib/bundler/spec_set.rb
+++ b/lib/bundler/spec_set.rb
@@ -7,8 +7,17 @@ module Bundler
include Enumerable
include TSort
+ attr_accessor :overrides
+
def initialize(specs)
@specs = specs
+ @overrides = []
+ end
+
+ def with_overrides(overrides)
+ @overrides = overrides || []
+ @specs.each {|s| s.overrides = @overrides if s.respond_to?(:overrides=) }
+ self
end
def for(dependencies, platforms = [nil], legacy_platforms = [nil], skips: [])
@@ -125,7 +134,7 @@ module Bundler
def materialize(deps)
materialize_dependencies(deps)
- SpecSet.new(materialized_specs)
+ SpecSet.new(materialized_specs).with_overrides(@overrides)
end
# Materialize for all the specs in the spec set, regardless of what platform they're for
@@ -229,7 +238,7 @@ module Bundler
end
def valid?(s)
- s.matches_current_metadata? && valid_dependencies?(s)
+ s.matches_current_metadata_with_overrides?(@overrides) && valid_dependencies?(s)
end
def to_s
diff --git a/spec/bundler/bundler/override_spec.rb b/spec/bundler/bundler/override_spec.rb
index 78f7a62900..d1a5568bd8 100644
--- a/spec/bundler/bundler/override_spec.rb
+++ b/spec/bundler/bundler/override_spec.rb
@@ -1,5 +1,39 @@
# frozen_string_literal: true
+RSpec.describe "MatchMetadata override-aware checks" do
+ let(:spec_class) do
+ Class.new do
+ include Bundler::MatchMetadata
+ attr_accessor :name
+ def initialize(name, ruby_req, rubygems_req)
+ @name = name
+ @required_ruby_version = ruby_req
+ @required_rubygems_version = rubygems_req
+ end
+ end
+ end
+
+ it "matches_current_metadata? ignores overrides (strict path)" do
+ spec = spec_class.new("rails", Gem::Requirement.new("< #{Gem.ruby_version}"), Gem::Requirement.default)
+ overrides = [Bundler::Override.new("rails", :required_ruby_version, :ignore_upper)]
+ # Strict method MUST NOT apply overrides; guards SelfManager and other generic callers.
+ expect(spec.matches_current_metadata?).to be(false)
+ expect(spec.matches_current_metadata_with_overrides?(overrides)).to be(true)
+ end
+
+ it "matches_current_ruby_with_overrides? returns the strict result for an empty override list" do
+ spec = spec_class.new("rails", Gem::Requirement.new(">= #{Gem.ruby_version}"), Gem::Requirement.default)
+ expect(spec.matches_current_ruby_with_overrides?([])).to be(true)
+ expect(spec.matches_current_ruby_with_overrides?(nil)).to be(true)
+ end
+
+ it "matches_current_rubygems_with_overrides? honors :all override" do
+ spec = spec_class.new("rails", Gem::Requirement.default, Gem::Requirement.new("< #{Gem.rubygems_version}"))
+ overrides = [Bundler::Override.new(:all, :required_rubygems_version, :ignore_upper)]
+ expect(spec.matches_current_rubygems_with_overrides?(overrides)).to be(true)
+ end
+end
+
RSpec.describe Bundler::Override do
describe ".find_for" do
it "returns the matching override by target and field" do