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.
Files changed (38) hide show
  1. checksums.yaml +5 -5
  2. data/ARCHITECTURE.md +102 -0
  3. data/CHANGELOG.md +352 -0
  4. data/lib/molinillo.rb +2 -0
  5. data/lib/molinillo/compatibility.rb +26 -0
  6. data/lib/molinillo/delegates/resolution_state.rb +7 -0
  7. data/lib/molinillo/delegates/specification_provider.rb +1 -0
  8. data/lib/molinillo/dependency_graph.rb +3 -2
  9. data/lib/molinillo/dependency_graph/action.rb +1 -0
  10. data/lib/molinillo/dependency_graph/add_edge_no_circular.rb +1 -0
  11. data/lib/molinillo/dependency_graph/add_vertex.rb +1 -0
  12. data/lib/molinillo/dependency_graph/delete_edge.rb +1 -0
  13. data/lib/molinillo/dependency_graph/detach_vertex_named.rb +1 -0
  14. data/lib/molinillo/dependency_graph/log.rb +1 -0
  15. data/lib/molinillo/dependency_graph/set_payload.rb +1 -0
  16. data/lib/molinillo/dependency_graph/tag.rb +1 -0
  17. data/lib/molinillo/dependency_graph/vertex.rb +3 -2
  18. data/lib/molinillo/errors.rb +69 -6
  19. data/lib/molinillo/gem_metadata.rb +2 -1
  20. data/lib/molinillo/modules/specification_provider.rb +1 -0
  21. data/lib/molinillo/modules/ui.rb +3 -1
  22. data/lib/molinillo/resolution.rb +495 -145
  23. data/lib/molinillo/resolver.rb +1 -0
  24. data/lib/molinillo/state.rb +8 -4
  25. metadata +8 -30
  26. data/spec/dependency_graph/log_spec.rb +0 -29
  27. data/spec/dependency_graph_spec.rb +0 -74
  28. data/spec/errors_spec.rb +0 -26
  29. data/spec/fuzz_spec.rb +0 -94
  30. data/spec/resolver_integration_specs/index_from_rubygems.rb +0 -76
  31. data/spec/resolver_spec.rb +0 -235
  32. data/spec/spec_helper.rb +0 -44
  33. data/spec/spec_helper/equal_dependency_graph.rb +0 -16
  34. data/spec/spec_helper/index.rb +0 -186
  35. data/spec/spec_helper/naive_resolver.rb +0 -48
  36. data/spec/spec_helper/specification.rb +0 -28
  37. data/spec/spec_helper/ui.rb +0 -14
  38. data/spec/state_spec.rb +0 -30
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require 'molinillo/compatibility'
2
4
  require 'molinillo/gem_metadata'
3
5
  require 'molinillo/errors'
4
6
  require 'molinillo/resolver'
@@ -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
  module Molinillo
3
4
  module Delegates
4
5
  # Delegates all {Molinillo::SpecificationProvider} methods to a
@@ -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
- parent_node = vertex_named(parent_name)
151
- add_edge(parent_node, vertex, requirement)
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
  # An action that modifies a {DependencyGraph} that is reversible.
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'molinillo/dependency_graph/action'
3
4
  module Molinillo
4
5
  class DependencyGraph
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'molinillo/dependency_graph/action'
3
4
  module Molinillo
4
5
  class DependencyGraph
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'molinillo/dependency_graph/action'
3
4
  module Molinillo
4
5
  class DependencyGraph
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'molinillo/dependency_graph/action'
3
4
  module Molinillo
4
5
  class DependencyGraph
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'molinillo/dependency_graph/add_edge_no_circular'
3
4
  require 'molinillo/dependency_graph/add_vertex'
4
5
  require 'molinillo/dependency_graph/delete_edge'
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'molinillo/dependency_graph/action'
3
4
  module Molinillo
4
5
  class DependencyGraph
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'molinillo/dependency_graph/action'
3
4
  module Molinillo
4
5
  class DependencyGraph
@@ -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 += vertices.map(&:recursive_predecessors).flatten(1)
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 += vertices.map(&:recursive_successors).flatten(1)
72
+ vertices += Compatibility.flat_map(vertices, &:recursive_successors)
72
73
  vertices.uniq!
73
74
  vertices
74
75
  end
@@ -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>] nodes the nodes in the dependency
45
+ # @param [Array<DependencyGraph::Vertex>] vertices the vertices in the dependency
45
46
  # that caused the error
46
- def initialize(nodes)
47
- super "There is a circular dependency between #{nodes.map(&:name).join(' and ')}"
48
- @dependencies = nodes.map(&:payload).to_set
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
- def initialize(conflicts)
65
+ # @param [SpecificationProvider] specification_provider see {#specification_provider}
66
+ def initialize(conflicts, specification_provider)
61
67
  pairs = []
62
- conflicts.values.flatten.map(&:requirements).flatten.each do |conflicting|
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
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Molinillo
3
4
  # The version of Molinillo.
4
- VERSION = '0.5.7'.freeze
5
+ VERSION = '0.6.0'.freeze
5
6
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Molinillo
3
4
  # Provides information about specifcations and dependencies to the resolver,
4
5
  # allowing the {Resolver} class to remain generic while still providing power
@@ -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
- output.puts debug_info.split("\n").map { |s| ' ' * depth + s }
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
 
@@ -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] possibility the spec that was unable to be activated due
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
- :possibility,
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 unless state.requirements.any? || state.requirement
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
- activated.freeze
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 if state.is_a? PossibilityState
140
- unwind_for_conflict until possibility && state.is_a?(DependencyState)
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 { |r| dg.add_vertex(name_for(r), nil, true).tap { |v| v.explicit_requirements << r } }
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 && search_for(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
- debug(depth) { "Unwinding for conflict: #{requirement} to #{state_index_for_unwind / 2}" }
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!((state_index_for_unwind + 1)..-1)
184
- raise VersionConflict.new(c) unless state
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
- # @return [Integer] The index to which the resolution should unwind in the
193
- # case of conflict.
194
- def state_index_for_unwind
195
- current_requirement = requirement
196
- existing_requirement = requirement_for_existing_name(name)
197
- index = -1
198
- [current_requirement, existing_requirement].each do |r|
199
- until r.nil?
200
- current_state = find_state_for(r)
201
- if state_any?(current_state)
202
- current_index = states.index(current_state)
203
- index = current_index if current_index > index
204
- break
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
- r = parent_of(r)
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
- index
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).payload
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.reverse_each.find { |i| requirement == i.requirement && i.is_a?(DependencyState) }
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 { |edge| (requirements[edge.origin.payload] ||= []).unshift(edge.requirement) }
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
- existing_node = activated.vertex_named(name)
315
- if existing_node.payload
316
- debug(depth) { "Found existing spec (#{existing_node.payload})" }
317
- attempt_to_activate_existing_spec(existing_node)
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
- attempt_to_activate_new_spec
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 activate the current {#possibility} (given that it has
324
- # already been activated)
687
+ # Attempts to update the existing vertex's `PossibilitySet` with a filtered version
325
688
  # @return [void]
326
- def attempt_to_activate_existing_spec(existing_node)
327
- existing_spec = existing_node.payload
328
- if requirement_satisfied_by?(requirement, activated, existing_spec)
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 (#{existing_node.payload})" }
698
+ debug(depth) { "Unsatisfied by existing spec (#{vertex.payload})" }
335
699
  unwind_for_conflict
336
700
  end
337
701
  end
338
702
 
339
- # Attempts to swp the current {#possibility} with the already-activated
340
- # spec with the given name
341
- # @return [Boolean] Whether the possibility was swapped into {#activated}
342
- def attempt_to_swap_possibility
343
- activated.tag(:swap)
344
- vertex = activated.vertex_named(name)
345
- activated.set_payload(name, possibility)
346
- if !vertex.requirements.
347
- all? { |r| requirement_satisfied_by?(r, activated, possibility) } ||
348
- !new_spec_satisfied?
349
- activated.rewind_to(:swap)
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
- fixup_swapped_children(vertex)
353
- activate_spec
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
- # Attempts to activate the current {#possibility} (given that it hasn't
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 activate_spec
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] activated_spec the specification that has just been
741
+ # @param [Object] activated_possibility the PossibilitySet that has just been
446
742
  # activated
447
743
  # @return [void]
448
- def require_nested_dependencies_for(activated_spec)
449
- nested_dependencies = dependencies_for(activated_spec)
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(activated_spec)], d)
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 = new_requirements.shift
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 = new_requirement ? search_for(new_requirement) : []
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 node in {#activated} is removed, and we continue
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]