lib/solve/ruby_solver.rb in solve-3.1.1 vs lib/solve/ruby_solver.rb in solve-4.0.0

- old
+ new

@@ -1,8 +1,9 @@ -require 'set' -require 'molinillo' -require_relative 'solver/serializer' +require "set" +require "molinillo" +require "molinillo/modules/specification_provider" +require_relative "solver/serializer" module Solve class RubySolver class << self # The timeout (in seconds) to use when resolving graphs. Default is 10. This can be @@ -12,11 +13,11 @@ def timeout seconds = 30 unless seconds = ENV["SOLVE_TIMEOUT"] seconds.to_i * 1_000 end - # For optinal solver engines, this attempts to load depenencies. The + # For optional solver engines, this attempts to load depenencies. The # RubySolver is a non-optional component, so this is a no-op def activate true end end @@ -43,11 +44,11 @@ @graph = graph @demands_array = demands @timeout_ms = self.class.timeout @ui = options[:ui] # could be nil, but that's okay - @dependency_source = options[:dependency_source] || 'user-specified dependency' + @dependency_source = options[:dependency_source] || "user-specified dependency" @molinillo_graph = Molinillo::DependencyGraph.new @resolver = Molinillo::Resolver.new(self, self) end @@ -72,14 +73,14 @@ def resolve(options = {}) @ui = options[:ui] if options[:ui] solved_graph = resolve_with_error_wrapping - solution = solved_graph.map(&:payload) + solution = solved_graph.map(&:payload) unsorted_solution = solution.inject({}) do |stringified_soln, artifact| - stringified_soln[artifact.name] = artifact.version.to_s if artifact + stringified_soln[artifact.name] = artifact.version.to_s stringified_soln end if options[:sorted] build_sorted_solution(unsorted_solution) @@ -104,84 +105,146 @@ end # Callback required by Molinillo, called when the solve starts # @return nil def before_resolution - @ui.say('Starting dependency resolution') if @ui + @ui.say("Starting dependency resolution") if @ui end # Callback required by Molinillo, called when the solve is complete. # @return nil def after_resolution - @ui.say('Finished dependency resolution') if @ui + @ui.say("Finished dependency resolution") if @ui end # Callback required by Molinillo, called when resolving every progress_rate # @return nil def indicate_progress nil end # Callback required by Molinillo, gives debug information about the solution # @return nil - def debug(current_resolver_depth) + def debug(current_resolver_depth = 0) # debug info will be returned if you call yield here, but it seems to be # broken in current Molinillo @ui.say(yield) if @ui end - # Callback required by Molinillo - # @return [String] the dependency's name - def name_for(dependency) - dependency.name - end + include Molinillo::SpecificationProvider # Callback required by Molinillo - # @return [Array<Solve::Dependency>] the dependencies sorted by preference. - def sort_dependencies(dependencies, activated, conflicts) - dependencies.sort_by do |dependency| - name = name_for(dependency) - [ - activated.vertex_named(name).payload ? 0 : 1, - conflicts[name] ? 0 : 1, - activated.vertex_named(name).payload ? 0 : graph.versions(dependency.name).count, - ] - end - end - - # Callback required by Molinillo + # Search for the specifications that match the given dependency. + # The specifications in the returned array will be considered in reverse + # order, so the latest version ought to be last. + # @note This method should be 'pure', i.e. the return value should depend + # only on the `dependency` parameter. + # + # @param [Object] dependency # @return [Array<Solve::Artifact>] the artifacts that match the dependency. def search_for(dependency) # This array gets mutated by Molinillo; it's okay because sort returns a # new array. graph.versions(dependency.name, dependency.constraint).sort end # Callback required by Molinillo - # @return [Boolean] + # Returns the dependencies of `specification`. + # @note This method should be 'pure', i.e. the return value should depend + # only on the `specification` parameter. + # + # @param [Object] specification + # @return [Array<Solve::Dependency>] the dependencies of the given artifact + def dependencies_for(specification) + specification.dependencies + end + + # Callback required by Molinillo + # Determines whether the given `requirement` is satisfied by the given + # `spec`, in the context of the current `activated` dependency graph. + # + # @param [Object] requirement + # @param [DependencyGraph] activated the current dependency graph in the + # resolution process. + # @param [Object] spec + # @return [Boolean] whether `requirement` is satisfied by `spec` in the + # context of the current `activated` dependency graph. def requirement_satisfied_by?(requirement, activated, spec) - requirement.constraint.satisfies?(spec.version) + version = spec.version + return false unless requirement.constraint.satisfies?(version) + shared_possibility_versions = possibility_versions(requirement, activated) + return false if !shared_possibility_versions.empty? && !shared_possibility_versions.include?(version) + true end + # Searches the current dependency graph to find previously activated + # requirements for the current artifact. + # + # @param [Object] requirement + # @param [DependencyGraph] activated the current dependency graph in the + # resolution process. + # @return [Array<Semverse::Version> the list of currently activated versions + # of this requirement + def possibility_versions(requirement, activated) + activated.vertices.values.flat_map do |vertex| + + next unless vertex.payload + + next unless vertex.name == requirement.name + + if vertex.payload.respond_to?(:possibilities) + vertex.payload.possibilities.map(&:version) + else + vertex.payload.version + end + end.compact + end + private :possibility_versions + # Callback required by Molinillo - # @return [Array<Solve::Dependency>] the dependencies of the given artifact - def dependencies_for(specification) - specification.dependencies + # Returns the name for the given `dependency`. + # @note This method should be 'pure', i.e. the return value should depend + # only on the `dependency` parameter. + # + # @param [Object] dependency + # @return [String] the name for the given `dependency`. + def name_for(dependency) + dependency.name end + # Callback required by Molinillo # @return [String] the name of the source of explicit dependencies, i.e. # those passed to {Resolver#resolve} directly. def name_for_explicit_dependency_source @dependency_source end - # @return [String] the name of the source of 'locked' dependencies, i.e. - # those passed to {Resolver#resolve} directly as the `base` - def name_for_locking_dependency_source - 'Lockfile' + # Callback required by Molinillo + # Sort dependencies so that the ones that are easiest to resolve are first. + # Easiest to resolve is (usually) defined by: + # 1) Is this dependency already activated? + # 2) How relaxed are the requirements? + # 3) Are there any conflicts for this dependency? + # 4) How many possibilities are there to satisfy this dependency? + # + # @param [Array<Object>] dependencies + # @param [DependencyGraph] activated the current dependency graph in the + # resolution process. + # @param [{String => Array<Conflict>}] conflicts + # @return [Array<Solve::Dependency>] the dependencies sorted by preference. + def sort_dependencies(dependencies, activated, conflicts) + dependencies.sort_by do |dependency| + name = name_for(dependency) + [ + activated.vertex_named(name).payload ? 0 : 1, + conflicts[name] ? 0 : 1, + search_for(dependency).count, + ] + end end + # Callback required by Molinillo # Returns whether this dependency, which has no possible matching # specifications, can safely be ignored. # # @param [Object] dependency # @return [Boolean] whether this dependency can safely be skipped. @@ -189,37 +252,37 @@ false end private - def resolve_with_error_wrapping - @resolver.resolve(demands, @molinillo_graph) - rescue Molinillo::VersionConflict, Molinillo::CircularDependencyError => e - raise Solve::Errors::NoSolutionError.new(e.message) + def resolve_with_error_wrapping + @resolver.resolve(demands, @molinillo_graph) + rescue Molinillo::VersionConflict, Molinillo::CircularDependencyError => e + raise Solve::Errors::NoSolutionError.new(e.message) + end + + def build_sorted_solution(unsorted_solution) + nodes = Hash.new + unsorted_solution.each do |name, version| + nodes[name] = @graph.artifact(name, version).dependencies.map(&:name) end - def build_sorted_solution(unsorted_solution) - nodes = Hash.new - unsorted_solution.each do |name, version| - nodes[name] = @graph.artifact(name, version).dependencies.map(&:name) + # Modified from http://ruby-doc.org/stdlib-1.9.3/libdoc/tsort/rdoc/TSort.html + class << nodes + include TSort + alias tsort_each_node each_key + def tsort_each_child(node, &block) + fetch(node).each(&block) end + end + begin + sorted_names = nodes.tsort + rescue TSort::Cyclic => e + raise Solve::Errors::UnsortableSolutionError.new(e, unsorted_solution) + end - # Modified from http://ruby-doc.org/stdlib-1.9.3/libdoc/tsort/rdoc/TSort.html - class << nodes - include TSort - alias tsort_each_node each_key - def tsort_each_child(node, &block) - fetch(node).each(&block) - end - end - begin - sorted_names = nodes.tsort - rescue TSort::Cyclic => e - raise Solve::Errors::UnsortableSolutionError.new(e, unsorted_solution) - end - - sorted_names.map do |artifact| - [artifact, unsorted_solution[artifact]] - end + sorted_names.map do |artifact| + [artifact, unsorted_solution[artifact]] end + end end end