solve 1.2.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +11 -6
- data/Gemfile +8 -8
- data/NoGecode.gemfile +4 -0
- data/README.md +16 -0
- data/Rakefile +1 -0
- data/Thorfile +5 -0
- data/lib/solve.rb +43 -2
- data/lib/solve/demand.rb +2 -2
- data/lib/solve/errors.rb +6 -0
- data/lib/solve/{solver.rb → gecode_solver.rb} +12 -4
- data/lib/solve/ruby_solver.rb +207 -0
- data/lib/solve/solver/serializer.rb +37 -15
- data/lib/solve/version.rb +1 -1
- data/solve.gemspec +7 -1
- data/spec/acceptance/benchmark.rb +17 -3
- data/spec/acceptance/ruby_solver_solutions_spec.rb +307 -0
- data/spec/acceptance/solutions_spec.rb +6 -1
- data/spec/unit/solve/{solver_spec.rb → gecode_solver_spec.rb} +34 -4
- data/spec/unit/solve/ruby_solver_spec.rb +166 -0
- data/spec/unit/solve/solver/serializer_spec.rb +22 -14
- data/spec/unit/solve_spec.rb +2 -2
- metadata +71 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 674f7a18cf70685b1cd690b5babdb327b8186c5e
|
4
|
+
data.tar.gz: 784d73c2042284cc1c8100f2e4097b07d1dc4ae3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8dd9fe4edfff28b0a34b9223ae2163d346bec2a306e3319d3e559362477e82883080cc4031a567a065f17da5b99cc9231e9fed21a5cf08f102056f32400b08e9
|
7
|
+
data.tar.gz: 7a5b6622bd17b7d8d492c62f8e941db4137a05d963a5fc71b0ea8f8a8d5868cadb360456ae723e21cd9ffb451ff84f81f6e82409775c355926512fff18e8ab44
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -4,9 +4,14 @@ before_install:
|
|
4
4
|
script: "bundle exec thor spec"
|
5
5
|
language: ruby
|
6
6
|
env: USE_SYSTEM_GECODE=1
|
7
|
-
bundler_args: --without
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
7
|
+
bundler_args: --without dev
|
8
|
+
|
9
|
+
matrix:
|
10
|
+
include:
|
11
|
+
- rvm: 1.9.3
|
12
|
+
- rvm: 2.0.0
|
13
|
+
- rvm: 2.1.0
|
14
|
+
- rvm: jruby-19mode
|
15
|
+
- rvm: 2.1
|
16
|
+
gemfile: NoGecode.gemfile
|
17
|
+
script: "bundle exec thor nogecode_spec"
|
data/Gemfile
CHANGED
@@ -2,7 +2,14 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
gemspec
|
4
4
|
|
5
|
-
group :
|
5
|
+
group :gecode do
|
6
|
+
gem "dep_selector", "~> 1.0"
|
7
|
+
end
|
8
|
+
|
9
|
+
# If this group is named "development", then `bundle install --without
|
10
|
+
# development` automagically excludes development dependencies that are listed
|
11
|
+
# in the gemspec, which will skip installing rspec and then we can't run tests.
|
12
|
+
group :dev do
|
6
13
|
gem 'fuubar'
|
7
14
|
gem 'yard'
|
8
15
|
gem 'redcarpet'
|
@@ -24,10 +31,3 @@ group :development do
|
|
24
31
|
end
|
25
32
|
end
|
26
33
|
|
27
|
-
group :test do
|
28
|
-
gem 'thor', '>= 0.16.0'
|
29
|
-
gem 'rake', '>= 0.9.2.2'
|
30
|
-
|
31
|
-
gem 'spork'
|
32
|
-
gem 'rspec'
|
33
|
-
end
|
data/NoGecode.gemfile
ADDED
data/README.md
CHANGED
@@ -38,6 +38,22 @@ NOTE: This will raise Solve::Errors::UnsortableSolutionError if the solution con
|
|
38
38
|
|
39
39
|
Solve.it!(graph, [['nginx', '>= 0.100.0']], sorted: true)
|
40
40
|
|
41
|
+
|
42
|
+
### Selecting A Resolver
|
43
|
+
|
44
|
+
Solve supports two different resolvers. A pure Ruby solver implemented using [Molinillo](https://github.com/CocoaPods/Molinillo) and the same dependency resolver the Chef Server uses, [dep-selector](https://github.com/chef/dep-selector), which is a Ruby C extension for [Gecode](https://github.com/ampl/gecode).
|
45
|
+
|
46
|
+
You can set the resolver by calling `Solver.engine=` with the symbol `:ruby` or `:gecode`.
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
Solver.engine = :ruby
|
50
|
+
Solver.engine = :gecode
|
51
|
+
```
|
52
|
+
|
53
|
+
The Ruby solver is installed and enabled by default. If you'd like to use the Gecode solver you can do so by installing the dep-selector gem or adding it to your Gemfile:
|
54
|
+
|
55
|
+
$ gem install dep_selector
|
56
|
+
|
41
57
|
### Increasing the solver's timeout
|
42
58
|
|
43
59
|
By default the solver will wait 30 seconds before giving up on finding a solution. Under certain conditions a graph may be too complicated to solve within the alotted time. To increase the timeout you can set the "SOLVE_TIMEOUT" environment variable to the amount of seconds desired.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
data/Thorfile
CHANGED
data/lib/solve.rb
CHANGED
@@ -7,12 +7,42 @@ module Solve
|
|
7
7
|
require_relative 'solve/version'
|
8
8
|
require_relative 'solve/errors'
|
9
9
|
require_relative 'solve/graph'
|
10
|
-
require_relative 'solve/
|
10
|
+
require_relative 'solve/ruby_solver'
|
11
|
+
require_relative 'solve/gecode_solver'
|
11
12
|
|
12
13
|
class << self
|
13
14
|
# @return [Solve::Formatter]
|
14
15
|
attr_reader :tracer
|
15
16
|
|
17
|
+
@engine = :ruby
|
18
|
+
|
19
|
+
# Returns the currently configured engine.
|
20
|
+
# @see #engine=
|
21
|
+
# @return [Symbol]
|
22
|
+
attr_reader :engine
|
23
|
+
|
24
|
+
|
25
|
+
# Sets the solving backend engine. Solve supports 2 engines:
|
26
|
+
# * `:ruby` - Molinillo, a pure ruby solver
|
27
|
+
# * `:gecode` - dep-selector, a wrapper around the Gecode CSP solver library
|
28
|
+
#
|
29
|
+
# Note that dep-selector is an optional dependency and may therefore not be
|
30
|
+
# available.
|
31
|
+
#
|
32
|
+
# @param [Symbol] selected_engine
|
33
|
+
# @return [Symbol] selected_engine
|
34
|
+
# @raise [Solve::Errors::EngineNotAvailable] when the selected engine's deps aren't installed.
|
35
|
+
# @raise [Solve::Errors::InvalidEngine] when `selected_engine` is invalid.
|
36
|
+
def engine=(selected_engine)
|
37
|
+
engine_class = solver_for_engine(selected_engine)
|
38
|
+
if engine_class.nil?
|
39
|
+
raise Errors::InvalidEngine, "Engine `#{selected_engine}` is not supported. Valid values are `:ruby`, `:gecode`"
|
40
|
+
else
|
41
|
+
engine_class.activate
|
42
|
+
end
|
43
|
+
@engine = selected_engine
|
44
|
+
end
|
45
|
+
|
16
46
|
# A quick solve. Given the "world" as we know it (the graph) and a list of
|
17
47
|
# requirements (demands) which must be met. Return me the best solution of
|
18
48
|
# artifacts and verisons that I should use.
|
@@ -29,7 +59,18 @@ module Solve
|
|
29
59
|
#
|
30
60
|
# @return [Hash]
|
31
61
|
def it!(graph, demands, options = {})
|
32
|
-
|
62
|
+
solver_for_engine(engine).new(graph, demands, options).resolve(options)
|
33
63
|
end
|
64
|
+
|
65
|
+
def solver_for_engine(engine_name)
|
66
|
+
case engine_name
|
67
|
+
when :ruby
|
68
|
+
RubySolver
|
69
|
+
when :gecode
|
70
|
+
GecodeSolver
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private :solver_for_engine
|
34
75
|
end
|
35
76
|
end
|
data/lib/solve/demand.rb
CHANGED
@@ -2,7 +2,7 @@ module Solve
|
|
2
2
|
class Demand
|
3
3
|
# A reference to the solver this demand belongs to
|
4
4
|
#
|
5
|
-
# @return [Solve::
|
5
|
+
# @return [Solve::RubySolver,Solve::GecodeSolver]
|
6
6
|
attr_reader :solver
|
7
7
|
|
8
8
|
# The name of the artifact this demand is for
|
@@ -15,7 +15,7 @@ module Solve
|
|
15
15
|
# @return [Semverse::Constraint]
|
16
16
|
attr_reader :constraint
|
17
17
|
|
18
|
-
# @param [Solve::
|
18
|
+
# @param [Solve::RubySolver,Solve::GecodeSolver] solver
|
19
19
|
# @param [#to_s] name
|
20
20
|
# @param [Semverse::Constraint, #to_s] constraint
|
21
21
|
def initialize(solver, name, constraint = Semverse::DEFAULT_CONSTRAINT)
|
data/lib/solve/errors.rb
CHANGED
@@ -4,6 +4,12 @@ module Solve
|
|
4
4
|
alias_method :mesage, :to_s
|
5
5
|
end
|
6
6
|
|
7
|
+
class EngineNotAvailable < SolveError
|
8
|
+
end
|
9
|
+
|
10
|
+
class InvalidEngine < SolveError
|
11
|
+
end
|
12
|
+
|
7
13
|
class NoSolutionError < SolveError
|
8
14
|
|
9
15
|
# Artifacts that don't exist at any version but are required for a valid
|
@@ -1,9 +1,9 @@
|
|
1
|
-
require 'dep_selector'
|
2
1
|
require 'set'
|
2
|
+
require 'solve/errors'
|
3
3
|
require_relative 'solver/serializer'
|
4
4
|
|
5
5
|
module Solve
|
6
|
-
class
|
6
|
+
class GecodeSolver
|
7
7
|
class << self
|
8
8
|
# The timeout (in seconds) to use when resolving graphs. Default is 10. This can be
|
9
9
|
# configured by setting the SOLVE_TIMEOUT environment variable.
|
@@ -13,6 +13,13 @@ module Solve
|
|
13
13
|
seconds = 30 unless seconds = ENV["SOLVE_TIMEOUT"]
|
14
14
|
seconds.to_i * 1_000
|
15
15
|
end
|
16
|
+
|
17
|
+
# Attemp to load the dep_selector gem which this solver engine requires.
|
18
|
+
def activate
|
19
|
+
require 'dep_selector'
|
20
|
+
rescue LoadError => e
|
21
|
+
raise Errors::EngineNotAvailable, "dep_selector is not installed, GecodeSolver cannot be used (#{e})"
|
22
|
+
end
|
16
23
|
end
|
17
24
|
|
18
25
|
# Graph object with references to all known artifacts and dependency
|
@@ -30,10 +37,11 @@ module Solve
|
|
30
37
|
# graph = Solve::Graph.new
|
31
38
|
# graph.artifacts("mysql", "1.2.0")
|
32
39
|
# demands = [["mysql"]]
|
33
|
-
#
|
40
|
+
# GecodeSolver.new(graph, demands)
|
34
41
|
# @param [Solve::Graph] graph
|
35
42
|
# @param [Array<String>, Array<Array<String, String>>] demands
|
36
|
-
|
43
|
+
# @param [Hash] options - unused, only present for API compatibility with RubySolver
|
44
|
+
def initialize(graph, demands, options = {})
|
37
45
|
@ds_graph = DepSelector::DependencyGraph.new
|
38
46
|
@graph = graph
|
39
47
|
@demands_array = demands
|
@@ -0,0 +1,207 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'molinillo'
|
3
|
+
require_relative 'solver/serializer'
|
4
|
+
|
5
|
+
module Solve
|
6
|
+
class RubySolver
|
7
|
+
class << self
|
8
|
+
# The timeout (in seconds) to use when resolving graphs. Default is 10. This can be
|
9
|
+
# configured by setting the SOLVE_TIMEOUT environment variable.
|
10
|
+
#
|
11
|
+
# @return [Integer]
|
12
|
+
def timeout
|
13
|
+
seconds = 30 unless seconds = ENV["SOLVE_TIMEOUT"]
|
14
|
+
seconds.to_i * 1_000
|
15
|
+
end
|
16
|
+
|
17
|
+
# For optinal solver engines, this attempts to load depenencies. The
|
18
|
+
# RubySolver is a non-optional component, so this is a no-op
|
19
|
+
def activate
|
20
|
+
true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Graph object with references to all known artifacts and dependency
|
25
|
+
# constraints.
|
26
|
+
#
|
27
|
+
# @return [Solve::Graph]
|
28
|
+
attr_reader :graph
|
29
|
+
|
30
|
+
# @example Demands are Arrays of Arrays with an artifact name and optional constraint:
|
31
|
+
# [['nginx', '= 1.0.0'], ['mysql']]
|
32
|
+
# @return [Array<String>, Array<Array<String, String>>] demands
|
33
|
+
attr_reader :demands_array
|
34
|
+
|
35
|
+
# @example Basic use:
|
36
|
+
# graph = Solve::Graph.new
|
37
|
+
# graph.artifacts("mysql", "1.2.0")
|
38
|
+
# demands = [["mysql"]]
|
39
|
+
# RubySolver.new(graph, demands)
|
40
|
+
# @param [Solve::Graph] graph
|
41
|
+
# @param [Array<String>, Array<Array<String, String>>] demands
|
42
|
+
def initialize(graph, demands, options = {})
|
43
|
+
@graph = graph
|
44
|
+
@demands_array = demands
|
45
|
+
@timeout_ms = self.class.timeout
|
46
|
+
|
47
|
+
@dependency_source = options[:dependency_source] || 'user-specified dependency'
|
48
|
+
|
49
|
+
@molinillo_graph = Molinillo::DependencyGraph.new
|
50
|
+
@resolver = Molinillo::Resolver.new(self, self)
|
51
|
+
end
|
52
|
+
|
53
|
+
# The problem demands given as Demand model objects
|
54
|
+
# @return [Array<Solve::Demand>]
|
55
|
+
def demands
|
56
|
+
demands_array.map do |name, constraint|
|
57
|
+
Demand.new(self, name, constraint)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# @option options [Boolean] :sorted
|
62
|
+
# return the solution as a sorted list instead of a Hash
|
63
|
+
#
|
64
|
+
# @return [Hash, List] Returns a hash like { "Artifact Name" => "Version",... }
|
65
|
+
# unless the :sorted option is true, then it returns a list like [["Artifact Name", "Version],...]
|
66
|
+
# @raise [Errors::NoSolutionError] when the demands cannot be met for the
|
67
|
+
# given graph.
|
68
|
+
# @raise [Errors::UnsortableSolutionError] when the :sorted option is true
|
69
|
+
# and the demands have a solution, but the solution contains a cyclic
|
70
|
+
# dependency
|
71
|
+
def resolve(options = {})
|
72
|
+
solved_graph = resolve_with_error_wrapping
|
73
|
+
|
74
|
+
solution = solved_graph.map(&:payload)
|
75
|
+
|
76
|
+
unsorted_solution = solution.inject({}) do |stringified_soln, artifact|
|
77
|
+
stringified_soln[artifact.name] = artifact.version.to_s
|
78
|
+
stringified_soln
|
79
|
+
end
|
80
|
+
|
81
|
+
if options[:sorted]
|
82
|
+
build_sorted_solution(unsorted_solution)
|
83
|
+
else
|
84
|
+
unsorted_solution
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
###
|
89
|
+
# Molinillo Callbacks
|
90
|
+
#
|
91
|
+
# Molinillo calls back to this class to get information about our
|
92
|
+
# dependency model objects. An abstract implementation is provided at
|
93
|
+
# https://github.com/CocoaPods/Molinillo/blob/master/lib/molinillo/modules/specification_provider.rb
|
94
|
+
#
|
95
|
+
###
|
96
|
+
|
97
|
+
# Callback required by Molinillo, called when the solve starts
|
98
|
+
# @return [Integer]
|
99
|
+
def progress_rate
|
100
|
+
1
|
101
|
+
end
|
102
|
+
|
103
|
+
# Callback required by Molinillo, called when the solve starts
|
104
|
+
# @return nil
|
105
|
+
def before_resolution
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
|
109
|
+
# Callback required by Molinillo, called when the solve is complete.
|
110
|
+
# @return nil
|
111
|
+
def after_resolution
|
112
|
+
nil
|
113
|
+
end
|
114
|
+
|
115
|
+
# Callback required by Molinillo, gives debug information about the solution
|
116
|
+
# @return nil
|
117
|
+
def debug(current_resolver_depth)
|
118
|
+
# debug info will be returned if you call yield here, but it seems to be
|
119
|
+
# broken in current Molinillo
|
120
|
+
nil
|
121
|
+
end
|
122
|
+
|
123
|
+
# Callback required by Molinillo
|
124
|
+
# @return [String] the dependency's name
|
125
|
+
def name_for(dependency)
|
126
|
+
dependency.name
|
127
|
+
end
|
128
|
+
|
129
|
+
# Callback required by Molinillo
|
130
|
+
# @return [Array<Solve::Dependency>] the dependencies sorted by preference.
|
131
|
+
def sort_dependencies(dependencies, activated, conflicts)
|
132
|
+
dependencies.sort_by do |dependency|
|
133
|
+
name = name_for(dependency)
|
134
|
+
[
|
135
|
+
activated.vertex_named(name).payload ? 0 : 1,
|
136
|
+
conflicts[name] ? 0 : 1,
|
137
|
+
activated.vertex_named(name).payload ? 0 : graph.versions(dependency.name).count,
|
138
|
+
]
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Callback required by Molinillo
|
143
|
+
# @return [Array<Solve::Artifact>] the artifacts that match the dependency.
|
144
|
+
def search_for(dependency)
|
145
|
+
# This array gets mutated by Molinillo; it's okay because sort returns a
|
146
|
+
# new array.
|
147
|
+
graph.versions(dependency.name, dependency.constraint).sort
|
148
|
+
end
|
149
|
+
|
150
|
+
# Callback required by Molinillo
|
151
|
+
# @return [Boolean]
|
152
|
+
def requirement_satisfied_by?(requirement, activated, spec)
|
153
|
+
requirement.constraint.satisfies?(spec.version)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Callback required by Molinillo
|
157
|
+
# @return [Array<Solve::Dependency>] the dependencies of the given artifact
|
158
|
+
def dependencies_for(specification)
|
159
|
+
specification.dependencies
|
160
|
+
end
|
161
|
+
|
162
|
+
# @return [String] the name of the source of explicit dependencies, i.e.
|
163
|
+
# those passed to {Resolver#resolve} directly.
|
164
|
+
def name_for_explicit_dependency_source
|
165
|
+
@dependency_source
|
166
|
+
end
|
167
|
+
|
168
|
+
# @return [String] the name of the source of 'locked' dependencies, i.e.
|
169
|
+
# those passed to {Resolver#resolve} directly as the `base`
|
170
|
+
def name_for_locking_dependency_source
|
171
|
+
'Lockfile'
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
def resolve_with_error_wrapping
|
177
|
+
@resolver.resolve(demands, @molinillo_graph)
|
178
|
+
rescue Molinillo::VersionConflict, Molinillo::CircularDependencyError => e
|
179
|
+
raise Solve::Errors::NoSolutionError.new(e.message)
|
180
|
+
end
|
181
|
+
|
182
|
+
def build_sorted_solution(unsorted_solution)
|
183
|
+
nodes = Hash.new
|
184
|
+
unsorted_solution.each do |name, version|
|
185
|
+
nodes[name] = @graph.artifact(name, version).dependencies.map(&:name)
|
186
|
+
end
|
187
|
+
|
188
|
+
# Modified from http://ruby-doc.org/stdlib-1.9.3/libdoc/tsort/rdoc/TSort.html
|
189
|
+
class << nodes
|
190
|
+
include TSort
|
191
|
+
alias tsort_each_node each_key
|
192
|
+
def tsort_each_child(node, &block)
|
193
|
+
fetch(node).each(&block)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
begin
|
197
|
+
sorted_names = nodes.tsort
|
198
|
+
rescue TSort::Cyclic => e
|
199
|
+
raise Solve::Errors::UnsortableSolutionError.new(e, unsorted_solution)
|
200
|
+
end
|
201
|
+
|
202
|
+
sorted_names.map do |artifact|
|
203
|
+
[artifact, unsorted_solution[artifact]]
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|