solve 3.1.1 → 4.0.4

Sign up to get free protection for your applications and to get access to all the features.
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