solve 0.8.2 → 1.0.0.rc1

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.
data/lib/solve/demand.rb CHANGED
@@ -12,31 +12,16 @@ module Solve
12
12
 
13
13
  # The acceptable constraint of the artifact this demand is for
14
14
  #
15
- # @return [Solve::Constraint]
15
+ # @return [Semverse::Constraint]
16
16
  attr_reader :constraint
17
17
 
18
18
  # @param [Solve::Solver] solver
19
19
  # @param [#to_s] name
20
- # @param [Solve::Constraint, #to_s] constraint
21
- def initialize(solver, name, constraint = ">= 0.0.0")
22
- @solver = solver
23
- @name = name
24
- @constraint = if constraint.is_a?(Solve::Constraint)
25
- constraint
26
- else
27
- Constraint.new(constraint.to_s)
28
- end
29
- end
30
-
31
- # Remove this demand from the solver it belongs to
32
- #
33
- # @return [Solve::Demand, nil]
34
- def delete
35
- unless solver.nil?
36
- result = solver.remove_demand(self)
37
- @solver = nil
38
- result
39
- end
20
+ # @param [Semverse::Constraint, #to_s] constraint
21
+ def initialize(solver, name, constraint = Semverse::DEFAULT_CONSTRAINT)
22
+ @solver = solver
23
+ @name = name
24
+ @constraint = Semverse::Constraint.coerce(constraint)
40
25
  end
41
26
 
42
27
  def to_s
@@ -12,32 +12,20 @@ module Solve
12
12
 
13
13
  # The constraint requirement of this dependency
14
14
  #
15
- # @return [Solve::Constraint]
15
+ # @return [Semverse::Constraint]
16
16
  attr_reader :constraint
17
17
 
18
18
  # @param [Solve::Artifact] artifact
19
19
  # @param [#to_s] name
20
- # @param [Solve::Constraint, #to_s] constraint
21
- def initialize(artifact, name, constraint = ">= 0.0.0")
22
- @artifact = artifact
23
- @name = name
24
- @constraint = case constraint
25
- when Solve::Constraint
26
- constraint
27
- else
28
- Constraint.new(constraint)
29
- end
20
+ # @param [Semverse::Constraint, #to_s] constraint
21
+ def initialize(artifact, name, constraint = Semverse::DEFAULT_CONSTRAINT)
22
+ @artifact = artifact
23
+ @name = name
24
+ @constraint = Semverse::Constraint.coerce(constraint)
30
25
  end
31
26
 
32
- # Remove this dependency from the artifact it belongs to
33
- #
34
- # @return [Solve::Dependency, nil]
35
- def delete
36
- unless artifact.nil?
37
- result = artifact.remove_dependency(self)
38
- @artifact = nil
39
- result
40
- end
27
+ def to_s
28
+ "#{name} (#{constraint})"
41
29
  end
42
30
 
43
31
  # @param [Object] other
@@ -45,8 +33,8 @@ module Solve
45
33
  # @return [Boolean]
46
34
  def ==(other)
47
35
  other.is_a?(self.class) &&
48
- self.artifact == other.artifact &&
49
- self.constraint == other.constraint
36
+ self.artifact == other.artifact &&
37
+ self.constraint == other.constraint
50
38
  end
51
39
  alias_method :eql?, :==
52
40
  end
data/lib/solve/errors.rb CHANGED
@@ -4,33 +4,59 @@ module Solve
4
4
  alias_method :mesage, :to_s
5
5
  end
6
6
 
7
- class InvalidVersionFormat < SolveError
8
- attr_reader :version
7
+ class NoSolutionError < SolveError
9
8
 
10
- # @param [#to_s] version
11
- def initialize(version)
12
- @version = version
13
- end
9
+ # Artifacts that don't exist at any version but are required for a valid
10
+ # solution
11
+ # @return [Array<String>] Missing artifact names
12
+ attr_reader :missing_artifacts
14
13
 
15
- def to_s
16
- "'#{version}' did not contain a valid version string: 'x.y.z' or 'x.y'."
17
- end
18
- end
14
+ # Constraints that eliminate all versions of an artifact, e.g. you ask
15
+ # for mysql >= 2.0.0 but only 1.0.0 exists.
16
+ # @return [Array<String>] Invalid constraints as strings
17
+ attr_reader :constraints_excluding_all_artifacts
18
+
19
+ # A demand that has conflicting dependencies
20
+ # @return [String] the unsatisfiable demand
21
+ attr_reader :unsatisfiable_demand
19
22
 
20
- class InvalidConstraintFormat < SolveError
21
- attr_reader :constraint
23
+ # The artifact for which there are conflicting dependencies
24
+ # @return [Array<String>] The "most constrained" artifacts
25
+ attr_reader :artifacts_with_no_satisfactory_version
22
26
 
23
- # @param [#to_s] constraint
24
- def initialize(constraint)
25
- @constraint = constraint
27
+ # @param [#to_s] message
28
+ # @option causes [Array<String>] :missing_artifacts ([])
29
+ # @option causes [Array<String>] :constraints_excluding_all_artifacts ([])
30
+ # @option causes [#to_s] :unsatisfiable_demand (nil)
31
+ # @option causes [Array<String>] :artifacts_with_no_satisfactory_version ([])
32
+ def initialize(message = nil, causes = {})
33
+ super(message)
34
+ @message = message
35
+ @missing_artifacts = causes[:missing_artifacts] || []
36
+ @constraints_excluding_all_artifacts = causes[:constraints_excluding_all_artifacts] || []
37
+ @unsatisfiable_demand = causes[:unsatisfiable_demand] || nil
38
+ @artifacts_with_no_satisfactory_version = causes[:artifacts_with_no_satisfactory_version] || []
26
39
  end
27
40
 
28
41
  def to_s
29
- "'#{constraint}' did not contain a valid operator or a valid version string."
42
+ s = ""
43
+ s << "#{@message}\n"
44
+ s << "Missing artifacts: #{missing_artifacts.join(',')}\n" unless missing_artifacts.empty?
45
+ unless constraints_excluding_all_artifacts.empty?
46
+ s << "Constraints that match no available version: #{constraints_excluding_all_artifacts.join(',')}\n"
47
+ end
48
+ s << "Demand that cannot be met: #{unsatisfiable_demand}\n" if unsatisfiable_demand
49
+ unless artifacts_with_no_satisfactory_version.empty?
50
+ s << "Artifacts for which there are conflicting dependencies: #{artifacts_with_no_satisfactory_version.join(',')}"
51
+ end
52
+ s
30
53
  end
54
+
31
55
  end
32
56
 
33
- class NoSolutionError < SolveError; end
57
+ # Indicates that the solver could not find the conflicting constraints when
58
+ # solving the given demands and graph.
59
+ class NoSolutionCauseUnknown < NoSolutionError; end
34
60
 
35
61
  class UnsortableSolutionError < SolveError
36
62
  attr_reader :internal_exception
@@ -1,3 +1,3 @@
1
1
  module Solve
2
- VERSION = "0.8.2"
2
+ VERSION = "1.0.0.rc1"
3
3
  end
data/lib/solve/graph.rb CHANGED
@@ -1,143 +1,71 @@
1
1
  module Solve
2
2
  class Graph
3
- class << self
4
- # Create a key for a graph from an instance of an Artifact or Dependency
5
- #
6
- # @param [Solve::Artifact, Solve::Dependency] object
7
- #
8
- # @raise [ArgumentError] if an instance of an object of an unknown type is given
9
- #
10
- # @return [Symbol]
11
- def key_for(object)
12
- case object
13
- when Solve::Artifact
14
- artifact_key(object.name, object.version)
15
- when Solve::Dependency
16
- dependency_key(object.name, object.constraint)
17
- else
18
- raise ArgumentError, "Could not generate graph key for Class: #{object.class}"
19
- end
20
- end
21
-
22
- # Create a key representing an artifact for an instance of Graph
23
- #
24
- # @param [#to_s] name
25
- # @param [#to_s] version
26
- #
27
- # @return [Symbol]
28
- def artifact_key(name, version)
29
- "#{name}-#{version}".to_sym
30
- end
31
-
32
- # Create a key representing an dependency for an instance of Graph
33
- #
34
- # @param [#to_s] name
35
- # @param [#to_s] constraint
36
- #
37
- # @return [Symbol]
38
- def dependency_key(name, constraint)
39
- "#{name}-#{constraint}".to_sym
40
- end
41
- end
42
-
43
3
  def initialize
44
- @artifacts = Hash.new
45
- end
46
-
47
- # @overload artifacts(name, version)
48
- # Return the Solve::Artifact from the collection of artifacts
49
- # with the given name and version.
50
- #
51
- # @param [#to_s]
52
- # @param [Solve::Version, #to_s]
53
- #
54
- # @return [Solve::Artifact]
55
- # @overload artifacts
56
- # Return the collection of artifacts
57
- #
58
- # @return [Array<Solve::Artifact>]
59
- def artifacts(*args)
60
- if args.empty?
61
- return artifact_collection
62
- end
63
- unless args.length == 2
64
- raise ArgumentError, "Unexpected number of arguments. You gave: #{args.length}. Expected: 0 or 2."
65
- end
66
-
67
- name, version = args
68
-
69
- if name.nil? || version.nil?
70
- raise ArgumentError, "A name and version must be specified. You gave: #{args}."
71
- end
72
-
73
- artifact = Artifact.new(self, name, version)
74
- add_artifact(artifact)
4
+ @artifacts = {}
5
+ @artifacts_by_name = Hash.new { |hash, key| hash[key] = [] }
75
6
  end
76
7
 
77
- # Return all the artifacts from the collection of artifacts
78
- # with the given name.
8
+ # Check if an artifact with a matching name and version is a member of this instance
9
+ # of graph
79
10
  #
80
11
  # @param [String] name
12
+ # @param [Semverse::Version, #to_s] version
81
13
  #
82
- # @return [Array<Solve::Artifact>]
83
- def versions(name, constraint = ">= 0.0.0")
84
- constraint = constraint.is_a?(Constraint) ? constraint : Constraint.new(constraint)
85
-
86
- artifacts.select do |art|
87
- art.name == name && constraint.satisfies?(art.version)
88
- end
14
+ # @return [Boolean]
15
+ def artifact?(name, version)
16
+ !find(name, version).nil?
89
17
  end
18
+ alias_method :has_artifact?, :artifact?
90
19
 
91
- # Add a Solve::Artifact to the collection of artifacts and
92
- # return the added Solve::Artifact. No change will be made
93
- # if the artifact is already a member of the collection.
94
- #
95
- # @param [Solve::Artifact] artifact
96
- #
97
- # @return [Solve::Artifact]
98
- def add_artifact(artifact)
99
- unless has_artifact?(artifact.name, artifact.version)
100
- @artifacts[self.class.key_for(artifact)] = artifact
101
- end
102
-
103
- get_artifact(artifact.name, artifact.version)
20
+ def find(name, version)
21
+ @artifacts["#{name}-#{version}"]
104
22
  end
105
23
 
106
- # Retrieve the artifact from the graph with the matching name and version
24
+ # Add an artifact to the graph
107
25
  #
108
26
  # @param [String] name
109
- # @param [Solve::Version, #to_s] version
110
- #
111
- # @return [Solve::Artifact, nil]
112
- def get_artifact(name, version)
113
- @artifacts.fetch(self.class.artifact_key(name, version.to_s), nil)
27
+ # @Param [String] version
28
+ def artifact(name, version)
29
+ unless artifact?(name, version)
30
+ artifact = Artifact.new(self, name, version)
31
+ @artifacts["#{name}-#{version}"] = artifact
32
+ @artifacts_by_name[name] << artifact
33
+ end
34
+
35
+ @artifacts["#{name}-#{version}"]
114
36
  end
115
37
 
116
- # Remove the given instance of artifact from the graph
38
+ # Return the collection of artifacts
117
39
  #
118
- # @param [Solve::Artifact, nil] artifact
119
- def remove_artifact(artifact)
120
- if has_artifact?(artifact.name, artifact.version)
121
- @artifacts.delete(self.class.key_for(artifact))
122
- end
40
+ # @return [Array<Solve::Artifact>]
41
+ def artifacts
42
+ @artifacts.values
123
43
  end
124
44
 
125
- # Check if an artifact with a matching name and version is a member of this instance
126
- # of graph
45
+ # Return all the artifacts from the collection of artifacts
46
+ # with the given name.
127
47
  #
128
48
  # @param [String] name
129
- # @param [Solve::Version, #to_s] version
130
49
  #
131
- # @return [Boolean]
132
- def has_artifact?(name, version)
133
- !get_artifact(name, version).nil?
50
+ # @return [Array<Solve::Artifact>]
51
+ def versions(name, constraint = Semverse::DEFAULT_CONSTRAINT)
52
+ constraint = Semverse::Constraint.coerce(constraint)
53
+
54
+ if constraint == Semverse::DEFAULT_CONSTRAINT
55
+ @artifacts_by_name[name]
56
+ else
57
+ @artifacts_by_name[name].select do |artifact|
58
+ constraint.satisfies?(artifact.version)
59
+ end
60
+ end
134
61
  end
135
62
 
136
63
  # @param [Object] other
137
64
  #
138
65
  # @return [Boolean]
139
66
  def ==(other)
140
- return false unless other.is_a?(self.class)
67
+ return false unless other.is_a?(Graph)
68
+ return false unless artifacts.size == other.artifacts.size
141
69
 
142
70
  self_artifacts = self.artifacts
143
71
  other_artifacts = other.artifacts
@@ -150,18 +78,10 @@ module Solve
150
78
  list << artifact.dependencies
151
79
  end.flatten
152
80
 
153
- self_artifacts.size == other_artifacts.size &&
154
- self_dependencies.size == other_dependencies.size &&
155
- self_artifacts.all? { |artifact| other_artifacts.include?(artifact) } &&
156
- self_dependencies.all? { |dependency| other_dependencies.include?(dependency) }
81
+ self_dependencies.size == other_dependencies.size &&
82
+ self_artifacts.all? { |artifact| other_artifacts.include?(artifact) } &&
83
+ self_dependencies.all? { |dependency| other_dependencies.include?(dependency) }
157
84
  end
158
85
  alias_method :eql?, :==
159
-
160
- private
161
-
162
- # @return [Array<Solve::Artifact>]
163
- def artifact_collection
164
- @artifacts.collect { |name, artifact| artifact }
165
- end
166
86
  end
167
87
  end
data/lib/solve/solver.rb CHANGED
@@ -1,90 +1,40 @@
1
- require 'tsort'
2
- require_relative 'solver/variable_table'
3
- require_relative 'solver/variable_row'
4
- require_relative 'solver/constraint_table'
5
- require_relative 'solver/constraint_row'
1
+ require 'dep_selector'
2
+ require 'set'
6
3
  require_relative 'solver/serializer'
7
4
 
8
5
  module Solve
9
6
  class Solver
10
- class << self
11
- # Create a key to identify a demand on a Solver.
12
- #
13
- # @param [Solve::Demand] demand
14
- #
15
- # @raise [NoSolutionError]
16
- #
17
- # @return [Symbol]
18
- def demand_key(demand)
19
- "#{demand.name}-#{demand.constraint}".to_sym
20
- end
21
-
22
- # Returns all of the versions which satisfy all of the given constraints
23
- #
24
- # @param [Array<Solve::Constraint>, Array<String>] constraints
25
- # @param [Array<Solve::Version>, Array<String>] versions
26
- #
27
- # @return [Array<Solve::Version>]
28
- def satisfy_all(constraints, versions)
29
- constraints = Array(constraints).collect do |con|
30
- con.is_a?(Constraint) ? con : Constraint.new(con.to_s)
31
- end.uniq
32
-
33
- versions = Array(versions).collect do |ver|
34
- ver.is_a?(Version) ? ver : Version.new(ver.to_s)
35
- end.uniq
36
-
37
- versions.select do |ver|
38
- constraints.all? { |constraint| constraint.satisfies?(ver) }
39
- end
40
- end
41
-
42
- # Return the best version from the given list of versions for the given list of constraints
43
- #
44
- # @param [Array<Solve::Constraint>, Array<String>] constraints
45
- # @param [Array<Solve::Version>, Array<String>] versions
46
- #
47
- # @raise [NoSolutionError] if version matches the given constraints
48
- #
49
- # @return [Solve::Version]
50
- def satisfy_best(constraints, versions)
51
- solution = satisfy_all(constraints, versions)
52
-
53
- if solution.empty?
54
- raise Errors::NoSolutionError
55
- end
56
-
57
- solution.sort.last
58
- end
59
- end
60
-
61
- # The world as we know it
7
+ # Graph object with references to all known artifacts and dependency
8
+ # constraints.
62
9
  #
63
10
  # @return [Solve::Graph]
64
11
  attr_reader :graph
65
- attr_reader :demands
66
- attr_reader :ui
67
12
 
68
- attr_reader :domain
69
- attr_reader :variable_table
70
- attr_reader :constraint_table
71
- attr_reader :possible_values
13
+ # @example Demands are Arrays of Arrays with an artifact name and optional constraint:
14
+ # [['nginx', '= 1.0.0'], ['mysql']]
15
+ # @return [Array<String>, Array<Array<String, String>>] demands
16
+ attr_reader :demands_array
72
17
 
18
+ # @example Basic use:
19
+ # graph = Solve::Graph.new
20
+ # graph.artifacts("mysql", "1.2.0")
21
+ # demands = [["mysql"]]
22
+ # Solver.new(graph, demands)
73
23
  # @param [Solve::Graph] graph
74
24
  # @param [Array<String>, Array<Array<String, String>>] demands
75
25
  # @param [#say] ui
76
- def initialize(graph, demands = Array.new, ui=nil)
26
+ def initialize(graph, demands, ui = nil)
27
+ @ds_graph = DepSelector::DependencyGraph.new
77
28
  @graph = graph
78
- @demands = Hash.new
79
- @ui = ui.respond_to?(:say) ? ui : nil
80
-
81
- @domain = Hash.new
82
- @possible_values = Hash.new
83
- @constraint_table = ConstraintTable.new
84
- @variable_table = VariableTable.new
29
+ @demands_array = demands
30
+ @timeout_ms = 1_000
31
+ end
85
32
 
86
- Array(demands).each do |l_demand|
87
- demands(*l_demand)
33
+ # The problem demands given as Demand model objects
34
+ # @return [Array<Solve::Demand>]
35
+ def demands
36
+ demands_array.map do |name, constraint|
37
+ Demand.new(self, name, constraint)
88
38
  end
89
39
  end
90
40
 
@@ -92,227 +42,149 @@ module Solve
92
42
  # return the solution as a sorted list instead of a Hash
93
43
  #
94
44
  # @return [Hash, List] Returns a hash like { "Artifact Name" => "Version",... }
95
- # unless options[:sorted], then it returns a list like [["Artifact Name", "Version],...]
45
+ # unless the :sorted option is true, then it returns a list like [["Artifact Name", "Version],...]
46
+ # @raise [Errors::NoSolutionError] when the demands cannot be met for the
47
+ # given graph.
48
+ # @raise [Errors::UnsortableSolutionError] when the :sorted option is true
49
+ # and the demands have a solution, but the solution contains a cyclic
50
+ # dependency
96
51
  def resolve(options = {})
97
- Solve.tracer.start
98
- seed_demand_dependencies
99
-
100
- while unbound_variable = variable_table.first_unbound
101
- possible_values_for_unbound = possible_values_for(unbound_variable)
102
- constraints = constraint_table.constraints_on_artifact(unbound_variable.artifact)
103
- Solve.tracer.searching_for(unbound_variable, constraints, possible_values)
104
-
105
- while possible_value = possible_values_for_unbound.shift
106
- possible_artifact = graph.get_artifact(unbound_variable.artifact, possible_value.version)
107
- possible_dependencies = possible_artifact.dependencies
108
- all_ok = possible_dependencies.all? { |dependency| can_add_new_constraint?(dependency) }
109
- if all_ok
110
- Solve.tracer.trying(possible_artifact)
111
- add_dependencies(possible_dependencies, possible_artifact)
112
- unbound_variable.bind(possible_value)
113
- break
114
- end
115
- end
52
+ solution = solve_demands(demands_as_constraints)
116
53
 
117
- unless unbound_variable.bound?
118
- backtrack(unbound_variable)
119
- end
54
+ unsorted_solution = solution.inject({}) do |stringified_soln, (name, version)|
55
+ stringified_soln[name] = version.to_s
56
+ stringified_soln
120
57
  end
121
58
 
122
- solution = (options[:sorted]) ? build_sorted_solution : build_unsorted_solution
123
-
124
- Solve.tracer.solution(solution)
125
-
126
- solution
127
- end
128
-
129
- def build_unsorted_solution
130
- {}.tap do |solution|
131
- variable_table.rows.each do |variable|
132
- solution[variable.artifact] = variable.value.version.to_s
133
- end
59
+ if options[:sorted]
60
+ build_sorted_solution(unsorted_solution)
61
+ else
62
+ unsorted_solution
134
63
  end
135
64
  end
136
65
 
137
- def build_sorted_solution
138
- unsorted_solution = build_unsorted_solution
139
- nodes = Hash.new
140
- unsorted_solution.each do |name, version|
141
- nodes[name] = @graph.get_artifact(name, version).dependencies.map(&:name)
142
- end
66
+ private
143
67
 
144
- # Modified from http://ruby-doc.org/stdlib-1.9.3/libdoc/tsort/rdoc/TSort.html
145
- class << nodes
146
- include TSort
147
- alias tsort_each_node each_key
148
- def tsort_each_child(node, &block)
149
- fetch(node).each(&block)
68
+ # DepSelector::DependencyGraph object representing the problem.
69
+ attr_reader :ds_graph
70
+
71
+ # Timeout in milliseconds. Hardcoded to 1s for now.
72
+ attr_reader :timeout_ms
73
+
74
+ # Runs the solver with the set of demands given. If any DepSelector
75
+ # exceptions are raised, they are rescued and re-raised
76
+ def solve_demands(demands_as_constraints)
77
+ selector = DepSelector::Selector.new(ds_graph, (timeout_ms / 1000.0))
78
+ selector.find_solution(demands_as_constraints, all_artifacts)
79
+ rescue DepSelector::Exceptions::InvalidSolutionConstraints => e
80
+ report_invalid_constraints_error(e)
81
+ rescue DepSelector::Exceptions::NoSolutionExists => e
82
+ report_no_solution_error(e)
83
+ rescue DepSelector::Exceptions::TimeBoundExceeded
84
+ # DepSelector timed out trying to find the solution. There may or may
85
+ # not be a solution.
86
+ raise Solve::Errors::NoSolutionError.new(
87
+ "The dependency constraints could not be solved in the time allotted.")
88
+ rescue DepSelector::Exceptions::TimeBoundExceededNoSolution
89
+ # DepSelector determined there wasn't a solution to the problem, then
90
+ # timed out trying to determine which constraints cause the conflict.
91
+ raise Solve::Errors::NoSolutionCauseUnknown.new(
92
+ "There is a dependency conflict, but the solver could not determine the precise cause in the time allotted.")
93
+ end
94
+
95
+ # Maps demands to corresponding DepSelector::SolutionConstraint objects.
96
+ def demands_as_constraints
97
+ @demands_as_constraints ||= demands_array.map do |demands_item|
98
+ item_name, constraint_with_operator = demands_item
99
+ version_constraint = Semverse::Constraint.new(constraint_with_operator)
100
+ DepSelector::SolutionConstraint.new(ds_graph.package(item_name), version_constraint)
150
101
  end
151
102
  end
152
- begin
153
- sorted_names = nodes.tsort
154
- rescue TSort::Cyclic => e
155
- raise Solve::Errors::UnsortableSolutionError.new(e, unsorted_solution)
156
- end
157
-
158
- sorted_names.map do |artifact|
159
- [artifact, unsorted_solution[artifact]]
160
- end
161
- end
162
-
163
- # @overload demands(name, constraint)
164
- # Return the Solve::Demand from the collection of demands
165
- # with the given name and constraint.
166
- #
167
- # @param [#to_s]
168
- # @param [Solve::Constraint, #to_s]
169
- #
170
- # @return [Solve::Demand]
171
- # @overload demands(name)
172
- # Return the Solve::Demand from the collection of demands
173
- # with the given name.
174
- #
175
- # @param [#to_s]
176
- #
177
- # @return [Solve::Demand]
178
- # @overload demands
179
- # Return the collection of demands
180
- #
181
- # @return [Array<Solve::Demand>]
182
- def demands(*args)
183
- if args.empty?
184
- return demand_collection
185
- end
186
- if args.length > 2
187
- raise ArgumentError, "Unexpected number of arguments. You gave: #{args.length}. Expected: 2 or less."
188
- end
189
103
 
190
- name, constraint = args
191
- constraint ||= ">= 0.0.0"
192
-
193
- if name.nil?
194
- raise ArgumentError, "A name must be specified. You gave: #{args}."
104
+ # Maps all artifacts in the graph to DepSelector::Package objects. If not
105
+ # already done, artifacts are added to the ds_graph as a necessary side effect.
106
+ def all_artifacts
107
+ return @all_artifacts if @all_artifacts
108
+ populate_ds_graph!
109
+ @all_artifacts
195
110
  end
196
111
 
197
- demand = Demand.new(self, name, constraint)
198
- add_demand(demand)
199
- end
112
+ # Converts artifacts to DepSelector::Package objects and adds them to the
113
+ # DepSelector graph. This should only be called once; use #all_artifacts
114
+ # to safely get the set of all artifacts.
115
+ def populate_ds_graph!
116
+ @all_artifacts = Set.new
200
117
 
201
- # Add a Solve::Demand to the collection of demands and
202
- # return the added Solve::Demand. No change will be made
203
- # if the demand is already a member of the collection.
204
- #
205
- # @param [Solve::Demand] demand
206
- #
207
- # @return [Solve::Demand]
208
- def add_demand(demand)
209
- unless has_demand?(demand)
210
- @demands[self.class.demand_key(demand)] = demand
118
+ graph.artifacts.each do |artifact|
119
+ add_artifact_to_ds_graph(artifact)
120
+ @all_artifacts << ds_graph.package(artifact.name)
121
+ end
211
122
  end
212
123
 
213
- demand
214
- end
215
- alias_method :demand, :add_demand
216
-
217
- # @param [Solve::Demand, nil] demand
218
- def remove_demand(demand)
219
- if has_demand?(demand)
220
- @demands.delete(self.class.demand_key(demand))
124
+ def add_artifact_to_ds_graph(artifact)
125
+ package_version = ds_graph.package(artifact.name).add_version(artifact.version)
126
+ artifact.dependencies.each do |dependency|
127
+ dependency = DepSelector::Dependency.new(ds_graph.package(dependency.name), dependency.constraint)
128
+ package_version.dependencies << dependency
129
+ end
130
+ package_version
221
131
  end
222
- end
223
-
224
- # @param [Solve::Demand] demand
225
- #
226
- # @return [Boolean]
227
- def has_demand?(demand)
228
- @demands.has_key?(self.class.demand_key(demand))
229
- end
230
132
 
231
- private
232
-
233
- # @return [Array<Solve::Demand>]
234
- def demand_collection
235
- @demands.collect { |name, demand| demand }
236
- end
133
+ def report_invalid_constraints_error(e)
134
+ non_existent_cookbooks = e.non_existent_packages.inject([]) do |list, constraint|
135
+ list << constraint.package.name
136
+ end
237
137
 
238
- def seed_demand_dependencies
239
- add_dependencies(demands, :root)
240
- end
138
+ constrained_to_no_versions = e.constrained_to_no_versions.inject([]) do |list, constraint|
139
+ list << constraint.to_s
140
+ end
241
141
 
242
- def can_add_new_constraint?(dependency)
243
- current_binding = variable_table.find_artifact(dependency.name)
244
- #haven't seen it before, haven't bound it yet or the binding is ok
245
- current_binding.nil? || current_binding.value.nil? || dependency.constraint.satisfies?(current_binding.value.version)
142
+ raise Solve::Errors::NoSolutionError.new(
143
+ "Required artifacts do not exist at the desired version",
144
+ missing_artifacts: non_existent_cookbooks,
145
+ constraints_excluding_all_artifacts: constrained_to_no_versions
146
+ )
246
147
  end
247
148
 
248
- def possible_values_for(variable)
249
- possible_values_for_variable = possible_values[variable.artifact]
250
- if possible_values_for_variable.nil?
251
- constraints_for_variable = constraint_table.constraints_on_artifact(variable.artifact)
252
- all_values_for_variable = domain[variable.artifact]
253
- possible_values_for_variable = constraints_for_variable.inject(all_values_for_variable) do |remaining_values, constraint|
254
- remaining_values.reject { |value| !constraint.satisfies?(value.version) }
255
- end
256
- possible_values[variable.artifact] = possible_values_for_variable
149
+ def report_no_solution_error(e)
150
+ most_constrained_cookbooks = e.disabled_most_constrained_packages.inject([]) do |list, package|
151
+ list << "#{package.name} = #{package.versions.first.to_s}"
257
152
  end
258
- possible_values_for_variable
259
- end
260
-
261
- def add_dependencies(dependencies, source)
262
- dependencies.each do |dependency|
263
- next if (source.respond_to?(:name) && dependency.name == source.name)
264
- Solve.tracer.add_constraint(dependency, source)
265
- variable_table.add(dependency.name, source)
266
- constraint_table.add(dependency, source)
267
- dependency_domain = graph.versions(dependency.name, dependency.constraint)
268
- domain[dependency.name] = [(domain[dependency.name] || []), dependency_domain]
269
- .flatten
270
- .uniq
271
- .sort { |left, right| right.version <=> left.version }
272
-
273
- #if the variable we are constraining is still unbound, we want to filter
274
- #its possible values, if its already bound, we know its ok to add this constraint because
275
- #we can never change a previously bound value without removing this constraint and we check above
276
- #whether or not its ok to add this constraint given the current value
277
-
278
- variable = variable_table.find_artifact(dependency.name)
279
- if variable.value.nil?
280
- reset_possible_values_for(variable)
281
- end
282
153
 
154
+ non_existent_cookbooks = e.disabled_non_existent_packages.inject([]) do |list, package|
155
+ list << package.name
283
156
  end
284
- end
285
157
 
286
- def reset_possible_values_for(variable)
287
- Solve.tracer.reset_domain(variable)
288
- possible_values[variable.artifact] = nil
289
- possible_values_for(variable).tap { |values| Solve.tracer.possible_values(values) }
158
+ raise Solve::Errors::NoSolutionError.new(
159
+ e.message,
160
+ unsatisfiable_demand: e.unsatisfiable_solution_constraint.to_s,
161
+ missing_artifacts: non_existent_cookbooks,
162
+ artifacts_with_no_satisfactory_version: most_constrained_cookbooks
163
+ )
290
164
  end
291
165
 
292
- def backtrack(unbound_variable)
293
- Solve.tracer.backtrack(unbound_variable)
294
- previous_variable = variable_table.before(unbound_variable.artifact)
295
-
296
- if previous_variable.nil?
297
- Solve.tracer.cannot_backtrack
298
- raise Errors::NoSolutionError
166
+ def build_sorted_solution(unsorted_solution)
167
+ nodes = Hash.new
168
+ unsorted_solution.each do |name, version|
169
+ nodes[name] = @graph.artifact(name, version).dependencies.map(&:name)
299
170
  end
300
171
 
301
- Solve.tracer.unbind(previous_variable)
302
-
303
- source = previous_variable.value
304
- removed_variables = variable_table.remove_all_with_only_this_source!(source)
305
- removed_variables.each do |removed_variable|
306
- possible_values[removed_variable.artifact] = nil
307
- Solve.tracer.remove_variable(removed_variable)
172
+ # Modified from http://ruby-doc.org/stdlib-1.9.3/libdoc/tsort/rdoc/TSort.html
173
+ class << nodes
174
+ include TSort
175
+ alias tsort_each_node each_key
176
+ def tsort_each_child(node, &block)
177
+ fetch(node).each(&block)
178
+ end
308
179
  end
309
- removed_constraints = constraint_table.remove_constraints_from_source!(source)
310
- removed_constraints.each do |removed_constraint|
311
- Solve.tracer.remove_constraint(removed_constraint)
180
+ begin
181
+ sorted_names = nodes.tsort
182
+ rescue TSort::Cyclic => e
183
+ raise Solve::Errors::UnsortableSolutionError.new(e, unsorted_solution)
312
184
  end
313
- previous_variable.unbind
314
- variable_table.all_after(previous_variable.artifact).each do |variable|
315
- new_possibles = reset_possible_values_for(variable)
185
+
186
+ sorted_names.map do |artifact|
187
+ [artifact, unsorted_solution[artifact]]
316
188
  end
317
189
  end
318
190
  end