solve 1.2.1 → 2.0.0
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 +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
|