summaryrefslogtreecommitdiff
path: root/lib/rubygems/resolver
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rubygems/resolver')
-rw-r--r--lib/rubygems/resolver/api_set.rb10
-rw-r--r--lib/rubygems/resolver/api_set/gem_parser.rb9
-rw-r--r--lib/rubygems/resolver/api_specification.rb6
-rw-r--r--lib/rubygems/resolver/best_set.rb2
-rw-r--r--lib/rubygems/resolver/conflict.rb146
-rw-r--r--lib/rubygems/resolver/incompatibility.rb10
-rw-r--r--lib/rubygems/resolver/installer_set.rb2
-rw-r--r--lib/rubygems/resolver/source_set.rb2
-rw-r--r--lib/rubygems/resolver/stats.rb46
-rw-r--r--lib/rubygems/resolver/strategy.rb44
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