From 95ce1c5228a86fffdce7535a374008e7cbcd076c Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 8 May 2026 11:56:53 +0900 Subject: [ruby/rubygems] Honor LazySpec overrides in SpecSet#complete_platform complete_platform validates platform-specific candidates returned by spec.source.specs.search, which are remote specs that do not carry the override list. Borrow the override list from the LazySpec exemplar already in scope so platform-variant validation uses the same effective metadata as the install/resolve path. Also propagate the overrides onto the synthesized LazySpec built from platform_spec. Without this, the next complete_platform call could pick the synthesized variant as its exemplar (it is now in the set returned by lookup) and fall back to strict matching, dropping platforms that the user's override would otherwise allow. https://github.com/ruby/rubygems/commit/205955c5b3 Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/bundler/spec_set.rb | 16 +++++++++-- spec/bundler/bundler/spec_set_spec.rb | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/lib/bundler/spec_set.rb b/lib/bundler/spec_set.rb index e8d990d207..ae5e5cbaa9 100644 --- a/lib/bundler/spec_set.rb +++ b/lib/bundler/spec_set.rb @@ -274,13 +274,25 @@ module Bundler valid_platform = lookup.all? do |_, specs| spec = specs.first + # The matching candidates returned by source.specs.search are remote + # specs that do not carry the override list themselves. Borrow it from + # the LazySpec we are validating so platform-variant validation honors + # the same overrides the install/resolve path already applies. + overrides = spec.is_a?(LazySpecification) ? Array(spec.overrides) : [] matching_specs = spec.source.specs.search([spec.name, spec.version]) platform_spec = MatchPlatform.select_best_platform_match(matching_specs, platform).find do |s| - valid?(s) + s.matches_current_metadata_with_overrides?(overrides) && valid_dependencies?(s) end if platform_spec - new_specs << LazySpecification.from_spec(platform_spec) unless specs.include?(platform_spec) + unless specs.include?(platform_spec) + new_lazy = LazySpecification.from_spec(platform_spec) + # Carry the overrides forward so a follow-up complete_platform + # call that picks this synthesized variant as its exemplar still + # honors the user's override list. + new_lazy.overrides = overrides if overrides.any? + new_specs << new_lazy + end true else false diff --git a/spec/bundler/bundler/spec_set_spec.rb b/spec/bundler/bundler/spec_set_spec.rb index 3e630555e6..1e1ceadf26 100644 --- a/spec/bundler/bundler/spec_set_spec.rb +++ b/spec/bundler/bundler/spec_set_spec.rb @@ -93,4 +93,56 @@ RSpec.describe Bundler::SpecSet do end end + describe "#complete_platform" do + let(:platform) { Gem::Platform.new("x86_64-linux") } + + let(:platform_variant) do + build_spec("needs_old_ruby", "1.0", platform).first.tap do |s| + s.required_ruby_version = Gem::Requirement.new("< #{Gem.ruby_version}") + end + end + + let(:lazy_spec) do + lazy = Bundler::LazySpecification.new("needs_old_ruby", Gem::Version.new("1.0"), Gem::Platform::RUBY) + lazy.required_ruby_version = Gem::Requirement.new("< #{Gem.ruby_version}") + source = double("source") + source_specs = double("source_specs") + allow(source).to receive(:specs).and_return(source_specs) + allow(source_specs).to receive(:search). + with(["needs_old_ruby", Gem::Version.new("1.0")]).and_return([platform_variant]) + lazy.source = source + lazy + end + + it "rejects a platform variant whose strict metadata is incompatible when no override is attached" do + set = described_class.new([lazy_spec]) + expect(set.send(:complete_platform, platform)).to be(false) + end + + it "accepts a platform variant when the LazySpec carries an override that allows it" do + lazy_spec.overrides = [Bundler::Override.new("needs_old_ruby", :required_ruby_version, :ignore_upper)] + set = described_class.new([lazy_spec]) + expect(set.send(:complete_platform, platform)).to be(true) + end + + it "carries overrides onto a synthesized LazySpec so a follow-up complete_platform still honors them" do + override = Bundler::Override.new("needs_old_ruby", :required_ruby_version, :ignore_upper) + lazy_spec.overrides = [override] + second_platform = Gem::Platform.new("aarch64-linux") + second_variant = build_spec("needs_old_ruby", "1.0", second_platform).first.tap do |s| + s.required_ruby_version = Gem::Requirement.new("< #{Gem.ruby_version}") + end + allow(lazy_spec.source.specs).to receive(:search). + with(["needs_old_ruby", Gem::Version.new("1.0")]).and_return([platform_variant, second_variant]) + + set = described_class.new([lazy_spec]) + expect(set.send(:complete_platform, platform)).to be(true) + # The synthesized x86_64-linux variant is now in the set. If lookup + # picks it as exemplar for the next platform check, the override list + # must still be reachable via its overrides accessor. + synthesized = set.to_a.find {|s| s.platform == platform } + expect(synthesized.overrides).to eq([override]) + expect(set.send(:complete_platform, second_platform)).to be(true) + end + end end -- cgit v1.2.3