solve 3.1.1 → 4.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 892c2181eaeac640092b4589fa4f8dbce3347618
4
- data.tar.gz: e003bdcc9234dfc11b9150de314d7095b982bb5a
2
+ SHA256:
3
+ metadata.gz: 5d0ae05eff693dfa5424fa6d2c92f96839d997da2d585505992e70f91e6ab863
4
+ data.tar.gz: 4ddae0850f4f234d98208ed8e42f6a2073b43e6f1d6696935c09127cd30c0bf8
5
5
  SHA512:
6
- metadata.gz: 337dcf09b925483a50494a1682bce00c4bf3653f69a163490f9764517b740f0213063b1f3fce53ceb1a138cf6c540ffd11a4afc1f75905993c1151c76424a110
7
- data.tar.gz: 0f7de65be2bb07b365c5db821d8802d6d87319fa5b4890e8546bb9be434631a6cb232e40883323231bbcbcec00dc94bc43db12f4d60085044fdf9e00d6c6243f
6
+ metadata.gz: 8c4b234445b2b64082b1cdfecad5f4b49b3c13b6757237033c4b23d359a28d9899f24eb2545cf686f238a037504f1151c6924f0c376e7819b142335e6d5255a3
7
+ data.tar.gz: 67dc423bc183e3c7b680ba9078340179556561837ef0eb14b0f3f650bbc3e4bd0430137d20a1e1998dca4e7c17f1890f7379147bf0e258a1fa1fad0bbbf3278e
@@ -1,14 +1,14 @@
1
- require 'semverse'
1
+ require "semverse"
2
2
 
3
3
  module Solve
4
- require_relative 'solve/artifact'
5
- require_relative 'solve/demand'
6
- require_relative 'solve/dependency'
7
- require_relative 'solve/version'
8
- require_relative 'solve/errors'
9
- require_relative 'solve/graph'
10
- require_relative 'solve/ruby_solver'
11
- require_relative 'solve/gecode_solver'
4
+ require_relative "solve/artifact"
5
+ require_relative "solve/demand"
6
+ require_relative "solve/dependency"
7
+ require_relative "solve/version"
8
+ require_relative "solve/errors"
9
+ require_relative "solve/graph"
10
+ require_relative "solve/ruby_solver"
11
+ require_relative "solve/gecode_solver"
12
12
 
13
13
  # We have to set the default engine here, it gets set on the wrong object if
14
14
  # we put this in the metaclass context below.
@@ -23,7 +23,6 @@ module Solve
23
23
  # @return [Symbol]
24
24
  attr_reader :engine
25
25
 
26
-
27
26
  # Sets the solving backend engine. Solve supports 2 engines:
28
27
  # * `:ruby` - Molinillo, a pure ruby solver
29
28
  # * `:gecode` - dep-selector, a wrapper around the Gecode CSP solver library
@@ -42,6 +41,7 @@ module Solve
42
41
  else
43
42
  engine_class.activate
44
43
  end
44
+
45
45
  @engine = selected_engine
46
46
  end
47
47
 
@@ -77,4 +77,3 @@ module Solve
77
77
  end
78
78
 
79
79
  end
80
-
@@ -74,7 +74,7 @@ module Solve
74
74
  # .depends('ntp', '~> 1.3')
75
75
  #
76
76
  # @return [Solve::Artifact]
77
- def depends(name, constraint = '>= 0.0.0')
77
+ def depends(name, constraint = ">= 0.0.0")
78
78
  unless dependency?(name, constraint)
79
79
  set_dependency(name, constraint)
80
80
  end
@@ -91,8 +91,8 @@ module Solve
91
91
  # @return [Boolean]
92
92
  def ==(other)
93
93
  other.is_a?(self.class) &&
94
- self.name == other.name &&
95
- self.version == other.version
94
+ name == other.name &&
95
+ version == other.version
96
96
  end
97
97
  alias_method :eql?, :==
98
98
 
@@ -100,17 +100,17 @@ module Solve
100
100
  #
101
101
  # @return [Integer]
102
102
  def <=>(other)
103
- self.version <=> other.version
103
+ version <=> other.version
104
104
  end
105
105
 
106
106
  private
107
107
 
108
- def get_dependency(name, constraint)
109
- @dependencies["#{name}-#{constraint}"]
110
- end
108
+ def get_dependency(name, constraint)
109
+ @dependencies["#{name}-#{constraint}"]
110
+ end
111
111
 
112
- def set_dependency(name, constraint)
113
- @dependencies["#{name}-#{constraint}"] = Dependency.new(self, name, constraint)
114
- end
112
+ def set_dependency(name, constraint)
113
+ @dependencies["#{name}-#{constraint}"] = Dependency.new(self, name, constraint)
114
+ end
115
115
  end
116
116
  end
@@ -48,17 +48,17 @@ module Solve
48
48
  end
49
49
 
50
50
  split_version = case version.to_s
51
- when /^(\d+)\.(\d+)\.(\d+)(-([0-9a-z\-\.]+))?(\+([0-9a-z\-\.]+))?$/i
52
- [ $1.to_i, $2.to_i, $3.to_i, $5, $7 ]
53
- when /^(\d+)\.(\d+)\.(\d+)?$/
54
- [ $1.to_i, $2.to_i, $3.to_i, nil, nil ]
55
- when /^(\d+)\.(\d+)?$/
56
- [ $1.to_i, $2.to_i, nil, nil, nil ]
57
- when /^(\d+)$/
58
- [ $1.to_i, nil, nil, nil, nil ]
59
- else
60
- raise Errors::InvalidConstraintFormat.new(constraint)
61
- end
51
+ when /^(\d+)\.(\d+)\.(\d+)(-([0-9a-z\-\.]+))?(\+([0-9a-z\-\.]+))?$/i
52
+ [ $1.to_i, $2.to_i, $3.to_i, $5, $7 ]
53
+ when /^(\d+)\.(\d+)\.(\d+)?$/
54
+ [ $1.to_i, $2.to_i, $3.to_i, nil, nil ]
55
+ when /^(\d+)\.(\d+)?$/
56
+ [ $1.to_i, $2.to_i, nil, nil, nil ]
57
+ when /^(\d+)$/
58
+ [ $1.to_i, nil, nil, nil, nil ]
59
+ else
60
+ raise Errors::InvalidConstraintFormat.new(constraint)
61
+ end
62
62
 
63
63
  [ operator, split_version ].flatten
64
64
  end
@@ -110,30 +110,30 @@ module Solve
110
110
  def compare_approx(constraint, target_version)
111
111
  min = constraint.version
112
112
  max = if constraint.patch.nil?
113
- Semverse::Version.new([min.major + 1, 0, 0, 0])
114
- elsif constraint.build
115
- identifiers = constraint.version.identifiers(:build)
116
- replace = identifiers.last.to_i.to_s == identifiers.last.to_s ? "-" : nil
117
- Semverse::Version.new([min.major, min.minor, min.patch, min.pre_release, identifiers.fill(replace, -1).join('.')])
118
- elsif constraint.pre_release
119
- identifiers = constraint.version.identifiers(:pre_release)
120
- replace = identifiers.last.to_i.to_s == identifiers.last.to_s ? "-" : nil
121
- Semverse::Version.new([min.major, min.minor, min.patch, identifiers.fill(replace, -1).join('.')])
122
- else
123
- Semverse::Version.new([min.major, min.minor + 1, 0, 0])
124
- end
113
+ Semverse::Version.new([min.major + 1, 0, 0, 0])
114
+ elsif constraint.build
115
+ identifiers = constraint.version.identifiers(:build)
116
+ replace = identifiers.last.to_i.to_s == identifiers.last.to_s ? "-" : nil
117
+ Semverse::Version.new([min.major, min.minor, min.patch, min.pre_release, identifiers.fill(replace, -1).join(".")])
118
+ elsif constraint.pre_release
119
+ identifiers = constraint.version.identifiers(:pre_release)
120
+ replace = identifiers.last.to_i.to_s == identifiers.last.to_s ? "-" : nil
121
+ Semverse::Version.new([min.major, min.minor, min.patch, identifiers.fill(replace, -1).join(".")])
122
+ else
123
+ Semverse::Version.new([min.major, min.minor + 1, 0, 0])
124
+ end
125
125
  min <= target_version && target_version < max
126
126
  end
127
127
  end
128
128
 
129
129
  OPERATOR_TYPES = {
130
130
  "~>" => :approx,
131
- "~" => :approx,
131
+ "~" => :approx,
132
132
  ">=" => :greater_than_equal,
133
133
  "<=" => :less_than_equal,
134
- "=" => :equal,
135
- ">" => :greater_than,
136
- "<" => :less_than,
134
+ "=" => :equal,
135
+ ">" => :greater_than,
136
+ "<" => :less_than,
137
137
  }.freeze
138
138
 
139
139
  COMPARE_FUNS = {
@@ -142,10 +142,10 @@ module Solve
142
142
  greater_than: method(:compare_gt),
143
143
  less_than_equal: method(:compare_lte),
144
144
  less_than: method(:compare_lt),
145
- equal: method(:compare_equal)
145
+ equal: method(:compare_equal),
146
146
  }.freeze
147
147
 
148
- REGEXP = /^(#{OPERATOR_TYPES.keys.join('|')})\s?(.+)$/
148
+ REGEXP = /^(#{OPERATOR_TYPES.keys.join('|')})\s?(.+)$/.freeze
149
149
 
150
150
  attr_reader :operator
151
151
  attr_reader :major
@@ -164,7 +164,7 @@ module Solve
164
164
  def initialize(constraint = nil)
165
165
  constraint = constraint.to_s
166
166
  if constraint.nil? || constraint.empty?
167
- constraint = '>= 0.0.0'
167
+ constraint = ">= 0.0.0"
168
168
  end
169
169
 
170
170
  @operator, @major, @minor, @patch, @pre_release, @build = self.class.split(constraint)
@@ -175,18 +175,18 @@ module Solve
175
175
  end
176
176
 
177
177
  @version = Semverse::Version.new([
178
- self.major,
179
- self.minor,
180
- self.patch,
181
- self.pre_release,
182
- self.build,
178
+ major,
179
+ minor,
180
+ patch,
181
+ pre_release,
182
+ build,
183
183
  ])
184
184
  end
185
185
 
186
186
  # @return [Symbol]
187
187
  def operator_type
188
- unless type = OPERATOR_TYPES.fetch(operator)
189
- raise RuntimeError, "unknown operator type: #{operator}"
188
+ unless ( type = OPERATOR_TYPES.fetch(operator) )
189
+ raise "unknown operator type: #{operator}"
190
190
  end
191
191
 
192
192
  type
@@ -201,7 +201,7 @@ module Solve
201
201
  def satisfies?(target)
202
202
  target = Semverse::Version.coerce(target)
203
203
 
204
- return false if !version.zero? && greedy_match?(target)
204
+ return false if !(version == 0) && greedy_match?(target)
205
205
 
206
206
  compare(target)
207
207
  end
@@ -215,13 +215,13 @@ module Solve
215
215
  # @return [Boolean]
216
216
  def ==(other)
217
217
  other.is_a?(self.class) &&
218
- self.operator == other.operator &&
219
- self.version == other.version
218
+ operator == other.operator &&
219
+ version == other.version
220
220
  end
221
221
  alias_method :eql?, :==
222
222
 
223
223
  def inspect
224
- "#<#{self.class.to_s} #{to_s}>"
224
+ "#<#{self.class} #{self}>"
225
225
  end
226
226
 
227
227
  def to_s
@@ -241,15 +241,15 @@ module Solve
241
241
  #
242
242
  # @param [Semverse::Version] target_version
243
243
  #
244
- def greedy_match?(target_version)
245
- operator_type !~ /less/ && target_version.pre_release? && !version.pre_release?
246
- end
244
+ def greedy_match?(target_version)
245
+ operator_type !~ /less/ && target_version.pre_release? && !version.pre_release?
246
+ end
247
247
 
248
248
  # @param [Semverse::Version] target
249
249
  #
250
250
  # @return [Boolean]
251
- def compare(target)
252
- COMPARE_FUNS.fetch(operator_type).call(self, target)
253
- end
251
+ def compare(target)
252
+ COMPARE_FUNS.fetch(operator_type).call(self, target)
253
+ end
254
254
  end
255
255
  end
@@ -30,8 +30,8 @@ module Solve
30
30
 
31
31
  def ==(other)
32
32
  other.is_a?(self.class) &&
33
- self.name == other.name &&
34
- self.constraint == other.constraint
33
+ name == other.name &&
34
+ constraint == other.constraint
35
35
  end
36
36
  alias_method :eql?, :==
37
37
  end
@@ -27,14 +27,16 @@ module Solve
27
27
  def to_s
28
28
  "#{name} (#{constraint})"
29
29
  end
30
+ alias :inspect :to_s
30
31
 
31
32
  # @param [Object] other
32
33
  #
33
34
  # @return [Boolean]
34
35
  def ==(other)
35
36
  other.is_a?(self.class) &&
36
- self.artifact == other.artifact &&
37
- self.constraint == other.constraint
37
+ name == other.name &&
38
+ artifact == other.artifact &&
39
+ constraint == other.constraint
38
40
  end
39
41
  alias_method :eql?, :==
40
42
  end
@@ -47,14 +47,14 @@ module Solve
47
47
  def to_s
48
48
  s = ""
49
49
  s << "#{@message}\n"
50
- s << "Missing artifacts: #{missing_artifacts.join(',')}\n" unless missing_artifacts.empty?
50
+ s << "Missing artifacts: #{missing_artifacts.join(",")}\n" unless missing_artifacts.empty?
51
51
  unless constraints_excluding_all_artifacts.empty?
52
- pretty = constraints_excluding_all_artifacts.map { |constraint| "(#{constraint[0]} #{constraint[1]})" }.join(',')
52
+ pretty = constraints_excluding_all_artifacts.map { |constraint| "(#{constraint[0]} #{constraint[1]})" }.join(",")
53
53
  s << "Constraints that match no available version: #{pretty}\n"
54
54
  end
55
55
  s << "Demand that cannot be met: #{unsatisfiable_demand}\n" if unsatisfiable_demand
56
56
  unless artifacts_with_no_satisfactory_version.empty?
57
- s << "Artifacts for which there are conflicting dependencies: #{artifacts_with_no_satisfactory_version.join(',')}"
57
+ s << "Artifacts for which there are conflicting dependencies: #{artifacts_with_no_satisfactory_version.join(",")}"
58
58
  end
59
59
  s
60
60
  end
@@ -1,6 +1,6 @@
1
- require 'set'
2
- require 'solve/errors'
3
- require_relative 'solver/serializer'
1
+ require "set" unless defined?(Set)
2
+ require_relative "errors"
3
+ require_relative "solver/serializer"
4
4
 
5
5
  module Solve
6
6
  class GecodeSolver
@@ -10,13 +10,13 @@ module Solve
10
10
  #
11
11
  # @return [Integer]
12
12
  def timeout
13
- seconds = 30 unless seconds = ENV["SOLVE_TIMEOUT"]
13
+ seconds = 30 unless ( seconds = ENV["SOLVE_TIMEOUT"] )
14
14
  seconds.to_i * 1_000
15
15
  end
16
16
 
17
17
  # Attemp to load the dep_selector gem which this solver engine requires.
18
18
  def activate
19
- require 'dep_selector'
19
+ require "dep_selector"
20
20
  rescue LoadError => e
21
21
  raise Errors::EngineNotAvailable, "dep_selector is not installed, GecodeSolver cannot be used (#{e})"
22
22
  end
@@ -84,126 +84,129 @@ module Solve
84
84
  private
85
85
 
86
86
  # DepSelector::DependencyGraph object representing the problem.
87
- attr_reader :ds_graph
87
+ attr_reader :ds_graph
88
88
 
89
89
  # Timeout in milliseconds. Hardcoded to 1s for now.
90
- attr_reader :timeout_ms
90
+ attr_reader :timeout_ms
91
91
 
92
92
  # Runs the solver with the set of demands given. If any DepSelector
93
93
  # exceptions are raised, they are rescued and re-raised
94
- def solve_demands(demands_as_constraints)
95
- selector = DepSelector::Selector.new(ds_graph, (timeout_ms / 1000.0))
96
- selector.find_solution(demands_as_constraints, all_artifacts)
97
- rescue DepSelector::Exceptions::InvalidSolutionConstraints => e
98
- report_invalid_constraints_error(e)
99
- rescue DepSelector::Exceptions::NoSolutionExists => e
100
- report_no_solution_error(e)
101
- rescue DepSelector::Exceptions::TimeBoundExceeded
102
- # DepSelector timed out trying to find the solution. There may or may
103
- # not be a solution.
104
- raise Solve::Errors::NoSolutionError.new(
105
- "The dependency constraints could not be solved in the time allotted.")
106
- rescue DepSelector::Exceptions::TimeBoundExceededNoSolution
107
- # DepSelector determined there wasn't a solution to the problem, then
108
- # timed out trying to determine which constraints cause the conflict.
109
- raise Solve::Errors::NoSolutionCauseUnknown.new(
110
- "There is a dependency conflict, but the solver could not determine the precise cause in the time allotted.")
111
- end
94
+ def solve_demands(demands_as_constraints)
95
+ selector = DepSelector::Selector.new(ds_graph, (timeout_ms / 1000.0))
96
+ selector.find_solution(demands_as_constraints, all_artifacts)
97
+ rescue DepSelector::Exceptions::InvalidSolutionConstraints => e
98
+ report_invalid_constraints_error(e)
99
+ rescue DepSelector::Exceptions::NoSolutionExists => e
100
+ report_no_solution_error(e)
101
+ rescue DepSelector::Exceptions::TimeBoundExceeded
102
+ # DepSelector timed out trying to find the solution. There may or may
103
+ # not be a solution.
104
+ raise Solve::Errors::NoSolutionError.new(
105
+ "The dependency constraints could not be solved in the time allotted."
106
+ )
107
+ rescue DepSelector::Exceptions::TimeBoundExceededNoSolution
108
+ # DepSelector determined there wasn't a solution to the problem, then
109
+ # timed out trying to determine which constraints cause the conflict.
110
+ raise Solve::Errors::NoSolutionCauseUnknown.new(
111
+ "There is a dependency conflict, but the solver could not determine the precise cause in the time allotted."
112
+ )
113
+ end
112
114
 
113
115
  # Maps demands to corresponding DepSelector::SolutionConstraint objects.
114
- def demands_as_constraints
115
- @demands_as_constraints ||= demands_array.map do |demands_item|
116
- item_name, constraint_with_operator = demands_item
117
- version_constraint = Semverse::Constraint.new(constraint_with_operator)
118
- DepSelector::SolutionConstraint.new(ds_graph.package(item_name), version_constraint)
119
- end
116
+ def demands_as_constraints
117
+ @demands_as_constraints ||= demands_array.map do |demands_item|
118
+ item_name, constraint_with_operator = demands_item
119
+ version_constraint = Semverse::Constraint.new(constraint_with_operator)
120
+ DepSelector::SolutionConstraint.new(ds_graph.package(item_name), version_constraint)
120
121
  end
122
+ end
121
123
 
122
124
  # Maps all artifacts in the graph to DepSelector::Package objects. If not
123
125
  # already done, artifacts are added to the ds_graph as a necessary side effect.
124
- def all_artifacts
125
- return @all_artifacts if @all_artifacts
126
- populate_ds_graph!
127
- @all_artifacts
128
- end
126
+ def all_artifacts
127
+ return @all_artifacts if @all_artifacts
128
+
129
+ populate_ds_graph!
130
+ @all_artifacts
131
+ end
129
132
 
130
133
  # Converts artifacts to DepSelector::Package objects and adds them to the
131
134
  # DepSelector graph. This should only be called once; use #all_artifacts
132
135
  # to safely get the set of all artifacts.
133
- def populate_ds_graph!
134
- @all_artifacts = Set.new
136
+ def populate_ds_graph!
137
+ @all_artifacts = Set.new
135
138
 
136
- graph.artifacts.each do |artifact|
137
- add_artifact_to_ds_graph(artifact)
138
- @all_artifacts << ds_graph.package(artifact.name)
139
- end
139
+ graph.artifacts.each do |artifact|
140
+ add_artifact_to_ds_graph(artifact)
141
+ @all_artifacts << ds_graph.package(artifact.name)
140
142
  end
143
+ end
141
144
 
142
- def add_artifact_to_ds_graph(artifact)
143
- package_version = ds_graph.package(artifact.name).add_version(artifact.version)
144
- artifact.dependencies.each do |dependency|
145
- dependency = DepSelector::Dependency.new(ds_graph.package(dependency.name), dependency.constraint)
146
- package_version.dependencies << dependency
147
- end
148
- package_version
145
+ def add_artifact_to_ds_graph(artifact)
146
+ package_version = ds_graph.package(artifact.name).add_version(artifact.version)
147
+ artifact.dependencies.each do |dependency|
148
+ dependency = DepSelector::Dependency.new(ds_graph.package(dependency.name), dependency.constraint)
149
+ package_version.dependencies << dependency
149
150
  end
151
+ package_version
152
+ end
150
153
 
151
- def report_invalid_constraints_error(e)
152
- non_existent_cookbooks = e.non_existent_packages.inject([]) do |list, constraint|
153
- list << constraint.package.name
154
- end
155
-
156
- constrained_to_no_versions = e.constrained_to_no_versions.inject([]) do |list, constraint|
157
- list << [constraint.package.name, constraint.constraint.to_s]
158
- end
154
+ def report_invalid_constraints_error(e)
155
+ non_existent_cookbooks = e.non_existent_packages.inject([]) do |list, constraint|
156
+ list << constraint.package.name
157
+ end
159
158
 
160
- raise Solve::Errors::NoSolutionError.new(
161
- "Required artifacts do not exist at the desired version",
162
- missing_artifacts: non_existent_cookbooks,
163
- constraints_excluding_all_artifacts: constrained_to_no_versions
164
- )
159
+ constrained_to_no_versions = e.constrained_to_no_versions.inject([]) do |list, constraint|
160
+ list << [constraint.package.name, constraint.constraint.to_s]
165
161
  end
166
162
 
167
- def report_no_solution_error(e)
168
- most_constrained_cookbooks = e.disabled_most_constrained_packages.inject([]) do |list, package|
169
- list << "#{package.name} = #{package.versions.first.to_s}"
170
- end
163
+ raise Solve::Errors::NoSolutionError.new(
164
+ "Required artifacts do not exist at the desired version",
165
+ missing_artifacts: non_existent_cookbooks,
166
+ constraints_excluding_all_artifacts: constrained_to_no_versions
167
+ )
168
+ end
171
169
 
172
- non_existent_cookbooks = e.disabled_non_existent_packages.inject([]) do |list, package|
173
- list << package.name
174
- end
170
+ def report_no_solution_error(e)
171
+ most_constrained_cookbooks = e.disabled_most_constrained_packages.inject([]) do |list, package|
172
+ list << "#{package.name} = #{package.versions.first}"
173
+ end
175
174
 
176
- raise Solve::Errors::NoSolutionError.new(
177
- e.message,
178
- unsatisfiable_demand: e.unsatisfiable_solution_constraint.to_s,
179
- missing_artifacts: non_existent_cookbooks,
180
- artifacts_with_no_satisfactory_version: most_constrained_cookbooks
181
- )
175
+ non_existent_cookbooks = e.disabled_non_existent_packages.inject([]) do |list, package|
176
+ list << package.name
182
177
  end
183
178
 
184
- def build_sorted_solution(unsorted_solution)
185
- nodes = Hash.new
186
- unsorted_solution.each do |name, version|
187
- nodes[name] = @graph.artifact(name, version).dependencies.map(&:name)
188
- end
179
+ raise Solve::Errors::NoSolutionError.new(
180
+ e.message,
181
+ unsatisfiable_demand: e.unsatisfiable_solution_constraint.to_s,
182
+ missing_artifacts: non_existent_cookbooks,
183
+ artifacts_with_no_satisfactory_version: most_constrained_cookbooks
184
+ )
185
+ end
189
186
 
190
- # Modified from http://ruby-doc.org/stdlib-1.9.3/libdoc/tsort/rdoc/TSort.html
191
- class << nodes
192
- include TSort
193
- alias tsort_each_node each_key
194
- def tsort_each_child(node, &block)
195
- fetch(node).each(&block)
196
- end
197
- end
198
- begin
199
- sorted_names = nodes.tsort
200
- rescue TSort::Cyclic => e
201
- raise Solve::Errors::UnsortableSolutionError.new(e, unsorted_solution)
202
- end
187
+ def build_sorted_solution(unsorted_solution)
188
+ nodes = {}
189
+ unsorted_solution.each do |name, version|
190
+ nodes[name] = @graph.artifact(name, version).dependencies.map(&:name)
191
+ end
203
192
 
204
- sorted_names.map do |artifact|
205
- [artifact, unsorted_solution[artifact]]
193
+ # Modified from http://ruby-doc.org/stdlib-1.9.3/libdoc/tsort/rdoc/TSort.html
194
+ class << nodes
195
+ include TSort
196
+ alias tsort_each_node each_key
197
+ def tsort_each_child(node, &block)
198
+ fetch(node).each(&block)
206
199
  end
207
200
  end
201
+ begin
202
+ sorted_names = nodes.tsort
203
+ rescue TSort::Cyclic => e
204
+ raise Solve::Errors::UnsortableSolutionError.new(e, unsorted_solution)
205
+ end
206
+
207
+ sorted_names.map do |artifact|
208
+ [artifact, unsorted_solution[artifact]]
209
+ end
210
+ end
208
211
  end
209
212
  end