summaryrefslogtreecommitdiff
path: root/lib/rubygems/resolver
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rubygems/resolver')
-rw-r--r--lib/rubygems/resolver/activation_request.rb159
-rw-r--r--lib/rubygems/resolver/api_set.rb139
-rw-r--r--lib/rubygems/resolver/api_set/gem_parser.rb21
-rw-r--r--lib/rubygems/resolver/api_specification.rb105
-rw-r--r--lib/rubygems/resolver/best_set.rb49
-rw-r--r--lib/rubygems/resolver/composed_set.rb65
-rw-r--r--lib/rubygems/resolver/current_set.rb12
-rw-r--r--lib/rubygems/resolver/dependency_request.rb119
-rw-r--r--lib/rubygems/resolver/git_set.rb120
-rw-r--r--lib/rubygems/resolver/git_specification.rb57
-rw-r--r--lib/rubygems/resolver/incompatibility.rb10
-rw-r--r--lib/rubygems/resolver/index_set.rb79
-rw-r--r--lib/rubygems/resolver/index_specification.rb101
-rw-r--r--lib/rubygems/resolver/installed_specification.rb57
-rw-r--r--lib/rubygems/resolver/installer_set.rb271
-rw-r--r--lib/rubygems/resolver/local_specification.rb40
-rw-r--r--lib/rubygems/resolver/lock_set.rb81
-rw-r--r--lib/rubygems/resolver/lock_specification.rb86
-rw-r--r--lib/rubygems/resolver/requirement_list.rb82
-rw-r--r--lib/rubygems/resolver/set.rb55
-rw-r--r--lib/rubygems/resolver/source_set.rb47
-rw-r--r--lib/rubygems/resolver/spec_specification.rb76
-rw-r--r--lib/rubygems/resolver/specification.rb126
-rw-r--r--lib/rubygems/resolver/strategy.rb44
-rw-r--r--lib/rubygems/resolver/vendor_set.rb86
-rw-r--r--lib/rubygems/resolver/vendor_specification.rb23
26 files changed, 2110 insertions, 0 deletions
diff --git a/lib/rubygems/resolver/activation_request.rb b/lib/rubygems/resolver/activation_request.rb
new file mode 100644
index 0000000000..5c722001b1
--- /dev/null
+++ b/lib/rubygems/resolver/activation_request.rb
@@ -0,0 +1,159 @@
+# frozen_string_literal: true
+
+##
+# Specifies a Specification object that should be activated. Also contains a
+# dependency that was used to introduce this activation.
+
+class Gem::Resolver::ActivationRequest
+ ##
+ # The parent request for this activation request.
+
+ attr_reader :request
+
+ ##
+ # The specification to be activated.
+
+ attr_reader :spec
+
+ ##
+ # Creates a new ActivationRequest that will activate +spec+. The parent
+ # +request+ is used to provide diagnostics in case of conflicts.
+
+ def initialize(spec, request)
+ @spec = spec
+ @request = request
+ end
+
+ def ==(other) # :nodoc:
+ case other
+ when Gem::Specification
+ @spec == other
+ when Gem::Resolver::ActivationRequest
+ @spec == other.spec
+ else
+ false
+ end
+ end
+
+ def eql?(other)
+ self == other
+ end
+
+ def hash
+ @spec.hash
+ end
+
+ ##
+ # Is this activation request for a development dependency?
+
+ def development?
+ @request.development?
+ end
+
+ ##
+ # Downloads a gem at +path+ and returns the file path.
+
+ def download(path)
+ Gem.ensure_gem_subdirectories path
+
+ if @spec.respond_to? :sources
+ exception = nil
+ path = @spec.sources.find do |source|
+ source.download full_spec, path
+ rescue exception
+ end
+ return path if path
+ raise exception if exception
+
+ elsif @spec.respond_to? :source
+ source = @spec.source
+ source.download full_spec, path
+
+ else
+ source = Gem.sources.first
+ source.download full_spec, path
+ end
+ end
+
+ ##
+ # The full name of the specification to be activated.
+
+ def full_name
+ name_tuple.full_name
+ end
+
+ alias_method :to_s, :full_name
+
+ ##
+ # The Gem::Specification for this activation request.
+
+ def full_spec
+ Gem::Specification === @spec ? @spec : @spec.spec
+ end
+
+ def inspect # :nodoc:
+ format("#<%s for %p from %s>", self.class, @spec, @request)
+ end
+
+ ##
+ # True if the requested gem has already been installed.
+
+ def installed?
+ case @spec
+ when Gem::Resolver::VendorSpecification then
+ true
+ else
+ this_spec = full_spec
+
+ Gem::Specification.any? do |s|
+ s == this_spec && s.base_dir == this_spec.base_dir
+ end
+ end
+ end
+
+ ##
+ # The name of this activation request's specification
+
+ def name
+ @spec.name
+ end
+
+ ##
+ # Return the ActivationRequest that contained the dependency
+ # that we were activated for.
+
+ def parent
+ @request.requester
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[Activation request", "]" do
+ q.breakable
+ q.pp @spec
+
+ q.breakable
+ q.text " for "
+ q.pp @request
+ end
+ end
+
+ ##
+ # The version of this activation request's specification
+
+ def version
+ @spec.version
+ end
+
+ ##
+ # The platform of this activation request's specification
+
+ def platform
+ @spec.platform
+ end
+
+ private
+
+ def name_tuple
+ @name_tuple ||= Gem::NameTuple.new(name, version, platform)
+ end
+end
diff --git a/lib/rubygems/resolver/api_set.rb b/lib/rubygems/resolver/api_set.rb
new file mode 100644
index 0000000000..3f443519d8
--- /dev/null
+++ b/lib/rubygems/resolver/api_set.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+##
+# 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 Compact Index API this APISet uses.
+
+ attr_reader :dep_uri # :nodoc:
+
+ ##
+ # The Gem::Source that gems are fetched from
+
+ attr_reader :source
+
+ ##
+ # The corresponding place to fetch gems.
+
+ attr_reader :uri
+
+ ##
+ # 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()
+
+ dep_uri = Gem::URI dep_uri unless Gem::URI === dep_uri
+
+ @dep_uri = dep_uri
+ @uri = dep_uri + ".."
+
+ @data = Hash.new {|h,k| h[k] = [] }
+ @source = Gem::Source.new @uri
+
+ @to_fetch = []
+ end
+
+ ##
+ # Return an array of APISpecification objects matching
+ # DependencyRequest +req+.
+
+ def find_all(req)
+ res = []
+
+ return res unless @remote
+
+ if @to_fetch.include?(req.name)
+ prefetch_now
+ end
+
+ versions(req.name).each do |ver|
+ if req.dependency.match? req.name, ver[:number], @prerelease
+ res << Gem::Resolver::APISpecification.new(self, ver)
+ end
+ end
+
+ res
+ end
+
+ ##
+ # A hint run by the resolver to allow the Set to fetch
+ # data for DependencyRequests +reqs+.
+
+ def prefetch(reqs)
+ return unless @remote
+ names = reqs.map {|r| r.dependency.name }
+ needed = names - @data.keys - @to_fetch
+
+ @to_fetch += needed
+ end
+
+ def prefetch_now # :nodoc:
+ needed = @to_fetch
+ @to_fetch = []
+
+ needed.sort.each do |name|
+ versions(name)
+ end
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[APISet", "]" do
+ q.breakable
+ q.text "URI: #{@dep_uri}"
+
+ q.breakable
+ q.text "gem names:"
+ q.pp @data.keys
+ end
+ end
+
+ ##
+ # Return data for all versions of the gem +name+.
+
+ def versions(name) # :nodoc:
+ if @data.key?(name)
+ return @data[name]
+ end
+
+ uri = @dep_uri + name
+
+ begin
+ str = Gem::RemoteFetcher.fetcher.fetch_path uri
+ rescue Gem::RemoteFetcher::FetchError
+ @data[name] = []
+ else
+ lines(str).each do |ver|
+ number, platform, dependencies, requirements = parse_gem(ver)
+
+ platform ||= "ruby"
+ dependencies = dependencies.map {|dep_name, reqs| [dep_name, reqs.join(", ")] }
+ requirements = requirements.map {|req_name, reqs| [req_name.to_sym, reqs] }.to_h
+
+ @data[name] << { name: name, number: number, platform: platform, dependencies: dependencies, requirements: requirements }
+ end
+ end
+
+ @data[name]
+ end
+
+ private
+
+ def lines(str)
+ lines = str.split("\n")
+ header = lines.index("---")
+ header ? lines[header + 1..-1] : lines
+ end
+
+ def parse_gem(string)
+ @gem_parser ||= GemParser.new
+ @gem_parser.parse(string)
+ end
+end
diff --git a/lib/rubygems/resolver/api_set/gem_parser.rb b/lib/rubygems/resolver/api_set/gem_parser.rb
new file mode 100644
index 0000000000..4d827f4980
--- /dev/null
+++ b/lib/rubygems/resolver/api_set/gem_parser.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class Gem::Resolver::APISet::GemParser
+ 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) } : []
+ requirements = requirements ? requirements.map! {|d| parse_dependency(d) } : []
+ [version, platform, dependencies, requirements]
+ end
+
+ private
+
+ def parse_dependency(string)
+ dependency = string.split(":", 2)
+ dependency[-1] = dependency[-1].split("&") if dependency.size > 1
+ dependency[0] = -dependency[0]
+ dependency
+ end
+end
diff --git a/lib/rubygems/resolver/api_specification.rb b/lib/rubygems/resolver/api_specification.rb
new file mode 100644
index 0000000000..ccfd6fe084
--- /dev/null
+++ b/lib/rubygems/resolver/api_specification.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+##
+# 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.
+
+class Gem::Resolver::APISpecification < Gem::Resolver::Specification
+ ##
+ # We assume that all instances of this class are immutable;
+ # so avoid duplicated generation for performance.
+ @@cache = {}
+ def self.new(set, api_data)
+ cache_key = [set, api_data]
+ cache = @@cache[cache_key]
+ return cache if cache
+ @@cache[cache_key] = super
+ end
+
+ ##
+ # Creates an APISpecification for the given +set+ from the Compact Index API
+ # +api_data+.
+ #
+ # See https://guides.rubygems.org/rubygems-org-compact-index-api for the
+ # format of the +api_data+.
+
+ def initialize(set, api_data)
+ super()
+
+ @set = set
+ @name = api_data[:name]
+ @version = Gem::Version.new(api_data[:number]).freeze
+ @platform = Gem::Platform.new(api_data[:platform]).freeze
+ @original_platform = api_data[:platform].freeze
+ @dependencies = api_data[:dependencies].map do |name, ver|
+ Gem::Dependency.new(name, ver.split(/\s*,\s*/)).freeze
+ end.freeze
+ @required_ruby_version = Gem::Requirement.new(api_data.dig(:requirements, :ruby)).freeze
+ @required_rubygems_version = Gem::Requirement.new(api_data.dig(:requirements, :rubygems)).freeze
+ end
+
+ def ==(other) # :nodoc:
+ self.class === other &&
+ @set == other.set &&
+ @name == other.name &&
+ @version == other.version &&
+ @platform == other.platform
+ end
+
+ def hash
+ @set.hash ^ @name.hash ^ @version.hash ^ @platform.hash
+ end
+
+ def fetch_development_dependencies # :nodoc:
+ spec = source.fetch_spec Gem::NameTuple.new @name, @version, @platform
+
+ @dependencies = spec.dependencies
+ end
+
+ def installable_platform? # :nodoc:
+ Gem::Platform.match_gem? @platform, @name
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[APISpecification", "]" do
+ q.breakable
+ q.text "name: #{name}"
+
+ q.breakable
+ q.text "version: #{version}"
+
+ q.breakable
+ q.text "platform: #{platform}"
+
+ q.breakable
+ q.text "dependencies:"
+ q.breakable
+ q.pp @dependencies
+
+ q.breakable
+ q.text "set uri: #{@set.dep_uri}"
+ end
+ end
+
+ ##
+ # Fetches a Gem::Specification for this APISpecification.
+
+ def spec # :nodoc:
+ @spec ||=
+ begin
+ tuple = Gem::NameTuple.new @name, @version, @platform
+ source.fetch_spec tuple
+ rescue Gem::RemoteFetcher::FetchError
+ raise if @original_platform == @platform
+
+ tuple = Gem::NameTuple.new @name, @version, @original_platform
+ source.fetch_spec tuple
+ end
+ end
+
+ def source # :nodoc:
+ @set.source
+ end
+end
diff --git a/lib/rubygems/resolver/best_set.rb b/lib/rubygems/resolver/best_set.rb
new file mode 100644
index 0000000000..e647a2c11b
--- /dev/null
+++ b/lib/rubygems/resolver/best_set.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+##
+# The BestSet chooses the best available method to query a remote index.
+#
+# It combines IndexSet and APISet
+
+class Gem::Resolver::BestSet < Gem::Resolver::ComposedSet
+ ##
+ # Creates a BestSet for the given +sources+ or Gem::sources if none are
+ # specified. +sources+ must be a Gem::SourceList.
+
+ def initialize(sources = Gem.sources)
+ super()
+
+ @sources = sources
+ end
+
+ ##
+ # Picks which sets to use for the configured sources.
+
+ def pick_sets # :nodoc:
+ @sources.each_source do |source|
+ @sets << source.dependency_resolver_set(@prerelease)
+ end
+ end
+
+ def find_all(req) # :nodoc:
+ pick_sets if @remote && @sets.empty?
+
+ super
+ end
+
+ def prefetch(reqs) # :nodoc:
+ pick_sets if @remote && @sets.empty?
+
+ super
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[BestSet", "]" do
+ q.breakable
+ q.text "sets:"
+
+ q.breakable
+ q.pp @sets
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/composed_set.rb b/lib/rubygems/resolver/composed_set.rb
new file mode 100644
index 0000000000..e67dd41754
--- /dev/null
+++ b/lib/rubygems/resolver/composed_set.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+##
+# A ComposedSet allows multiple sets to be queried like a single set.
+#
+# To create a composed set with any number of sets use:
+#
+# Gem::Resolver.compose_sets set1, set2
+#
+# This method will eliminate nesting of composed sets.
+
+class Gem::Resolver::ComposedSet < Gem::Resolver::Set
+ attr_reader :sets # :nodoc:
+
+ ##
+ # Creates a new ComposedSet containing +sets+. Use
+ # Gem::Resolver::compose_sets instead.
+
+ def initialize(*sets)
+ super()
+
+ @sets = sets
+ end
+
+ ##
+ # When +allow_prerelease+ is set to +true+ prereleases gems are allowed to
+ # match dependencies.
+
+ def prerelease=(allow_prerelease)
+ super
+
+ sets.each do |set|
+ set.prerelease = allow_prerelease
+ end
+ end
+
+ ##
+ # Sets the remote network access for all composed sets.
+
+ def remote=(remote)
+ super
+
+ @sets.each {|set| set.remote = remote }
+ end
+
+ def errors
+ @errors + @sets.flat_map(&:errors)
+ end
+
+ ##
+ # Finds all specs matching +req+ in all sets.
+
+ def find_all(req)
+ @sets.flat_map do |s|
+ s.find_all req
+ end
+ end
+
+ ##
+ # Prefetches +reqs+ in all sets.
+
+ def prefetch(reqs)
+ @sets.each {|s| s.prefetch(reqs) }
+ end
+end
diff --git a/lib/rubygems/resolver/current_set.rb b/lib/rubygems/resolver/current_set.rb
new file mode 100644
index 0000000000..370e445089
--- /dev/null
+++ b/lib/rubygems/resolver/current_set.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+##
+# A set which represents the installed gems. Respects
+# all the normal settings that control where to look
+# for installed gems.
+
+class Gem::Resolver::CurrentSet < Gem::Resolver::Set
+ def find_all(req)
+ req.dependency.matching_specs
+ end
+end
diff --git a/lib/rubygems/resolver/dependency_request.rb b/lib/rubygems/resolver/dependency_request.rb
new file mode 100644
index 0000000000..60b338277f
--- /dev/null
+++ b/lib/rubygems/resolver/dependency_request.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+##
+# Used Internally. Wraps a Dependency object to also track which spec
+# contained the Dependency.
+
+class Gem::Resolver::DependencyRequest
+ ##
+ # The wrapped Gem::Dependency
+
+ attr_reader :dependency
+
+ ##
+ # The request for this dependency.
+
+ attr_reader :requester
+
+ ##
+ # Creates a new DependencyRequest for +dependency+ from +requester+.
+ # +requester may be nil if the request came from a user.
+
+ def initialize(dependency, requester)
+ @dependency = dependency
+ @requester = requester
+ end
+
+ def ==(other) # :nodoc:
+ case other
+ when Gem::Dependency
+ @dependency == other
+ when Gem::Resolver::DependencyRequest
+ @dependency == other.dependency
+ else
+ false
+ end
+ end
+
+ ##
+ # Is this dependency a development dependency?
+
+ def development?
+ @dependency.type == :development
+ end
+
+ ##
+ # Does this dependency request match +spec+?
+ #
+ # NOTE: #match? only matches prerelease versions when #dependency is a
+ # prerelease dependency.
+
+ def match?(spec, allow_prerelease = false)
+ @dependency.match? spec, nil, allow_prerelease
+ end
+
+ ##
+ # Does this dependency request match +spec+?
+ #
+ # NOTE: #matches_spec? matches prerelease versions. See also #match?
+
+ def matches_spec?(spec)
+ @dependency.matches_spec? spec
+ end
+
+ ##
+ # The name of the gem this dependency request is requesting.
+
+ def name
+ @dependency.name
+ end
+
+ def type
+ @dependency.type
+ end
+
+ ##
+ # Indicate that the request is for a gem explicitly requested by the user
+
+ def explicit?
+ @requester.nil?
+ end
+
+ ##
+ # Indicate that the request is for a gem requested as a dependency of
+ # another gem
+
+ def implicit?
+ !explicit?
+ end
+
+ ##
+ # Return a String indicating who caused this request to be added (only
+ # valid for implicit requests)
+
+ def request_context
+ @requester ? @requester.request : "(unknown)"
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[Dependency request ", "]" do
+ q.breakable
+ q.text @dependency.to_s
+
+ q.breakable
+ q.text " requested by "
+ q.pp @requester
+ end
+ end
+
+ ##
+ # The version requirement for this dependency request
+
+ def requirement
+ @dependency.requirement
+ end
+
+ def to_s # :nodoc:
+ @dependency.to_s
+ end
+end
diff --git a/lib/rubygems/resolver/git_set.rb b/lib/rubygems/resolver/git_set.rb
new file mode 100644
index 0000000000..2912378fe7
--- /dev/null
+++ b/lib/rubygems/resolver/git_set.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+##
+# A GitSet represents gems that are sourced from git repositories.
+#
+# This is used for gem dependency file support.
+#
+# Example:
+#
+# set = Gem::Resolver::GitSet.new
+# set.add_git_gem 'rake', 'git://example/rake.git', tag: 'rake-10.1.0'
+
+class Gem::Resolver::GitSet < Gem::Resolver::Set
+ ##
+ # The root directory for git gems in this set. This is usually Gem.dir, the
+ # installation directory for regular gems.
+
+ attr_accessor :root_dir
+
+ ##
+ # Contains repositories needing submodules
+
+ attr_reader :need_submodules # :nodoc:
+
+ ##
+ # A Hash containing git gem names for keys and a Hash of repository and
+ # git commit reference as values.
+
+ attr_reader :repositories # :nodoc:
+
+ ##
+ # A hash of gem names to Gem::Resolver::GitSpecifications
+
+ attr_reader :specs # :nodoc:
+
+ def initialize # :nodoc:
+ super()
+
+ @need_submodules = {}
+ @repositories = {}
+ @root_dir = Gem.dir
+ @specs = {}
+ end
+
+ def add_git_gem(name, repository, reference, submodules) # :nodoc:
+ @repositories[name] = [repository, reference]
+ @need_submodules[repository] = submodules
+ end
+
+ ##
+ # Adds and returns a GitSpecification with the given +name+ and +version+
+ # which came from a +repository+ at the given +reference+. If +submodules+
+ # is true they are checked out along with the repository.
+ #
+ # This fills in the prefetch information as enough information about the gem
+ # is present in the arguments.
+
+ def add_git_spec(name, version, repository, reference, submodules) # :nodoc:
+ add_git_gem name, repository, reference, submodules
+
+ source = Gem::Source::Git.new name, repository, reference
+ source.root_dir = @root_dir
+
+ spec = Gem::Specification.new do |s|
+ s.name = name
+ s.version = version
+ end
+
+ git_spec = Gem::Resolver::GitSpecification.new self, spec, source
+
+ @specs[spec.name] = git_spec
+
+ git_spec
+ end
+
+ ##
+ # Finds all git gems matching +req+
+
+ def find_all(req)
+ prefetch nil
+
+ specs.values.select do |spec|
+ req.match? spec
+ end
+ end
+
+ ##
+ # Prefetches specifications from the git repositories in this set.
+
+ def prefetch(reqs)
+ return unless @specs.empty?
+
+ @repositories.each do |name, (repository, reference)|
+ source = Gem::Source::Git.new name, repository, reference
+ source.root_dir = @root_dir
+ source.remote = @remote
+
+ source.specs.each do |spec|
+ git_spec = Gem::Resolver::GitSpecification.new self, spec, source
+
+ @specs[spec.name] = git_spec
+ end
+ end
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[GitSet", "]" do
+ next if @repositories.empty?
+ q.breakable
+
+ repos = @repositories.map do |name, (repository, reference)|
+ "#{name}: #{repository}@#{reference}"
+ end
+
+ q.seplist repos do |repo|
+ q.text repo
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/git_specification.rb b/lib/rubygems/resolver/git_specification.rb
new file mode 100644
index 0000000000..e587c17d2a
--- /dev/null
+++ b/lib/rubygems/resolver/git_specification.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+##
+# A GitSpecification represents a gem that is sourced from a git repository
+# and is being loaded through a gem dependencies file through the +git:+
+# option.
+
+class Gem::Resolver::GitSpecification < Gem::Resolver::SpecSpecification
+ def ==(other) # :nodoc:
+ self.class === other &&
+ @set == other.set &&
+ @spec == other.spec &&
+ @source == other.source
+ end
+
+ def add_dependency(dependency) # :nodoc:
+ spec.dependencies << dependency
+ end
+
+ ##
+ # Installing a git gem only involves building the extensions and generating
+ # the executables.
+
+ def install(options = {})
+ require_relative "../installer"
+
+ installer = Gem::Installer.for_spec spec, options
+
+ yield installer if block_given?
+
+ installer.run_pre_install_hooks
+ installer.build_extensions
+ installer.run_post_build_hooks
+ installer.generate_bin
+ installer.run_post_install_hooks
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[GitSpecification", "]" do
+ q.breakable
+ q.text "name: #{name}"
+
+ q.breakable
+ q.text "version: #{version}"
+
+ q.breakable
+ q.text "dependencies:"
+ q.breakable
+ q.pp dependencies
+
+ q.breakable
+ q.text "source:"
+ q.breakable
+ q.pp @source
+ end
+ 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/index_set.rb b/lib/rubygems/resolver/index_set.rb
new file mode 100644
index 0000000000..cddaf8773f
--- /dev/null
+++ b/lib/rubygems/resolver/index_set.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+##
+# The global rubygems pool represented via the traditional
+# source index.
+
+class Gem::Resolver::IndexSet < Gem::Resolver::Set
+ def initialize(source = nil) # :nodoc:
+ super()
+
+ @f =
+ if source
+ sources = Gem::SourceList.from [source]
+
+ Gem::SpecFetcher.new sources
+ else
+ Gem::SpecFetcher.fetcher
+ end
+
+ @all = Hash.new {|h,k| h[k] = [] }
+
+ list, errors = @f.available_specs :complete
+
+ @errors.concat errors
+
+ list.each do |uri, specs|
+ specs.each do |n|
+ @all[n.name] << [uri, n]
+ end
+ end
+
+ @specs = {}
+ end
+
+ ##
+ # Return an array of IndexSpecification objects matching
+ # DependencyRequest +req+.
+
+ def find_all(req)
+ res = []
+
+ return res unless @remote
+
+ name = req.dependency.name
+
+ @all[name].each do |uri, n|
+ next unless req.match? n, @prerelease
+ res << Gem::Resolver::IndexSpecification.new(
+ self, n.name, n.version, uri, n.platform
+ )
+ end
+
+ res
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[IndexSet", "]" do
+ q.breakable
+ q.text "sources:"
+ q.breakable
+ q.pp @f.sources
+
+ q.breakable
+ q.text "specs:"
+
+ q.breakable
+
+ names = @all.values.flat_map do |tuples|
+ tuples.map do |_, tuple|
+ tuple.full_name
+ end
+ end
+
+ q.seplist names do |name|
+ q.text name
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/index_specification.rb b/lib/rubygems/resolver/index_specification.rb
new file mode 100644
index 0000000000..7b95608071
--- /dev/null
+++ b/lib/rubygems/resolver/index_specification.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+##
+# Represents a possible Specification object returned from IndexSet. Used to
+# delay needed to download full Specification objects when only the +name+
+# and +version+ are needed.
+
+class Gem::Resolver::IndexSpecification < Gem::Resolver::Specification
+ ##
+ # An IndexSpecification is created from the index format described in `gem
+ # help generate_index`.
+ #
+ # The +set+ contains other specifications for this (URL) +source+.
+ #
+ # The +name+, +version+ and +platform+ are the name, version and platform of
+ # the gem.
+
+ def initialize(set, name, version, source, platform)
+ super()
+
+ @set = set
+ @name = name
+ @version = version
+ @source = source
+ @platform = Gem::Platform.new(platform.to_s)
+ @original_platform = platform.to_s
+
+ @spec = nil
+ end
+
+ ##
+ # The dependencies of the gem for this specification
+
+ def dependencies
+ spec.dependencies
+ end
+
+ ##
+ # The required_ruby_version constraint for this specification
+ #
+ # A fallback is included because when generated, some marshalled specs have it
+ # set to +nil+.
+
+ def required_ruby_version
+ spec.required_ruby_version || Gem::Requirement.default
+ end
+
+ ##
+ # The required_rubygems_version constraint for this specification
+ #
+ # A fallback is included because the original version of the specification
+ # API didn't include that field, so some marshalled specs in the index have it
+ # set to +nil+.
+
+ def required_rubygems_version
+ spec.required_rubygems_version || Gem::Requirement.default
+ end
+
+ def ==(other)
+ self.class === other &&
+ @name == other.name &&
+ @version == other.version &&
+ @platform == other.platform
+ end
+
+ def hash
+ @name.hash ^ @version.hash ^ @platform.hash
+ end
+
+ def inspect # :nodoc:
+ format("#<%s %s source %s>", self.class, full_name, @source)
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[Index specification", "]" do
+ q.breakable
+ q.text full_name
+
+ unless @platform == Gem::Platform::RUBY
+ q.breakable
+ q.text @platform.to_s
+ end
+
+ q.breakable
+ q.text "source "
+ q.pp @source
+ end
+ end
+
+ ##
+ # Fetches a Gem::Specification for this IndexSpecification from the #source.
+
+ def spec # :nodoc:
+ @spec ||=
+ begin
+ tuple = Gem::NameTuple.new @name, @version, @original_platform
+
+ @source.fetch_spec tuple
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/installed_specification.rb b/lib/rubygems/resolver/installed_specification.rb
new file mode 100644
index 0000000000..8280ae4672
--- /dev/null
+++ b/lib/rubygems/resolver/installed_specification.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+##
+# An InstalledSpecification represents a gem that is already installed
+# locally.
+
+class Gem::Resolver::InstalledSpecification < Gem::Resolver::SpecSpecification
+ def ==(other) # :nodoc:
+ self.class === other &&
+ @set == other.set &&
+ @spec == other.spec
+ end
+
+ ##
+ # This is a null install as this specification is already installed.
+ # +options+ are ignored.
+
+ def install(options = {})
+ yield nil
+ end
+
+ ##
+ # Returns +true+ if this gem is installable for the current platform.
+
+ def installable_platform?
+ # BACKCOMPAT If the file is coming out of a specified file, then we
+ # ignore the platform. This code can be removed in RG 3.0.
+ return true if @source.is_a? Gem::Source::SpecificFile
+
+ super
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[InstalledSpecification", "]" do
+ q.breakable
+ q.text "name: #{name}"
+
+ q.breakable
+ q.text "version: #{version}"
+
+ q.breakable
+ q.text "platform: #{platform}"
+
+ q.breakable
+ q.text "dependencies:"
+ q.breakable
+ q.pp spec.dependencies
+ end
+ end
+
+ ##
+ # The source for this specification
+
+ def source
+ @source ||= Gem::Source::Installed.new
+ end
+end
diff --git a/lib/rubygems/resolver/installer_set.rb b/lib/rubygems/resolver/installer_set.rb
new file mode 100644
index 0000000000..42ce0890e2
--- /dev/null
+++ b/lib/rubygems/resolver/installer_set.rb
@@ -0,0 +1,271 @@
+# frozen_string_literal: true
+
+##
+# A set of gems for installation sourced from remote sources and local .gem
+# files
+
+class Gem::Resolver::InstallerSet < Gem::Resolver::Set
+ ##
+ # List of Gem::Specification objects that must always be installed.
+
+ attr_reader :always_install # :nodoc:
+
+ ##
+ # Only install gems in the always_install list
+
+ attr_accessor :ignore_dependencies # :nodoc:
+
+ ##
+ # Do not look in the installed set when finding specifications. This is
+ # used by the --install-dir option to `gem install`
+
+ attr_accessor :ignore_installed # :nodoc:
+
+ ##
+ # The remote_set looks up remote gems for installation.
+
+ attr_reader :remote_set # :nodoc:
+
+ ##
+ # Ignore ruby & rubygems specification constraints.
+ #
+
+ attr_accessor :force # :nodoc:
+
+ ##
+ # Creates a new InstallerSet that will look for gems in +domain+.
+
+ def initialize(domain)
+ super()
+
+ @domain = domain
+
+ @f = Gem::SpecFetcher.fetcher
+
+ @always_install = []
+ @ignore_dependencies = false
+ @ignore_installed = false
+ @local = {}
+ @local_source = Gem::Source::Local.new
+ @remote_set = Gem::Resolver::BestSet.new
+ @force = false
+ @specs = {}
+ end
+
+ ##
+ # Looks up the latest specification for +dependency+ and adds it to the
+ # always_install list.
+
+ def add_always_install(dependency)
+ request = Gem::Resolver::DependencyRequest.new dependency, nil
+
+ found = find_all request
+
+ found.delete_if do |s|
+ s.version.prerelease? && !s.local?
+ end unless dependency.prerelease?
+
+ found = found.select do |s|
+ Gem::Source::SpecificFile === s.source ||
+ Gem::Platform.match_spec?(s)
+ end
+
+ found = found.sort_by do |s|
+ [s.version, Gem::Platform.sort_priority(s.platform)]
+ end
+
+ newest = found.last
+
+ unless newest
+ exc = Gem::UnsatisfiableDependencyError.new request
+ exc.errors = errors
+
+ raise exc
+ end
+
+ unless @force
+ found_matching_metadata = found.reverse.find do |spec|
+ metadata_satisfied?(spec)
+ end
+
+ if found_matching_metadata.nil?
+ ensure_required_ruby_version_met(newest.spec)
+ ensure_required_rubygems_version_met(newest.spec)
+ else
+ newest = found_matching_metadata
+ end
+ end
+
+ @always_install << newest.spec
+ end
+
+ ##
+ # Adds a local gem requested using +dep_name+ with the given +spec+ that can
+ # be loaded and installed using the +source+.
+
+ def add_local(dep_name, spec, source)
+ @local[dep_name] = [spec, source]
+ end
+
+ ##
+ # Should local gems should be considered?
+
+ def consider_local? # :nodoc:
+ @domain == :both || @domain == :local
+ end
+
+ ##
+ # Should remote gems should be considered?
+
+ def consider_remote? # :nodoc:
+ @domain == :both || @domain == :remote
+ end
+
+ ##
+ # Errors encountered while resolving gems
+
+ def errors
+ @errors + @remote_set.errors
+ end
+
+ ##
+ # Returns an array of IndexSpecification objects matching DependencyRequest
+ # +req+.
+
+ def find_all(req)
+ res = []
+
+ dep = req.dependency
+
+ return res if @ignore_dependencies &&
+ @always_install.none? {|spec| dep.match? spec }
+
+ name = dep.name
+
+ dep.matching_specs.each do |gemspec|
+ next if @always_install.any? {|spec| spec.name == gemspec.name }
+
+ res << Gem::Resolver::InstalledSpecification.new(self, gemspec)
+ end unless @ignore_installed
+
+ matching_local = []
+
+ if consider_local?
+ matching_local = @local.values.select do |spec, _|
+ req.match? spec
+ end.map do |spec, source|
+ Gem::Resolver::LocalSpecification.new self, spec, source
+ end
+
+ res.concat matching_local
+
+ begin
+ @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
+ )
+ end
+ rescue Gem::Package::FormatError
+ # ignore
+ end
+ end
+
+ res.concat @remote_set.find_all req if consider_remote? && matching_local.empty?
+
+ res
+ end
+
+ def prefetch(reqs)
+ @remote_set.prefetch(reqs) if consider_remote?
+ end
+
+ def prerelease=(allow_prerelease)
+ super
+
+ @remote_set.prerelease = allow_prerelease
+ end
+
+ def inspect # :nodoc:
+ always_install = @always_install.map(&:full_name)
+
+ format("#<%s domain: %s specs: %p always install: %p>", self.class, @domain, @specs.keys, always_install)
+ end
+
+ ##
+ # Called from IndexSpecification to get a true Specification
+ # object.
+
+ def load_spec(name, ver, platform, source) # :nodoc:
+ key = "#{name}-#{ver}-#{platform}"
+
+ @specs.fetch key do
+ tuple = Gem::NameTuple.new name, ver, platform
+
+ @specs[key] = source.fetch_spec tuple
+ end
+ end
+
+ ##
+ # Has a local gem for +dep_name+ been added to this set?
+
+ def local?(dep_name) # :nodoc:
+ spec, _ = @local[dep_name]
+
+ spec
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[InstallerSet", "]" do
+ q.breakable
+ q.text "domain: #{@domain}"
+
+ q.breakable
+ q.text "specs: "
+ q.pp @specs.keys
+
+ q.breakable
+ q.text "always install: "
+ q.pp @always_install
+ end
+ end
+
+ def remote=(remote) # :nodoc:
+ case @domain
+ when :local then
+ @domain = :both if remote
+ when :remote then
+ @domain = nil unless remote
+ when :both then
+ @domain = :local unless remote
+ end
+ end
+
+ private
+
+ def metadata_satisfied?(spec)
+ spec.required_ruby_version.satisfied_by?(Gem.ruby_version) &&
+ spec.required_rubygems_version.satisfied_by?(Gem.rubygems_version)
+ end
+
+ def ensure_required_ruby_version_met(spec) # :nodoc:
+ if rrv = spec.required_ruby_version
+ ruby_version = Gem.ruby_version
+ unless rrv.satisfied_by? ruby_version
+ raise Gem::RuntimeRequirementNotMetError,
+ "#{spec.full_name} requires Ruby version #{rrv}. The current ruby version is #{ruby_version}."
+ end
+ end
+ end
+
+ def ensure_required_rubygems_version_met(spec) # :nodoc:
+ if rrgv = spec.required_rubygems_version
+ unless rrgv.satisfied_by? Gem.rubygems_version
+ rg_version = Gem::VERSION
+ raise Gem::RuntimeRequirementNotMetError,
+ "#{spec.full_name} requires RubyGems version #{rrgv}. The current RubyGems version is #{rg_version}. " \
+ "Try 'gem update --system' to update RubyGems itself."
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/local_specification.rb b/lib/rubygems/resolver/local_specification.rb
new file mode 100644
index 0000000000..b57d40e795
--- /dev/null
+++ b/lib/rubygems/resolver/local_specification.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+##
+# A LocalSpecification comes from a .gem file on the local filesystem.
+
+class Gem::Resolver::LocalSpecification < Gem::Resolver::SpecSpecification
+ ##
+ # Returns +true+ if this gem is installable for the current platform.
+
+ def installable_platform?
+ return true if @source.is_a? Gem::Source::SpecificFile
+
+ super
+ end
+
+ def local? # :nodoc:
+ true
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[LocalSpecification", "]" do
+ q.breakable
+ q.text "name: #{name}"
+
+ q.breakable
+ q.text "version: #{version}"
+
+ q.breakable
+ q.text "platform: #{platform}"
+
+ q.breakable
+ q.text "dependencies:"
+ q.breakable
+ q.pp dependencies
+
+ q.breakable
+ q.text "source: #{@source.path}"
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/lock_set.rb b/lib/rubygems/resolver/lock_set.rb
new file mode 100644
index 0000000000..e5ee32a9a6
--- /dev/null
+++ b/lib/rubygems/resolver/lock_set.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+##
+# A set of gems from a gem dependencies lockfile.
+
+class Gem::Resolver::LockSet < Gem::Resolver::Set
+ attr_reader :specs # :nodoc:
+
+ ##
+ # Creates a new LockSet from the given +sources+
+
+ def initialize(sources)
+ super()
+
+ @sources = sources.map do |source|
+ Gem::Source::Lock.new source
+ end
+
+ @specs = []
+ end
+
+ ##
+ # Creates a new IndexSpecification in this set using the given +name+,
+ # +version+ and +platform+.
+ #
+ # The specification's set will be the current set, and the source will be
+ # the current set's source.
+
+ def add(name, version, platform) # :nodoc:
+ version = Gem::Version.new version
+ specs = [
+ Gem::Resolver::LockSpecification.new(self, name, version, @sources, platform),
+ ]
+
+ @specs.concat specs
+
+ specs
+ end
+
+ ##
+ # Returns an Array of IndexSpecification objects matching the
+ # DependencyRequest +req+.
+
+ def find_all(req)
+ @specs.select do |spec|
+ req.match? spec
+ end
+ end
+
+ ##
+ # Loads a Gem::Specification with the given +name+, +version+ and
+ # +platform+. +source+ is ignored.
+
+ def load_spec(name, version, platform, source) # :nodoc:
+ dep = Gem::Dependency.new name, version
+
+ found = @specs.find do |spec|
+ dep.matches_spec?(spec) && spec.platform == platform
+ end
+
+ tuple = Gem::NameTuple.new found.name, found.version, found.platform
+
+ found.source.fetch_spec tuple
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[LockSet", "]" do
+ q.breakable
+ q.text "source:"
+
+ q.breakable
+ q.pp @source
+
+ q.breakable
+ q.text "specs:"
+
+ q.breakable
+ q.pp @specs.map(&:full_name)
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/lock_specification.rb b/lib/rubygems/resolver/lock_specification.rb
new file mode 100644
index 0000000000..06f912dd85
--- /dev/null
+++ b/lib/rubygems/resolver/lock_specification.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+##
+# The LockSpecification comes from a lockfile (Gem::RequestSet::Lockfile).
+#
+# A LockSpecification's dependency information is pre-filled from the
+# lockfile.
+
+class Gem::Resolver::LockSpecification < Gem::Resolver::Specification
+ attr_reader :sources
+
+ def initialize(set, name, version, sources, platform)
+ super()
+
+ @name = name
+ @platform = platform
+ @set = set
+ @source = sources.first
+ @sources = sources
+ @version = version
+
+ @dependencies = []
+ @spec = nil
+ end
+
+ ##
+ # This is a null install as a locked specification is considered installed.
+ # +options+ are ignored.
+
+ def install(options = {})
+ destination = options[:install_dir] || Gem.dir
+
+ if File.exist? File.join(destination, "specifications", spec.spec_name)
+ yield nil
+ return
+ end
+
+ super
+ end
+
+ ##
+ # Adds +dependency+ from the lockfile to this specification
+
+ def add_dependency(dependency) # :nodoc:
+ @dependencies << dependency
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[LockSpecification", "]" do
+ q.breakable
+ q.text "name: #{@name}"
+
+ q.breakable
+ q.text "version: #{@version}"
+
+ unless @platform == Gem::Platform::RUBY
+ q.breakable
+ q.text "platform: #{@platform}"
+ end
+
+ unless @dependencies.empty?
+ q.breakable
+ q.text "dependencies:"
+ q.breakable
+ q.pp @dependencies
+ end
+ end
+ end
+
+ ##
+ # A specification constructed from the lockfile is returned
+
+ def spec
+ @spec ||= Gem::Specification.find do |spec|
+ spec.name == @name && spec.version == @version
+ end
+
+ @spec ||= Gem::Specification.new do |s|
+ s.name = @name
+ s.version = @version
+ s.platform = @platform
+
+ s.dependencies.concat @dependencies
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/requirement_list.rb b/lib/rubygems/resolver/requirement_list.rb
new file mode 100644
index 0000000000..6f86f0f412
--- /dev/null
+++ b/lib/rubygems/resolver/requirement_list.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+##
+# The RequirementList is used to hold the requirements being considered
+# while resolving a set of gems.
+#
+# The RequirementList acts like a queue where the oldest items are removed
+# first.
+
+class Gem::Resolver::RequirementList
+ include Enumerable
+
+ ##
+ # Creates a new RequirementList.
+
+ def initialize
+ @exact = []
+ @list = []
+ end
+
+ def initialize_copy(other) # :nodoc:
+ @exact = @exact.dup
+ @list = @list.dup
+ end
+
+ ##
+ # Adds Resolver::DependencyRequest +req+ to this requirements list.
+
+ def add(req)
+ if req.requirement.exact?
+ @exact.push req
+ else
+ @list.push req
+ end
+ req
+ end
+
+ ##
+ # Enumerates requirements in the list
+
+ def each # :nodoc:
+ return enum_for __method__ unless block_given?
+
+ @exact.each do |requirement|
+ yield requirement
+ end
+
+ @list.each do |requirement|
+ yield requirement
+ end
+ end
+
+ ##
+ # How many elements are in the list
+
+ def size
+ @exact.size + @list.size
+ end
+
+ ##
+ # Is the list empty?
+
+ def empty?
+ @exact.empty? && @list.empty?
+ end
+
+ ##
+ # Remove the oldest DependencyRequest from the list.
+
+ def remove
+ return @exact.shift unless @exact.empty?
+ @list.shift
+ end
+
+ ##
+ # Returns the oldest five entries from the list.
+
+ def next5
+ x = @exact[0,5]
+ x + @list[0,5 - x.size]
+ end
+end
diff --git a/lib/rubygems/resolver/set.rb b/lib/rubygems/resolver/set.rb
new file mode 100644
index 0000000000..243fee5fd5
--- /dev/null
+++ b/lib/rubygems/resolver/set.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+##
+# Resolver sets are used to look up specifications (and their
+# dependencies) used in resolution. This set is abstract.
+
+class Gem::Resolver::Set
+ ##
+ # Set to true to disable network access for this set
+
+ attr_accessor :remote
+
+ ##
+ # Errors encountered when resolving gems
+
+ attr_accessor :errors
+
+ ##
+ # When true, allows matching of requests to prerelease gems.
+
+ attr_accessor :prerelease
+
+ def initialize # :nodoc:
+ @prerelease = false
+ @remote = true
+ @errors = []
+ end
+
+ ##
+ # The find_all method must be implemented. It returns all Resolver
+ # Specification objects matching the given DependencyRequest +req+.
+
+ def find_all(req)
+ raise NotImplementedError
+ end
+
+ ##
+ # The #prefetch method may be overridden, but this is not necessary. This
+ # default implementation does nothing, which is suitable for sets where
+ # looking up a specification is cheap (such as installed gems).
+ #
+ # When overridden, the #prefetch method should look up specifications
+ # matching +reqs+.
+
+ def prefetch(reqs)
+ end
+
+ ##
+ # When true, this set is allowed to access the network when looking up
+ # specifications or dependencies.
+
+ def remote? # :nodoc:
+ @remote
+ end
+end
diff --git a/lib/rubygems/resolver/source_set.rb b/lib/rubygems/resolver/source_set.rb
new file mode 100644
index 0000000000..074b473edc
--- /dev/null
+++ b/lib/rubygems/resolver/source_set.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+##
+# The SourceSet chooses the best available method to query a remote index.
+#
+# Kind off like BestSet but filters the sources for gems
+
+class Gem::Resolver::SourceSet < Gem::Resolver::Set
+ ##
+ # Creates a SourceSet for the given +sources+ or Gem::sources if none are
+ # specified. +sources+ must be a Gem::SourceList.
+
+ def initialize
+ super()
+
+ @links = {}
+ @sets = {}
+ end
+
+ def find_all(req) # :nodoc:
+ if set = get_set(req.dependency.name)
+ set.find_all req
+ else
+ []
+ end
+ end
+
+ # potentially no-op
+ def prefetch(reqs) # :nodoc:
+ reqs.each do |req|
+ if set = get_set(req.dependency.name)
+ set.prefetch reqs
+ end
+ end
+ end
+
+ def add_source_gem(name, source)
+ @links[name] = source
+ end
+
+ private
+
+ def get_set(name)
+ link = @links[name]
+ @sets[link] ||= Gem::Source.new(link).dependency_resolver_set(@prerelease) if link
+ end
+end
diff --git a/lib/rubygems/resolver/spec_specification.rb b/lib/rubygems/resolver/spec_specification.rb
new file mode 100644
index 0000000000..00ef9fdba0
--- /dev/null
+++ b/lib/rubygems/resolver/spec_specification.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+##
+# The Resolver::SpecSpecification contains common functionality for
+# Resolver specifications that are backed by a Gem::Specification.
+
+class Gem::Resolver::SpecSpecification < Gem::Resolver::Specification
+ ##
+ # A SpecSpecification is created for a +set+ for a Gem::Specification in
+ # +spec+. The +source+ is either where the +spec+ came from, or should be
+ # loaded from.
+
+ def initialize(set, spec, source = nil)
+ @set = set
+ @source = source
+ @spec = spec
+ end
+
+ ##
+ # The dependencies of the gem for this specification
+
+ def dependencies
+ spec.dependencies
+ end
+
+ ##
+ # The required_ruby_version constraint for this specification
+
+ def required_ruby_version
+ spec.required_ruby_version
+ end
+
+ ##
+ # The required_rubygems_version constraint for this specification
+
+ def required_rubygems_version
+ spec.required_rubygems_version
+ end
+
+ ##
+ # The name and version of the specification.
+ #
+ # Unlike Gem::Specification#full_name, the platform is not included.
+
+ def full_name
+ "#{spec.name}-#{spec.version}"
+ end
+
+ ##
+ # The name of the gem for this specification
+
+ def name
+ spec.name
+ end
+
+ ##
+ # The platform this gem works on.
+
+ def platform
+ spec.platform
+ end
+
+ ##
+ # The version of the gem for this specification.
+
+ def version
+ spec.version
+ end
+
+ ##
+ # The hash value for this specification.
+
+ def hash
+ spec.hash
+ end
+end
diff --git a/lib/rubygems/resolver/specification.rb b/lib/rubygems/resolver/specification.rb
new file mode 100644
index 0000000000..d2098ef0e2
--- /dev/null
+++ b/lib/rubygems/resolver/specification.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+##
+# A Resolver::Specification contains a subset of the information
+# contained in a Gem::Specification. Only the information necessary for
+# dependency resolution in the resolver is included.
+
+class Gem::Resolver::Specification
+ ##
+ # The dependencies of the gem for this specification
+
+ attr_reader :dependencies
+
+ ##
+ # The name of the gem for this specification
+
+ attr_reader :name
+
+ ##
+ # The platform this gem works on.
+
+ attr_reader :platform
+
+ ##
+ # The set this specification came from.
+
+ attr_reader :set
+
+ ##
+ # The source for this specification
+
+ attr_reader :source
+
+ ##
+ # The Gem::Specification for this Resolver::Specification.
+ #
+ # Implementers, note that #install updates @spec, so be sure to cache the
+ # Gem::Specification in @spec when overriding.
+
+ attr_reader :spec
+
+ ##
+ # The version of the gem for this specification.
+
+ attr_reader :version
+
+ ##
+ # The required_ruby_version constraint for this specification.
+
+ attr_reader :required_ruby_version
+
+ ##
+ # The required_ruby_version constraint for this specification.
+
+ attr_reader :required_rubygems_version
+
+ ##
+ # Sets default instance variables for the specification.
+
+ def initialize
+ @dependencies = nil
+ @name = nil
+ @platform = nil
+ @set = nil
+ @source = nil
+ @version = nil
+ @required_ruby_version = Gem::Requirement.default
+ @required_rubygems_version = Gem::Requirement.default
+ end
+
+ ##
+ # Fetches development dependencies if the source does not provide them by
+ # default (see APISpecification).
+
+ def fetch_development_dependencies # :nodoc:
+ end
+
+ ##
+ # The name and version of the specification.
+ #
+ # Unlike Gem::Specification#full_name, the platform is not included.
+
+ def full_name
+ "#{@name}-#{@version}"
+ end
+
+ ##
+ # Installs this specification using the Gem::Installer +options+. The
+ # install method yields a Gem::Installer instance, which indicates the
+ # gem will be installed, or +nil+, which indicates the gem is already
+ # installed.
+ #
+ # After installation #spec is updated to point to the just-installed
+ # specification.
+
+ def install(options = {})
+ require_relative "../installer"
+
+ gem = download options
+
+ installer = Gem::Installer.at gem, options
+
+ yield installer if block_given?
+
+ @spec = installer.install
+ end
+
+ def download(options)
+ dir = options[:install_dir] || Gem.dir
+
+ Gem.ensure_gem_subdirectories dir
+
+ source.download spec, dir
+ end
+
+ ##
+ # Returns true if this specification is installable on this platform.
+
+ def installable_platform?
+ Gem::Platform.match_spec? spec
+ end
+
+ def local? # :nodoc:
+ false
+ 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
diff --git a/lib/rubygems/resolver/vendor_set.rb b/lib/rubygems/resolver/vendor_set.rb
new file mode 100644
index 0000000000..293a1e3331
--- /dev/null
+++ b/lib/rubygems/resolver/vendor_set.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+##
+# A VendorSet represents gems that have been unpacked into a specific
+# directory that contains a gemspec.
+#
+# This is used for gem dependency file support.
+#
+# Example:
+#
+# set = Gem::Resolver::VendorSet.new
+#
+# set.add_vendor_gem 'rake', 'vendor/rake'
+#
+# The directory vendor/rake must contain an unpacked rake gem along with a
+# rake.gemspec (watching the given name).
+
+class Gem::Resolver::VendorSet < Gem::Resolver::Set
+ ##
+ # The specifications for this set.
+
+ attr_reader :specs # :nodoc:
+
+ def initialize # :nodoc:
+ super()
+
+ @directories = {}
+ @specs = {}
+ end
+
+ ##
+ # Adds a specification to the set with the given +name+ which has been
+ # unpacked into the given +directory+.
+
+ def add_vendor_gem(name, directory) # :nodoc:
+ gemspec = File.join directory, "#{name}.gemspec"
+
+ spec = Gem::Specification.load gemspec
+
+ raise Gem::GemNotFoundException,
+ "unable to find #{gemspec} for gem #{name}" unless spec
+
+ spec.full_gem_path = File.expand_path directory
+
+ @specs[spec.name] = spec
+ @directories[spec] = directory
+
+ spec
+ end
+
+ ##
+ # Returns an Array of VendorSpecification objects matching the
+ # DependencyRequest +req+.
+
+ def find_all(req)
+ @specs.values.select do |spec|
+ req.match? spec
+ end.map do |spec|
+ source = Gem::Source::Vendor.new @directories[spec]
+ Gem::Resolver::VendorSpecification.new self, spec, source
+ end
+ end
+
+ ##
+ # Loads a spec with the given +name+. +version+, +platform+ and +source+ are
+ # ignored.
+
+ def load_spec(name, version, platform, source) # :nodoc:
+ @specs.fetch name
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[VendorSet", "]" do
+ next if @directories.empty?
+ q.breakable
+
+ dirs = @directories.map do |spec, directory|
+ "#{spec.full_name}: #{directory}"
+ end
+
+ q.seplist dirs do |dir|
+ q.text dir
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/vendor_specification.rb b/lib/rubygems/resolver/vendor_specification.rb
new file mode 100644
index 0000000000..ac78f54558
--- /dev/null
+++ b/lib/rubygems/resolver/vendor_specification.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+##
+# A VendorSpecification represents a gem that has been unpacked into a project
+# and is being loaded through a gem dependencies file through the +path:+
+# option.
+
+class Gem::Resolver::VendorSpecification < Gem::Resolver::SpecSpecification
+ def ==(other) # :nodoc:
+ self.class === other &&
+ @set == other.set &&
+ @spec == other.spec &&
+ @source == other.source
+ end
+
+ ##
+ # This is a null install as this gem was unpacked into a directory.
+ # +options+ are ignored.
+
+ def install(options = {})
+ yield nil
+ end
+end