diff options
Diffstat (limited to 'lib/rubygems/resolver')
| -rw-r--r-- | lib/rubygems/resolver/api_set.rb | 10 | ||||
| -rw-r--r-- | lib/rubygems/resolver/api_set/gem_parser.rb | 9 | ||||
| -rw-r--r-- | lib/rubygems/resolver/api_specification.rb | 6 | ||||
| -rw-r--r-- | lib/rubygems/resolver/best_set.rb | 2 | ||||
| -rw-r--r-- | lib/rubygems/resolver/conflict.rb | 146 | ||||
| -rw-r--r-- | lib/rubygems/resolver/incompatibility.rb | 10 | ||||
| -rw-r--r-- | lib/rubygems/resolver/installer_set.rb | 2 | ||||
| -rw-r--r-- | lib/rubygems/resolver/source_set.rb | 2 | ||||
| -rw-r--r-- | lib/rubygems/resolver/stats.rb | 46 | ||||
| -rw-r--r-- | lib/rubygems/resolver/strategy.rb | 44 |
10 files changed, 68 insertions, 209 deletions
diff --git a/lib/rubygems/resolver/api_set.rb b/lib/rubygems/resolver/api_set.rb index 9f6695a6a9..3f443519d8 100644 --- a/lib/rubygems/resolver/api_set.rb +++ b/lib/rubygems/resolver/api_set.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true ## -# The global rubygems pool, available via the rubygems.org API. +# The global rubygems pool, available via the Compact Index API. # Returns instances of APISpecification. class Gem::Resolver::APISet < Gem::Resolver::Set autoload :GemParser, File.expand_path("api_set/gem_parser", __dir__) ## - # The URI for the dependency API this APISet uses. + # The URI for the Compact Index API this APISet uses. attr_reader :dep_uri # :nodoc: @@ -23,9 +23,9 @@ class Gem::Resolver::APISet < Gem::Resolver::Set attr_reader :uri ## - # Creates a new APISet that will retrieve gems from +uri+ using the RubyGems - # API URL +dep_uri+ which is described at - # https://guides.rubygems.org/rubygems-org-api + # Creates a new APISet that will retrieve gems from +uri+ using the Compact + # Index API URL +dep_uri+ which is described at + # https://guides.rubygems.org/rubygems-org-compact-index-api def initialize(dep_uri = "https://index.rubygems.org/info/") super() diff --git a/lib/rubygems/resolver/api_set/gem_parser.rb b/lib/rubygems/resolver/api_set/gem_parser.rb index 643b857107..4d827f4980 100644 --- a/lib/rubygems/resolver/api_set/gem_parser.rb +++ b/lib/rubygems/resolver/api_set/gem_parser.rb @@ -1,22 +1,19 @@ # frozen_string_literal: true class Gem::Resolver::APISet::GemParser - EMPTY_ARRAY = [].freeze - private_constant :EMPTY_ARRAY - def parse(line) version_and_platform, rest = line.split(" ", 2) version, platform = version_and_platform.split("-", 2) dependencies, requirements = rest.split("|", 2).map! {|s| s.split(",") } if rest - dependencies = dependencies ? dependencies.map! {|d| parse_dependency(d) } : EMPTY_ARRAY - requirements = requirements ? requirements.map! {|d| parse_dependency(d) } : EMPTY_ARRAY + dependencies = dependencies ? dependencies.map! {|d| parse_dependency(d) } : [] + requirements = requirements ? requirements.map! {|d| parse_dependency(d) } : [] [version, platform, dependencies, requirements] end private def parse_dependency(string) - dependency = string.split(":") + dependency = string.split(":", 2) dependency[-1] = dependency[-1].split("&") if dependency.size > 1 dependency[0] = -dependency[0] dependency diff --git a/lib/rubygems/resolver/api_specification.rb b/lib/rubygems/resolver/api_specification.rb index a14bcbfeb1..ccfd6fe084 100644 --- a/lib/rubygems/resolver/api_specification.rb +++ b/lib/rubygems/resolver/api_specification.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true ## -# Represents a specification retrieved via the rubygems.org API. +# Represents a specification retrieved via the Compact Index API. # # This is used to avoid loading the full Specification object when all we need # is the name, version, and dependencies. @@ -19,10 +19,10 @@ class Gem::Resolver::APISpecification < Gem::Resolver::Specification end ## - # Creates an APISpecification for the given +set+ from the rubygems.org + # Creates an APISpecification for the given +set+ from the Compact Index API # +api_data+. # - # See https://guides.rubygems.org/rubygems-org-api/#misc-methods for the + # See https://guides.rubygems.org/rubygems-org-compact-index-api for the # format of the +api_data+. def initialize(set, api_data) diff --git a/lib/rubygems/resolver/best_set.rb b/lib/rubygems/resolver/best_set.rb index e2307f6e02..e647a2c11b 100644 --- a/lib/rubygems/resolver/best_set.rb +++ b/lib/rubygems/resolver/best_set.rb @@ -21,7 +21,7 @@ class Gem::Resolver::BestSet < Gem::Resolver::ComposedSet def pick_sets # :nodoc: @sources.each_source do |source| - @sets << source.dependency_resolver_set + @sets << source.dependency_resolver_set(@prerelease) end end diff --git a/lib/rubygems/resolver/conflict.rb b/lib/rubygems/resolver/conflict.rb deleted file mode 100644 index 367a36b43d..0000000000 --- a/lib/rubygems/resolver/conflict.rb +++ /dev/null @@ -1,146 +0,0 @@ -# frozen_string_literal: true - -## -# Used internally to indicate that a dependency conflicted -# with a spec that would be activated. - -class Gem::Resolver::Conflict - ## - # The specification that was activated prior to the conflict - - attr_reader :activated - - ## - # The dependency that is in conflict with the activated gem. - - attr_reader :dependency - - attr_reader :failed_dep # :nodoc: - - ## - # Creates a new resolver conflict when +dependency+ is in conflict with an - # already +activated+ specification. - - def initialize(dependency, activated, failed_dep=dependency) - @dependency = dependency - @activated = activated - @failed_dep = failed_dep - end - - def ==(other) # :nodoc: - self.class === other && - @dependency == other.dependency && - @activated == other.activated && - @failed_dep == other.failed_dep - end - - ## - # A string explanation of the conflict. - - def explain - "<Conflict wanted: #{@failed_dep}, had: #{activated.spec.full_name}>" - end - - ## - # Return the 2 dependency objects that conflicted - - def conflicting_dependencies - [@failed_dep.dependency, @activated.request.dependency] - end - - ## - # Explanation of the conflict used by exceptions to print useful messages - - def explanation - activated = @activated.spec.full_name - dependency = @failed_dep.dependency - requirement = dependency.requirement - alternates = dependency.matching_specs.map(&:full_name) - - unless alternates.empty? - matching = <<-MATCHING.chomp - - Gems matching %s: - %s - MATCHING - - matching = format(matching, dependency, alternates.join(", ")) - end - - explanation = <<-EXPLANATION - Activated %s - which does not match conflicting dependency (%s) - - Conflicting dependency chains: - %s - - versus: - %s -%s - EXPLANATION - - format(explanation, activated, requirement, request_path(@activated).reverse.join(", depends on\n "), request_path(@failed_dep).reverse.join(", depends on\n "), matching) - end - - ## - # Returns true if the conflicting dependency's name matches +spec+. - - def for_spec?(spec) - @dependency.name == spec.name - end - - def pretty_print(q) # :nodoc: - q.group 2, "[Dependency conflict: ", "]" do - q.breakable - - q.text "activated " - q.pp @activated - - q.breakable - q.text " dependency " - q.pp @dependency - - q.breakable - if @dependency == @failed_dep - q.text " failed" - else - q.text " failed dependency " - q.pp @failed_dep - end - end - end - - ## - # Path of activations from the +current+ list. - - def request_path(current) - path = [] - - while current do - case current - when Gem::Resolver::ActivationRequest then - path << - "#{current.request.dependency}, #{current.spec.version} activated" - - current = current.parent - when Gem::Resolver::DependencyRequest then - path << current.dependency.to_s - - current = current.requester - else - raise Gem::Exception, "[BUG] unknown request class #{current.class}" - end - end - - path = ["user request (gem command or Gemfile)"] if path.empty? - - path - end - - ## - # Return the Specification that listed the dependency - - def requester - @failed_dep.requester - end -end diff --git a/lib/rubygems/resolver/incompatibility.rb b/lib/rubygems/resolver/incompatibility.rb new file mode 100644 index 0000000000..57a60affb4 --- /dev/null +++ b/lib/rubygems/resolver/incompatibility.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Gem::Resolver::Incompatibility < Gem::PubGrub::Incompatibility + attr_reader :extended_explanation + + def initialize(terms, cause:, custom_explanation: nil, extended_explanation: nil) + @extended_explanation = extended_explanation + super(terms, cause: cause, custom_explanation: custom_explanation) + end +end diff --git a/lib/rubygems/resolver/installer_set.rb b/lib/rubygems/resolver/installer_set.rb index d9fe36c589..42ce0890e2 100644 --- a/lib/rubygems/resolver/installer_set.rb +++ b/lib/rubygems/resolver/installer_set.rb @@ -160,7 +160,7 @@ class Gem::Resolver::InstallerSet < Gem::Resolver::Set res.concat matching_local begin - if local_spec = @local_source.find_gem(name, dep.requirement) + @local_source.find_all_gems(name, dep.requirement).each do |local_spec| res << Gem::Resolver::IndexSpecification.new( self, local_spec.name, local_spec.version, @local_source, local_spec.platform diff --git a/lib/rubygems/resolver/source_set.rb b/lib/rubygems/resolver/source_set.rb index 296cf41078..074b473edc 100644 --- a/lib/rubygems/resolver/source_set.rb +++ b/lib/rubygems/resolver/source_set.rb @@ -42,6 +42,6 @@ class Gem::Resolver::SourceSet < Gem::Resolver::Set def get_set(name) link = @links[name] - @sets[link] ||= Gem::Source.new(link).dependency_resolver_set if link + @sets[link] ||= Gem::Source.new(link).dependency_resolver_set(@prerelease) if link end end diff --git a/lib/rubygems/resolver/stats.rb b/lib/rubygems/resolver/stats.rb deleted file mode 100644 index 9920976b2a..0000000000 --- a/lib/rubygems/resolver/stats.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -class Gem::Resolver::Stats - def initialize - @max_depth = 0 - @max_requirements = 0 - @requirements = 0 - @backtracking = 0 - @iterations = 0 - end - - def record_depth(stack) - if stack.size > @max_depth - @max_depth = stack.size - end - end - - def record_requirements(reqs) - if reqs.size > @max_requirements - @max_requirements = reqs.size - end - end - - def requirement! - @requirements += 1 - end - - def backtracking! - @backtracking += 1 - end - - def iteration! - @iterations += 1 - end - - PATTERN = "%20s: %d\n" - - def display - $stdout.puts "=== Resolver Statistics ===" - $stdout.printf PATTERN, "Max Depth", @max_depth - $stdout.printf PATTERN, "Total Requirements", @requirements - $stdout.printf PATTERN, "Max Requirements", @max_requirements - $stdout.printf PATTERN, "Backtracking #", @backtracking - $stdout.printf PATTERN, "Iteration #", @iterations - end -end diff --git a/lib/rubygems/resolver/strategy.rb b/lib/rubygems/resolver/strategy.rb new file mode 100644 index 0000000000..bf0dbb6adc --- /dev/null +++ b/lib/rubygems/resolver/strategy.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# Custom PubGrub strategy with caching for version selection. +# Modeled after Bundler's strategy to avoid redundant versions_for +# calls during the solver's package selection loop. + +class Gem::Resolver::Strategy + def initialize(source) + @source = source + @package_priority_cache = Hash.new {|h, pkg| h[pkg] = {} } + + @version_indexes = Hash.new do |h, k| + if Gem::PubGrub::Package.root?(k) + h[k] = { Gem::PubGrub::Package.root_version => 0 } + else + h[k] = @source.all_versions_for(k).each.with_index.to_h + end + end + end + + def next_package_and_version(unsatisfied) + package, range = next_term_to_try_from(unsatisfied) + [package, most_preferred_version_of(package, range)] + end + + private + + def most_preferred_version_of(package, range) + versions = @source.versions_for(package, range) + indexes = @version_indexes[package] + versions.min_by {|version| indexes[version] || Float::INFINITY } + end + + def next_term_to_try_from(unsatisfied) + unsatisfied.min_by do |package, range| + @package_priority_cache[package][range] ||= begin + matching_versions = @source.versions_for(package, range) + higher_versions = @source.versions_for(package, range.upper_invert) + + [matching_versions.count <= 1 ? 0 : 1, higher_versions.count] + end + end + end +end |
