# frozen_string_literal: true
module Gem::Resolver::Molinillo
class Resolver
# A specific resolution from a given {Resolver}
class Resolution
# A conflict that the resolution process encountered
# @attr [Object] requirement the requirement that immediately led to the conflict
# @attr [{String,Nil=>[Object]}] requirements the requirements that caused the conflict
# @attr [Object, nil] existing the existing spec that was in conflict with
# the {#possibility}
# @attr [Object] possibility the spec that was unable to be activated due
# to a conflict
# @attr [Object] locked_requirement the relevant locking requirement.
# @attr [Array>] requirement_trees the different requirement
# trees that led to every requirement for the conflicting name.
# @attr [{String=>Object}] activated_by_name the already-activated specs.
Conflict = Struct.new(
:requirement,
:requirements,
:existing,
:possibility,
:locked_requirement,
:requirement_trees,
:activated_by_name
)
# @return [SpecificationProvider] the provider that knows about
# dependencies, requirements, specifications, versions, etc.
attr_reader :specification_provider
# @return [UI] the UI that knows how to communicate feedback about the
# resolution process back to the user
attr_reader :resolver_ui
# @return [DependencyGraph] the base dependency graph to which
# dependencies should be 'locked'
attr_reader :base
# @return [Array] the dependencies that were explicitly required
attr_reader :original_requested
# Initializes a new resolution.
# @param [SpecificationProvider] specification_provider
# see {#specification_provider}
# @param [UI] resolver_ui see {#resolver_ui}
# @param [Array] requested see {#original_requested}
# @param [DependencyGraph] base see {#base}
def initialize(specification_provider, resolver_ui, requested, base)
@specification_provider = specification_provider
@resolver_ui = resolver_ui
@original_requested = requested
@base = base
@states = []
@iteration_counter = 0
@parents_of = Hash.new { |h, k| h[k] = [] }
end
# Resolves the {#original_requested} dependencies into a full dependency
# graph
# @raise [ResolverError] if successful resolution is impossible
# @return [DependencyGraph] the dependency graph of successfully resolved
# dependencies
def resolve
start_resolution
while state
break unless state.requirements.any? || state.requirement
indicate_progress
if state.respond_to?(:pop_possibility_state) # DependencyState
debug(depth) { "Creating possibility state for #{requirement} (#{possibilities.count} remaining)" }
state.pop_possibility_state.tap do |s|
if s
states.push(s)
activated.tag(s)
end
end
end
process_topmost_state
end
activated.freeze
ensure
end_resolution
end
# @return [Integer] the number of resolver iterations in between calls to
# {#resolver_ui}'s {UI#indicate_progress} method
attr_accessor :iteration_rate
private :iteration_rate
# @return [Time] the time at which resolution began
attr_accessor :started_at
private :started_at
# @return [Array] the stack of states for the resolution
attr_accessor :states
private :states
private
# Sets up the resolution process
# @return [void]
def start_resolution
@started_at = Time.now
handle_missing_or_push_dependency_state(initial_state)
debug { "Starting resolution (#{@started_at})\nUser-requested dependencies: #{original_requested}" }
resolver_ui.before_resolution
end
# Ends the resolution process
# @return [void]
def end_resolution
resolver_ui.after_resolution
debug do
"Finished resolution (#{@iteration_counter} steps) " \
"(Took #{(ended_at = Time.now) - @started_at} seconds) (#{ended_at})"
end
debug { 'Unactivated: ' + Hash[activated.vertices.reject { |_n, v| v.payload }].keys.join(', ') } if state
debug { 'Activated: ' + Hash[activated.vertices.select { |_n, v| v.payload }].keys.join(', ') } if state
end
require 'rubygems/resolver/molinillo/lib/molinillo/state'
require 'rubygems/resolver/molinillo/lib/molinillo/modules/specification_provider'
require 'rubygems/resolver/molinillo/lib/molinillo/delegates/resolution_state'
require 'rubygems/resolver/molinillo/lib/molinillo/delegates/specification_provider'
include Gem::Resolver::Molinillo::Delegates::ResolutionState
include Gem::Resolver::Molinillo::Delegates::SpecificationProvider
# Processes the topmost available {RequirementState} on the stack
# @return [void]
def process_topmost_state
if possibility
attempt_to_activate
else
create_conflict if state.is_a? PossibilityState
unwind_for_conflict until possibility && state.is_a?(DependencyState)
end
end
# @return [Object] the current possibility that the resolution is trying
# to activate
def possibility
possibilities.last
end
# @return [RequirementState] the current state the resolution is
# operating upon
def state
states.last
end
# Creates the initial state for the resolution, based upon the
# {#requested} dependencies
# @return [DependencyState] the initial state for the resolution
def initial_state
graph = DependencyGraph.new.tap do |dg|
original_requested.each { |r| dg.add_vertex(name_for(r), nil, true).tap { |v| v.explicit_requirements << r } }
dg.tag(:initial_state)
end
requirements = sort_dependencies(original_requested, graph, {})
initial_requirement = requirements.shift
DependencyState.new(
initial_requirement && name_for(initial_requirement),
requirements,
graph,
initial_requirement,
initial_requirement && search_for(initial_requirement),
0,
{}
)
end
# Unwinds the states stack because a conflict has been encountered
# @return [void]
def unwind_for_conflict
debug(depth) { "Unwinding for conflict: #{requirement} to #{state_index_for_unwind / 2}" }
conflicts.tap do |c|
sliced_states = states.slice!((state_index_for_unwind + 1)..-1)
raise VersionConflict.new(c) unless state
activated.rewind_to(sliced_states.first || :initial_state) if sliced_states
state.conflicts = c
index = states.size - 1
@parents_of.each { |_, a| a.reject! { |i| i >= index } }
end
end
# @return [Integer] The index to which the resolution should unwind in the
# case of conflict.
def state_index_for_unwind
current_requirement = requirement
existing_requirement = requirement_for_existing_name(name)
index = -1
[current_requirement, existing_requirement].each do |r|
until r.nil?
current_state = find_state_for(r)
if state_any?(current_state)
current_index = states.index(current_state)
index = current_index if current_index > index
break
end
r = parent_of(r)
end
end
index
end
# @return [Object] the requirement that led to `requirement` being added
# to the list of requirements.
def parent_of(requirement)
return unless requirement
return unless index = @parents_of[requirement].last
return unless parent_state = @states[index]
parent_state.requirement
end
# @return [Object] the requirement that led to a version of a possibility
# with the given name being activated.
def requirement_for_existing_name(name)
return nil unless activated.vertex_named(name).payload
states.find { |s| s.name == name }.requirement
end
# @return [ResolutionState] the state whose `requirement` is the given
# `requirement`.
def find_state_for(requirement)
return nil unless requirement
states.reverse_each.find { |i| requirement == i.requirement && i.is_a?(DependencyState) }
end
# @return [Boolean] whether or not the given state has any possibilities
# left.
def state_any?(state)
state && state.possibilities.any?
end
# @return [Conflict] a {Conflict} that reflects the failure to activate
# the {#possibility} in conjunction with the current {#state}
def create_conflict
vertex = activated.vertex_named(name)
locked_requirement = locked_requirement_named(name)
requirements = {}
unless vertex.explicit_requirements.empty?
requirements[name_for_explicit_dependency_source] = vertex.explicit_requirements
end
requirements[name_for_locking_dependency_source] = [locked_requirement] if locked_requirement
vertex.incoming_edges.each { |edge| (requirements[edge.origin.payload] ||= []).unshift(edge.requirement) }
activated_by_name = {}
activated.each { |v| activated_by_name[v.name] = v.payload if v.payload }
conflicts[name] = Conflict.new(
requirement,
requirements,
vertex.payload,
possibility,
locked_requirement,
requirement_trees,
activated_by_name
)
end
# @return [Array>] The different requirement
# trees that led to every requirement for the current spec.
def requirement_trees
vertex = activated.vertex_named(name)
vertex.requirements.map { |r| requirement_tree_for(r) }
end
# @return [Array