diff options
author | Samuel Giddins <segiddins@segiddins.me> | 2017-07-27 11:45:35 -0500 |
---|---|---|
committer | Samuel Giddins <segiddins@segiddins.me> | 2017-08-01 10:07:20 -0500 |
commit | dc7558968305fcf6ced06dcfbfa34092e64f2b3b (patch) | |
tree | bdb63b9c058f6bbbff58d4c9ee60319deecfd59c /lib/bundler/vendor | |
parent | b64331dbe20efe3a8953ba06a540c26deb3eae48 (diff) | |
download | bundler-dc7558968305fcf6ced06dcfbfa34092e64f2b3b.tar.gz |
Update vendored Molinillo to 0.6.0
Diffstat (limited to 'lib/bundler/vendor')
21 files changed, 629 insertions, 161 deletions
diff --git a/lib/bundler/vendor/molinillo/lib/molinillo.rb b/lib/bundler/vendor/molinillo/lib/molinillo.rb index 134bf1d720..9e2867144f 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo.rb @@ -1,4 +1,6 @@ # frozen_string_literal: true + +require 'bundler/vendor/molinillo/lib/molinillo/compatibility' require 'bundler/vendor/molinillo/lib/molinillo/gem_metadata' require 'bundler/vendor/molinillo/lib/molinillo/errors' require 'bundler/vendor/molinillo/lib/molinillo/resolver' diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/compatibility.rb b/lib/bundler/vendor/molinillo/lib/molinillo/compatibility.rb new file mode 100644 index 0000000000..3eba8e4083 --- /dev/null +++ b/lib/bundler/vendor/molinillo/lib/molinillo/compatibility.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Bundler::Molinillo + # Hacks needed for old Ruby versions. + module Compatibility + module_function + + if [].respond_to?(:flat_map) + # Flat map + # @param [Enumerable] enum an enumerable object + # @block the block to flat-map with + # @return The enum, flat-mapped + def flat_map(enum, &blk) + enum.flat_map(&blk) + end + else + # Flat map + # @param [Enumerable] enum an enumerable object + # @block the block to flat-map with + # @return The enum, flat-mapped + def flat_map(enum, &blk) + enum.map(&blk).flatten(1) + end + end + end +end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb b/lib/bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb index 253c18764f..bcacf35243 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler::Molinillo # @!visibility private module Delegates @@ -45,6 +46,12 @@ module Bundler::Molinillo current_state = state || Bundler::Molinillo::ResolutionState.empty current_state.conflicts end + + # (see Bundler::Molinillo::ResolutionState#unused_unwind_options) + def unused_unwind_options + current_state = state || Bundler::Molinillo::ResolutionState.empty + current_state.unused_unwind_options + end end end end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb b/lib/bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb index 29f48d5b3c..ec9c770a28 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler::Molinillo module Delegates # Delegates all {Bundler::Molinillo::SpecificationProvider} methods to a diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph.rb index 76e84ab7e6..677a8bd916 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'set' require 'tsort' @@ -147,8 +148,8 @@ module Bundler::Molinillo vertex = add_vertex(name, payload, root) vertex.explicit_requirements << requirement if root parent_names.each do |parent_name| - parent_node = vertex_named(parent_name) - add_edge(parent_node, vertex, requirement) + parent_vertex = vertex_named(parent_name) + add_edge(parent_vertex, vertex, requirement) end vertex end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/action.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/action.rb index e0dfe6cbbd..c04c7eec9c 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/action.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/action.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler::Molinillo class DependencyGraph # An action that modifies a {DependencyGraph} that is reversible. diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb index 9092e4d546..9849aea2fe 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' module Bundler::Molinillo class DependencyGraph diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb index eda4251801..0a1e08255b 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' module Bundler::Molinillo class DependencyGraph diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb index e9125a59c6..1d9f4b327d 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' module Bundler::Molinillo class DependencyGraph diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb index d20b2cb0e0..385dcbdd06 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' module Bundler::Molinillo class DependencyGraph diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/log.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/log.rb index 72a705e023..8582dd19c1 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/log.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/log.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular' require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex' require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge' diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb index 8d8e10fedf..37286d104a 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' module Bundler::Molinillo class DependencyGraph diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb index 53524d36ad..d6ad16e07a 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph/action' module Bundler::Molinillo class DependencyGraph diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb index eab989e7bc..16c7d65c4d 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler::Molinillo class DependencyGraph # A vertex in a {DependencyGraph} that encapsulates a {#name} and a @@ -53,7 +54,7 @@ module Bundler::Molinillo # {#descendent?} def recursive_predecessors vertices = predecessors - vertices += vertices.map(&:recursive_predecessors).flatten(1) + vertices += Compatibility.flat_map(vertices, &:recursive_predecessors) vertices.uniq! vertices end @@ -68,7 +69,7 @@ module Bundler::Molinillo # {#ancestor?} def recursive_successors vertices = successors - vertices += vertices.map(&:recursive_successors).flatten(1) + vertices += Compatibility.flat_map(vertices, &:recursive_successors) vertices.uniq! vertices end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/errors.rb b/lib/bundler/vendor/molinillo/lib/molinillo/errors.rb index f904bd0814..fb343250b1 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/errors.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/errors.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler::Molinillo # An error that occurred during the resolution process class ResolverError < StandardError; end @@ -41,11 +42,11 @@ module Bundler::Molinillo attr_reader :dependencies # Initializes a new error with the given circular vertices. - # @param [Array<DependencyGraph::Vertex>] nodes the nodes in the dependency + # @param [Array<DependencyGraph::Vertex>] vertices the vertices in the dependency # that caused the error - def initialize(nodes) - super "There is a circular dependency between #{nodes.map(&:name).join(' and ')}" - @dependencies = nodes.map(&:payload).to_set + def initialize(vertices) + super "There is a circular dependency between #{vertices.map(&:name).join(' and ')}" + @dependencies = vertices.map { |vertex| vertex.payload.possibilities.last }.to_set end end @@ -55,11 +56,16 @@ module Bundler::Molinillo # resolution to fail attr_reader :conflicts + # @return [SpecificationProvider] the specification provider used during + # resolution + attr_reader :specification_provider + # Initializes a new error with the given version conflicts. # @param [{String => Resolution::Conflict}] conflicts see {#conflicts} - def initialize(conflicts) + # @param [SpecificationProvider] specification_provider see {#specification_provider} + def initialize(conflicts, specification_provider) pairs = [] - conflicts.values.flatten.map(&:requirements).flatten.each do |conflicting| + Compatibility.flat_map(conflicts.values.flatten, &:requirements).each do |conflicting| conflicting.each do |source, conflict_requirements| conflict_requirements.each do |c| pairs << [c, source] @@ -69,7 +75,64 @@ module Bundler::Molinillo super "Unable to satisfy the following requirements:\n\n" \ "#{pairs.map { |r, d| "- `#{r}` required by `#{d}`" }.join("\n")}" + @conflicts = conflicts + @specification_provider = specification_provider + end + + require 'bundler/vendor/molinillo/lib/molinillo/delegates/specification_provider' + include Delegates::SpecificationProvider + + # @return [String] An error message that includes requirement trees, + # which is much more detailed & customizable than the default message + # @param [Hash] opts the options to create a message with. + # @option opts [String] :solver_name The user-facing name of the solver + # @option opts [String] :possibility_type The generic name of a possibility + # @option opts [Proc] :reduce_trees A proc that reduced the list of requirement trees + # @option opts [Proc] :printable_requirement A proc that pretty-prints requirements + # @option opts [Proc] :additional_message_for_conflict A proc that appends additional + # messages for each conflict + # @option opts [Proc] :version_for_spec A proc that returns the version number for a + # possibility + def message_with_trees(opts = {}) + solver_name = opts.delete(:solver_name) { self.class.name.split('::').first } + possibility_type = opts.delete(:possibility_type) { 'possibility named' } + reduce_trees = opts.delete(:reduce_trees) { proc { |trees| trees.uniq.sort_by(&:to_s) } } + printable_requirement = opts.delete(:printable_requirement) { proc { |req| req.to_s } } + additional_message_for_conflict = opts.delete(:additional_message_for_conflict) { proc {} } + version_for_spec = opts.delete(:version_for_spec) { proc(&:to_s) } + + conflicts.sort.reduce(''.dup) do |o, (name, conflict)| + o << %(\n#{solver_name} could not find compatible versions for #{possibility_type} "#{name}":\n) + if conflict.locked_requirement + o << %( In snapshot (#{name_for_locking_dependency_source}):\n) + o << %( #{printable_requirement.call(conflict.locked_requirement)}\n) + o << %(\n) + end + o << %( In #{name_for_explicit_dependency_source}:\n) + trees = reduce_trees.call(conflict.requirement_trees) + + o << trees.map do |tree| + t = ''.dup + depth = 2 + tree.each do |req| + t << ' ' * depth << req.to_s + unless tree.last == req + if spec = conflict.activated_by_name[name_for(req)] + t << %( was resolved to #{version_for_spec.call(spec)}, which) + end + t << %( depends on) + end + t << %(\n) + depth += 1 + end + t + end.join("\n") + + additional_message_for_conflict.call(o, name, conflict) + + o + end.strip end end end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/gem_metadata.rb b/lib/bundler/vendor/molinillo/lib/molinillo/gem_metadata.rb index a4fb6dd68e..cccdde53f3 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/gem_metadata.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/gem_metadata.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true + module Bundler::Molinillo # The version of Bundler::Molinillo. - VERSION = '0.5.7'.freeze + VERSION = '0.6.0'.freeze end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/modules/specification_provider.rb b/lib/bundler/vendor/molinillo/lib/molinillo/modules/specification_provider.rb index 0f1ad195f2..fa094c1981 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/modules/specification_provider.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/modules/specification_provider.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler::Molinillo # Provides information about specifcations and dependencies to the resolver, # allowing the {Resolver} class to remain generic while still providing power diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/modules/ui.rb b/lib/bundler/vendor/molinillo/lib/molinillo/modules/ui.rb index d47cfa2928..a166bc6991 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/modules/ui.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/modules/ui.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler::Molinillo # Conveys information about the resolution process to a user. module UI @@ -48,7 +49,8 @@ module Bundler::Molinillo if debug? debug_info = yield debug_info = debug_info.inspect unless debug_info.is_a?(String) - output.puts debug_info.split("\n").map { |s| ' ' * depth + s } + debug_info = debug_info.split("\n").map { |s| ":#{depth.to_s.rjust 4}: #{s}" } + output.puts debug_info end end diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/resolution.rb b/lib/bundler/vendor/molinillo/lib/molinillo/resolution.rb index 1845966a75..f8d170bace 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/resolution.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/resolution.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler::Molinillo class Resolver # A specific resolution from a given {Resolver} @@ -8,22 +9,125 @@ module Bundler::Molinillo # @attr [{String,Nil=>[Object]}] requirements the requirements that caused the conflict # @attr [Object, nil] existing the existing spec that was in conflict with # the {#possibility} - # @attr [Object] possibility the spec that was unable to be activated due - # to a conflict + # @attr [Object] possibility_set the set of specs that was unable to be + # activated due to a conflict. # @attr [Object] locked_requirement the relevant locking requirement. # @attr [Array<Array<Object>>] requirement_trees the different requirement # trees that led to every requirement for the conflicting name. # @attr [{String=>Object}] activated_by_name the already-activated specs. + # @attr [Object] underlying_error an error that has occurred during resolution, and + # will be raised at the end of it if no resolution is found. Conflict = Struct.new( :requirement, :requirements, :existing, - :possibility, + :possibility_set, :locked_requirement, :requirement_trees, - :activated_by_name + :activated_by_name, + :underlying_error + ) + + class Conflict + # @return [Object] a spec that was unable to be activated due to a conflict + def possibility + possibility_set && possibility_set.latest_version + end + end + + # A collection of possibility states that share the same dependencies + # @attr [Array] dependencies the dependencies for this set of possibilities + # @attr [Array] possibilities the possibilities + PossibilitySet = Struct.new(:dependencies, :possibilities) + + class PossibilitySet + # String representation of the possibility set, for debugging + def to_s + "[#{possibilities.join(', ')}]" + end + + # @return [Object] most up-to-date dependency in the possibility set + def latest_version + possibilities.last + end + end + + # Details of the state to unwind to when a conflict occurs, and the cause of the unwind + # @attr [Integer] state_index the index of the state to unwind to + # @attr [Object] state_requirement the requirement of the state we're unwinding to + # @attr [Array] requirement_tree for the requirement we're relaxing + # @attr [Array] conflicting_requirements the requirements that combined to cause the conflict + # @attr [Array] requirement_trees for the conflict + # @attr [Array] requirements_unwound_to_instead array of unwind requirements that were chosen over this unwind + UnwindDetails = Struct.new( + :state_index, + :state_requirement, + :requirement_tree, + :conflicting_requirements, + :requirement_trees, + :requirements_unwound_to_instead ) + class UnwindDetails + include Comparable + + # We compare UnwindDetails when choosing which state to unwind to. If + # two options have the same state_index we prefer the one most + # removed from a requirement that caused the conflict. Both options + # would unwind to the same state, but a `grandparent` option will + # filter out fewer of its possibilities after doing so - where a state + # is both a `parent` and a `grandparent` to requirements that have + # caused a conflict this is the correct behaviour. + # @param [UnwindDetail] other UnwindDetail to be compared + # @return [Integer] integer specifying ordering + def <=>(other) + if state_index > other.state_index + 1 + elsif state_index == other.state_index + reversed_requirement_tree_index <=> other.reversed_requirement_tree_index + else + -1 + end + end + + # @return [Integer] index of state requirement in reversed requirement tree + # (the conflicting requirement itself will be at position 0) + def reversed_requirement_tree_index + @reversed_requirement_tree_index ||= + if state_requirement + requirement_tree.reverse.index(state_requirement) + else + 999_999 + end + end + + # @return [Boolean] where the requirement of the state we're unwinding + # to directly caused the conflict. Note: in this case, it is + # impossible for the state we're unwinding to to be a parent of + # any of the other conflicting requirements (or we would have + # circularity) + def unwinding_to_primary_requirement? + requirement_tree.last == state_requirement + end + + # @return [Array] array of sub-dependencies to avoid when choosing a + # new possibility for the state we've unwound to. Only relevant for + # non-primary unwinds + def sub_dependencies_to_avoid + @requirements_to_avoid ||= + requirement_trees.map do |tree| + index = tree.index(state_requirement) + tree[index + 1] if index + end.compact + end + + # @return [Array] array of all the requirements that led to the need for + # this unwind + def all_requirements + @all_requirements ||= requirement_trees.flatten(1) + end + end + # @return [SpecificationProvider] the provider that knows about # dependencies, requirements, specifications, versions, etc. attr_reader :specification_provider @@ -64,7 +168,7 @@ module Bundler::Molinillo start_resolution while state - break unless state.requirements.any? || state.requirement + break if !state.requirement && state.requirements.empty? indicate_progress if state.respond_to?(:pop_possibility_state) # DependencyState debug(depth) { "Creating possibility state for #{requirement} (#{possibilities.count} remaining)" } @@ -78,7 +182,7 @@ module Bundler::Molinillo process_topmost_state end - activated.freeze + resolve_activated_specs ensure end_resolution end @@ -109,6 +213,19 @@ module Bundler::Molinillo resolver_ui.before_resolution end + def resolve_activated_specs + activated.vertices.each do |_, vertex| + next unless vertex.payload + + latest_version = vertex.payload.possibilities.reverse_each.find do |possibility| + vertex.requirements.uniq.all? { |req| requirement_satisfied_by?(req, activated, possibility) } + end + + activated.set_payload(vertex.name, latest_version) + end + activated.freeze + end + # Ends the resolution process # @return [void] def end_resolution @@ -136,9 +253,12 @@ module Bundler::Molinillo if possibility attempt_to_activate else - create_conflict if state.is_a? PossibilityState - unwind_for_conflict until possibility && state.is_a?(DependencyState) + create_conflict + unwind_for_conflict end + rescue CircularDependencyError => underlying_error + create_conflict(underlying_error) + unwind_for_conflict end # @return [Object] the current possibility that the resolution is trying @@ -158,7 +278,10 @@ module Bundler::Molinillo # @return [DependencyState] the initial state for the resolution def initial_state graph = DependencyGraph.new.tap do |dg| - original_requested.each { |r| dg.add_vertex(name_for(r), nil, true).tap { |v| v.explicit_requirements << r } } + original_requested.each do |requested| + vertex = dg.add_vertex(name_for(requested), nil, true) + vertex.explicit_requirements << requested + end dg.tag(:initial_state) end @@ -169,45 +292,276 @@ module Bundler::Molinillo requirements, graph, initial_requirement, - initial_requirement && search_for(initial_requirement), + possibilities_for_requirement(initial_requirement, graph), 0, - {} + {}, + [] ) end # Unwinds the states stack because a conflict has been encountered # @return [void] def unwind_for_conflict - debug(depth) { "Unwinding for conflict: #{requirement} to #{state_index_for_unwind / 2}" } + details_for_unwind = build_details_for_unwind + unwind_options = unused_unwind_options + debug(depth) { "Unwinding for conflict: #{requirement} to #{details_for_unwind.state_index / 2}" } conflicts.tap do |c| - sliced_states = states.slice!((state_index_for_unwind + 1)..-1) - raise VersionConflict.new(c) unless state + sliced_states = states.slice!((details_for_unwind.state_index + 1)..-1) + raise_error_unless_state(c) activated.rewind_to(sliced_states.first || :initial_state) if sliced_states state.conflicts = c + state.unused_unwind_options = unwind_options + filter_possibilities_after_unwind(details_for_unwind) index = states.size - 1 @parents_of.each { |_, a| a.reject! { |i| i >= index } } + state.unused_unwind_options.reject! { |uw| uw.state_index >= index } end end - # @return [Integer] The index to which the resolution should unwind in the - # case of conflict. - def state_index_for_unwind - current_requirement = requirement - existing_requirement = requirement_for_existing_name(name) - index = -1 - [current_requirement, existing_requirement].each do |r| - until r.nil? - current_state = find_state_for(r) - if state_any?(current_state) - current_index = states.index(current_state) - index = current_index if current_index > index - break + # Raises a VersionConflict error, or any underlying error, if there is no + # current state + # @return [void] + def raise_error_unless_state(conflicts) + return if state + + error = conflicts.values.map(&:underlying_error).compact.first + raise error || VersionConflict.new(conflicts, specification_provider) + end + + # @return [UnwindDetails] Details of the nearest index to which we could unwind + def build_details_for_unwind + # Get the possible unwinds for the current conflict + current_conflict = conflicts[name] + binding_requirements = binding_requirements_for_conflict(current_conflict) + unwind_details = unwind_options_for_requirements(binding_requirements) + + last_detail_for_current_unwind = unwind_details.sort.last + current_detail = last_detail_for_current_unwind + + # Look for past conflicts that could be unwound to affect the + # requirement tree for the current conflict + relevant_unused_unwinds = unused_unwind_options.select do |alternative| + intersecting_requirements = + last_detail_for_current_unwind.all_requirements & + alternative.requirements_unwound_to_instead + next if intersecting_requirements.empty? + # Find the highest index unwind whilst looping through + current_detail = alternative if alternative > current_detail + alternative + end + + # Add the current unwind options to the `unused_unwind_options` array. + # The "used" option will be filtered out during `unwind_for_conflict`. + state.unused_unwind_options += unwind_details.reject { |detail| detail.state_index == -1 } + + # Update the requirements_unwound_to_instead on any relevant unused unwinds + relevant_unused_unwinds.each { |d| d.requirements_unwound_to_instead << current_detail.state_requirement } + unwind_details.each { |d| d.requirements_unwound_to_instead << current_detail.state_requirement } + + current_detail + end + + # @param [Array<Object>] array of requirements that combine to create a conflict + # @return [Array<UnwindDetails>] array of UnwindDetails that have a chance + # of resolving the passed requirements + def unwind_options_for_requirements(binding_requirements) + unwind_details = [] + + trees = [] + binding_requirements.reverse_each do |r| + partial_tree = [r] + trees << partial_tree + unwind_details << UnwindDetails.new(-1, nil, partial_tree, binding_requirements, trees, []) + + # If this requirement has alternative possibilities, check if any would + # satisfy the other requirements that created this conflict + requirement_state = find_state_for(r) + if conflict_fixing_possibilities?(requirement_state, binding_requirements) + unwind_details << UnwindDetails.new( + states.index(requirement_state), + r, + partial_tree, + binding_requirements, + trees, + [] + ) + end + + # Next, look at the parent of this requirement, and check if the requirement + # could have been avoided if an alternative PossibilitySet had been chosen + parent_r = parent_of(r) + next if parent_r.nil? + partial_tree.unshift(parent_r) + requirement_state = find_state_for(parent_r) + if requirement_state.possibilities.any? { |set| !set.dependencies.include?(r) } + unwind_details << UnwindDetails.new( + states.index(requirement_state), + parent_r, + partial_tree, + binding_requirements, + trees, + [] + ) + end + + # Finally, look at the grandparent and up of this requirement, looking + # for any possibilities that wouldn't create their parent requirement + grandparent_r = parent_of(parent_r) + until grandparent_r.nil? + partial_tree.unshift(grandparent_r) + requirement_state = find_state_for(grandparent_r) + if requirement_state.possibilities.any? { |set| !set.dependencies.include?(parent_r) } + unwind_details << UnwindDetails.new( + states.index(requirement_state), + grandparent_r, + partial_tree, + binding_requirements, + trees, + [] + ) end - r = parent_of(r) + parent_r = grandparent_r + grandparent_r = parent_of(parent_r) + end + end + + unwind_details + end + + # @param [DependencyState] state + # @param [Array] array of requirements + # @return [Boolean] whether or not the given state has any possibilities + # that could satisfy the given requirements + def conflict_fixing_possibilities?(state, binding_requirements) + return false unless state + + state.possibilities.any? do |possibility_set| + possibility_set.possibilities.any? do |poss| + possibility_satisfies_requirements?(poss, binding_requirements) end end + end + + # Filter's a state's possibilities to remove any that would not fix the + # conflict we've just rewound from + # @param [UnwindDetails] details of the conflict just unwound from + # @return [void] + def filter_possibilities_after_unwind(unwind_details) + return unless state && !state.possibilities.empty? + + if unwind_details.unwinding_to_primary_requirement? + filter_possibilities_for_primary_unwind(unwind_details) + else + filter_possibilities_for_parent_unwind(unwind_details) + end + end + + # Filter's a state's possibilities to remove any that would not satisfy + # the requirements in the conflict we've just rewound from + # @param [UnwindDetails] details of the conflict just unwound from + # @return [void] + def filter_possibilities_for_primary_unwind(unwind_details) + all_requirements = unwind_details.conflicting_requirements - index + state.possibilities.reject! do |possibility_set| + possibility_set.possibilities.none? do |poss| + possibility_satisfies_requirements?(poss, all_requirements) + end + end + end + + # @param [Object] possibility a single possibility + # @param [Array] requirements an array of requirements + # @return [Boolean] whether the possibility satisfies all of the + # given requirements + def possibility_satisfies_requirements?(possibility, requirements) + name = name_for(possibility) + + activated.tag(:swap) + activated.set_payload(name, possibility) if activated.vertex_named(name) + satisfied = requirements.all? { |r| requirement_satisfied_by?(r, activated, possibility) } + activated.rewind_to(:swap) + + satisfied + end + + # Filter's a state's possibilities to remove any that would (eventually) + # create a requirement in the conflict we've just rewound from + # @param [UnwindDetails] details of the conflict just unwound from + # @return [void] + def filter_possibilities_for_parent_unwind(unwind_details) + unwinds_to_state = unused_unwind_options.select { |uw| uw.state_index == unwind_details.state_index } + unwinds_to_state << unwind_details + + primary_unwinds = unwinds_to_state.select(&:unwinding_to_primary_requirement?).uniq + parent_unwinds = unwinds_to_state.uniq - primary_unwinds + + allowed_possibility_sets = Compatibility.flat_map(primary_unwinds) do |unwind| + states[unwind.state_index].possibilities.select do |possibility_set| + possibility_set.possibilities.any? do |poss| + possibility_satisfies_requirements?(poss, unwind.conflicting_requirements) + end + end + end + + requirements_to_avoid = Compatibility.flat_map(parent_unwinds, &:sub_dependencies_to_avoid) + + state.possibilities.reject! do |possibility_set| + !allowed_possibility_sets.include?(possibility_set) && + (requirements_to_avoid - possibility_set.dependencies).empty? + end + end + + # @param [Conflict] conflict + # @return [Array] minimal array of requirements that would cause the passed + # conflict to occur. + def binding_requirements_for_conflict(conflict) + return [conflict.requirement] if conflict.possibility.nil? + + possible_binding_requirements = conflict.requirements.values.flatten(1).uniq + + # When there’s a `CircularDependency` error the conflicting requirement + # (the one causing the circular) won’t be `conflict.requirement` + # (which won’t be for the right state, because we won’t have created it, + # because it’s circular). + # We need to make sure we have that requirement in the conflict’s list, + # otherwise we won’t be able to unwind properly, so we just return all + # the requirements for the conflict. + return possible_binding_requirements if conflict.underlying_error + + possibilities = search_for(conflict.requirement) + + # If all the requirements together don't filter out all possibilities, + # then the only two requirements we need to consider are the initial one + # (where the dependency's version was first chosen) and the last + if binding_requirement_in_set?(nil, possible_binding_requirements, possibilities) + return [conflict.requirement, requirement_for_existing_name(name_for(conflict.requirement))].compact + end + + # Loop through the possible binding requirements, removing each one + # that doesn't bind. Use a `reverse_each` as we want the earliest set of + # binding requirements, and don't use `reject!` as we wish to refine the + # array *on each iteration*. + binding_requirements = possible_binding_requirements.dup + possible_binding_requirements.reverse_each do |req| + next if req == conflict.requirement + unless binding_requirement_in_set?(req, binding_requirements, possibilities) + binding_requirements -= [req] + end + end + + binding_requirements + end + + # @param [Object] requirement we wish to check + # @param [Array] array of requirements + # @param [Array] array of possibilities the requirements will be used to filter + # @return [Boolean] whether or not the given requirement is required to filter + # out all elements of the array of possibilities. + def binding_requirement_in_set?(requirement, possible_binding_requirements, possibilities) + possibilities.any? do |poss| + possibility_satisfies_requirements?(poss, possible_binding_requirements - [requirement]) + end end # @return [Object] the requirement that led to `requirement` being added @@ -222,7 +576,8 @@ module Bundler::Molinillo # @return [Object] the requirement that led to a version of a possibility # with the given name being activated. def requirement_for_existing_name(name) - return nil unless activated.vertex_named(name).payload + return nil unless vertex = activated.vertex_named(name) + return nil unless vertex.payload states.find { |s| s.name == name }.requirement end @@ -230,18 +585,12 @@ module Bundler::Molinillo # `requirement`. def find_state_for(requirement) return nil unless requirement - states.reverse_each.find { |i| requirement == i.requirement && i.is_a?(DependencyState) } - end - - # @return [Boolean] whether or not the given state has any possibilities - # left. - def state_any?(state) - state && state.possibilities.any? + states.find { |i| requirement == i.requirement } end # @return [Conflict] a {Conflict} that reflects the failure to activate # the {#possibility} in conjunction with the current {#state} - def create_conflict + def create_conflict(underlying_error = nil) vertex = activated.vertex_named(name) locked_requirement = locked_requirement_named(name) @@ -250,18 +599,21 @@ module Bundler::Molinillo requirements[name_for_explicit_dependency_source] = vertex.explicit_requirements end requirements[name_for_locking_dependency_source] = [locked_requirement] if locked_requirement - vertex.incoming_edges.each { |edge| (requirements[edge.origin.payload] ||= []).unshift(edge.requirement) } + vertex.incoming_edges.each do |edge| + (requirements[edge.origin.payload.latest_version] ||= []).unshift(edge.requirement) + end activated_by_name = {} - activated.each { |v| activated_by_name[v.name] = v.payload if v.payload } + activated.each { |v| activated_by_name[v.name] = v.payload.latest_version if v.payload } conflicts[name] = Conflict.new( requirement, requirements, - vertex.payload, + vertex.payload && vertex.payload.latest_version, possibility, locked_requirement, requirement_trees, - activated_by_name + activated_by_name, + underlying_error ) end @@ -311,116 +663,60 @@ module Bundler::Molinillo # @return [void] def attempt_to_activate debug(depth) { 'Attempting to activate ' + possibility.to_s } - existing_node = activated.vertex_named(name) - if existing_node.payload - debug(depth) { "Found existing spec (#{existing_node.payload})" } - attempt_to_activate_existing_spec(existing_node) + existing_vertex = activated.vertex_named(name) + if existing_vertex.payload + debug(depth) { "Found existing spec (#{existing_vertex.payload})" } + attempt_to_filter_existing_spec(existing_vertex) else - attempt_to_activate_new_spec + latest = possibility.latest_version + # use reject!(!satisfied) for 1.8.7 compatibility + possibility.possibilities.reject! do |possibility| + !requirement_satisfied_by?(requirement, activated, possibility) + end + if possibility.latest_version.nil? + # ensure there's a possibility for better error messages + possibility.possibilities << latest if latest + create_conflict + unwind_for_conflict + else + activate_new_spec + end end end - # Attempts to activate the current {#possibility} (given that it has - # already been activated) + # Attempts to update the existing vertex's `PossibilitySet` with a filtered version # @return [void] - def attempt_to_activate_existing_spec(existing_node) - existing_spec = existing_node.payload - if requirement_satisfied_by?(requirement, activated, existing_spec) + def attempt_to_filter_existing_spec(vertex) + filtered_set = filtered_possibility_set(vertex) + if !filtered_set.possibilities.empty? && + (vertex.payload.dependencies == dependencies_for(possibility.latest_version)) + activated.set_payload(name, filtered_set) new_requirements = requirements.dup push_state_for_requirements(new_requirements, false) else - return if attempt_to_swap_possibility create_conflict - debug(depth) { "Unsatisfied by existing spec (#{existing_node.payload})" } + debug(depth) { "Unsatisfied by existing spec (#{vertex.payload})" } unwind_for_conflict end end - # Attempts to swp the current {#possibility} with the already-activated - # spec with the given name - # @return [Boolean] Whether the possibility was swapped into {#activated} - def attempt_to_swap_possibility - activated.tag(:swap) - vertex = activated.vertex_named(name) - activated.set_payload(name, possibility) - if !vertex.requirements. - all? { |r| requirement_satisfied_by?(r, activated, possibility) } || - !new_spec_satisfied? - activated.rewind_to(:swap) - return + # Generates a filtered version of the existing vertex's `PossibilitySet` using the + # current state's `requirement` + # @param [Object] existing vertex + # @return [PossibilitySet] filtered possibility set + def filtered_possibility_set(vertex) + # Note: we can't just look at the intersection of `vertex.payload.possibilities` + # and `possibility.possibilities`, because if one of our requirements contains + # a prerelease version the associated prerelease versions will only appear in + # one set (but may match all requirements) + filtered_old_values = vertex.payload.possibilities.select do |poss| + requirement_satisfied_by?(requirement, activated, poss) end - fixup_swapped_children(vertex) - activate_spec - end - - # Ensures there are no orphaned successors to the given {vertex}. - # @param [DependencyGraph::Vertex] vertex the vertex to fix up. - # @return [void] - def fixup_swapped_children(vertex) # rubocop:disable Metrics/CyclomaticComplexity - payload = vertex.payload - deps = dependencies_for(payload).group_by(&method(:name_for)) - vertex.outgoing_edges.each do |outgoing_edge| - requirement = outgoing_edge.requirement - parent_index = @parents_of[requirement].last - succ = outgoing_edge.destination - matching_deps = Array(deps[succ.name]) - dep_matched = matching_deps.include?(requirement) - - # only push the current index when it was originally required by the - # same named spec - if parent_index && states[parent_index].name == name - @parents_of[requirement].push(states.size - 1) - end - - if matching_deps.empty? && !succ.root? && succ.predecessors.to_a == [vertex] - debug(depth) { "Removing orphaned spec #{succ.name} after swapping #{name}" } - succ.requirements.each { |r| @parents_of.delete(r) } - - removed_names = activated.detach_vertex_named(succ.name).map(&:name) - requirements.delete_if do |r| - # the only removed vertices are those with no other requirements, - # so it's safe to delete only based upon name here - removed_names.include?(name_for(r)) - end - elsif !dep_matched - debug(depth) { "Removing orphaned dependency #{requirement} after swapping #{name}" } - # also reset if we're removing the edge, but only if its parent has - # already been fixed up - @parents_of[requirement].push(states.size - 1) if @parents_of[requirement].empty? - - activated.delete_edge(outgoing_edge) - requirements.delete(requirement) - end + filtered_new_values = possibility.possibilities.select do |poss| + vertex.requirements.uniq.all? { |req| requirement_satisfied_by?(req, activated, poss) } end - end - # Attempts to activate the current {#possibility} (given that it hasn't - # already been activated) - # @return [void] - def attempt_to_activate_new_spec - if new_spec_satisfied? - activate_spec - else - create_conflict - unwind_for_conflict - end - end - - # @return [Boolean] whether the current spec is satisfied as a new - # possibility. - def new_spec_satisfied? - unless requirement_satisfied_by?(requirement, activated, possibility) - debug(depth) { 'Unsatisfied by requested spec' } - return false - end - - locked_requirement = locked_requirement_named(name) - - locked_spec_satisfied = !locked_requirement || - requirement_satisfied_by?(locked_requirement, activated, possibility) - debug(depth) { 'Unsatisfied by locked spec' } unless locked_spec_satisfied - - locked_spec_satisfied + PossibilitySet.new(vertex.payload.dependencies, filtered_old_values | filtered_new_values) end # @param [String] requirement_name the spec name to search for @@ -434,7 +730,7 @@ module Bundler::Molinillo # Add the current {#possibility} to the dependency graph of the current # {#state} # @return [void] - def activate_spec + def activate_new_spec conflicts.delete(name) debug(depth) { "Activated #{name} at #{possibility}" } activated.set_payload(name, possibility) @@ -442,14 +738,14 @@ module Bundler::Molinillo end # Requires the dependencies that the recently activated spec has - # @param [Object] activated_spec the specification that has just been + # @param [Object] activated_possibility the PossibilitySet that has just been # activated # @return [void] - def require_nested_dependencies_for(activated_spec) - nested_dependencies = dependencies_for(activated_spec) + def require_nested_dependencies_for(possibility_set) + nested_dependencies = dependencies_for(possibility_set.latest_version) debug(depth) { "Requiring nested dependencies (#{nested_dependencies.join(', ')})" } nested_dependencies.each do |d| - activated.add_child_vertex(name_for(d), nil, [name_for(activated_spec)], d) + activated.add_child_vertex(name_for(d), nil, [name_for(possibility_set.latest_version)], d) parent_index = states.size - 1 parents = @parents_of[d] parents << parent_index if parents.empty? @@ -464,20 +760,74 @@ module Bundler::Molinillo # @return [void] def push_state_for_requirements(new_requirements, requires_sort = true, new_activated = activated) new_requirements = sort_dependencies(new_requirements.uniq, new_activated, conflicts) if requires_sort - new_requirement = new_requirements.shift + new_requirement = nil + loop do + new_requirement = new_requirements.shift + break if new_requirement.nil? || states.none? { |s| s.requirement == new_requirement } + end new_name = new_requirement ? name_for(new_requirement) : ''.freeze - possibilities = new_requirement ? search_for(new_requirement) : [] + possibilities = possibilities_for_requirement(new_requirement) handle_missing_or_push_dependency_state DependencyState.new( new_name, new_requirements, new_activated, - new_requirement, possibilities, depth, conflicts.dup + new_requirement, possibilities, depth, conflicts.dup, unused_unwind_options.dup ) end + # Checks a proposed requirement with any existing locked requirement + # before generating an array of possibilities for it. + # @param [Object] the proposed requirement + # @return [Array] possibilities + def possibilities_for_requirement(requirement, activated = self.activated) + return [] unless requirement + if locked_requirement_named(name_for(requirement)) + return locked_requirement_possibility_set(requirement, activated) + end + + group_possibilities(search_for(requirement)) + end + + # @param [Object] the proposed requirement + # @return [Array] possibility set containing only the locked requirement, if any + def locked_requirement_possibility_set(requirement, activated = self.activated) + all_possibilities = search_for(requirement) + locked_requirement = locked_requirement_named(name_for(requirement)) + + # Longwinded way to build a possibilities array with either the locked + # requirement or nothing in it. Required, since the API for + # locked_requirement isn't guaranteed. + locked_possibilities = all_possibilities.select do |possibility| + requirement_satisfied_by?(locked_requirement, activated, possibility) + end + + group_possibilities(locked_possibilities) + end + + # Build an array of PossibilitySets, with each element representing a group of + # dependency versions that all have the same sub-dependency version constraints. + # @param [Array] an array of possibilities + # @return [Array] an array of possibility sets + def group_possibilities(possibilities) + possibility_sets = [] + possibility_sets_index = {} + + possibilities.reverse_each do |possibility| + dependencies = dependencies_for(possibility) + if index = possibility_sets_index[dependencies] + possibility_sets[index].possibilities.unshift(possibility) + else + possibility_sets << PossibilitySet.new(dependencies, [possibility]) + possibility_sets_index[dependencies] = possibility_sets.count - 1 + end + end + + possibility_sets.reverse + end + # Pushes a new {DependencyState}. # If the {#specification_provider} says to # {SpecificationProvider#allow_missing?} that particular requirement, and # there are no possibilities for that requirement, then `state` is not - # pushed, and the node in {#activated} is removed, and we continue + # pushed, and the vertex in {#activated} is removed, and we continue # resolving the remaining requirements. # @param [DependencyState] state # @return [void] diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/resolver.rb b/lib/bundler/vendor/molinillo/lib/molinillo/resolver.rb index 50d853b146..7d36858778 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/resolver.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/resolver.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'bundler/vendor/molinillo/lib/molinillo/dependency_graph' module Bundler::Molinillo diff --git a/lib/bundler/vendor/molinillo/lib/molinillo/state.rb b/lib/bundler/vendor/molinillo/lib/molinillo/state.rb index 3a8107cf1a..68fa1f54e3 100644 --- a/lib/bundler/vendor/molinillo/lib/molinillo/state.rb +++ b/lib/bundler/vendor/molinillo/lib/molinillo/state.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Bundler::Molinillo # A state that a {Resolution} can be in # @attr [String] name the name of the current requirement @@ -7,7 +8,8 @@ module Bundler::Molinillo # @attr [Object] requirement the current requirement # @attr [Object] possibilities the possibilities to satisfy the current requirement # @attr [Integer] depth the depth of the resolution - # @attr [Set<Object>] conflicts unresolved conflicts + # @attr [Hash] conflicts unresolved conflicts, indexed by dependency name + # @attr [Array<UnwindDetails>] unused_unwind_options unwinds for previous conflicts that weren't explored ResolutionState = Struct.new( :name, :requirements, @@ -15,14 +17,15 @@ module Bundler::Molinillo :requirement, :possibilities, :depth, - :conflicts + :conflicts, + :unused_unwind_options ) class ResolutionState # Returns an empty resolution state # @return [ResolutionState] an empty state def self.empty - new(nil, [], DependencyGraph.new, nil, nil, 0, Set.new) + new(nil, [], DependencyGraph.new, nil, nil, 0, {}, []) end end @@ -40,7 +43,8 @@ module Bundler::Molinillo requirement, [possibilities.pop], depth + 1, - conflicts.dup + conflicts.dup, + unused_unwind_options.dup ).tap do |state| state.activated.tag(state) end |