# frozen_string_literal: true
module Gem::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_set the set of specs 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.
# @attr [Object] underlying_error an error that has occurred during resolution, and
# will be raised at the end of it if no resolution is found.
Conflict = Struct.new(
:requirement,
:requirements,
:existing,
:possibility_set,
:locked_requirement,
:requirement_trees,
:activated_by_name,
:underlying_error
)
class Conflict
# @return [Object] a spec that was unable to be activated due to a conflict
def possibility
possibility_set && possibility_set.latest_version
end
end
# A collection of possibility states that share the same dependencies
# @attr [Array] dependencies the dependencies for this set of possibilities
# @attr [Array] possibilities the possibilities
PossibilitySet = Struct.new(:dependencies, :possibilities)
class PossibilitySet
# String representation of the possibility set, for debugging
def to_s
"[#{possibilities.join(', ')}]"
end
# @return [Object] most up-to-date dependency in the possibility set
def latest_version
possibilities.last
end
end
# Details of the state to unwind to when a conflict occurs, and the cause of the unwind
# @attr [Integer] state_index the index of the state to unwind to
# @attr [Object] state_requirement the requirement of the state we're unwinding to
# @attr [Array] requirement_tree for the requirement we're relaxing
# @attr [Array] conflicting_requirements the requirements that combined to cause the conflict
# @attr [Array] requirement_trees for the conflict
# @attr [Array] requirements_unwound_to_instead array of unwind requirements that were chosen over this unwind
UnwindDetails = Struct.new(
:state_index,
:state_requirement,
:requirement_tree,
:conflicting_requirements,
:requirement_trees,
:requirements_unwound_to_instead
)
class UnwindDetails
include Comparable
# We compare UnwindDetails when choosing which state to unwind to. If
# two options have the same state_index we prefer the one most
# removed from a requirement that caused the conflict. Both options
# would unwind to the same state, but a `grandparent` option will
# filter out fewer of its possibilities after doing so - where a state
# is both a `parent` and a `grandparent` to requirements that have
# caused a conflict this is the correct behaviour.
# @param [UnwindDetail] other UnwindDetail to be compared
# @return [Integer] integer specifying ordering
def <=>(other)
if state_index > other.state_index
1
elsif state_index == other.state_index
reversed_requirement_tree_index <=> other.reversed_requirement_tree_index
else
-1
end
end
# @return [Integer] index of state requirement in reversed requirement tree
# (the conflicting requirement itself will be at position 0)
def reversed_requirement_tree_index
@reversed_requirement_tree_index ||=
if state_requirement
requirement_tree.reverse.index(state_requirement)
else
999_999
end
end
# @return [Boolean] where the requirement of the state we're unwinding
# to directly caused the conflict. Note: in this case, it is
# impossible for the state we're unwinding to be a parent of
# any of the other conflicting requirements (or we would have
# circularity)
def unwinding_to_primary_requirement?
requirement_tree.last == state_requirement
end
# @return [Array] array of sub-dependencies to avoid when choosing a
# new possibility for the state we've unwound to. Only relevant for
# non-primary unwinds
def sub_dependencies_to_avoid
@requirements_to_avoid ||=
requirement_trees.map do |tree|
index = tree.index(state_requirement)
tree[index + 1] if index
end.compact
end
# @return [Array] array of all the requirements that led to the need for
# this unwind
def all_requirements
@all_requirements ||= requirement_trees.flatten(1)
end
end
# @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 if !state.requirement && state.requirements.empty?
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
resolve_activated_specs
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
push_initial_state
debug { "Starting resolution (#{@started_at})\nUser-requested dependencies: #{original_requested}" }
resolver_ui.before_resolution
end
def resolve_activated_specs
activated.vertices.each do |_, vertex|
next unless vertex.payload
latest_version = vertex.payload.possibilities.reverse_each.find do |possibility|
vertex.requirements.all? { |req| requirement_satisfied_by?(req, activated, possibility) }
end
activated.set_payload(vertex.name, latest_version)
end
activated.freeze
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_relative 'state'
require_relative 'modules/specification_provider'
require_relative 'delegates/resolution_state'
require_relative 'delegates/specification_provider'
include Gem::Molinillo::Delegates::ResolutionState
include Gem::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
unwind_for_conflict
end
rescue CircularDependencyError => underlying_error
create_conflict(underlying_error)
unwind_for_conflict
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 and pushes the initial state for the resolution, based upon the
# {#requested} dependencies
# @return [void]
def push_initial_state
graph = DependencyGraph.new.tap do |dg|
original_requested.each do |requested|
vertex = dg.add_vertex(name_for(requested), nil, true)
vertex.explicit_requirements << requested
end
dg.tag(:initial_state)
end
push_state_for_requirements(original_requested, true, graph)
end
# Unwinds the states stack because a conflict has been encountered
# @return [void]
def unwind_for_conflict
details_for_unwind = build_details_for_unwind
unwind_options = unused_unwind_options
debug(depth) { "Unwinding for conflict: #{requirement} to #{details_for_unwind.state_index / 2}" }
conflicts.tap do |c|
sliced_states = states.slice!((details_for_unwind.state_index + 1)..-1)
raise_error_unless_state(c)
activated.rewind_to(sliced_states.first || :initial_state) if sliced_states
state.conflicts = c
state.unused_unwind_options = unwind_options
filter_possibilities_after_unwind(details_for_unwind)
index = states.size - 1
@parents_of.each { |_, a| a.reject! { |i| i >= index } }
state.unused_unwind_options.reject! { |uw| uw.state_index >= index }
end
end
# Raises a VersionConflict error, or any underlying error, if there is no
# current state
# @return [void]
def raise_error_unless_state(conflicts)
return if state
error = conflicts.values.map(&:underlying_error).compact.first
raise error || VersionConflict.new(conflicts, specification_provider)
end
# @return [UnwindDetails] Details of the nearest index to which we could unwind
def build_details_for_unwind
# Get the possible unwinds for the current conflict
current_conflict = conflicts[name]
binding_requirements = binding_requirements_for_conflict(current_conflict)
unwind_details = unwind_options_for_requirements(binding_requirements)
last_detail_for_current_unwind = unwind_details.sort.last
current_detail = last_detail_for_current_unwind
# Look for past conflicts that could be unwound to affect the
# requirement tree for the current conflict
all_reqs = last_detail_for_current_unwind.all_requirements
all_reqs_size = all_reqs.size
relevant_unused_unwinds = unused_unwind_options.select do |alternative|
diff_reqs = all_reqs - alternative.requirements_unwound_to_instead
next if diff_reqs.size == all_reqs_size
# Find the highest index unwind whilst looping through
current_detail = alternative if alternative > current_detail
alternative
end
# Add the current unwind options to the `unused_unwind_options` array.
# The "used" option will be filtered out during `unwind_for_conflict`.
state.unused_unwind_options += unwind_details.reject { |detail| detail.state_index == -1 }
# Update the requirements_unwound_to_instead on any relevant unused unwinds
relevant_unused_unwinds.each do |d|
(d.requirements_unwound_to_instead << current_detail.state_requirement).uniq!
end
unwind_details.each do |d|
(d.requirements_unwound_to_instead << current_detail.state_requirement).uniq!
end
current_detail
end
# @param [Array