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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f8183623942ab701e9117e2912fd2fb4fefdfc19
4
- data.tar.gz: a001a2bb59751962e18719575d3b59ff3192a753
3
+ metadata.gz: 674f7a18cf70685b1cd690b5babdb327b8186c5e
4
+ data.tar.gz: 784d73c2042284cc1c8100f2e4097b07d1dc4ae3
5
5
  SHA512:
6
- metadata.gz: 548d9980b3b6e1f32922abcc0da8fd5146cf02f220453d6633a57a30eb982a18198fb7f4e21f7cab995c208a61a55af4417dc7b7161f36d6e48a7c1c0fe2e8ee
7
- data.tar.gz: b0009f814da7354eea4a7c0fc90b378fd477cc14ab6207c94b6782c1fe48b4ce349ba84831d36a4886dcfb233026edc4c2fe435eba04e5a63b555ba10a0f2bcd
6
+ metadata.gz: 8dd9fe4edfff28b0a34b9223ae2163d346bec2a306e3319d3e559362477e82883080cc4031a567a065f17da5b99cc9231e9fed21a5cf08f102056f32400b08e9
7
+ data.tar.gz: 7a5b6622bd17b7d8d492c62f8e941db4137a05d963a5fc71b0ea8f8a8d5868cadb360456ae723e21cd9ffb451ff84f81f6e82409775c355926512fff18e8ab44
data/.gitignore CHANGED
@@ -4,6 +4,7 @@
4
4
  .config
5
5
  .yardoc
6
6
  Gemfile.lock
7
+ NoGecode.gemfile.lock
7
8
  InstalledFiles
8
9
  _yardoc
9
10
  coverage
@@ -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 development
8
- rvm:
9
- - 1.9.3
10
- - 2.0.0
11
- - 2.1.0
12
- - jruby-19mode
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 :development do
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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
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.
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
data/Thorfile CHANGED
@@ -28,4 +28,9 @@ class Default < Thor
28
28
  def spec
29
29
  exec "rspec --color --format=documentation spec"
30
30
  end
31
+
32
+ desc "spec", "Run RSpec code examples"
33
+ def nogecode_spec
34
+ exec "rspec -t '~gecode' --color --format=documentation spec"
35
+ end
31
36
  end
@@ -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/solver'
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
- Solver.new(graph, demands).resolve(options)
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
@@ -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::Solver]
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::Solver] solver
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)
@@ -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 Solver
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
- # Solver.new(graph, demands)
40
+ # GecodeSolver.new(graph, demands)
34
41
  # @param [Solve::Graph] graph
35
42
  # @param [Array<String>, Array<Array<String, String>>] demands
36
- def initialize(graph, demands)
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