class Molinillo::Resolver::Resolution

A specific resolution from a given {Resolver}

def activate_spec

Returns:
  • (void) -
def activate_spec
  conflicts.delete(name)
  debug(depth) { 'Activated ' + name + ' at ' + possibility.to_s }
  activated.set_payload(name, possibility)
  require_nested_dependencies_for(possibility)
end

def attempt_to_activate

Returns:
  • (void) -
def attempt_to_activate
  debug(depth) { 'Attempting to activate ' + possibility.to_s }
  existing_node = activated.vertex_named(name)
  if existing_node.payload
    debug(depth) { "Found existing spec (#{existing_node.payload})" }
    attempt_to_activate_existing_spec(existing_node)
  else
    attempt_to_activate_new_spec
  end
end

def attempt_to_activate_existing_spec(existing_node)

Returns:
  • (void) -
def attempt_to_activate_existing_spec(existing_node)
  existing_spec = existing_node.payload
  if requirement_satisfied_by?(requirement, activated, existing_spec)
    new_requirements = requirements.dup
    push_state_for_requirements(new_requirements, false)
  else
    return if attempt_to_swap_possibility
    create_conflict
    debug(depth) { "Unsatisfied by existing spec (#{existing_node.payload})" }
    unwind_for_conflict
  end
end

def attempt_to_activate_new_spec

Returns:
  • (void) -
def attempt_to_activate_new_spec
  if new_spec_satisfied?
    activate_spec
  else
    create_conflict
    unwind_for_conflict
  end
end

def attempt_to_swap_possibility

Returns:
  • (Boolean) - Whether the possibility was swapped into {#activated}
def attempt_to_swap_possibility
  activated.tag(:swap)
  vertex = activated.vertex_named(name)
  activated.set_payload(name, possibility)
  if !vertex.requirements.
     all? { |r| requirement_satisfied_by?(r, activated, possibility) } ||
      !new_spec_satisfied?
    activated.rewind_to(:swap)
    return
  end
  fixup_swapped_children(vertex)
  activate_spec
end

def create_conflict

Returns:
  • (Conflict) - a {Conflict} that reflects the failure to activate
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

def debug(depth = 0, &block)

Returns:
  • (void) -

Parameters:
  • block (Proc) -- a block that yields a {#to_s}
  • depth (Integer) -- the depth of the {#states} stack
def debug(depth = 0, &block)
  resolver_ui.debug(depth, &block)
end

def end_resolution

Returns:
  • (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

def find_state_for(requirement)

Returns:
  • (ResolutionState) - the state whose `requirement` is the given
def find_state_for(requirement)
  return nil unless requirement
  states.reverse_each.find { |i| requirement == i.requirement && i.is_a?(DependencyState) }
end

def fixup_swapped_children(vertex)

Returns:
  • (void) -

Parameters:
  • vertex (DependencyGraph::Vertex) -- the vertex to fix up.
def fixup_swapped_children(vertex)
  payload = vertex.payload
  deps = dependencies_for(payload).group_by(&method(:name_for))
  vertex.outgoing_edges.each do |outgoing_edge|
    @parent_of[outgoing_edge.requirement] = states.size - 1
    succ = outgoing_edge.destination
    matching_deps = Array(deps[succ.name])
    if matching_deps.empty? && !succ.root? && succ.predecessors.to_a == [vertex]
      debug(depth) { "Removing orphaned spec #{succ.name} after swapping #{name}" }
      succ.requirements.each { |r| @parent_of.delete(r) }
      activated.detach_vertex_named(succ.name)
      all_successor_names = succ.recursive_successors.map(&:name)
      requirements.delete_if do |requirement|
        requirement_name = name_for(requirement)
        (requirement_name == succ.name) || all_successor_names.include?(requirement_name)
      end
    elsif !matching_deps.include?(outgoing_edge.requirement)
      outgoing_edge.requirement = matching_deps.first
    end
    matching_deps.delete(outgoing_edge.requirement)
  end
end

def handle_missing_or_push_dependency_state(state)

Returns:
  • (void) -

Parameters:
  • state (DependencyState) --
def handle_missing_or_push_dependency_state(state)
  if state.requirement && state.possibilities.empty? && allow_missing?(state.requirement)
    state.activated.detach_vertex_named(state.name)
    push_state_for_requirements(state.requirements.dup, false, state.activated)
  else
    states.push(state).tap { activated.tag(state) }
  end
end

def indicate_progress

Returns:
  • (void) -
def indicate_progress
  @iteration_counter += 1
  @progress_rate ||= resolver_ui.progress_rate
  if iteration_rate.nil?
    if Time.now - started_at >= @progress_rate
      self.iteration_rate = @iteration_counter
    end
  end
  if iteration_rate && (@iteration_counter % iteration_rate) == 0
    resolver_ui.indicate_progress
  end
end

def initial_state

Returns:
  • (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

def initialize(specification_provider, resolver_ui, requested, base)

Parameters:
  • base (DependencyGraph) -- see {#base}
  • requested (Array) -- see {#original_requested}
  • resolver_ui (UI) -- see {#resolver_ui}
  • specification_provider (SpecificationProvider) --
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
  @parent_of = {}
end

def locked_requirement_named(requirement_name)

Returns:
  • (Object) - the locked spec named `requirement_name`, if one

Parameters:
  • requirement_name (String) -- the spec name to search for
def locked_requirement_named(requirement_name)
  vertex = base.vertex_named(requirement_name)
  vertex && vertex.payload
end

def new_spec_satisfied?

Returns:
  • (Boolean) - whether the current spec is satisfied as a new
def new_spec_satisfied?
  locked_requirement = locked_requirement_named(name)
  requested_spec_satisfied = requirement_satisfied_by?(requirement, activated, possibility)
  locked_spec_satisfied = !locked_requirement ||
    requirement_satisfied_by?(locked_requirement, activated, possibility)
  debug(depth) { 'Unsatisfied by requested spec' } unless requested_spec_satisfied
  debug(depth) { 'Unsatisfied by locked spec' } unless locked_spec_satisfied
  requested_spec_satisfied && locked_spec_satisfied
end

def parent_of(requirement)

Returns:
  • (Object) - the requirement that led to `requirement` being added
def parent_of(requirement)
  return unless requirement
  return unless index = @parent_of[requirement]
  return unless parent_state = @states[index]
  parent_state.requirement
end

def possibility

Returns:
  • (Object) - the current possibility that the resolution is trying
def possibility
  possibilities.last
end

def process_topmost_state

Returns:
  • (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

def push_state_for_requirements(new_requirements, requires_sort = true, new_activated = activated)

Returns:
  • (void) -

Parameters:
  • new_requirements (Array) --
def push_state_for_requirements(new_requirements, requires_sort = true, new_activated = activated)
  new_requirements = sort_dependencies(new_requirements.uniq, new_activated, conflicts) if requires_sort
  new_requirement = new_requirements.shift
  new_name = new_requirement ? name_for(new_requirement) : ''.freeze
  possibilities = new_requirement ? search_for(new_requirement) : []
  handle_missing_or_push_dependency_state DependencyState.new(
    new_name, new_requirements, new_activated,
    new_requirement, possibilities, depth, conflicts.dup
  )
end

def require_nested_dependencies_for(activated_spec)

Returns:
  • (void) -

Parameters:
  • activated_spec (Object) -- the specification that has just been
def require_nested_dependencies_for(activated_spec)
  nested_dependencies = dependencies_for(activated_spec)
  debug(depth) { "Requiring nested dependencies (#{nested_dependencies.join(', ')})" }
  nested_dependencies.each do |d|
    activated.add_child_vertex(name_for(d), nil, [name_for(activated_spec)], d)
    parent_index = states.size - 1
    @parent_of[d] ||= parent_index
  end
  push_state_for_requirements(requirements + nested_dependencies, !nested_dependencies.empty?)
end

def requirement_for_existing_name(name)

Returns:
  • (Object) - the requirement that led to a version of a possibility
def requirement_for_existing_name(name)
  return nil unless activated.vertex_named(name).payload
  states.find { |s| s.name == name }.requirement
end

def requirement_tree_for(requirement)

Returns:
  • (Array) - the list of requirements that led to
    def requirement_tree_for(requirement)
      tree = []
      while requirement
        tree.unshift(requirement)
        requirement = parent_of(requirement)
      end
      tree
    end

    def requirement_trees

    Returns:
    • (Array>) - The different requirement
    def requirement_trees
      vertex = activated.vertex_named(name)
      vertex.requirements.map { |r| requirement_tree_for(r) }
    end

    def resolve

    Returns:
    • (DependencyGraph) - the dependency graph of successfully resolved

    Raises:
    • (ResolverError) - if successful resolution is impossible
    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

    def start_resolution

    Returns:
    • (void) -
    def start_resolution
      @started_at = Time.now
      handle_missing_or_push_dependency_state(initial_state)
      debug { "Starting resolution (#{@started_at})" }
      resolver_ui.before_resolution
    end

    def state

    Returns:
    • (RequirementState) - the current state the resolution is
    def state
      states.last
    end

    def state_any?(state)

    Returns:
    • (Boolean) - whether or not the given state has any possibilities
    def state_any?(state)
      state && state.possibilities.any?
    end

    def state_index_for_unwind

    Returns:
    • (Integer) - The index to which the resolution should unwind in the
    def state_index_for_unwind
      current_requirement = requirement
      existing_requirement = requirement_for_existing_name(name)
      until current_requirement.nil?
        current_state = find_state_for(current_requirement)
        return states.index(current_state) if state_any?(current_state)
        current_requirement = parent_of(current_requirement)
      end
      until existing_requirement.nil?
        existing_state = find_state_for(existing_requirement)
        return states.index(existing_state) if state_any?(existing_state)
        existing_requirement = parent_of(existing_requirement)
      end
      -1
    end

    def unwind_for_conflict

    Returns:
    • (void) -
    def unwind_for_conflict
      debug(depth) { "Unwinding for conflict: #{requirement}" }
      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
        @parent_of.reject! { |_, i| i >= index }
      end
    end