diff options
| author | Hiroshi SHIBATA <hsbt@ruby-lang.org> | 2026-05-07 20:07:14 +0900 |
|---|---|---|
| committer | git <svn-admin@ruby-lang.org> | 2026-05-08 06:43:45 +0000 |
| commit | 0beb804ef79c3355c19ed272106c42b02780b607 (patch) | |
| tree | d16262dff90c570f67af779d35e96448de14b85d | |
| parent | 0d5a5a613fc4c77586166e2a663315b083a0a451 (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.rb | 5 | ||||
| -rw-r--r-- | lib/bundler/definition.rb | 12 | ||||
| -rw-r--r-- | lib/bundler/installer.rb | 5 | ||||
| -rw-r--r-- | lib/bundler/lazy_specification.rb | 5 | ||||
| -rw-r--r-- | lib/bundler/match_metadata.rb | 32 | ||||
| -rw-r--r-- | lib/bundler/match_remote_metadata.rb | 27 | ||||
| -rw-r--r-- | lib/bundler/spec_set.rb | 13 | ||||
| -rw-r--r-- | spec/bundler/bundler/override_spec.rb | 34 |
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 |
