From 44ad2e3f388180a2018abf4415a73522aca52bdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= Date: Mon, 18 Nov 2024 14:18:40 +0100 Subject: [PATCH] [rubygems/rubygems] Allow some materialized specs to be missing As long as some spec in the materialization is complete. https://github.com/rubygems/rubygems/commit/9a673b0bbb --- lib/bundler/definition.rb | 8 +++ lib/bundler/gem_helpers.rb | 24 ++++++-- lib/bundler/materialization.rb | 10 +++- lib/bundler/spec_set.rb | 6 +- spec/bundler/install/gems/resolving_spec.rb | 49 ++++++++++++++++ spec/bundler/install/yanked_spec.rb | 62 +++++++++++++++------ 6 files changed, 133 insertions(+), 26 deletions(-) diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 6ca0ed156a..37e80956eb 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -668,6 +668,14 @@ module Bundler raise GemNotFound, "Could not find #{missing_specs_list.join(" nor ")}" end + partially_missing_specs = resolve.partially_missing_specs + + if partially_missing_specs.any? && !sources.local_mode? + Bundler.ui.warn "Some locked specs have possibly been yanked (#{partially_missing_specs.map(&:full_name).join(", ")}). Ignoring them..." + + resolve.delete(partially_missing_specs) + end + incomplete_specs = resolve.incomplete_specs loop do break if incomplete_specs.empty? diff --git a/lib/bundler/gem_helpers.rb b/lib/bundler/gem_helpers.rb index 9d94b8db9a..75243873f2 100644 --- a/lib/bundler/gem_helpers.rb +++ b/lib/bundler/gem_helpers.rb @@ -46,7 +46,7 @@ module Bundler end module_function :platform_specificity_match - def select_best_platform_match(specs, platform, force_ruby: false, prefer_locked: false) + def select_all_platform_match(specs, platform, force_ruby: false, prefer_locked: false) matching = if force_ruby specs.select {|spec| spec.match_platform(Gem::Platform::RUBY) && spec.force_ruby_platform! } else @@ -58,26 +58,40 @@ module Bundler return locked_originally if locked_originally.any? end - sort_best_platform_match(matching, platform) + matching + end + module_function :select_all_platform_match + + def select_best_platform_match(specs, platform, force_ruby: false, prefer_locked: false) + matching = select_all_platform_match(specs, platform, force_ruby: force_ruby, prefer_locked: prefer_locked) + + sort_and_filter_best_platform_match(matching, platform) end module_function :select_best_platform_match def select_best_local_platform_match(specs, force_ruby: false) - select_best_platform_match(specs, local_platform, force_ruby: force_ruby).filter_map(&:materialized_for_installation) + matching = select_all_platform_match(specs, local_platform, force_ruby: force_ruby).filter_map(&:materialized_for_installation) + + sort_best_platform_match(matching, local_platform) end module_function :select_best_local_platform_match - def sort_best_platform_match(matching, platform) + def sort_and_filter_best_platform_match(matching, platform) return matching if matching.one? exact = matching.select {|spec| spec.platform == platform } return exact if exact.any? - sorted_matching = matching.sort_by {|spec| platform_specificity_match(spec.platform, platform) } + sorted_matching = sort_best_platform_match(matching, platform) exemplary_spec = sorted_matching.first sorted_matching.take_while {|spec| same_specificity(platform, spec, exemplary_spec) && same_deps(spec, exemplary_spec) } end + module_function :sort_and_filter_best_platform_match + + def sort_best_platform_match(matching, platform) + matching.sort_by {|spec| platform_specificity_match(spec.platform, platform) } + end module_function :sort_best_platform_match class PlatformMatch diff --git a/lib/bundler/materialization.rb b/lib/bundler/materialization.rb index 5b414b74f9..6542c07649 100644 --- a/lib/bundler/materialization.rb +++ b/lib/bundler/materialization.rb @@ -33,10 +33,16 @@ module Bundler end def materialized_spec - specs.first&.materialization + specs.reject(&:missing?).first&.materialization end - def missing_specs + def completely_missing_specs + return [] unless specs.all?(&:missing?) + + specs + end + + def partially_missing_specs specs.select(&:missing?) end diff --git a/lib/bundler/spec_set.rb b/lib/bundler/spec_set.rb index 9661da2cce..05921cb655 100644 --- a/lib/bundler/spec_set.rb +++ b/lib/bundler/spec_set.rb @@ -140,7 +140,11 @@ module Bundler end def missing_specs - @materializations.flat_map(&:missing_specs) + @materializations.flat_map(&:completely_missing_specs) + end + + def partially_missing_specs + @materializations.flat_map(&:partially_missing_specs) end def incomplete_specs diff --git a/spec/bundler/install/gems/resolving_spec.rb b/spec/bundler/install/gems/resolving_spec.rb index bd1cece9ef..b4af8ab6d4 100644 --- a/spec/bundler/install/gems/resolving_spec.rb +++ b/spec/bundler/install/gems/resolving_spec.rb @@ -501,6 +501,55 @@ RSpec.describe "bundle install with install-time dependencies" do end end + context "when locked generic variant supports current Ruby, but locked specific variant does not" do + let(:original_lockfile) do + <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.16.3) + nokogiri (1.16.3-x86_64-linux) + + PLATFORMS + ruby + x86_64-linux + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + before do + build_repo4 do + build_gem "nokogiri", "1.16.3" + build_gem "nokogiri", "1.16.3" do |s| + s.required_ruby_version = "< #{Gem.ruby_version}" + s.platform = "x86_64-linux" + end + end + + gemfile <<~G + source "https://gem.repo4" + + gem "nokogiri" + G + + lockfile original_lockfile + end + + it "keeps both variants in the lockfile, and uses the generic one since it's compatible" do + simulate_platform "x86_64-linux" do + bundle "install --verbose" + + expect(lockfile).to eq(original_lockfile) + expect(the_bundle).to include_gems("nokogiri 1.16.3") + end + end + end + it "gives a meaningful error on ruby version mismatches between dependencies" do build_repo4 do build_gem "requires-old-ruby" do |s| diff --git a/spec/bundler/install/yanked_spec.rb b/spec/bundler/install/yanked_spec.rb index a7b5fc73b3..6919662671 100644 --- a/spec/bundler/install/yanked_spec.rb +++ b/spec/bundler/install/yanked_spec.rb @@ -30,7 +30,29 @@ RSpec.context "when installing a bundle that includes yanked gems" do expect(err).to include("Your bundle is locked to foo (10.0.0)") end - context "when a re-resolve is necessary, and a yanked version is considered by the resolver" do + context "when a platform specific yanked version is included in the lockfile, and a generic variant is available remotely" do + let(:original_lockfile) do + <<~L + GEM + remote: https://gem.repo4/ + specs: + actiontext (6.1.6) + nokogiri (>= 1.8) + foo (1.0.0) + nokogiri (1.13.8-#{Bundler.local_platform}) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + actiontext (= 6.1.6) + foo (= 1.0.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + end + before do skip "Materialization on Windows is not yet strict, so the example does not detect the gem has been yanked" if Gem.win_platform? @@ -51,29 +73,33 @@ RSpec.context "when installing a bundle that includes yanked gems" do gemfile <<~G source "https://gem.repo4" - gem "foo", "1.0.1" + gem "foo", "1.0.0" gem "actiontext", "6.1.6" G - lockfile <<~L - GEM - remote: https://gem.repo4/ - specs: - actiontext (6.1.6) - nokogiri (>= 1.8) - foo (1.0.0) - nokogiri (1.13.8-#{Bundler.local_platform}) + lockfile original_lockfile + end - PLATFORMS - #{lockfile_platforms} + context "and a re-resolve is necessary" do + before do + gemfile gemfile.sub('"foo", "1.0.0"', '"foo", "1.0.1"') + end - DEPENDENCIES - actiontext (= 6.1.6) - foo (= 1.0.0) + it "reresolves, and replaces the yanked gem with the generic version, printing a warning, when the old index is used" do + bundle "install", artifice: "endpoint", verbose: true - BUNDLED WITH - #{Bundler::VERSION} - L + expect(out).to include("Installing nokogiri 1.13.8").and include("Installing foo 1.0.1") + expect(lockfile).to eq(original_lockfile.sub("nokogiri (1.13.8-#{Bundler.local_platform})", "nokogiri (1.13.8)").gsub("1.0.0", "1.0.1")) + expect(err).to include("Some locked specs have possibly been yanked (nokogiri-1.13.8-#{Bundler.local_platform}). Ignoring them...") + end + + it "reresolves, and replaces the yanked gem with the generic version, printing a warning, when the compact index API is used" do + bundle "install", artifice: "compact_index", verbose: true + + expect(out).to include("Installing nokogiri 1.13.8").and include("Installing foo 1.0.1") + expect(lockfile).to eq(original_lockfile.sub("nokogiri (1.13.8-#{Bundler.local_platform})", "nokogiri (1.13.8)").gsub("1.0.0", "1.0.1")) + expect(err).to include("Some locked specs have possibly been yanked (nokogiri-1.13.8-#{Bundler.local_platform}). Ignoring them...") + end end it "reports the yanked gem properly when the old index is used" do