ruby/lib/rubygems/resolver.rb

349 строки
9.5 KiB
Ruby

# frozen_string_literal: true
require 'rubygems/dependency'
require 'rubygems/exceptions'
require 'rubygems/util'
require 'rubygems/util/list'
##
# Given a set of Gem::Dependency objects as +needed+ and a way to query the
# set of available specs via +set+, calculates a set of ActivationRequest
# objects which indicate all the specs that should be activated to meet the
# all the requirements.
class Gem::Resolver
require 'rubygems/resolver/molinillo'
##
# If the DEBUG_RESOLVER environment variable is set then debugging mode is
# enabled for the resolver. This will display information about the state
# of the resolver while a set of dependencies is being resolved.
DEBUG_RESOLVER = !ENV['DEBUG_RESOLVER'].nil?
##
# Set to true if all development dependencies should be considered.
attr_accessor :development
##
# Set to true if immediate development dependencies should be considered.
attr_accessor :development_shallow
##
# When true, no dependencies are looked up for requested gems.
attr_accessor :ignore_dependencies
##
# List of dependencies that could not be found in the configured sources.
attr_reader :missing
attr_reader :stats
##
# Hash of gems to skip resolution. Keyed by gem name, with arrays of
# gem specifications as values.
attr_accessor :skip_gems
##
# When a missing dependency, don't stop. Just go on and record what was
# missing.
attr_accessor :soft_missing
##
# Combines +sets+ into a ComposedSet that allows specification lookup in a
# uniform manner. If one of the +sets+ is itself a ComposedSet its sets are
# flattened into the result ComposedSet.
def self.compose_sets *sets
sets.compact!
sets = sets.map do |set|
case set
when Gem::Resolver::BestSet then
set
when Gem::Resolver::ComposedSet then
set.sets
else
set
end
end.flatten
case sets.length
when 0 then
raise ArgumentError, 'one set in the composition must be non-nil'
when 1 then
sets.first
else
Gem::Resolver::ComposedSet.new(*sets)
end
end
##
# Creates a Resolver that queries only against the already installed gems
# for the +needed+ dependencies.
def self.for_current_gems needed
new needed, Gem::Resolver::CurrentSet.new
end
##
# Create Resolver object which will resolve the tree starting
# with +needed+ Dependency objects.
#
# +set+ is an object that provides where to look for specifications to
# satisfy the Dependencies. This defaults to IndexSet, which will query
# rubygems.org.
def initialize needed, set = nil
@set = set || Gem::Resolver::IndexSet.new
@needed = needed
@development = false
@development_shallow = false
@ignore_dependencies = false
@missing = []
@skip_gems = {}
@soft_missing = false
@stats = Gem::Resolver::Stats.new
end
def explain stage, *data # :nodoc:
return unless DEBUG_RESOLVER
d = data.map { |x| x.pretty_inspect }.join(", ")
$stderr.printf "%10s %s\n", stage.to_s.upcase, d
end
def explain_list stage # :nodoc:
return unless DEBUG_RESOLVER
data = yield
$stderr.printf "%10s (%d entries)\n", stage.to_s.upcase, data.size
PP.pp data, $stderr unless data.empty?
end
##
# Creates an ActivationRequest for the given +dep+ and the last +possible+
# specification.
#
# Returns the Specification and the ActivationRequest
def activation_request dep, possible # :nodoc:
spec = possible.pop
explain :activate, [spec.full_name, possible.size]
explain :possible, possible
activation_request =
Gem::Resolver::ActivationRequest.new spec, dep, possible
return spec, activation_request
end
def requests s, act, reqs=[] # :nodoc:
return reqs if @ignore_dependencies
s.fetch_development_dependencies if @development
s.dependencies.reverse_each do |d|
next if d.type == :development and not @development
next if d.type == :development and @development_shallow and
act.development?
next if d.type == :development and @development_shallow and
act.parent
reqs << Gem::Resolver::DependencyRequest.new(d, act)
@stats.requirement!
end
@set.prefetch reqs
@stats.record_requirements reqs
reqs
end
include Molinillo::UI
def output
@output ||= debug? ? $stdout : File.open(Gem::Util::NULL_DEVICE, 'w')
end
def debug?
DEBUG_RESOLVER
end
include Molinillo::SpecificationProvider
##
# Proceed with resolution! Returns an array of ActivationRequest objects.
def resolve
locking_dg = Molinillo::DependencyGraph.new
Molinillo::Resolver.new(self, self).resolve(@needed.map { |d| DependencyRequest.new d, nil }, locking_dg).tsort.map(&:payload).compact
rescue Molinillo::VersionConflict => e
conflict = e.conflicts.values.first
raise Gem::DependencyResolutionError, Conflict.new(conflict.requirement_trees.first.first, conflict.existing, conflict.requirement)
ensure
@output.close if defined?(@output) and !debug?
end
##
# Extracts the specifications that may be able to fulfill +dependency+ and
# returns those that match the local platform and all those that match.
def find_possible dependency # :nodoc:
all = @set.find_all dependency
if (skip_dep_gems = skip_gems[dependency.name]) && !skip_dep_gems.empty?
matching = all.select do |api_spec|
skip_dep_gems.any? { |s| api_spec.version == s.version }
end
all = matching unless matching.empty?
end
matching_platform = select_local_platforms all
return matching_platform, all
end
##
# Returns the gems in +specs+ that match the local platform.
def select_local_platforms specs # :nodoc:
specs.select do |spec|
Gem::Platform.installable? spec
end
end
def search_for(dependency)
possibles, all = find_possible(dependency)
if !@soft_missing && possibles.empty?
@missing << dependency
exc = Gem::UnsatisfiableDependencyError.new dependency, all
exc.errors = @set.errors
raise exc
end
sources = []
groups = Hash.new { |hash, key| hash[key] = [] }
# create groups & sources in the same loop
sources = possibles.map { |spec|
source = spec.source
groups[source] << spec
source
}.uniq.reverse
activation_requests = []
sources.each do |source|
groups[source].
sort_by { |spec| [spec.version, Gem::Platform.local =~ spec.platform ? 1 : 0] }.
map { |spec| ActivationRequest.new spec, dependency, [] }.
each { |activation_request| activation_requests << activation_request }
end
activation_requests
end
def dependencies_for(specification)
return [] if @ignore_dependencies
spec = specification.spec
requests(spec, specification)
end
def requirement_satisfied_by?(requirement, activated, spec)
requirement.matches_spec? spec
end
def name_for(dependency)
dependency.name
end
def allow_missing?(dependency)
@missing << dependency
@soft_missing
end
def sort_dependencies(dependencies, activated, conflicts)
dependencies.sort_by.with_index do |dependency, i|
name = name_for(dependency)
[
activated.vertex_named(name).payload ? 0 : 1,
amount_constrained(dependency),
conflicts[name] ? 0 : 1,
activated.vertex_named(name).payload ? 0 : search_for(dependency).count,
i # for stable sort
]
end
end
SINGLE_POSSIBILITY_CONSTRAINT_PENALTY = 1_000_000
private_constant :SINGLE_POSSIBILITY_CONSTRAINT_PENALTY if defined?(private_constant)
# returns an integer \in (-\infty, 0]
# a number closer to 0 means the dependency is less constraining
#
# dependencies w/ 0 or 1 possibilities (ignoring version requirements)
# are given very negative values, so they _always_ sort first,
# before dependencies that are unconstrained
def amount_constrained(dependency)
@amount_constrained ||= {}
@amount_constrained[dependency.name] ||= begin
name_dependency = Gem::Dependency.new(dependency.name)
dependency_request_for_name = Gem::Resolver::DependencyRequest.new(name_dependency, dependency.requester)
all = @set.find_all(dependency_request_for_name).size
if all <= 1
all - SINGLE_POSSIBILITY_CONSTRAINT_PENALTY
else
search = search_for(dependency).size
search - all
end
end
end
private :amount_constrained
end
##
# TODO remove in RubyGems 3
Gem::DependencyResolver = Gem::Resolver # :nodoc:
require 'rubygems/resolver/activation_request'
require 'rubygems/resolver/conflict'
require 'rubygems/resolver/dependency_request'
require 'rubygems/resolver/requirement_list'
require 'rubygems/resolver/stats'
require 'rubygems/resolver/set'
require 'rubygems/resolver/api_set'
require 'rubygems/resolver/composed_set'
require 'rubygems/resolver/best_set'
require 'rubygems/resolver/current_set'
require 'rubygems/resolver/git_set'
require 'rubygems/resolver/index_set'
require 'rubygems/resolver/installer_set'
require 'rubygems/resolver/lock_set'
require 'rubygems/resolver/vendor_set'
require 'rubygems/resolver/source_set'
require 'rubygems/resolver/specification'
require 'rubygems/resolver/spec_specification'
require 'rubygems/resolver/api_specification'
require 'rubygems/resolver/git_specification'
require 'rubygems/resolver/index_specification'
require 'rubygems/resolver/installed_specification'
require 'rubygems/resolver/local_specification'
require 'rubygems/resolver/lock_specification'
require 'rubygems/resolver/vendor_specification'