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 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