solve 0.2.1 → 0.3.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.
- data/.travis.yml +5 -0
- data/README.md +8 -7
- data/lib/solve.rb +10 -29
- data/lib/solve/artifact.rb +91 -52
- data/lib/solve/constraint.rb +109 -24
- data/lib/solve/demand.rb +26 -17
- data/lib/solve/dependency.rb +15 -1
- data/lib/solve/errors.rb +23 -20
- data/lib/solve/gem_version.rb +1 -1
- data/lib/solve/graph.rb +76 -84
- data/lib/solve/solver.rb +246 -0
- data/lib/solve/solver/constraint_row.rb +17 -0
- data/lib/solve/solver/constraint_table.rb +27 -0
- data/lib/solve/solver/variable.rb +41 -0
- data/lib/solve/solver/variable_table.rb +50 -0
- data/lib/solve/version.rb +53 -16
- data/solve.gemspec +2 -3
- data/spec/acceptance/solutions_spec.rb +81 -8
- data/spec/unit/solve/artifact_spec.rb +73 -49
- data/spec/unit/solve/constraint_spec.rb +36 -30
- data/spec/unit/solve/demand_spec.rb +22 -13
- data/spec/unit/solve/dependency_spec.rb +15 -0
- data/spec/unit/solve/graph_spec.rb +98 -142
- data/spec/unit/solve/solver_spec.rb +302 -0
- data/spec/unit/solve/version_spec.rb +110 -0
- metadata +42 -96
- data/lib/solve/core_ext.rb +0 -3
- data/lib/solve/core_ext/kernel.rb +0 -33
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,4 +1,7 @@
|
|
1
1
|
# Solve
|
2
|
+
[](http://travis-ci.org/reset/solve)
|
3
|
+
[](https://gemnasium.com/reset/solve)
|
4
|
+
[](https://codeclimate.com/github/reset/solve)
|
2
5
|
|
3
6
|
A Ruby constraint solver
|
4
7
|
|
@@ -20,20 +23,18 @@ Now add another artifact that has a dependency
|
|
20
23
|
|
21
24
|
graph.artifacts("mysql", "1.2.4").depends("openssl", "~> 1.0.0")
|
22
25
|
|
23
|
-
|
26
|
+
Dependencies can be chained, too
|
24
27
|
|
25
|
-
graph.
|
28
|
+
graph.artifacts("ntp", "1.0.0").depends("build-essential").depends("yum")
|
26
29
|
|
27
|
-
And now solve the graph
|
30
|
+
And now solve the graph with some demands
|
28
31
|
|
29
|
-
Solve.it!(graph)
|
32
|
+
Solve.it!(graph, ['nginx', '>= 0.100.0'])
|
30
33
|
|
31
|
-
### Removing an artifact,
|
34
|
+
### Removing an artifact, or dependency from the graph
|
32
35
|
|
33
36
|
graph.artifacts("nginx", "1.0.0").delete
|
34
37
|
|
35
|
-
graph.demands('nginx', '>= 0.100.0').delete
|
36
|
-
|
37
38
|
artifact.dependencies("nginx", "~> 1.0.0").delete
|
38
39
|
|
39
40
|
## Authors
|
data/lib/solve.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
require 'solve/errors'
|
2
|
-
require 'solve/core_ext'
|
3
|
-
require 'dep_selector'
|
4
2
|
|
3
|
+
# @author Jamie Winsor <jamie@vialstudios.com>
|
5
4
|
module Solve
|
6
5
|
autoload :Version, 'solve/version'
|
7
6
|
autoload :Artifact, 'solve/artifact'
|
@@ -9,39 +8,21 @@ module Solve
|
|
9
8
|
autoload :Dependency, 'solve/dependency'
|
10
9
|
autoload :Graph, 'solve/graph'
|
11
10
|
autoload :Demand, 'solve/demand'
|
11
|
+
autoload :Solver, 'solve/solver'
|
12
12
|
|
13
13
|
class << self
|
14
|
-
|
15
|
-
|
16
|
-
#
|
14
|
+
# A quick solve. Given the "world" as we know it (the graph) and a list of
|
15
|
+
# requirements (demands) which must be met. Return me the best solution of
|
16
|
+
# artifacts and verisons that I should use.
|
17
17
|
#
|
18
|
-
# @return [Hash]
|
19
|
-
def it(graph)
|
20
|
-
it!(graph)
|
21
|
-
rescue NoSolutionError
|
22
|
-
nil
|
23
|
-
end
|
24
|
-
|
25
18
|
# @param [Solve::Graph] graph
|
19
|
+
# @param [Array<Solve::Demand>, Array<String, String>] demands
|
20
|
+
#
|
21
|
+
# @raise [NoSolutionError]
|
26
22
|
#
|
27
23
|
# @return [Hash]
|
28
|
-
def it!(graph)
|
29
|
-
|
30
|
-
selector = Selector.new(dep_graph)
|
31
|
-
|
32
|
-
solution_constraints = graph.demands.collect do |demand|
|
33
|
-
SolutionConstraint.new(dep_graph.package(demand.name), DepSelector::VersionConstraint.new(demand.constraint.to_s))
|
34
|
-
end
|
35
|
-
|
36
|
-
solution = quietly { selector.find_solution(solution_constraints) }
|
37
|
-
|
38
|
-
{}.tap do |artifacts|
|
39
|
-
solution.each do |name, constraint|
|
40
|
-
artifacts[name] = constraint.to_s
|
41
|
-
end
|
42
|
-
end
|
43
|
-
rescue DepSelector::Exceptions::InvalidSolutionConstraints
|
44
|
-
raise NoSolutionError
|
24
|
+
def it!(graph, demands)
|
25
|
+
Solver.new(graph, demands).resolve
|
45
26
|
end
|
46
27
|
end
|
47
28
|
end
|
data/lib/solve/artifact.rb
CHANGED
@@ -1,7 +1,21 @@
|
|
1
1
|
module Solve
|
2
|
+
# @author Jamie Winsor <jamie@vialstudios.com>
|
2
3
|
class Artifact
|
4
|
+
include Comparable
|
5
|
+
|
6
|
+
# A reference to the graph this artifact belongs to
|
7
|
+
#
|
8
|
+
# @return [Solve::Graph]
|
3
9
|
attr_reader :graph
|
10
|
+
|
11
|
+
# The name of the artifact
|
12
|
+
#
|
13
|
+
# @return [String]
|
4
14
|
attr_reader :name
|
15
|
+
|
16
|
+
# The version of this artifact
|
17
|
+
#
|
18
|
+
# @return [Solve::Version]
|
5
19
|
attr_reader :version
|
6
20
|
|
7
21
|
# @param [Solve::Graph] graph
|
@@ -14,73 +28,50 @@ module Solve
|
|
14
28
|
@dependencies = Hash.new
|
15
29
|
end
|
16
30
|
|
17
|
-
#
|
18
|
-
#
|
19
|
-
# dependencies with the given name and constraint.
|
31
|
+
# Return the Solve::Dependency from the collection of
|
32
|
+
# dependencies with the given name and constraint.
|
20
33
|
#
|
21
|
-
#
|
22
|
-
#
|
34
|
+
# @param [#to_s] name
|
35
|
+
# @param [Solve::Constraint, #to_s] constraint
|
23
36
|
#
|
24
|
-
#
|
25
|
-
# @
|
26
|
-
#
|
37
|
+
# @example adding dependencies
|
38
|
+
# artifact.depends("nginx") => <#Dependency: @name="nginx", @constraint=">= 0.0.0">
|
39
|
+
# artifact.depends("ntp", "= 1.0.0") => <#Dependency: @name="ntp", @constraint="= 1.0.0">
|
27
40
|
#
|
28
|
-
#
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
if args.length > 2
|
34
|
-
raise ArgumentError, "Unexpected number of arguments. You gave: #{args.length}. Expected: 2 or less."
|
35
|
-
end
|
36
|
-
|
37
|
-
name, constraint = args
|
38
|
-
constraint ||= ">= 0.0.0"
|
39
|
-
|
41
|
+
# @example chaining dependencies
|
42
|
+
# artifact.depends("nginx").depends("ntp")
|
43
|
+
#
|
44
|
+
# @return [Solve::Artifact]
|
45
|
+
def depends(name, constraint = ">= 0.0.0")
|
40
46
|
if name.nil?
|
41
47
|
raise ArgumentError, "A name must be specified. You gave: #{args}."
|
42
48
|
end
|
43
49
|
|
44
50
|
dependency = Dependency.new(self, name, constraint)
|
45
51
|
add_dependency(dependency)
|
46
|
-
end
|
47
|
-
alias_method :depends, :dependencies
|
48
52
|
|
49
|
-
|
50
|
-
# and return the added Solve::Dependency. No change will be
|
51
|
-
# made if the dependency is already a member of the collection.
|
52
|
-
#
|
53
|
-
# @param [Solve::Dependency] dependency
|
54
|
-
#
|
55
|
-
# @return [Solve::Dependency]
|
56
|
-
def add_dependency(dependency)
|
57
|
-
unless has_dependency?(dependency)
|
58
|
-
dep_graph = graph.send(:dep_graph)
|
59
|
-
a = dep_graph.package(self.name).add_version(DepSelector::Version.new(self.version.to_s))
|
60
|
-
dep_pack = dep_graph.package(dependency.name)
|
61
|
-
a.dependencies << DepSelector::Dependency.new(dep_pack, DepSelector::VersionConstraint.new(dependency.constraint.to_s))
|
62
|
-
@dependencies[dependency.to_s] = dependency
|
63
|
-
end
|
64
|
-
|
65
|
-
dependency
|
53
|
+
self
|
66
54
|
end
|
67
55
|
|
68
|
-
#
|
56
|
+
# Return the collection of dependencies on this instance of artifact
|
69
57
|
#
|
70
|
-
# @return [Solve::Dependency
|
71
|
-
def
|
72
|
-
|
73
|
-
@dependencies.delete(dependency.to_s)
|
74
|
-
end
|
58
|
+
# @return [Array<Solve::Dependency>]
|
59
|
+
def dependencies
|
60
|
+
@dependencies.collect { |name, dependency| dependency }
|
75
61
|
end
|
76
62
|
|
77
|
-
#
|
63
|
+
# Retrieve the dependency from the artifact with the matching name and constraint
|
78
64
|
#
|
79
|
-
# @
|
80
|
-
|
81
|
-
|
65
|
+
# @param [#to_s] name
|
66
|
+
# @param [#to_s] constraint
|
67
|
+
#
|
68
|
+
# @return [Solve::Artifact, nil]
|
69
|
+
def get_dependency(name, constraint)
|
70
|
+
@dependencies.fetch(Graph.dependency_key(name, constraint), nil)
|
82
71
|
end
|
83
72
|
|
73
|
+
# Remove this artifact from the graph it belongs to
|
74
|
+
#
|
84
75
|
# @return [Solve::Artifact, nil]
|
85
76
|
def delete
|
86
77
|
unless graph.nil?
|
@@ -94,11 +85,59 @@ module Solve
|
|
94
85
|
"#{name}-#{version}"
|
95
86
|
end
|
96
87
|
|
88
|
+
# @param [Object] other
|
89
|
+
#
|
90
|
+
# @return [Boolean]
|
91
|
+
def ==(other)
|
92
|
+
other.is_a?(self.class) &&
|
93
|
+
self.name == other.name &&
|
94
|
+
self.version == other.version
|
95
|
+
end
|
96
|
+
alias_method :eql?, :==
|
97
|
+
|
98
|
+
# @param [Solve::Version] other
|
99
|
+
#
|
100
|
+
# @return [Integer]
|
101
|
+
def <=>(other)
|
102
|
+
self.version <=> other.version
|
103
|
+
end
|
104
|
+
|
97
105
|
private
|
98
106
|
|
99
|
-
#
|
100
|
-
|
101
|
-
|
107
|
+
# Add a Solve::Dependency to the collection of dependencies
|
108
|
+
# and return the added Solve::Dependency. No change will be
|
109
|
+
# made if the dependency is already a member of the collection.
|
110
|
+
#
|
111
|
+
# @param [Solve::Dependency] dependency
|
112
|
+
#
|
113
|
+
# @return [Solve::Dependency]
|
114
|
+
def add_dependency(dependency)
|
115
|
+
unless has_dependency?(dependency.name, dependency.constraint)
|
116
|
+
@dependencies[Graph.key_for(dependency)] = dependency
|
117
|
+
end
|
118
|
+
|
119
|
+
get_dependency(dependency.name, dependency.constraint)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Remove the matching dependency from the artifact
|
123
|
+
#
|
124
|
+
# @param [Solve::Dependency] dependency
|
125
|
+
#
|
126
|
+
# @return [Solve::Dependency, nil]
|
127
|
+
def remove_dependency(dependency)
|
128
|
+
if has_dependency?(dependency)
|
129
|
+
@dependencies.delete(Graph.key_for(dependency))
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Check if the artifact has a dependency with the matching name and constraint
|
134
|
+
#
|
135
|
+
# @param [#to_s] name
|
136
|
+
# @param [#to_s] constraint
|
137
|
+
#
|
138
|
+
# @return [Boolean]
|
139
|
+
def has_dependency?(name, constraint)
|
140
|
+
@dependencies.has_key?(Graph.dependency_key(name, constraint))
|
102
141
|
end
|
103
142
|
end
|
104
143
|
end
|
data/lib/solve/constraint.rb
CHANGED
@@ -1,10 +1,29 @@
|
|
1
1
|
module Solve
|
2
|
+
# @author Jamie Winsor <jamie@vialstudios.com>
|
2
3
|
class Constraint
|
3
4
|
class << self
|
5
|
+
# Split a constraint string into an Array of two elements. The first
|
6
|
+
# element being the operator and second being the version string.
|
7
|
+
#
|
8
|
+
# If the given string does not contain a constraint operator then (=)
|
9
|
+
# will be used.
|
10
|
+
#
|
11
|
+
# If the given string does not contain a valid version string then
|
12
|
+
# nil will be returned.
|
13
|
+
#
|
4
14
|
# @param [#to_s] string
|
5
15
|
#
|
16
|
+
# @example splitting a string with a constraint operator and valid version string
|
17
|
+
# Constraint.split(">= 1.0.0") => [ ">=", "1.0.0" ]
|
18
|
+
#
|
19
|
+
# @example splitting a string without a constraint operator
|
20
|
+
# Constraint.split("0.0.0") => [ "=", "1.0.0" ]
|
21
|
+
#
|
22
|
+
# @example splitting a string without a valid version string
|
23
|
+
# Constraint.split("hello") => nil
|
24
|
+
#
|
6
25
|
# @return [Array, nil]
|
7
|
-
def
|
26
|
+
def split(string)
|
8
27
|
if string =~ /^[0-9]/
|
9
28
|
op = "="
|
10
29
|
ver = string
|
@@ -16,40 +35,108 @@ module Solve
|
|
16
35
|
|
17
36
|
[ op, ver ]
|
18
37
|
end
|
38
|
+
|
39
|
+
# @param [Solve::Constraint] constraint
|
40
|
+
# @param [Solve::Version] target_version
|
41
|
+
#
|
42
|
+
# @return [Boolean]
|
43
|
+
def compare_equal(constraint, target_version)
|
44
|
+
target_version == constraint.version
|
45
|
+
end
|
46
|
+
|
47
|
+
# @param [Solve::Constraint] constraint
|
48
|
+
# @param [Solve::Version] target_version
|
49
|
+
#
|
50
|
+
# @return [Boolean]
|
51
|
+
def compare_gt(constraint, target_version)
|
52
|
+
target_version > constraint.version
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param [Solve::Constraint] constraint
|
56
|
+
# @param [Solve::Version] target_version
|
57
|
+
#
|
58
|
+
# @return [Boolean]
|
59
|
+
def compare_lt(constraint, target_version)
|
60
|
+
target_version < constraint.version
|
61
|
+
end
|
62
|
+
|
63
|
+
# @param [Solve::Constraint] constraint
|
64
|
+
# @param [Solve::Version] target_version
|
65
|
+
#
|
66
|
+
# @return [Boolean]
|
67
|
+
def compare_gte(constraint, target_version)
|
68
|
+
target_version >= constraint.version
|
69
|
+
end
|
70
|
+
|
71
|
+
# @param [Solve::Constraint] constraint
|
72
|
+
# @param [Solve::Version] target_version
|
73
|
+
#
|
74
|
+
# @return [Boolean]
|
75
|
+
def compare_lte(constraint, target_version)
|
76
|
+
target_version <= constraint.version
|
77
|
+
end
|
78
|
+
|
79
|
+
# @param [Solve::Constraint] constraint
|
80
|
+
# @param [Solve::Version] target_version
|
81
|
+
#
|
82
|
+
# @return [Boolean]
|
83
|
+
def compare_aprox(constraint, target_version)
|
84
|
+
unless constraint.patch.nil?
|
85
|
+
target_version.patch >= constraint.patch &&
|
86
|
+
target_version.minor == constraint.minor &&
|
87
|
+
target_version.major == constraint.major
|
88
|
+
else
|
89
|
+
target_version.minor >= constraint.minor &&
|
90
|
+
target_version.major == constraint.major
|
91
|
+
end
|
92
|
+
end
|
19
93
|
end
|
20
94
|
|
21
|
-
OPERATORS =
|
22
|
-
"=",
|
23
|
-
">",
|
24
|
-
"<",
|
25
|
-
">=",
|
26
|
-
"<=",
|
27
|
-
"~>"
|
28
|
-
|
29
|
-
|
95
|
+
OPERATORS = {
|
96
|
+
"=" => method(:compare_equal),
|
97
|
+
">" => method(:compare_gt),
|
98
|
+
"<" => method(:compare_lt),
|
99
|
+
">=" => method(:compare_gte),
|
100
|
+
"<=" => method(:compare_lte),
|
101
|
+
"~>" => method(:compare_aprox)
|
102
|
+
}.freeze
|
103
|
+
|
104
|
+
REGEXP = /^(#{OPERATORS.keys.join('|')}) (.+)$/
|
30
105
|
|
31
106
|
attr_reader :operator
|
32
|
-
attr_reader :
|
107
|
+
attr_reader :major
|
108
|
+
attr_reader :minor
|
109
|
+
attr_reader :patch
|
33
110
|
|
34
111
|
# @param [#to_s] constraint
|
35
112
|
def initialize(constraint = ">= 0.0.0")
|
36
|
-
@operator, ver_str = self.class.
|
113
|
+
@operator, ver_str = self.class.split(constraint)
|
37
114
|
if @operator.nil? || ver_str.nil?
|
38
|
-
raise InvalidConstraintFormat.new(constraint)
|
115
|
+
raise Errors::InvalidConstraintFormat.new(constraint)
|
39
116
|
end
|
40
117
|
|
41
|
-
@
|
42
|
-
@
|
118
|
+
@major, @minor, @patch = Version.split(ver_str)
|
119
|
+
@compare_fun = OPERATORS.fetch(self.operator)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Return the Solve::Version representation of the major, minor, and patch
|
123
|
+
# attributes of this instance
|
124
|
+
#
|
125
|
+
# @return [Solve::Version]
|
126
|
+
def version
|
127
|
+
@version ||= Version.new([self.major, self.minor, self.patch])
|
43
128
|
end
|
44
129
|
|
45
130
|
# Returns true or false if the given version would be satisfied by
|
46
131
|
# the version constraint.
|
47
132
|
#
|
48
|
-
# @param [
|
133
|
+
# @param [#to_s] target_version
|
49
134
|
#
|
50
135
|
# @return [Boolean]
|
51
|
-
def satisfies?(
|
52
|
-
|
136
|
+
def satisfies?(target_version)
|
137
|
+
target_version = Version.new(target_version.to_s)
|
138
|
+
|
139
|
+
@compare_fun.call(self, target_version)
|
53
140
|
end
|
54
141
|
|
55
142
|
# @param [Object] other
|
@@ -58,16 +145,14 @@ module Solve
|
|
58
145
|
def ==(other)
|
59
146
|
other.is_a?(self.class) &&
|
60
147
|
self.operator == other.operator &&
|
61
|
-
self.
|
148
|
+
self.major == other.minor &&
|
149
|
+
self.minor == other.minor &&
|
150
|
+
self.patch == other.patch
|
62
151
|
end
|
63
152
|
alias_method :eql?, :==
|
64
153
|
|
65
154
|
def to_s
|
66
|
-
"#{operator} #{
|
155
|
+
"#{operator} #{major}.#{minor}.#{patch}"
|
67
156
|
end
|
68
|
-
|
69
|
-
private
|
70
|
-
|
71
|
-
attr_reader :dep_constraint
|
72
157
|
end
|
73
158
|
end
|