molinillo 0.5.7 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/ARCHITECTURE.md +102 -0
- data/CHANGELOG.md +352 -0
- data/lib/molinillo.rb +2 -0
- data/lib/molinillo/compatibility.rb +26 -0
- data/lib/molinillo/delegates/resolution_state.rb +7 -0
- data/lib/molinillo/delegates/specification_provider.rb +1 -0
- data/lib/molinillo/dependency_graph.rb +3 -2
- data/lib/molinillo/dependency_graph/action.rb +1 -0
- data/lib/molinillo/dependency_graph/add_edge_no_circular.rb +1 -0
- data/lib/molinillo/dependency_graph/add_vertex.rb +1 -0
- data/lib/molinillo/dependency_graph/delete_edge.rb +1 -0
- data/lib/molinillo/dependency_graph/detach_vertex_named.rb +1 -0
- data/lib/molinillo/dependency_graph/log.rb +1 -0
- data/lib/molinillo/dependency_graph/set_payload.rb +1 -0
- data/lib/molinillo/dependency_graph/tag.rb +1 -0
- data/lib/molinillo/dependency_graph/vertex.rb +3 -2
- data/lib/molinillo/errors.rb +69 -6
- data/lib/molinillo/gem_metadata.rb +2 -1
- data/lib/molinillo/modules/specification_provider.rb +1 -0
- data/lib/molinillo/modules/ui.rb +3 -1
- data/lib/molinillo/resolution.rb +495 -145
- data/lib/molinillo/resolver.rb +1 -0
- data/lib/molinillo/state.rb +8 -4
- metadata +8 -30
- data/spec/dependency_graph/log_spec.rb +0 -29
- data/spec/dependency_graph_spec.rb +0 -74
- data/spec/errors_spec.rb +0 -26
- data/spec/fuzz_spec.rb +0 -94
- data/spec/resolver_integration_specs/index_from_rubygems.rb +0 -76
- data/spec/resolver_spec.rb +0 -235
- data/spec/spec_helper.rb +0 -44
- data/spec/spec_helper/equal_dependency_graph.rb +0 -16
- data/spec/spec_helper/index.rb +0 -186
- data/spec/spec_helper/naive_resolver.rb +0 -48
- data/spec/spec_helper/specification.rb +0 -28
- data/spec/spec_helper/ui.rb +0 -14
- data/spec/state_spec.rb +0 -30
    
        data/lib/molinillo.rb
    CHANGED
    
    
| @@ -0,0 +1,26 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Molinillo
         | 
| 4 | 
            +
              # Hacks needed for old Ruby versions.
         | 
| 5 | 
            +
              module Compatibility
         | 
| 6 | 
            +
                module_function
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                if [].respond_to?(:flat_map)
         | 
| 9 | 
            +
                  # Flat map
         | 
| 10 | 
            +
                  # @param [Enumerable] enum an enumerable object
         | 
| 11 | 
            +
                  # @block the block to flat-map with
         | 
| 12 | 
            +
                  # @return The enum, flat-mapped
         | 
| 13 | 
            +
                  def flat_map(enum, &blk)
         | 
| 14 | 
            +
                    enum.flat_map(&blk)
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
                else
         | 
| 17 | 
            +
                  # Flat map
         | 
| 18 | 
            +
                  # @param [Enumerable] enum an enumerable object
         | 
| 19 | 
            +
                  # @block the block to flat-map with
         | 
| 20 | 
            +
                  # @return The enum, flat-mapped
         | 
| 21 | 
            +
                  def flat_map(enum, &blk)
         | 
| 22 | 
            +
                    enum.map(&blk).flatten(1)
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
            end
         | 
| @@ -1,4 +1,5 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 2 3 | 
             
            module Molinillo
         | 
| 3 4 | 
             
              # @!visibility private
         | 
| 4 5 | 
             
              module Delegates
         | 
| @@ -45,6 +46,12 @@ module Molinillo | |
| 45 46 | 
             
                    current_state = state || Molinillo::ResolutionState.empty
         | 
| 46 47 | 
             
                    current_state.conflicts
         | 
| 47 48 | 
             
                  end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  # (see Molinillo::ResolutionState#unused_unwind_options)
         | 
| 51 | 
            +
                  def unused_unwind_options
         | 
| 52 | 
            +
                    current_state = state || Molinillo::ResolutionState.empty
         | 
| 53 | 
            +
                    current_state.unused_unwind_options
         | 
| 54 | 
            +
                  end
         | 
| 48 55 | 
             
                end
         | 
| 49 56 | 
             
              end
         | 
| 50 57 | 
             
            end
         | 
| @@ -1,4 +1,5 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 2 3 | 
             
            require 'set'
         | 
| 3 4 | 
             
            require 'tsort'
         | 
| 4 5 |  | 
| @@ -147,8 +148,8 @@ module Molinillo | |
| 147 148 | 
             
                  vertex = add_vertex(name, payload, root)
         | 
| 148 149 | 
             
                  vertex.explicit_requirements << requirement if root
         | 
| 149 150 | 
             
                  parent_names.each do |parent_name|
         | 
| 150 | 
            -
                     | 
| 151 | 
            -
                    add_edge( | 
| 151 | 
            +
                    parent_vertex = vertex_named(parent_name)
         | 
| 152 | 
            +
                    add_edge(parent_vertex, vertex, requirement)
         | 
| 152 153 | 
             
                  end
         | 
| 153 154 | 
             
                  vertex
         | 
| 154 155 | 
             
                end
         | 
| @@ -1,4 +1,5 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 2 3 | 
             
            module Molinillo
         | 
| 3 4 | 
             
              class DependencyGraph
         | 
| 4 5 | 
             
                # A vertex in a {DependencyGraph} that encapsulates a {#name} and a
         | 
| @@ -53,7 +54,7 @@ module Molinillo | |
| 53 54 | 
             
                  #   {#descendent?}
         | 
| 54 55 | 
             
                  def recursive_predecessors
         | 
| 55 56 | 
             
                    vertices = predecessors
         | 
| 56 | 
            -
                    vertices +=  | 
| 57 | 
            +
                    vertices += Compatibility.flat_map(vertices, &:recursive_predecessors)
         | 
| 57 58 | 
             
                    vertices.uniq!
         | 
| 58 59 | 
             
                    vertices
         | 
| 59 60 | 
             
                  end
         | 
| @@ -68,7 +69,7 @@ module Molinillo | |
| 68 69 | 
             
                  #   {#ancestor?}
         | 
| 69 70 | 
             
                  def recursive_successors
         | 
| 70 71 | 
             
                    vertices = successors
         | 
| 71 | 
            -
                    vertices +=  | 
| 72 | 
            +
                    vertices += Compatibility.flat_map(vertices, &:recursive_successors)
         | 
| 72 73 | 
             
                    vertices.uniq!
         | 
| 73 74 | 
             
                    vertices
         | 
| 74 75 | 
             
                  end
         | 
    
        data/lib/molinillo/errors.rb
    CHANGED
    
    | @@ -1,4 +1,5 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 2 3 | 
             
            module Molinillo
         | 
| 3 4 | 
             
              # An error that occurred during the resolution process
         | 
| 4 5 | 
             
              class ResolverError < StandardError; end
         | 
| @@ -41,11 +42,11 @@ module Molinillo | |
| 41 42 | 
             
                attr_reader :dependencies
         | 
| 42 43 |  | 
| 43 44 | 
             
                # Initializes a new error with the given circular vertices.
         | 
| 44 | 
            -
                # @param [Array<DependencyGraph::Vertex>]  | 
| 45 | 
            +
                # @param [Array<DependencyGraph::Vertex>] vertices the vertices in the dependency
         | 
| 45 46 | 
             
                #   that caused the error
         | 
| 46 | 
            -
                def initialize( | 
| 47 | 
            -
                  super "There is a circular dependency between #{ | 
| 48 | 
            -
                  @dependencies =  | 
| 47 | 
            +
                def initialize(vertices)
         | 
| 48 | 
            +
                  super "There is a circular dependency between #{vertices.map(&:name).join(' and ')}"
         | 
| 49 | 
            +
                  @dependencies = vertices.map { |vertex| vertex.payload.possibilities.last }.to_set
         | 
| 49 50 | 
             
                end
         | 
| 50 51 | 
             
              end
         | 
| 51 52 |  | 
| @@ -55,11 +56,16 @@ module Molinillo | |
| 55 56 | 
             
                #   resolution to fail
         | 
| 56 57 | 
             
                attr_reader :conflicts
         | 
| 57 58 |  | 
| 59 | 
            +
                # @return [SpecificationProvider] the specification provider used during
         | 
| 60 | 
            +
                #   resolution
         | 
| 61 | 
            +
                attr_reader :specification_provider
         | 
| 62 | 
            +
             | 
| 58 63 | 
             
                # Initializes a new error with the given version conflicts.
         | 
| 59 64 | 
             
                # @param [{String => Resolution::Conflict}] conflicts see {#conflicts}
         | 
| 60 | 
            -
                 | 
| 65 | 
            +
                # @param [SpecificationProvider] specification_provider see {#specification_provider}
         | 
| 66 | 
            +
                def initialize(conflicts, specification_provider)
         | 
| 61 67 | 
             
                  pairs = []
         | 
| 62 | 
            -
                  conflicts.values.flatten | 
| 68 | 
            +
                  Compatibility.flat_map(conflicts.values.flatten, &:requirements).each do |conflicting|
         | 
| 63 69 | 
             
                    conflicting.each do |source, conflict_requirements|
         | 
| 64 70 | 
             
                      conflict_requirements.each do |c|
         | 
| 65 71 | 
             
                        pairs << [c, source]
         | 
| @@ -69,7 +75,64 @@ module Molinillo | |
| 69 75 |  | 
| 70 76 | 
             
                  super "Unable to satisfy the following requirements:\n\n" \
         | 
| 71 77 | 
             
                    "#{pairs.map { |r, d| "- `#{r}` required by `#{d}`" }.join("\n")}"
         | 
| 78 | 
            +
             | 
| 72 79 | 
             
                  @conflicts = conflicts
         | 
| 80 | 
            +
                  @specification_provider = specification_provider
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                require 'molinillo/delegates/specification_provider'
         | 
| 84 | 
            +
                include Delegates::SpecificationProvider
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                # @return [String] An error message that includes requirement trees,
         | 
| 87 | 
            +
                #   which is much more detailed & customizable than the default message
         | 
| 88 | 
            +
                # @param [Hash] opts the options to create a message with.
         | 
| 89 | 
            +
                # @option opts [String] :solver_name The user-facing name of the solver
         | 
| 90 | 
            +
                # @option opts [String] :possibility_type The generic name of a possibility
         | 
| 91 | 
            +
                # @option opts [Proc] :reduce_trees A proc that reduced the list of requirement trees
         | 
| 92 | 
            +
                # @option opts [Proc] :printable_requirement A proc that pretty-prints requirements
         | 
| 93 | 
            +
                # @option opts [Proc] :additional_message_for_conflict A proc that appends additional
         | 
| 94 | 
            +
                #   messages for each conflict
         | 
| 95 | 
            +
                # @option opts [Proc] :version_for_spec A proc that returns the version number for a
         | 
| 96 | 
            +
                #   possibility
         | 
| 97 | 
            +
                def message_with_trees(opts = {})
         | 
| 98 | 
            +
                  solver_name = opts.delete(:solver_name) { self.class.name.split('::').first }
         | 
| 99 | 
            +
                  possibility_type = opts.delete(:possibility_type) { 'possibility named' }
         | 
| 100 | 
            +
                  reduce_trees = opts.delete(:reduce_trees) { proc { |trees| trees.uniq.sort_by(&:to_s) } }
         | 
| 101 | 
            +
                  printable_requirement = opts.delete(:printable_requirement) { proc { |req| req.to_s } }
         | 
| 102 | 
            +
                  additional_message_for_conflict = opts.delete(:additional_message_for_conflict) { proc {} }
         | 
| 103 | 
            +
                  version_for_spec = opts.delete(:version_for_spec) { proc(&:to_s) }
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                  conflicts.sort.reduce(''.dup) do |o, (name, conflict)|
         | 
| 106 | 
            +
                    o << %(\n#{solver_name} could not find compatible versions for #{possibility_type} "#{name}":\n)
         | 
| 107 | 
            +
                    if conflict.locked_requirement
         | 
| 108 | 
            +
                      o << %(  In snapshot (#{name_for_locking_dependency_source}):\n)
         | 
| 109 | 
            +
                      o << %(    #{printable_requirement.call(conflict.locked_requirement)}\n)
         | 
| 110 | 
            +
                      o << %(\n)
         | 
| 111 | 
            +
                    end
         | 
| 112 | 
            +
                    o << %(  In #{name_for_explicit_dependency_source}:\n)
         | 
| 113 | 
            +
                    trees = reduce_trees.call(conflict.requirement_trees)
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                    o << trees.map do |tree|
         | 
| 116 | 
            +
                      t = ''.dup
         | 
| 117 | 
            +
                      depth = 2
         | 
| 118 | 
            +
                      tree.each do |req|
         | 
| 119 | 
            +
                        t << '  ' * depth << req.to_s
         | 
| 120 | 
            +
                        unless tree.last == req
         | 
| 121 | 
            +
                          if spec = conflict.activated_by_name[name_for(req)]
         | 
| 122 | 
            +
                            t << %( was resolved to #{version_for_spec.call(spec)}, which)
         | 
| 123 | 
            +
                          end
         | 
| 124 | 
            +
                          t << %( depends on)
         | 
| 125 | 
            +
                        end
         | 
| 126 | 
            +
                        t << %(\n)
         | 
| 127 | 
            +
                        depth += 1
         | 
| 128 | 
            +
                      end
         | 
| 129 | 
            +
                      t
         | 
| 130 | 
            +
                    end.join("\n")
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                    additional_message_for_conflict.call(o, name, conflict)
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                    o
         | 
| 135 | 
            +
                  end.strip
         | 
| 73 136 | 
             
                end
         | 
| 74 137 | 
             
              end
         | 
| 75 138 | 
             
            end
         | 
    
        data/lib/molinillo/modules/ui.rb
    CHANGED
    
    | @@ -1,4 +1,5 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 2 3 | 
             
            module Molinillo
         | 
| 3 4 | 
             
              # Conveys information about the resolution process to a user.
         | 
| 4 5 | 
             
              module UI
         | 
| @@ -48,7 +49,8 @@ module Molinillo | |
| 48 49 | 
             
                  if debug?
         | 
| 49 50 | 
             
                    debug_info = yield
         | 
| 50 51 | 
             
                    debug_info = debug_info.inspect unless debug_info.is_a?(String)
         | 
| 51 | 
            -
                     | 
| 52 | 
            +
                    debug_info = debug_info.split("\n").map { |s| ":#{depth.to_s.rjust 4}: #{s}" }
         | 
| 53 | 
            +
                    output.puts debug_info
         | 
| 52 54 | 
             
                  end
         | 
| 53 55 | 
             
                end
         | 
| 54 56 |  | 
    
        data/lib/molinillo/resolution.rb
    CHANGED
    
    | @@ -1,4 +1,5 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 2 3 | 
             
            module Molinillo
         | 
| 3 4 | 
             
              class Resolver
         | 
| 4 5 | 
             
                # A specific resolution from a given {Resolver}
         | 
| @@ -8,22 +9,125 @@ module Molinillo | |
| 8 9 | 
             
                  # @attr [{String,Nil=>[Object]}] requirements the requirements that caused the conflict
         | 
| 9 10 | 
             
                  # @attr [Object, nil] existing the existing spec that was in conflict with
         | 
| 10 11 | 
             
                  #   the {#possibility}
         | 
| 11 | 
            -
                  # @attr [Object]  | 
| 12 | 
            -
                  #   to a conflict
         | 
| 12 | 
            +
                  # @attr [Object] possibility_set the set of specs that was unable to be
         | 
| 13 | 
            +
                  #   activated due to a conflict.
         | 
| 13 14 | 
             
                  # @attr [Object] locked_requirement the relevant locking requirement.
         | 
| 14 15 | 
             
                  # @attr [Array<Array<Object>>] requirement_trees the different requirement
         | 
| 15 16 | 
             
                  #   trees that led to every requirement for the conflicting name.
         | 
| 16 17 | 
             
                  # @attr [{String=>Object}] activated_by_name the already-activated specs.
         | 
| 18 | 
            +
                  # @attr [Object] underlying_error an error that has occurred during resolution, and
         | 
| 19 | 
            +
                  #    will be raised at the end of it if no resolution is found.
         | 
| 17 20 | 
             
                  Conflict = Struct.new(
         | 
| 18 21 | 
             
                    :requirement,
         | 
| 19 22 | 
             
                    :requirements,
         | 
| 20 23 | 
             
                    :existing,
         | 
| 21 | 
            -
                    : | 
| 24 | 
            +
                    :possibility_set,
         | 
| 22 25 | 
             
                    :locked_requirement,
         | 
| 23 26 | 
             
                    :requirement_trees,
         | 
| 24 | 
            -
                    :activated_by_name
         | 
| 27 | 
            +
                    :activated_by_name,
         | 
| 28 | 
            +
                    :underlying_error
         | 
| 29 | 
            +
                  )
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  class Conflict
         | 
| 32 | 
            +
                    # @return [Object] a spec that was unable to be activated due to a conflict
         | 
| 33 | 
            +
                    def possibility
         | 
| 34 | 
            +
                      possibility_set && possibility_set.latest_version
         | 
| 35 | 
            +
                    end
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  # A collection of possibility states that share the same dependencies
         | 
| 39 | 
            +
                  # @attr [Array] dependencies the dependencies for this set of possibilities
         | 
| 40 | 
            +
                  # @attr [Array] possibilities the possibilities
         | 
| 41 | 
            +
                  PossibilitySet = Struct.new(:dependencies, :possibilities)
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  class PossibilitySet
         | 
| 44 | 
            +
                    # String representation of the possibility set, for debugging
         | 
| 45 | 
            +
                    def to_s
         | 
| 46 | 
            +
                      "[#{possibilities.join(', ')}]"
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    # @return [Object] most up-to-date dependency in the possibility set
         | 
| 50 | 
            +
                    def latest_version
         | 
| 51 | 
            +
                      possibilities.last
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  # Details of the state to unwind to when a conflict occurs, and the cause of the unwind
         | 
| 56 | 
            +
                  # @attr [Integer] state_index the index of the state to unwind to
         | 
| 57 | 
            +
                  # @attr [Object] state_requirement the requirement of the state we're unwinding to
         | 
| 58 | 
            +
                  # @attr [Array] requirement_tree for the requirement we're relaxing
         | 
| 59 | 
            +
                  # @attr [Array] conflicting_requirements the requirements that combined to cause the conflict
         | 
| 60 | 
            +
                  # @attr [Array] requirement_trees for the conflict
         | 
| 61 | 
            +
                  # @attr [Array] requirements_unwound_to_instead array of unwind requirements that were chosen over this unwind
         | 
| 62 | 
            +
                  UnwindDetails = Struct.new(
         | 
| 63 | 
            +
                    :state_index,
         | 
| 64 | 
            +
                    :state_requirement,
         | 
| 65 | 
            +
                    :requirement_tree,
         | 
| 66 | 
            +
                    :conflicting_requirements,
         | 
| 67 | 
            +
                    :requirement_trees,
         | 
| 68 | 
            +
                    :requirements_unwound_to_instead
         | 
| 25 69 | 
             
                  )
         | 
| 26 70 |  | 
| 71 | 
            +
                  class UnwindDetails
         | 
| 72 | 
            +
                    include Comparable
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                    # We compare UnwindDetails when choosing which state to unwind to. If
         | 
| 75 | 
            +
                    # two options have the same state_index we prefer the one most
         | 
| 76 | 
            +
                    # removed from a requirement that caused the conflict. Both options
         | 
| 77 | 
            +
                    # would unwind to the same state, but a `grandparent` option will
         | 
| 78 | 
            +
                    # filter out fewer of its possibilities after doing so - where a state
         | 
| 79 | 
            +
                    # is both a `parent` and a `grandparent` to requirements that have
         | 
| 80 | 
            +
                    # caused a conflict this is the correct behaviour.
         | 
| 81 | 
            +
                    # @param [UnwindDetail] other UnwindDetail to be compared
         | 
| 82 | 
            +
                    # @return [Integer] integer specifying ordering
         | 
| 83 | 
            +
                    def <=>(other)
         | 
| 84 | 
            +
                      if state_index > other.state_index
         | 
| 85 | 
            +
                        1
         | 
| 86 | 
            +
                      elsif state_index == other.state_index
         | 
| 87 | 
            +
                        reversed_requirement_tree_index <=> other.reversed_requirement_tree_index
         | 
| 88 | 
            +
                      else
         | 
| 89 | 
            +
                        -1
         | 
| 90 | 
            +
                      end
         | 
| 91 | 
            +
                    end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                    # @return [Integer] index of state requirement in reversed requirement tree
         | 
| 94 | 
            +
                    #    (the conflicting requirement itself will be at position 0)
         | 
| 95 | 
            +
                    def reversed_requirement_tree_index
         | 
| 96 | 
            +
                      @reversed_requirement_tree_index ||=
         | 
| 97 | 
            +
                        if state_requirement
         | 
| 98 | 
            +
                          requirement_tree.reverse.index(state_requirement)
         | 
| 99 | 
            +
                        else
         | 
| 100 | 
            +
                          999_999
         | 
| 101 | 
            +
                        end
         | 
| 102 | 
            +
                    end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                    # @return [Boolean] where the requirement of the state we're unwinding
         | 
| 105 | 
            +
                    #    to directly caused the conflict. Note: in this case, it is
         | 
| 106 | 
            +
                    #    impossible for the state we're unwinding to to be a parent of
         | 
| 107 | 
            +
                    #    any of the other conflicting requirements (or we would have
         | 
| 108 | 
            +
                    #    circularity)
         | 
| 109 | 
            +
                    def unwinding_to_primary_requirement?
         | 
| 110 | 
            +
                      requirement_tree.last == state_requirement
         | 
| 111 | 
            +
                    end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                    # @return [Array] array of sub-dependencies to avoid when choosing a
         | 
| 114 | 
            +
                    #    new possibility for the state we've unwound to. Only relevant for
         | 
| 115 | 
            +
                    #    non-primary unwinds
         | 
| 116 | 
            +
                    def sub_dependencies_to_avoid
         | 
| 117 | 
            +
                      @requirements_to_avoid ||=
         | 
| 118 | 
            +
                        requirement_trees.map do |tree|
         | 
| 119 | 
            +
                          index = tree.index(state_requirement)
         | 
| 120 | 
            +
                          tree[index + 1] if index
         | 
| 121 | 
            +
                        end.compact
         | 
| 122 | 
            +
                    end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                    # @return [Array] array of all the requirements that led to the need for
         | 
| 125 | 
            +
                    #    this unwind
         | 
| 126 | 
            +
                    def all_requirements
         | 
| 127 | 
            +
                      @all_requirements ||= requirement_trees.flatten(1)
         | 
| 128 | 
            +
                    end
         | 
| 129 | 
            +
                  end
         | 
| 130 | 
            +
             | 
| 27 131 | 
             
                  # @return [SpecificationProvider] the provider that knows about
         | 
| 28 132 | 
             
                  #   dependencies, requirements, specifications, versions, etc.
         | 
| 29 133 | 
             
                  attr_reader :specification_provider
         | 
| @@ -64,7 +168,7 @@ module Molinillo | |
| 64 168 | 
             
                    start_resolution
         | 
| 65 169 |  | 
| 66 170 | 
             
                    while state
         | 
| 67 | 
            -
                      break  | 
| 171 | 
            +
                      break if !state.requirement && state.requirements.empty?
         | 
| 68 172 | 
             
                      indicate_progress
         | 
| 69 173 | 
             
                      if state.respond_to?(:pop_possibility_state) # DependencyState
         | 
| 70 174 | 
             
                        debug(depth) { "Creating possibility state for #{requirement} (#{possibilities.count} remaining)" }
         | 
| @@ -78,7 +182,7 @@ module Molinillo | |
| 78 182 | 
             
                      process_topmost_state
         | 
| 79 183 | 
             
                    end
         | 
| 80 184 |  | 
| 81 | 
            -
                     | 
| 185 | 
            +
                    resolve_activated_specs
         | 
| 82 186 | 
             
                  ensure
         | 
| 83 187 | 
             
                    end_resolution
         | 
| 84 188 | 
             
                  end
         | 
| @@ -109,6 +213,19 @@ module Molinillo | |
| 109 213 | 
             
                    resolver_ui.before_resolution
         | 
| 110 214 | 
             
                  end
         | 
| 111 215 |  | 
| 216 | 
            +
                  def resolve_activated_specs
         | 
| 217 | 
            +
                    activated.vertices.each do |_, vertex|
         | 
| 218 | 
            +
                      next unless vertex.payload
         | 
| 219 | 
            +
             | 
| 220 | 
            +
                      latest_version = vertex.payload.possibilities.reverse_each.find do |possibility|
         | 
| 221 | 
            +
                        vertex.requirements.uniq.all? { |req| requirement_satisfied_by?(req, activated, possibility) }
         | 
| 222 | 
            +
                      end
         | 
| 223 | 
            +
             | 
| 224 | 
            +
                      activated.set_payload(vertex.name, latest_version)
         | 
| 225 | 
            +
                    end
         | 
| 226 | 
            +
                    activated.freeze
         | 
| 227 | 
            +
                  end
         | 
| 228 | 
            +
             | 
| 112 229 | 
             
                  # Ends the resolution process
         | 
| 113 230 | 
             
                  # @return [void]
         | 
| 114 231 | 
             
                  def end_resolution
         | 
| @@ -136,9 +253,12 @@ module Molinillo | |
| 136 253 | 
             
                    if possibility
         | 
| 137 254 | 
             
                      attempt_to_activate
         | 
| 138 255 | 
             
                    else
         | 
| 139 | 
            -
                      create_conflict | 
| 140 | 
            -
                      unwind_for_conflict | 
| 256 | 
            +
                      create_conflict
         | 
| 257 | 
            +
                      unwind_for_conflict
         | 
| 141 258 | 
             
                    end
         | 
| 259 | 
            +
                  rescue CircularDependencyError => underlying_error
         | 
| 260 | 
            +
                    create_conflict(underlying_error)
         | 
| 261 | 
            +
                    unwind_for_conflict
         | 
| 142 262 | 
             
                  end
         | 
| 143 263 |  | 
| 144 264 | 
             
                  # @return [Object] the current possibility that the resolution is trying
         | 
| @@ -158,7 +278,10 @@ module Molinillo | |
| 158 278 | 
             
                  # @return [DependencyState] the initial state for the resolution
         | 
| 159 279 | 
             
                  def initial_state
         | 
| 160 280 | 
             
                    graph = DependencyGraph.new.tap do |dg|
         | 
| 161 | 
            -
                      original_requested.each  | 
| 281 | 
            +
                      original_requested.each do |requested|
         | 
| 282 | 
            +
                        vertex = dg.add_vertex(name_for(requested), nil, true)
         | 
| 283 | 
            +
                        vertex.explicit_requirements << requested
         | 
| 284 | 
            +
                      end
         | 
| 162 285 | 
             
                      dg.tag(:initial_state)
         | 
| 163 286 | 
             
                    end
         | 
| 164 287 |  | 
| @@ -169,45 +292,276 @@ module Molinillo | |
| 169 292 | 
             
                      requirements,
         | 
| 170 293 | 
             
                      graph,
         | 
| 171 294 | 
             
                      initial_requirement,
         | 
| 172 | 
            -
                      initial_requirement  | 
| 295 | 
            +
                      possibilities_for_requirement(initial_requirement, graph),
         | 
| 173 296 | 
             
                      0,
         | 
| 174 | 
            -
                      {}
         | 
| 297 | 
            +
                      {},
         | 
| 298 | 
            +
                      []
         | 
| 175 299 | 
             
                    )
         | 
| 176 300 | 
             
                  end
         | 
| 177 301 |  | 
| 178 302 | 
             
                  # Unwinds the states stack because a conflict has been encountered
         | 
| 179 303 | 
             
                  # @return [void]
         | 
| 180 304 | 
             
                  def unwind_for_conflict
         | 
| 181 | 
            -
                     | 
| 305 | 
            +
                    details_for_unwind = build_details_for_unwind
         | 
| 306 | 
            +
                    unwind_options = unused_unwind_options
         | 
| 307 | 
            +
                    debug(depth) { "Unwinding for conflict: #{requirement} to #{details_for_unwind.state_index / 2}" }
         | 
| 182 308 | 
             
                    conflicts.tap do |c|
         | 
| 183 | 
            -
                      sliced_states = states.slice!(( | 
| 184 | 
            -
                       | 
| 309 | 
            +
                      sliced_states = states.slice!((details_for_unwind.state_index + 1)..-1)
         | 
| 310 | 
            +
                      raise_error_unless_state(c)
         | 
| 185 311 | 
             
                      activated.rewind_to(sliced_states.first || :initial_state) if sliced_states
         | 
| 186 312 | 
             
                      state.conflicts = c
         | 
| 313 | 
            +
                      state.unused_unwind_options = unwind_options
         | 
| 314 | 
            +
                      filter_possibilities_after_unwind(details_for_unwind)
         | 
| 187 315 | 
             
                      index = states.size - 1
         | 
| 188 316 | 
             
                      @parents_of.each { |_, a| a.reject! { |i| i >= index } }
         | 
| 317 | 
            +
                      state.unused_unwind_options.reject! { |uw| uw.state_index >= index }
         | 
| 189 318 | 
             
                    end
         | 
| 190 319 | 
             
                  end
         | 
| 191 320 |  | 
| 192 | 
            -
                  #  | 
| 193 | 
            -
                  # | 
| 194 | 
            -
                   | 
| 195 | 
            -
             | 
| 196 | 
            -
                     | 
| 197 | 
            -
             | 
| 198 | 
            -
                     | 
| 199 | 
            -
             | 
| 200 | 
            -
             | 
| 201 | 
            -
             | 
| 202 | 
            -
             | 
| 203 | 
            -
             | 
| 204 | 
            -
             | 
| 321 | 
            +
                  # Raises a VersionConflict error, or any underlying error, if there is no
         | 
| 322 | 
            +
                  # current state
         | 
| 323 | 
            +
                  # @return [void]
         | 
| 324 | 
            +
                  def raise_error_unless_state(conflicts)
         | 
| 325 | 
            +
                    return if state
         | 
| 326 | 
            +
             | 
| 327 | 
            +
                    error = conflicts.values.map(&:underlying_error).compact.first
         | 
| 328 | 
            +
                    raise error || VersionConflict.new(conflicts, specification_provider)
         | 
| 329 | 
            +
                  end
         | 
| 330 | 
            +
             | 
| 331 | 
            +
                  # @return [UnwindDetails] Details of the nearest index to which we could unwind
         | 
| 332 | 
            +
                  def build_details_for_unwind
         | 
| 333 | 
            +
                    # Get the possible unwinds for the current conflict
         | 
| 334 | 
            +
                    current_conflict = conflicts[name]
         | 
| 335 | 
            +
                    binding_requirements = binding_requirements_for_conflict(current_conflict)
         | 
| 336 | 
            +
                    unwind_details = unwind_options_for_requirements(binding_requirements)
         | 
| 337 | 
            +
             | 
| 338 | 
            +
                    last_detail_for_current_unwind = unwind_details.sort.last
         | 
| 339 | 
            +
                    current_detail = last_detail_for_current_unwind
         | 
| 340 | 
            +
             | 
| 341 | 
            +
                    # Look for past conflicts that could be unwound to affect the
         | 
| 342 | 
            +
                    # requirement tree for the current conflict
         | 
| 343 | 
            +
                    relevant_unused_unwinds = unused_unwind_options.select do |alternative|
         | 
| 344 | 
            +
                      intersecting_requirements =
         | 
| 345 | 
            +
                        last_detail_for_current_unwind.all_requirements &
         | 
| 346 | 
            +
                        alternative.requirements_unwound_to_instead
         | 
| 347 | 
            +
                      next if intersecting_requirements.empty?
         | 
| 348 | 
            +
                      # Find the highest index unwind whilst looping through
         | 
| 349 | 
            +
                      current_detail = alternative if alternative > current_detail
         | 
| 350 | 
            +
                      alternative
         | 
| 351 | 
            +
                    end
         | 
| 352 | 
            +
             | 
| 353 | 
            +
                    # Add the current unwind options to the `unused_unwind_options` array.
         | 
| 354 | 
            +
                    # The "used" option will be filtered out during `unwind_for_conflict`.
         | 
| 355 | 
            +
                    state.unused_unwind_options += unwind_details.reject { |detail| detail.state_index == -1 }
         | 
| 356 | 
            +
             | 
| 357 | 
            +
                    # Update the requirements_unwound_to_instead on any relevant unused unwinds
         | 
| 358 | 
            +
                    relevant_unused_unwinds.each { |d| d.requirements_unwound_to_instead << current_detail.state_requirement }
         | 
| 359 | 
            +
                    unwind_details.each { |d| d.requirements_unwound_to_instead << current_detail.state_requirement }
         | 
| 360 | 
            +
             | 
| 361 | 
            +
                    current_detail
         | 
| 362 | 
            +
                  end
         | 
| 363 | 
            +
             | 
| 364 | 
            +
                  # @param [Array<Object>] array of requirements that combine to create a conflict
         | 
| 365 | 
            +
                  # @return [Array<UnwindDetails>] array of UnwindDetails that have a chance
         | 
| 366 | 
            +
                  #    of resolving the passed requirements
         | 
| 367 | 
            +
                  def unwind_options_for_requirements(binding_requirements)
         | 
| 368 | 
            +
                    unwind_details = []
         | 
| 369 | 
            +
             | 
| 370 | 
            +
                    trees = []
         | 
| 371 | 
            +
                    binding_requirements.reverse_each do |r|
         | 
| 372 | 
            +
                      partial_tree = [r]
         | 
| 373 | 
            +
                      trees << partial_tree
         | 
| 374 | 
            +
                      unwind_details << UnwindDetails.new(-1, nil, partial_tree, binding_requirements, trees, [])
         | 
| 375 | 
            +
             | 
| 376 | 
            +
                      # If this requirement has alternative possibilities, check if any would
         | 
| 377 | 
            +
                      # satisfy the other requirements that created this conflict
         | 
| 378 | 
            +
                      requirement_state = find_state_for(r)
         | 
| 379 | 
            +
                      if conflict_fixing_possibilities?(requirement_state, binding_requirements)
         | 
| 380 | 
            +
                        unwind_details << UnwindDetails.new(
         | 
| 381 | 
            +
                          states.index(requirement_state),
         | 
| 382 | 
            +
                          r,
         | 
| 383 | 
            +
                          partial_tree,
         | 
| 384 | 
            +
                          binding_requirements,
         | 
| 385 | 
            +
                          trees,
         | 
| 386 | 
            +
                          []
         | 
| 387 | 
            +
                        )
         | 
| 388 | 
            +
                      end
         | 
| 389 | 
            +
             | 
| 390 | 
            +
                      # Next, look at the parent of this requirement, and check if the requirement
         | 
| 391 | 
            +
                      # could have been avoided if an alternative PossibilitySet had been chosen
         | 
| 392 | 
            +
                      parent_r = parent_of(r)
         | 
| 393 | 
            +
                      next if parent_r.nil?
         | 
| 394 | 
            +
                      partial_tree.unshift(parent_r)
         | 
| 395 | 
            +
                      requirement_state = find_state_for(parent_r)
         | 
| 396 | 
            +
                      if requirement_state.possibilities.any? { |set| !set.dependencies.include?(r) }
         | 
| 397 | 
            +
                        unwind_details << UnwindDetails.new(
         | 
| 398 | 
            +
                          states.index(requirement_state),
         | 
| 399 | 
            +
                          parent_r,
         | 
| 400 | 
            +
                          partial_tree,
         | 
| 401 | 
            +
                          binding_requirements,
         | 
| 402 | 
            +
                          trees,
         | 
| 403 | 
            +
                          []
         | 
| 404 | 
            +
                        )
         | 
| 405 | 
            +
                      end
         | 
| 406 | 
            +
             | 
| 407 | 
            +
                      # Finally, look at the grandparent and up of this requirement, looking
         | 
| 408 | 
            +
                      # for any possibilities that wouldn't create their parent requirement
         | 
| 409 | 
            +
                      grandparent_r = parent_of(parent_r)
         | 
| 410 | 
            +
                      until grandparent_r.nil?
         | 
| 411 | 
            +
                        partial_tree.unshift(grandparent_r)
         | 
| 412 | 
            +
                        requirement_state = find_state_for(grandparent_r)
         | 
| 413 | 
            +
                        if requirement_state.possibilities.any? { |set| !set.dependencies.include?(parent_r) }
         | 
| 414 | 
            +
                          unwind_details << UnwindDetails.new(
         | 
| 415 | 
            +
                            states.index(requirement_state),
         | 
| 416 | 
            +
                            grandparent_r,
         | 
| 417 | 
            +
                            partial_tree,
         | 
| 418 | 
            +
                            binding_requirements,
         | 
| 419 | 
            +
                            trees,
         | 
| 420 | 
            +
                            []
         | 
| 421 | 
            +
                          )
         | 
| 205 422 | 
             
                        end
         | 
| 206 | 
            -
                         | 
| 423 | 
            +
                        parent_r = grandparent_r
         | 
| 424 | 
            +
                        grandparent_r = parent_of(parent_r)
         | 
| 425 | 
            +
                      end
         | 
| 426 | 
            +
                    end
         | 
| 427 | 
            +
             | 
| 428 | 
            +
                    unwind_details
         | 
| 429 | 
            +
                  end
         | 
| 430 | 
            +
             | 
| 431 | 
            +
                  # @param [DependencyState] state
         | 
| 432 | 
            +
                  # @param [Array] array of requirements
         | 
| 433 | 
            +
                  # @return [Boolean] whether or not the given state has any possibilities
         | 
| 434 | 
            +
                  #    that could satisfy the given requirements
         | 
| 435 | 
            +
                  def conflict_fixing_possibilities?(state, binding_requirements)
         | 
| 436 | 
            +
                    return false unless state
         | 
| 437 | 
            +
             | 
| 438 | 
            +
                    state.possibilities.any? do |possibility_set|
         | 
| 439 | 
            +
                      possibility_set.possibilities.any? do |poss|
         | 
| 440 | 
            +
                        possibility_satisfies_requirements?(poss, binding_requirements)
         | 
| 207 441 | 
             
                      end
         | 
| 208 442 | 
             
                    end
         | 
| 443 | 
            +
                  end
         | 
| 444 | 
            +
             | 
| 445 | 
            +
                  # Filter's a state's possibilities to remove any that would not fix the
         | 
| 446 | 
            +
                  # conflict we've just rewound from
         | 
| 447 | 
            +
                  # @param [UnwindDetails] details of the conflict just unwound from
         | 
| 448 | 
            +
                  # @return [void]
         | 
| 449 | 
            +
                  def filter_possibilities_after_unwind(unwind_details)
         | 
| 450 | 
            +
                    return unless state && !state.possibilities.empty?
         | 
| 451 | 
            +
             | 
| 452 | 
            +
                    if unwind_details.unwinding_to_primary_requirement?
         | 
| 453 | 
            +
                      filter_possibilities_for_primary_unwind(unwind_details)
         | 
| 454 | 
            +
                    else
         | 
| 455 | 
            +
                      filter_possibilities_for_parent_unwind(unwind_details)
         | 
| 456 | 
            +
                    end
         | 
| 457 | 
            +
                  end
         | 
| 458 | 
            +
             | 
| 459 | 
            +
                  # Filter's a state's possibilities to remove any that would not satisfy
         | 
| 460 | 
            +
                  # the requirements in the conflict we've just rewound from
         | 
| 461 | 
            +
                  # @param [UnwindDetails] details of the conflict just unwound from
         | 
| 462 | 
            +
                  # @return [void]
         | 
| 463 | 
            +
                  def filter_possibilities_for_primary_unwind(unwind_details)
         | 
| 464 | 
            +
                    all_requirements = unwind_details.conflicting_requirements
         | 
| 209 465 |  | 
| 210 | 
            -
                     | 
| 466 | 
            +
                    state.possibilities.reject! do |possibility_set|
         | 
| 467 | 
            +
                      possibility_set.possibilities.none? do |poss|
         | 
| 468 | 
            +
                        possibility_satisfies_requirements?(poss, all_requirements)
         | 
| 469 | 
            +
                      end
         | 
| 470 | 
            +
                    end
         | 
| 471 | 
            +
                  end
         | 
| 472 | 
            +
             | 
| 473 | 
            +
                  # @param [Object] possibility a single possibility
         | 
| 474 | 
            +
                  # @param [Array] requirements an array of requirements
         | 
| 475 | 
            +
                  # @return [Boolean] whether the possibility satisfies all of the
         | 
| 476 | 
            +
                  #    given requirements
         | 
| 477 | 
            +
                  def possibility_satisfies_requirements?(possibility, requirements)
         | 
| 478 | 
            +
                    name = name_for(possibility)
         | 
| 479 | 
            +
             | 
| 480 | 
            +
                    activated.tag(:swap)
         | 
| 481 | 
            +
                    activated.set_payload(name, possibility) if activated.vertex_named(name)
         | 
| 482 | 
            +
                    satisfied = requirements.all? { |r| requirement_satisfied_by?(r, activated, possibility) }
         | 
| 483 | 
            +
                    activated.rewind_to(:swap)
         | 
| 484 | 
            +
             | 
| 485 | 
            +
                    satisfied
         | 
| 486 | 
            +
                  end
         | 
| 487 | 
            +
             | 
| 488 | 
            +
                  # Filter's a state's possibilities to remove any that would (eventually)
         | 
| 489 | 
            +
                  # create a requirement in the conflict we've just rewound from
         | 
| 490 | 
            +
                  # @param [UnwindDetails] details of the conflict just unwound from
         | 
| 491 | 
            +
                  # @return [void]
         | 
| 492 | 
            +
                  def filter_possibilities_for_parent_unwind(unwind_details)
         | 
| 493 | 
            +
                    unwinds_to_state = unused_unwind_options.select { |uw| uw.state_index == unwind_details.state_index }
         | 
| 494 | 
            +
                    unwinds_to_state << unwind_details
         | 
| 495 | 
            +
             | 
| 496 | 
            +
                    primary_unwinds = unwinds_to_state.select(&:unwinding_to_primary_requirement?).uniq
         | 
| 497 | 
            +
                    parent_unwinds = unwinds_to_state.uniq - primary_unwinds
         | 
| 498 | 
            +
             | 
| 499 | 
            +
                    allowed_possibility_sets = Compatibility.flat_map(primary_unwinds) do |unwind|
         | 
| 500 | 
            +
                      states[unwind.state_index].possibilities.select do |possibility_set|
         | 
| 501 | 
            +
                        possibility_set.possibilities.any? do |poss|
         | 
| 502 | 
            +
                          possibility_satisfies_requirements?(poss, unwind.conflicting_requirements)
         | 
| 503 | 
            +
                        end
         | 
| 504 | 
            +
                      end
         | 
| 505 | 
            +
                    end
         | 
| 506 | 
            +
             | 
| 507 | 
            +
                    requirements_to_avoid = Compatibility.flat_map(parent_unwinds, &:sub_dependencies_to_avoid)
         | 
| 508 | 
            +
             | 
| 509 | 
            +
                    state.possibilities.reject! do |possibility_set|
         | 
| 510 | 
            +
                      !allowed_possibility_sets.include?(possibility_set) &&
         | 
| 511 | 
            +
                        (requirements_to_avoid - possibility_set.dependencies).empty?
         | 
| 512 | 
            +
                    end
         | 
| 513 | 
            +
                  end
         | 
| 514 | 
            +
             | 
| 515 | 
            +
                  # @param [Conflict] conflict
         | 
| 516 | 
            +
                  # @return [Array] minimal array of requirements that would cause the passed
         | 
| 517 | 
            +
                  #    conflict to occur.
         | 
| 518 | 
            +
                  def binding_requirements_for_conflict(conflict)
         | 
| 519 | 
            +
                    return [conflict.requirement] if conflict.possibility.nil?
         | 
| 520 | 
            +
             | 
| 521 | 
            +
                    possible_binding_requirements = conflict.requirements.values.flatten(1).uniq
         | 
| 522 | 
            +
             | 
| 523 | 
            +
                    # When there’s a `CircularDependency` error the conflicting requirement
         | 
| 524 | 
            +
                    # (the one causing the circular) won’t be `conflict.requirement`
         | 
| 525 | 
            +
                    # (which won’t be for the right state, because we won’t have created it,
         | 
| 526 | 
            +
                    # because it’s circular).
         | 
| 527 | 
            +
                    # We need to make sure we have that requirement in the conflict’s list,
         | 
| 528 | 
            +
                    # otherwise we won’t be able to unwind properly, so we just return all
         | 
| 529 | 
            +
                    # the requirements for the conflict.
         | 
| 530 | 
            +
                    return possible_binding_requirements if conflict.underlying_error
         | 
| 531 | 
            +
             | 
| 532 | 
            +
                    possibilities = search_for(conflict.requirement)
         | 
| 533 | 
            +
             | 
| 534 | 
            +
                    # If all the requirements together don't filter out all possibilities,
         | 
| 535 | 
            +
                    # then the only two requirements we need to consider are the initial one
         | 
| 536 | 
            +
                    # (where the dependency's version was first chosen) and the last
         | 
| 537 | 
            +
                    if binding_requirement_in_set?(nil, possible_binding_requirements, possibilities)
         | 
| 538 | 
            +
                      return [conflict.requirement, requirement_for_existing_name(name_for(conflict.requirement))].compact
         | 
| 539 | 
            +
                    end
         | 
| 540 | 
            +
             | 
| 541 | 
            +
                    # Loop through the possible binding requirements, removing each one
         | 
| 542 | 
            +
                    # that doesn't bind. Use a `reverse_each` as we want the earliest set of
         | 
| 543 | 
            +
                    # binding requirements, and don't use `reject!` as we wish to refine the
         | 
| 544 | 
            +
                    # array *on each iteration*.
         | 
| 545 | 
            +
                    binding_requirements = possible_binding_requirements.dup
         | 
| 546 | 
            +
                    possible_binding_requirements.reverse_each do |req|
         | 
| 547 | 
            +
                      next if req == conflict.requirement
         | 
| 548 | 
            +
                      unless binding_requirement_in_set?(req, binding_requirements, possibilities)
         | 
| 549 | 
            +
                        binding_requirements -= [req]
         | 
| 550 | 
            +
                      end
         | 
| 551 | 
            +
                    end
         | 
| 552 | 
            +
             | 
| 553 | 
            +
                    binding_requirements
         | 
| 554 | 
            +
                  end
         | 
| 555 | 
            +
             | 
| 556 | 
            +
                  # @param [Object] requirement we wish to check
         | 
| 557 | 
            +
                  # @param [Array] array of requirements
         | 
| 558 | 
            +
                  # @param [Array] array of possibilities the requirements will be used to filter
         | 
| 559 | 
            +
                  # @return [Boolean] whether or not the given requirement is required to filter
         | 
| 560 | 
            +
                  #    out all elements of the array of possibilities.
         | 
| 561 | 
            +
                  def binding_requirement_in_set?(requirement, possible_binding_requirements, possibilities)
         | 
| 562 | 
            +
                    possibilities.any? do |poss|
         | 
| 563 | 
            +
                      possibility_satisfies_requirements?(poss, possible_binding_requirements - [requirement])
         | 
| 564 | 
            +
                    end
         | 
| 211 565 | 
             
                  end
         | 
| 212 566 |  | 
| 213 567 | 
             
                  # @return [Object] the requirement that led to `requirement` being added
         | 
| @@ -222,7 +576,8 @@ module Molinillo | |
| 222 576 | 
             
                  # @return [Object] the requirement that led to a version of a possibility
         | 
| 223 577 | 
             
                  #   with the given name being activated.
         | 
| 224 578 | 
             
                  def requirement_for_existing_name(name)
         | 
| 225 | 
            -
                    return nil unless activated.vertex_named(name) | 
| 579 | 
            +
                    return nil unless vertex = activated.vertex_named(name)
         | 
| 580 | 
            +
                    return nil unless vertex.payload
         | 
| 226 581 | 
             
                    states.find { |s| s.name == name }.requirement
         | 
| 227 582 | 
             
                  end
         | 
| 228 583 |  | 
| @@ -230,18 +585,12 @@ module Molinillo | |
| 230 585 | 
             
                  #   `requirement`.
         | 
| 231 586 | 
             
                  def find_state_for(requirement)
         | 
| 232 587 | 
             
                    return nil unless requirement
         | 
| 233 | 
            -
                    states. | 
| 234 | 
            -
                  end
         | 
| 235 | 
            -
             | 
| 236 | 
            -
                  # @return [Boolean] whether or not the given state has any possibilities
         | 
| 237 | 
            -
                  #   left.
         | 
| 238 | 
            -
                  def state_any?(state)
         | 
| 239 | 
            -
                    state && state.possibilities.any?
         | 
| 588 | 
            +
                    states.find { |i| requirement == i.requirement }
         | 
| 240 589 | 
             
                  end
         | 
| 241 590 |  | 
| 242 591 | 
             
                  # @return [Conflict] a {Conflict} that reflects the failure to activate
         | 
| 243 592 | 
             
                  #   the {#possibility} in conjunction with the current {#state}
         | 
| 244 | 
            -
                  def create_conflict
         | 
| 593 | 
            +
                  def create_conflict(underlying_error = nil)
         | 
| 245 594 | 
             
                    vertex = activated.vertex_named(name)
         | 
| 246 595 | 
             
                    locked_requirement = locked_requirement_named(name)
         | 
| 247 596 |  | 
| @@ -250,18 +599,21 @@ module Molinillo | |
| 250 599 | 
             
                      requirements[name_for_explicit_dependency_source] = vertex.explicit_requirements
         | 
| 251 600 | 
             
                    end
         | 
| 252 601 | 
             
                    requirements[name_for_locking_dependency_source] = [locked_requirement] if locked_requirement
         | 
| 253 | 
            -
                    vertex.incoming_edges.each  | 
| 602 | 
            +
                    vertex.incoming_edges.each do |edge|
         | 
| 603 | 
            +
                      (requirements[edge.origin.payload.latest_version] ||= []).unshift(edge.requirement)
         | 
| 604 | 
            +
                    end
         | 
| 254 605 |  | 
| 255 606 | 
             
                    activated_by_name = {}
         | 
| 256 | 
            -
                    activated.each { |v| activated_by_name[v.name] = v.payload if v.payload }
         | 
| 607 | 
            +
                    activated.each { |v| activated_by_name[v.name] = v.payload.latest_version if v.payload }
         | 
| 257 608 | 
             
                    conflicts[name] = Conflict.new(
         | 
| 258 609 | 
             
                      requirement,
         | 
| 259 610 | 
             
                      requirements,
         | 
| 260 | 
            -
                      vertex.payload,
         | 
| 611 | 
            +
                      vertex.payload && vertex.payload.latest_version,
         | 
| 261 612 | 
             
                      possibility,
         | 
| 262 613 | 
             
                      locked_requirement,
         | 
| 263 614 | 
             
                      requirement_trees,
         | 
| 264 | 
            -
                      activated_by_name
         | 
| 615 | 
            +
                      activated_by_name,
         | 
| 616 | 
            +
                      underlying_error
         | 
| 265 617 | 
             
                    )
         | 
| 266 618 | 
             
                  end
         | 
| 267 619 |  | 
| @@ -311,116 +663,60 @@ module Molinillo | |
| 311 663 | 
             
                  # @return [void]
         | 
| 312 664 | 
             
                  def attempt_to_activate
         | 
| 313 665 | 
             
                    debug(depth) { 'Attempting to activate ' + possibility.to_s }
         | 
| 314 | 
            -
                     | 
| 315 | 
            -
                    if  | 
| 316 | 
            -
                      debug(depth) { "Found existing spec (#{ | 
| 317 | 
            -
                       | 
| 666 | 
            +
                    existing_vertex = activated.vertex_named(name)
         | 
| 667 | 
            +
                    if existing_vertex.payload
         | 
| 668 | 
            +
                      debug(depth) { "Found existing spec (#{existing_vertex.payload})" }
         | 
| 669 | 
            +
                      attempt_to_filter_existing_spec(existing_vertex)
         | 
| 318 670 | 
             
                    else
         | 
| 319 | 
            -
                       | 
| 671 | 
            +
                      latest = possibility.latest_version
         | 
| 672 | 
            +
                      # use reject!(!satisfied) for 1.8.7 compatibility
         | 
| 673 | 
            +
                      possibility.possibilities.reject! do |possibility|
         | 
| 674 | 
            +
                        !requirement_satisfied_by?(requirement, activated, possibility)
         | 
| 675 | 
            +
                      end
         | 
| 676 | 
            +
                      if possibility.latest_version.nil?
         | 
| 677 | 
            +
                        # ensure there's a possibility for better error messages
         | 
| 678 | 
            +
                        possibility.possibilities << latest if latest
         | 
| 679 | 
            +
                        create_conflict
         | 
| 680 | 
            +
                        unwind_for_conflict
         | 
| 681 | 
            +
                      else
         | 
| 682 | 
            +
                        activate_new_spec
         | 
| 683 | 
            +
                      end
         | 
| 320 684 | 
             
                    end
         | 
| 321 685 | 
             
                  end
         | 
| 322 686 |  | 
| 323 | 
            -
                  # Attempts to  | 
| 324 | 
            -
                  # already been activated)
         | 
| 687 | 
            +
                  # Attempts to update the existing vertex's `PossibilitySet` with a filtered version
         | 
| 325 688 | 
             
                  # @return [void]
         | 
| 326 | 
            -
                  def  | 
| 327 | 
            -
                     | 
| 328 | 
            -
                    if  | 
| 689 | 
            +
                  def attempt_to_filter_existing_spec(vertex)
         | 
| 690 | 
            +
                    filtered_set = filtered_possibility_set(vertex)
         | 
| 691 | 
            +
                    if !filtered_set.possibilities.empty? &&
         | 
| 692 | 
            +
                        (vertex.payload.dependencies == dependencies_for(possibility.latest_version))
         | 
| 693 | 
            +
                      activated.set_payload(name, filtered_set)
         | 
| 329 694 | 
             
                      new_requirements = requirements.dup
         | 
| 330 695 | 
             
                      push_state_for_requirements(new_requirements, false)
         | 
| 331 696 | 
             
                    else
         | 
| 332 | 
            -
                      return if attempt_to_swap_possibility
         | 
| 333 697 | 
             
                      create_conflict
         | 
| 334 | 
            -
                      debug(depth) { "Unsatisfied by existing spec (#{ | 
| 698 | 
            +
                      debug(depth) { "Unsatisfied by existing spec (#{vertex.payload})" }
         | 
| 335 699 | 
             
                      unwind_for_conflict
         | 
| 336 700 | 
             
                    end
         | 
| 337 701 | 
             
                  end
         | 
| 338 702 |  | 
| 339 | 
            -
                  #  | 
| 340 | 
            -
                  #  | 
| 341 | 
            -
                  # @ | 
| 342 | 
            -
                   | 
| 343 | 
            -
             | 
| 344 | 
            -
                     | 
| 345 | 
            -
                     | 
| 346 | 
            -
                     | 
| 347 | 
            -
             | 
| 348 | 
            -
             | 
| 349 | 
            -
                      activated | 
| 350 | 
            -
                      return
         | 
| 703 | 
            +
                  # Generates a filtered version of the existing vertex's `PossibilitySet` using the
         | 
| 704 | 
            +
                  # current state's `requirement`
         | 
| 705 | 
            +
                  # @param [Object] existing vertex
         | 
| 706 | 
            +
                  # @return [PossibilitySet] filtered possibility set
         | 
| 707 | 
            +
                  def filtered_possibility_set(vertex)
         | 
| 708 | 
            +
                    # Note: we can't just look at the intersection of `vertex.payload.possibilities`
         | 
| 709 | 
            +
                    # and `possibility.possibilities`, because if one of our requirements contains
         | 
| 710 | 
            +
                    # a prerelease version the associated prerelease versions will only appear in
         | 
| 711 | 
            +
                    # one set (but may match all requirements)
         | 
| 712 | 
            +
                    filtered_old_values = vertex.payload.possibilities.select do |poss|
         | 
| 713 | 
            +
                      requirement_satisfied_by?(requirement, activated, poss)
         | 
| 351 714 | 
             
                    end
         | 
| 352 | 
            -
                     | 
| 353 | 
            -
             | 
| 354 | 
            -
                  end
         | 
| 355 | 
            -
             | 
| 356 | 
            -
                  # Ensures there are no orphaned successors to the given {vertex}.
         | 
| 357 | 
            -
                  # @param [DependencyGraph::Vertex] vertex the vertex to fix up.
         | 
| 358 | 
            -
                  # @return [void]
         | 
| 359 | 
            -
                  def fixup_swapped_children(vertex) # rubocop:disable Metrics/CyclomaticComplexity
         | 
| 360 | 
            -
                    payload = vertex.payload
         | 
| 361 | 
            -
                    deps = dependencies_for(payload).group_by(&method(:name_for))
         | 
| 362 | 
            -
                    vertex.outgoing_edges.each do |outgoing_edge|
         | 
| 363 | 
            -
                      requirement = outgoing_edge.requirement
         | 
| 364 | 
            -
                      parent_index = @parents_of[requirement].last
         | 
| 365 | 
            -
                      succ = outgoing_edge.destination
         | 
| 366 | 
            -
                      matching_deps = Array(deps[succ.name])
         | 
| 367 | 
            -
                      dep_matched = matching_deps.include?(requirement)
         | 
| 368 | 
            -
             | 
| 369 | 
            -
                      # only push the current index when it was originally required by the
         | 
| 370 | 
            -
                      # same named spec
         | 
| 371 | 
            -
                      if parent_index && states[parent_index].name == name
         | 
| 372 | 
            -
                        @parents_of[requirement].push(states.size - 1)
         | 
| 373 | 
            -
                      end
         | 
| 374 | 
            -
             | 
| 375 | 
            -
                      if matching_deps.empty? && !succ.root? && succ.predecessors.to_a == [vertex]
         | 
| 376 | 
            -
                        debug(depth) { "Removing orphaned spec #{succ.name} after swapping #{name}" }
         | 
| 377 | 
            -
                        succ.requirements.each { |r| @parents_of.delete(r) }
         | 
| 378 | 
            -
             | 
| 379 | 
            -
                        removed_names = activated.detach_vertex_named(succ.name).map(&:name)
         | 
| 380 | 
            -
                        requirements.delete_if do |r|
         | 
| 381 | 
            -
                          # the only removed vertices are those with no other requirements,
         | 
| 382 | 
            -
                          # so it's safe to delete only based upon name here
         | 
| 383 | 
            -
                          removed_names.include?(name_for(r))
         | 
| 384 | 
            -
                        end
         | 
| 385 | 
            -
                      elsif !dep_matched
         | 
| 386 | 
            -
                        debug(depth) { "Removing orphaned dependency #{requirement} after swapping #{name}" }
         | 
| 387 | 
            -
                        # also reset if we're removing the edge, but only if its parent has
         | 
| 388 | 
            -
                        # already been fixed up
         | 
| 389 | 
            -
                        @parents_of[requirement].push(states.size - 1) if @parents_of[requirement].empty?
         | 
| 390 | 
            -
             | 
| 391 | 
            -
                        activated.delete_edge(outgoing_edge)
         | 
| 392 | 
            -
                        requirements.delete(requirement)
         | 
| 393 | 
            -
                      end
         | 
| 715 | 
            +
                    filtered_new_values = possibility.possibilities.select do |poss|
         | 
| 716 | 
            +
                      vertex.requirements.uniq.all? { |req| requirement_satisfied_by?(req, activated, poss) }
         | 
| 394 717 | 
             
                    end
         | 
| 395 | 
            -
                  end
         | 
| 396 718 |  | 
| 397 | 
            -
             | 
| 398 | 
            -
                  # already been activated)
         | 
| 399 | 
            -
                  # @return [void]
         | 
| 400 | 
            -
                  def attempt_to_activate_new_spec
         | 
| 401 | 
            -
                    if new_spec_satisfied?
         | 
| 402 | 
            -
                      activate_spec
         | 
| 403 | 
            -
                    else
         | 
| 404 | 
            -
                      create_conflict
         | 
| 405 | 
            -
                      unwind_for_conflict
         | 
| 406 | 
            -
                    end
         | 
| 407 | 
            -
                  end
         | 
| 408 | 
            -
             | 
| 409 | 
            -
                  # @return [Boolean] whether the current spec is satisfied as a new
         | 
| 410 | 
            -
                  # possibility.
         | 
| 411 | 
            -
                  def new_spec_satisfied?
         | 
| 412 | 
            -
                    unless requirement_satisfied_by?(requirement, activated, possibility)
         | 
| 413 | 
            -
                      debug(depth) { 'Unsatisfied by requested spec' }
         | 
| 414 | 
            -
                      return false
         | 
| 415 | 
            -
                    end
         | 
| 416 | 
            -
             | 
| 417 | 
            -
                    locked_requirement = locked_requirement_named(name)
         | 
| 418 | 
            -
             | 
| 419 | 
            -
                    locked_spec_satisfied = !locked_requirement ||
         | 
| 420 | 
            -
                      requirement_satisfied_by?(locked_requirement, activated, possibility)
         | 
| 421 | 
            -
                    debug(depth) { 'Unsatisfied by locked spec' } unless locked_spec_satisfied
         | 
| 422 | 
            -
             | 
| 423 | 
            -
                    locked_spec_satisfied
         | 
| 719 | 
            +
                    PossibilitySet.new(vertex.payload.dependencies, filtered_old_values | filtered_new_values)
         | 
| 424 720 | 
             
                  end
         | 
| 425 721 |  | 
| 426 722 | 
             
                  # @param [String] requirement_name the spec name to search for
         | 
| @@ -434,7 +730,7 @@ module Molinillo | |
| 434 730 | 
             
                  # Add the current {#possibility} to the dependency graph of the current
         | 
| 435 731 | 
             
                  # {#state}
         | 
| 436 732 | 
             
                  # @return [void]
         | 
| 437 | 
            -
                  def  | 
| 733 | 
            +
                  def activate_new_spec
         | 
| 438 734 | 
             
                    conflicts.delete(name)
         | 
| 439 735 | 
             
                    debug(depth) { "Activated #{name} at #{possibility}" }
         | 
| 440 736 | 
             
                    activated.set_payload(name, possibility)
         | 
| @@ -442,14 +738,14 @@ module Molinillo | |
| 442 738 | 
             
                  end
         | 
| 443 739 |  | 
| 444 740 | 
             
                  # Requires the dependencies that the recently activated spec has
         | 
| 445 | 
            -
                  # @param [Object]  | 
| 741 | 
            +
                  # @param [Object] activated_possibility the PossibilitySet that has just been
         | 
| 446 742 | 
             
                  #   activated
         | 
| 447 743 | 
             
                  # @return [void]
         | 
| 448 | 
            -
                  def require_nested_dependencies_for( | 
| 449 | 
            -
                    nested_dependencies = dependencies_for( | 
| 744 | 
            +
                  def require_nested_dependencies_for(possibility_set)
         | 
| 745 | 
            +
                    nested_dependencies = dependencies_for(possibility_set.latest_version)
         | 
| 450 746 | 
             
                    debug(depth) { "Requiring nested dependencies (#{nested_dependencies.join(', ')})" }
         | 
| 451 747 | 
             
                    nested_dependencies.each do |d|
         | 
| 452 | 
            -
                      activated.add_child_vertex(name_for(d), nil, [name_for( | 
| 748 | 
            +
                      activated.add_child_vertex(name_for(d), nil, [name_for(possibility_set.latest_version)], d)
         | 
| 453 749 | 
             
                      parent_index = states.size - 1
         | 
| 454 750 | 
             
                      parents = @parents_of[d]
         | 
| 455 751 | 
             
                      parents << parent_index if parents.empty?
         | 
| @@ -464,20 +760,74 @@ module Molinillo | |
| 464 760 | 
             
                  # @return [void]
         | 
| 465 761 | 
             
                  def push_state_for_requirements(new_requirements, requires_sort = true, new_activated = activated)
         | 
| 466 762 | 
             
                    new_requirements = sort_dependencies(new_requirements.uniq, new_activated, conflicts) if requires_sort
         | 
| 467 | 
            -
                    new_requirement =  | 
| 763 | 
            +
                    new_requirement = nil
         | 
| 764 | 
            +
                    loop do
         | 
| 765 | 
            +
                      new_requirement = new_requirements.shift
         | 
| 766 | 
            +
                      break if new_requirement.nil? || states.none? { |s| s.requirement == new_requirement }
         | 
| 767 | 
            +
                    end
         | 
| 468 768 | 
             
                    new_name = new_requirement ? name_for(new_requirement) : ''.freeze
         | 
| 469 | 
            -
                    possibilities =  | 
| 769 | 
            +
                    possibilities = possibilities_for_requirement(new_requirement)
         | 
| 470 770 | 
             
                    handle_missing_or_push_dependency_state DependencyState.new(
         | 
| 471 771 | 
             
                      new_name, new_requirements, new_activated,
         | 
| 472 | 
            -
                      new_requirement, possibilities, depth, conflicts.dup
         | 
| 772 | 
            +
                      new_requirement, possibilities, depth, conflicts.dup, unused_unwind_options.dup
         | 
| 473 773 | 
             
                    )
         | 
| 474 774 | 
             
                  end
         | 
| 475 775 |  | 
| 776 | 
            +
                  # Checks a proposed requirement with any existing locked requirement
         | 
| 777 | 
            +
                  # before generating an array of possibilities for it.
         | 
| 778 | 
            +
                  # @param [Object] the proposed requirement
         | 
| 779 | 
            +
                  # @return [Array] possibilities
         | 
| 780 | 
            +
                  def possibilities_for_requirement(requirement, activated = self.activated)
         | 
| 781 | 
            +
                    return [] unless requirement
         | 
| 782 | 
            +
                    if locked_requirement_named(name_for(requirement))
         | 
| 783 | 
            +
                      return locked_requirement_possibility_set(requirement, activated)
         | 
| 784 | 
            +
                    end
         | 
| 785 | 
            +
             | 
| 786 | 
            +
                    group_possibilities(search_for(requirement))
         | 
| 787 | 
            +
                  end
         | 
| 788 | 
            +
             | 
| 789 | 
            +
                  # @param [Object] the proposed requirement
         | 
| 790 | 
            +
                  # @return [Array] possibility set containing only the locked requirement, if any
         | 
| 791 | 
            +
                  def locked_requirement_possibility_set(requirement, activated = self.activated)
         | 
| 792 | 
            +
                    all_possibilities = search_for(requirement)
         | 
| 793 | 
            +
                    locked_requirement = locked_requirement_named(name_for(requirement))
         | 
| 794 | 
            +
             | 
| 795 | 
            +
                    # Longwinded way to build a possibilities array with either the locked
         | 
| 796 | 
            +
                    # requirement or nothing in it. Required, since the API for
         | 
| 797 | 
            +
                    # locked_requirement isn't guaranteed.
         | 
| 798 | 
            +
                    locked_possibilities = all_possibilities.select do |possibility|
         | 
| 799 | 
            +
                      requirement_satisfied_by?(locked_requirement, activated, possibility)
         | 
| 800 | 
            +
                    end
         | 
| 801 | 
            +
             | 
| 802 | 
            +
                    group_possibilities(locked_possibilities)
         | 
| 803 | 
            +
                  end
         | 
| 804 | 
            +
             | 
| 805 | 
            +
                  # Build an array of PossibilitySets, with each element representing a group of
         | 
| 806 | 
            +
                  # dependency versions that all have the same sub-dependency version constraints.
         | 
| 807 | 
            +
                  # @param [Array] an array of possibilities
         | 
| 808 | 
            +
                  # @return [Array] an array of possibility sets
         | 
| 809 | 
            +
                  def group_possibilities(possibilities)
         | 
| 810 | 
            +
                    possibility_sets = []
         | 
| 811 | 
            +
                    possibility_sets_index = {}
         | 
| 812 | 
            +
             | 
| 813 | 
            +
                    possibilities.reverse_each do |possibility|
         | 
| 814 | 
            +
                      dependencies = dependencies_for(possibility)
         | 
| 815 | 
            +
                      if index = possibility_sets_index[dependencies]
         | 
| 816 | 
            +
                        possibility_sets[index].possibilities.unshift(possibility)
         | 
| 817 | 
            +
                      else
         | 
| 818 | 
            +
                        possibility_sets << PossibilitySet.new(dependencies, [possibility])
         | 
| 819 | 
            +
                        possibility_sets_index[dependencies] = possibility_sets.count - 1
         | 
| 820 | 
            +
                      end
         | 
| 821 | 
            +
                    end
         | 
| 822 | 
            +
             | 
| 823 | 
            +
                    possibility_sets.reverse
         | 
| 824 | 
            +
                  end
         | 
| 825 | 
            +
             | 
| 476 826 | 
             
                  # Pushes a new {DependencyState}.
         | 
| 477 827 | 
             
                  # If the {#specification_provider} says to
         | 
| 478 828 | 
             
                  # {SpecificationProvider#allow_missing?} that particular requirement, and
         | 
| 479 829 | 
             
                  # there are no possibilities for that requirement, then `state` is not
         | 
| 480 | 
            -
                  # pushed, and the  | 
| 830 | 
            +
                  # pushed, and the vertex in {#activated} is removed, and we continue
         | 
| 481 831 | 
             
                  # resolving the remaining requirements.
         | 
| 482 832 | 
             
                  # @param [DependencyState] state
         | 
| 483 833 | 
             
                  # @return [void]
         |