From c76b1ea2a6ed4901afac4f7a23b532a867354408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= Date: Sat, 16 Nov 2024 00:20:09 +0100 Subject: [PATCH] [rubygems/rubygems] Keep track of materializations in the original resolve This gives more flexibility to allow further improvements. https://github.com/rubygems/rubygems/commit/f11a890f5e --- lib/bundler.rb | 1 + lib/bundler/definition.rb | 10 ++-- lib/bundler/gem_helpers.rb | 2 +- lib/bundler/lazy_specification.rb | 17 +++++- lib/bundler/materialization.rb | 53 +++++++++++++++++ lib/bundler/spec_set.rb | 97 +++++++++++++++---------------- 6 files changed, 124 insertions(+), 56 deletions(-) create mode 100644 lib/bundler/materialization.rb diff --git a/lib/bundler.rb b/lib/bundler.rb index 4ede881446..177d7257e6 100644 --- a/lib/bundler.rb +++ b/lib/bundler.rb @@ -62,6 +62,7 @@ module Bundler autoload :LazySpecification, File.expand_path("bundler/lazy_specification", __dir__) autoload :LockfileParser, File.expand_path("bundler/lockfile_parser", __dir__) autoload :MatchRemoteMetadata, File.expand_path("bundler/match_remote_metadata", __dir__) + autoload :Materialization, File.expand_path("bundler/materialization", __dir__) autoload :NULL, File.expand_path("bundler/constants", __dir__) autoload :ProcessLock, File.expand_path("bundler/process_lock", __dir__) autoload :RemoteSpecification, File.expand_path("bundler/remote_specification", __dir__) diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 05b4474f42..325c9cdcf6 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -241,7 +241,7 @@ module Bundler end def missing_specs - resolve.materialize(requested_dependencies).missing_specs + resolve.missing_specs_for(requested_dependencies) end def missing_specs? @@ -639,7 +639,7 @@ module Bundler retry end - missing_specs = specs.missing_specs + missing_specs = resolve.missing_specs if missing_specs.any? missing_specs.each do |s| @@ -668,7 +668,7 @@ module Bundler raise GemNotFound, "Could not find #{missing_specs_list.join(" nor ")}" end - incomplete_specs = specs.incomplete_specs + incomplete_specs = resolve.incomplete_specs loop do break if incomplete_specs.empty? @@ -677,7 +677,7 @@ module Bundler reresolve_without(incomplete_specs) specs = resolve.materialize(dependencies) - still_incomplete_specs = specs.incomplete_specs + still_incomplete_specs = resolve.incomplete_specs if still_incomplete_specs == incomplete_specs package = resolution_packages.get_package(incomplete_specs.first.name) @@ -687,7 +687,7 @@ module Bundler incomplete_specs = still_incomplete_specs end - insecurely_materialized_specs = specs.insecurely_materialized_specs + insecurely_materialized_specs = resolve.insecurely_materialized_specs if insecurely_materialized_specs.any? Bundler.ui.warn "The following platform specific gems are getting installed, yet the lockfile includes only their generic ruby version:\n" \ diff --git a/lib/bundler/gem_helpers.rb b/lib/bundler/gem_helpers.rb index c0a42c59c1..9d94b8db9a 100644 --- a/lib/bundler/gem_helpers.rb +++ b/lib/bundler/gem_helpers.rb @@ -63,7 +63,7 @@ module Bundler 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(&:materialize_for_installation) + select_best_platform_match(specs, local_platform, force_ruby: force_ruby).filter_map(&:materialized_for_installation) end module_function :select_best_local_platform_match diff --git a/lib/bundler/lazy_specification.rb b/lib/bundler/lazy_specification.rb index a8f4351840..e3089f230f 100644 --- a/lib/bundler/lazy_specification.rb +++ b/lib/bundler/lazy_specification.rb @@ -8,7 +8,7 @@ module Bundler include MatchPlatform include ForcePlatform - attr_reader :name, :version, :platform + attr_reader :name, :version, :platform, :materialization attr_accessor :source, :remote, :force_ruby_platform, :dependencies, :required_ruby_version, :required_rubygems_version # @@ -46,6 +46,15 @@ module Bundler @force_ruby_platform = default_force_ruby_platform @most_specific_locked_platform = nil + @materialization = nil + end + + def missing? + @materialization == self + end + + def incomplete? + @materialization.nil? end def source_changed? @@ -121,6 +130,12 @@ module Bundler __materialize__(matching_specs) end + def materialized_for_installation + @materialization = materialize_for_installation + + self unless incomplete? + end + def materialize_for_installation source.local! diff --git a/lib/bundler/materialization.rb b/lib/bundler/materialization.rb new file mode 100644 index 0000000000..5b414b74f9 --- /dev/null +++ b/lib/bundler/materialization.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Bundler + # + # This class materializes a set of resolved specifications (`LazySpecification`) + # for a given gem into the most appropriate real specifications + # (`StubSepecification`, `EndpointSpecification`, etc), given a dependency and a + # target platform. + # + class Materialization + def initialize(dep, platform, candidates:) + @dep = dep + @platform = platform + @candidates = candidates + end + + def complete? + specs.any? + end + + def specs + @specs ||= if @candidates.nil? + [] + elsif platform + GemHelpers.select_best_platform_match(@candidates, platform, force_ruby: dep.force_ruby_platform) + else + GemHelpers.select_best_local_platform_match(@candidates, force_ruby: dep.force_ruby_platform || dep.default_force_ruby_platform) + end + end + + def dependencies + specs.first.runtime_dependencies.map {|d| [d, platform] } + end + + def materialized_spec + specs.first&.materialization + end + + def missing_specs + specs.select(&:missing?) + end + + def incomplete_specs + return [] if complete? + + @candidates || LazySpecification.new(dep.name, nil, nil) + end + + private + + attr_reader :dep, :platform + end +end diff --git a/lib/bundler/spec_set.rb b/lib/bundler/spec_set.rb index 862455b70e..ea4720fe67 100644 --- a/lib/bundler/spec_set.rb +++ b/lib/bundler/spec_set.rb @@ -7,44 +7,14 @@ module Bundler include Enumerable include TSort - attr_reader :incomplete_specs - - def initialize(specs, incomplete_specs = []) + def initialize(specs) @specs = specs - @incomplete_specs = incomplete_specs end def for(dependencies, check = false, platforms = [nil]) - handled = ["bundler"].product(platforms).map {|k| [k, true] }.to_h - deps = dependencies.product(platforms) - specs = [] + materialize_dependencies(dependencies, platforms) - loop do - break unless dep = deps.shift - - name = dep[0].name - platform = dep[1] - incomplete = false - - key = [name, platform] - next if handled.key?(key) - - handled[key] = true - - specs_for_dep = specs_for_dependency(*dep) - if specs_for_dep.any? - specs.concat(specs_for_dep) - deps.concat(specs_for_dep.first.runtime_dependencies.map {|d| [d, platform] }) - else - incomplete = true - end - - if incomplete && check - @incomplete_specs += lookup[name] || [LazySpecification.new(name, nil, nil)] - end - end - - specs.uniq + @materializations.flat_map(&:specs).uniq end def normalize_platforms!(deps, platforms) @@ -126,13 +96,12 @@ module Bundler end def materialize(deps) - materialized = self.for(deps, true) + materialize_dependencies(deps) - SpecSet.new(materialized, incomplete_specs) + SpecSet.new(materialized_specs) end # Materialize for all the specs in the spec set, regardless of what platform they're for - # This is in contrast to how for does platform filtering (and specifically different from how `materialize` calls `for` only for the current platform) # @return [Array] def materialized_for_all_platforms @specs.map do |s| @@ -153,12 +122,22 @@ module Bundler validation_set.incomplete_specs.any? end + def missing_specs_for(dependencies) + materialize_dependencies(dependencies) + + missing_specs + end + def missing_specs - @specs.select {|s| s.is_a?(LazySpecification) } + @materializations.flat_map(&:missing_specs) + end + + def incomplete_specs + @materializations.flat_map(&:incomplete_specs) end def insecurely_materialized_specs - @specs.select(&:insecurely_materialized?) + materialized_specs.select(&:insecurely_materialized?) end def -(other) @@ -218,6 +197,37 @@ module Bundler private + def materialize_dependencies(dependencies, platforms = [nil]) + handled = ["bundler"].product(platforms).map {|k| [k, true] }.to_h + deps = dependencies.product(platforms) + @materializations = [] + + loop do + break unless dep = deps.shift + + dependency = dep[0] + platform = dep[1] + name = dependency.name + + key = [name, platform] + next if handled.key?(key) + + handled[key] = true + + materialization = Materialization.new(dependency, platform, candidates: lookup[name]) + + deps.concat(materialization.dependencies) if materialization.complete? + + @materializations << materialization + end + + @materializations + end + + def materialized_specs + @materializations.filter_map(&:materialized_spec) + end + def reset! @sorted = nil @lookup = nil @@ -290,17 +300,6 @@ module Bundler @specs.sort_by(&:name).each {|s| yield s } end - def specs_for_dependency(dep, platform) - specs_for_name = lookup[dep.name] - return [] unless specs_for_name - - if platform - GemHelpers.select_best_platform_match(specs_for_name, platform, force_ruby: dep.force_ruby_platform) - else - GemHelpers.select_best_local_platform_match(specs_for_name, force_ruby: dep.force_ruby_platform || dep.default_force_ruby_platform) - end - end - def tsort_each_child(s) s.dependencies.sort_by(&:name).each do |d| next if d.type == :development