solve 0.8.2 → 1.0.0.rc1
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 +2 -0
- data/.travis.yml +4 -0
- data/README.md +8 -11
- data/lib/solve.rb +3 -8
- data/lib/solve/artifact.rb +44 -80
- data/lib/solve/constraint.rb +62 -46
- data/lib/solve/demand.rb +6 -21
- data/lib/solve/dependency.rb +10 -22
- data/lib/solve/errors.rb +43 -17
- data/lib/solve/gem_version.rb +1 -1
- data/lib/solve/graph.rb +43 -123
- data/lib/solve/solver.rb +134 -262
- data/lib/solve/solver/serializer.rb +1 -1
- data/solve.gemspec +3 -1
- data/spec/acceptance/benchmark.rb +45 -0
- data/spec/acceptance/large_graph_no_solution.rb +18730 -0
- data/spec/acceptance/opscode_ci_graph.rb +18600 -0
- data/spec/acceptance/solutions_spec.rb +117 -76
- data/spec/spec_helper.rb +3 -0
- data/spec/unit/solve/artifact_spec.rb +49 -64
- data/spec/unit/solve/demand_spec.rb +19 -56
- data/spec/unit/solve/dependency_spec.rb +7 -46
- data/spec/unit/solve/graph_spec.rb +72 -209
- data/spec/unit/solve/solver/serializer_spec.rb +3 -4
- data/spec/unit/solve/solver_spec.rb +103 -247
- metadata +43 -22
- data/.ruby-version +0 -1
- data/lib/solve/solver/constraint_row.rb +0 -25
- data/lib/solve/solver/constraint_table.rb +0 -31
- data/lib/solve/solver/variable_row.rb +0 -43
- data/lib/solve/solver/variable_table.rb +0 -55
- data/lib/solve/tracers.rb +0 -50
- data/lib/solve/tracers/human_readable.rb +0 -67
- data/lib/solve/tracers/silent.rb +0 -17
- data/lib/solve/version.rb +0 -140
- data/spec/unit/solve/constraint_spec.rb +0 -708
- data/spec/unit/solve/version_spec.rb +0 -355
data/lib/solve/demand.rb
CHANGED
@@ -12,31 +12,16 @@ module Solve
|
|
12
12
|
|
13
13
|
# The acceptable constraint of the artifact this demand is for
|
14
14
|
#
|
15
|
-
# @return [
|
15
|
+
# @return [Semverse::Constraint]
|
16
16
|
attr_reader :constraint
|
17
17
|
|
18
18
|
# @param [Solve::Solver] solver
|
19
19
|
# @param [#to_s] name
|
20
|
-
# @param [
|
21
|
-
def initialize(solver, name, constraint =
|
22
|
-
@solver
|
23
|
-
@name
|
24
|
-
@constraint =
|
25
|
-
constraint
|
26
|
-
else
|
27
|
-
Constraint.new(constraint.to_s)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
# Remove this demand from the solver it belongs to
|
32
|
-
#
|
33
|
-
# @return [Solve::Demand, nil]
|
34
|
-
def delete
|
35
|
-
unless solver.nil?
|
36
|
-
result = solver.remove_demand(self)
|
37
|
-
@solver = nil
|
38
|
-
result
|
39
|
-
end
|
20
|
+
# @param [Semverse::Constraint, #to_s] constraint
|
21
|
+
def initialize(solver, name, constraint = Semverse::DEFAULT_CONSTRAINT)
|
22
|
+
@solver = solver
|
23
|
+
@name = name
|
24
|
+
@constraint = Semverse::Constraint.coerce(constraint)
|
40
25
|
end
|
41
26
|
|
42
27
|
def to_s
|
data/lib/solve/dependency.rb
CHANGED
@@ -12,32 +12,20 @@ module Solve
|
|
12
12
|
|
13
13
|
# The constraint requirement of this dependency
|
14
14
|
#
|
15
|
-
# @return [
|
15
|
+
# @return [Semverse::Constraint]
|
16
16
|
attr_reader :constraint
|
17
17
|
|
18
18
|
# @param [Solve::Artifact] artifact
|
19
19
|
# @param [#to_s] name
|
20
|
-
# @param [
|
21
|
-
def initialize(artifact, name, constraint =
|
22
|
-
@artifact
|
23
|
-
@name
|
24
|
-
@constraint =
|
25
|
-
when Solve::Constraint
|
26
|
-
constraint
|
27
|
-
else
|
28
|
-
Constraint.new(constraint)
|
29
|
-
end
|
20
|
+
# @param [Semverse::Constraint, #to_s] constraint
|
21
|
+
def initialize(artifact, name, constraint = Semverse::DEFAULT_CONSTRAINT)
|
22
|
+
@artifact = artifact
|
23
|
+
@name = name
|
24
|
+
@constraint = Semverse::Constraint.coerce(constraint)
|
30
25
|
end
|
31
26
|
|
32
|
-
|
33
|
-
|
34
|
-
# @return [Solve::Dependency, nil]
|
35
|
-
def delete
|
36
|
-
unless artifact.nil?
|
37
|
-
result = artifact.remove_dependency(self)
|
38
|
-
@artifact = nil
|
39
|
-
result
|
40
|
-
end
|
27
|
+
def to_s
|
28
|
+
"#{name} (#{constraint})"
|
41
29
|
end
|
42
30
|
|
43
31
|
# @param [Object] other
|
@@ -45,8 +33,8 @@ module Solve
|
|
45
33
|
# @return [Boolean]
|
46
34
|
def ==(other)
|
47
35
|
other.is_a?(self.class) &&
|
48
|
-
|
49
|
-
|
36
|
+
self.artifact == other.artifact &&
|
37
|
+
self.constraint == other.constraint
|
50
38
|
end
|
51
39
|
alias_method :eql?, :==
|
52
40
|
end
|
data/lib/solve/errors.rb
CHANGED
@@ -4,33 +4,59 @@ module Solve
|
|
4
4
|
alias_method :mesage, :to_s
|
5
5
|
end
|
6
6
|
|
7
|
-
class
|
8
|
-
attr_reader :version
|
7
|
+
class NoSolutionError < SolveError
|
9
8
|
|
10
|
-
#
|
11
|
-
|
12
|
-
|
13
|
-
|
9
|
+
# Artifacts that don't exist at any version but are required for a valid
|
10
|
+
# solution
|
11
|
+
# @return [Array<String>] Missing artifact names
|
12
|
+
attr_reader :missing_artifacts
|
14
13
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
14
|
+
# Constraints that eliminate all versions of an artifact, e.g. you ask
|
15
|
+
# for mysql >= 2.0.0 but only 1.0.0 exists.
|
16
|
+
# @return [Array<String>] Invalid constraints as strings
|
17
|
+
attr_reader :constraints_excluding_all_artifacts
|
18
|
+
|
19
|
+
# A demand that has conflicting dependencies
|
20
|
+
# @return [String] the unsatisfiable demand
|
21
|
+
attr_reader :unsatisfiable_demand
|
19
22
|
|
20
|
-
|
21
|
-
|
23
|
+
# The artifact for which there are conflicting dependencies
|
24
|
+
# @return [Array<String>] The "most constrained" artifacts
|
25
|
+
attr_reader :artifacts_with_no_satisfactory_version
|
22
26
|
|
23
|
-
# @param [#to_s]
|
24
|
-
|
25
|
-
|
27
|
+
# @param [#to_s] message
|
28
|
+
# @option causes [Array<String>] :missing_artifacts ([])
|
29
|
+
# @option causes [Array<String>] :constraints_excluding_all_artifacts ([])
|
30
|
+
# @option causes [#to_s] :unsatisfiable_demand (nil)
|
31
|
+
# @option causes [Array<String>] :artifacts_with_no_satisfactory_version ([])
|
32
|
+
def initialize(message = nil, causes = {})
|
33
|
+
super(message)
|
34
|
+
@message = message
|
35
|
+
@missing_artifacts = causes[:missing_artifacts] || []
|
36
|
+
@constraints_excluding_all_artifacts = causes[:constraints_excluding_all_artifacts] || []
|
37
|
+
@unsatisfiable_demand = causes[:unsatisfiable_demand] || nil
|
38
|
+
@artifacts_with_no_satisfactory_version = causes[:artifacts_with_no_satisfactory_version] || []
|
26
39
|
end
|
27
40
|
|
28
41
|
def to_s
|
29
|
-
|
42
|
+
s = ""
|
43
|
+
s << "#{@message}\n"
|
44
|
+
s << "Missing artifacts: #{missing_artifacts.join(',')}\n" unless missing_artifacts.empty?
|
45
|
+
unless constraints_excluding_all_artifacts.empty?
|
46
|
+
s << "Constraints that match no available version: #{constraints_excluding_all_artifacts.join(',')}\n"
|
47
|
+
end
|
48
|
+
s << "Demand that cannot be met: #{unsatisfiable_demand}\n" if unsatisfiable_demand
|
49
|
+
unless artifacts_with_no_satisfactory_version.empty?
|
50
|
+
s << "Artifacts for which there are conflicting dependencies: #{artifacts_with_no_satisfactory_version.join(',')}"
|
51
|
+
end
|
52
|
+
s
|
30
53
|
end
|
54
|
+
|
31
55
|
end
|
32
56
|
|
33
|
-
|
57
|
+
# Indicates that the solver could not find the conflicting constraints when
|
58
|
+
# solving the given demands and graph.
|
59
|
+
class NoSolutionCauseUnknown < NoSolutionError; end
|
34
60
|
|
35
61
|
class UnsortableSolutionError < SolveError
|
36
62
|
attr_reader :internal_exception
|
data/lib/solve/gem_version.rb
CHANGED
data/lib/solve/graph.rb
CHANGED
@@ -1,143 +1,71 @@
|
|
1
1
|
module Solve
|
2
2
|
class Graph
|
3
|
-
class << self
|
4
|
-
# Create a key for a graph from an instance of an Artifact or Dependency
|
5
|
-
#
|
6
|
-
# @param [Solve::Artifact, Solve::Dependency] object
|
7
|
-
#
|
8
|
-
# @raise [ArgumentError] if an instance of an object of an unknown type is given
|
9
|
-
#
|
10
|
-
# @return [Symbol]
|
11
|
-
def key_for(object)
|
12
|
-
case object
|
13
|
-
when Solve::Artifact
|
14
|
-
artifact_key(object.name, object.version)
|
15
|
-
when Solve::Dependency
|
16
|
-
dependency_key(object.name, object.constraint)
|
17
|
-
else
|
18
|
-
raise ArgumentError, "Could not generate graph key for Class: #{object.class}"
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
# Create a key representing an artifact for an instance of Graph
|
23
|
-
#
|
24
|
-
# @param [#to_s] name
|
25
|
-
# @param [#to_s] version
|
26
|
-
#
|
27
|
-
# @return [Symbol]
|
28
|
-
def artifact_key(name, version)
|
29
|
-
"#{name}-#{version}".to_sym
|
30
|
-
end
|
31
|
-
|
32
|
-
# Create a key representing an dependency for an instance of Graph
|
33
|
-
#
|
34
|
-
# @param [#to_s] name
|
35
|
-
# @param [#to_s] constraint
|
36
|
-
#
|
37
|
-
# @return [Symbol]
|
38
|
-
def dependency_key(name, constraint)
|
39
|
-
"#{name}-#{constraint}".to_sym
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
3
|
def initialize
|
44
|
-
@artifacts =
|
45
|
-
|
46
|
-
|
47
|
-
# @overload artifacts(name, version)
|
48
|
-
# Return the Solve::Artifact from the collection of artifacts
|
49
|
-
# with the given name and version.
|
50
|
-
#
|
51
|
-
# @param [#to_s]
|
52
|
-
# @param [Solve::Version, #to_s]
|
53
|
-
#
|
54
|
-
# @return [Solve::Artifact]
|
55
|
-
# @overload artifacts
|
56
|
-
# Return the collection of artifacts
|
57
|
-
#
|
58
|
-
# @return [Array<Solve::Artifact>]
|
59
|
-
def artifacts(*args)
|
60
|
-
if args.empty?
|
61
|
-
return artifact_collection
|
62
|
-
end
|
63
|
-
unless args.length == 2
|
64
|
-
raise ArgumentError, "Unexpected number of arguments. You gave: #{args.length}. Expected: 0 or 2."
|
65
|
-
end
|
66
|
-
|
67
|
-
name, version = args
|
68
|
-
|
69
|
-
if name.nil? || version.nil?
|
70
|
-
raise ArgumentError, "A name and version must be specified. You gave: #{args}."
|
71
|
-
end
|
72
|
-
|
73
|
-
artifact = Artifact.new(self, name, version)
|
74
|
-
add_artifact(artifact)
|
4
|
+
@artifacts = {}
|
5
|
+
@artifacts_by_name = Hash.new { |hash, key| hash[key] = [] }
|
75
6
|
end
|
76
7
|
|
77
|
-
#
|
78
|
-
#
|
8
|
+
# Check if an artifact with a matching name and version is a member of this instance
|
9
|
+
# of graph
|
79
10
|
#
|
80
11
|
# @param [String] name
|
12
|
+
# @param [Semverse::Version, #to_s] version
|
81
13
|
#
|
82
|
-
# @return [
|
83
|
-
def
|
84
|
-
|
85
|
-
|
86
|
-
artifacts.select do |art|
|
87
|
-
art.name == name && constraint.satisfies?(art.version)
|
88
|
-
end
|
14
|
+
# @return [Boolean]
|
15
|
+
def artifact?(name, version)
|
16
|
+
!find(name, version).nil?
|
89
17
|
end
|
18
|
+
alias_method :has_artifact?, :artifact?
|
90
19
|
|
91
|
-
|
92
|
-
|
93
|
-
# if the artifact is already a member of the collection.
|
94
|
-
#
|
95
|
-
# @param [Solve::Artifact] artifact
|
96
|
-
#
|
97
|
-
# @return [Solve::Artifact]
|
98
|
-
def add_artifact(artifact)
|
99
|
-
unless has_artifact?(artifact.name, artifact.version)
|
100
|
-
@artifacts[self.class.key_for(artifact)] = artifact
|
101
|
-
end
|
102
|
-
|
103
|
-
get_artifact(artifact.name, artifact.version)
|
20
|
+
def find(name, version)
|
21
|
+
@artifacts["#{name}-#{version}"]
|
104
22
|
end
|
105
23
|
|
106
|
-
#
|
24
|
+
# Add an artifact to the graph
|
107
25
|
#
|
108
26
|
# @param [String] name
|
109
|
-
# @
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
27
|
+
# @Param [String] version
|
28
|
+
def artifact(name, version)
|
29
|
+
unless artifact?(name, version)
|
30
|
+
artifact = Artifact.new(self, name, version)
|
31
|
+
@artifacts["#{name}-#{version}"] = artifact
|
32
|
+
@artifacts_by_name[name] << artifact
|
33
|
+
end
|
34
|
+
|
35
|
+
@artifacts["#{name}-#{version}"]
|
114
36
|
end
|
115
37
|
|
116
|
-
#
|
38
|
+
# Return the collection of artifacts
|
117
39
|
#
|
118
|
-
# @
|
119
|
-
def
|
120
|
-
|
121
|
-
@artifacts.delete(self.class.key_for(artifact))
|
122
|
-
end
|
40
|
+
# @return [Array<Solve::Artifact>]
|
41
|
+
def artifacts
|
42
|
+
@artifacts.values
|
123
43
|
end
|
124
44
|
|
125
|
-
#
|
126
|
-
#
|
45
|
+
# Return all the artifacts from the collection of artifacts
|
46
|
+
# with the given name.
|
127
47
|
#
|
128
48
|
# @param [String] name
|
129
|
-
# @param [Solve::Version, #to_s] version
|
130
49
|
#
|
131
|
-
# @return [
|
132
|
-
def
|
133
|
-
|
50
|
+
# @return [Array<Solve::Artifact>]
|
51
|
+
def versions(name, constraint = Semverse::DEFAULT_CONSTRAINT)
|
52
|
+
constraint = Semverse::Constraint.coerce(constraint)
|
53
|
+
|
54
|
+
if constraint == Semverse::DEFAULT_CONSTRAINT
|
55
|
+
@artifacts_by_name[name]
|
56
|
+
else
|
57
|
+
@artifacts_by_name[name].select do |artifact|
|
58
|
+
constraint.satisfies?(artifact.version)
|
59
|
+
end
|
60
|
+
end
|
134
61
|
end
|
135
62
|
|
136
63
|
# @param [Object] other
|
137
64
|
#
|
138
65
|
# @return [Boolean]
|
139
66
|
def ==(other)
|
140
|
-
return false unless other.is_a?(
|
67
|
+
return false unless other.is_a?(Graph)
|
68
|
+
return false unless artifacts.size == other.artifacts.size
|
141
69
|
|
142
70
|
self_artifacts = self.artifacts
|
143
71
|
other_artifacts = other.artifacts
|
@@ -150,18 +78,10 @@ module Solve
|
|
150
78
|
list << artifact.dependencies
|
151
79
|
end.flatten
|
152
80
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
self_dependencies.all? { |dependency| other_dependencies.include?(dependency) }
|
81
|
+
self_dependencies.size == other_dependencies.size &&
|
82
|
+
self_artifacts.all? { |artifact| other_artifacts.include?(artifact) } &&
|
83
|
+
self_dependencies.all? { |dependency| other_dependencies.include?(dependency) }
|
157
84
|
end
|
158
85
|
alias_method :eql?, :==
|
159
|
-
|
160
|
-
private
|
161
|
-
|
162
|
-
# @return [Array<Solve::Artifact>]
|
163
|
-
def artifact_collection
|
164
|
-
@artifacts.collect { |name, artifact| artifact }
|
165
|
-
end
|
166
86
|
end
|
167
87
|
end
|
data/lib/solve/solver.rb
CHANGED
@@ -1,90 +1,40 @@
|
|
1
|
-
require '
|
2
|
-
|
3
|
-
require_relative 'solver/variable_row'
|
4
|
-
require_relative 'solver/constraint_table'
|
5
|
-
require_relative 'solver/constraint_row'
|
1
|
+
require 'dep_selector'
|
2
|
+
require 'set'
|
6
3
|
require_relative 'solver/serializer'
|
7
4
|
|
8
5
|
module Solve
|
9
6
|
class Solver
|
10
|
-
|
11
|
-
|
12
|
-
#
|
13
|
-
# @param [Solve::Demand] demand
|
14
|
-
#
|
15
|
-
# @raise [NoSolutionError]
|
16
|
-
#
|
17
|
-
# @return [Symbol]
|
18
|
-
def demand_key(demand)
|
19
|
-
"#{demand.name}-#{demand.constraint}".to_sym
|
20
|
-
end
|
21
|
-
|
22
|
-
# Returns all of the versions which satisfy all of the given constraints
|
23
|
-
#
|
24
|
-
# @param [Array<Solve::Constraint>, Array<String>] constraints
|
25
|
-
# @param [Array<Solve::Version>, Array<String>] versions
|
26
|
-
#
|
27
|
-
# @return [Array<Solve::Version>]
|
28
|
-
def satisfy_all(constraints, versions)
|
29
|
-
constraints = Array(constraints).collect do |con|
|
30
|
-
con.is_a?(Constraint) ? con : Constraint.new(con.to_s)
|
31
|
-
end.uniq
|
32
|
-
|
33
|
-
versions = Array(versions).collect do |ver|
|
34
|
-
ver.is_a?(Version) ? ver : Version.new(ver.to_s)
|
35
|
-
end.uniq
|
36
|
-
|
37
|
-
versions.select do |ver|
|
38
|
-
constraints.all? { |constraint| constraint.satisfies?(ver) }
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
# Return the best version from the given list of versions for the given list of constraints
|
43
|
-
#
|
44
|
-
# @param [Array<Solve::Constraint>, Array<String>] constraints
|
45
|
-
# @param [Array<Solve::Version>, Array<String>] versions
|
46
|
-
#
|
47
|
-
# @raise [NoSolutionError] if version matches the given constraints
|
48
|
-
#
|
49
|
-
# @return [Solve::Version]
|
50
|
-
def satisfy_best(constraints, versions)
|
51
|
-
solution = satisfy_all(constraints, versions)
|
52
|
-
|
53
|
-
if solution.empty?
|
54
|
-
raise Errors::NoSolutionError
|
55
|
-
end
|
56
|
-
|
57
|
-
solution.sort.last
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
# The world as we know it
|
7
|
+
# Graph object with references to all known artifacts and dependency
|
8
|
+
# constraints.
|
62
9
|
#
|
63
10
|
# @return [Solve::Graph]
|
64
11
|
attr_reader :graph
|
65
|
-
attr_reader :demands
|
66
|
-
attr_reader :ui
|
67
12
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
attr_reader :
|
13
|
+
# @example Demands are Arrays of Arrays with an artifact name and optional constraint:
|
14
|
+
# [['nginx', '= 1.0.0'], ['mysql']]
|
15
|
+
# @return [Array<String>, Array<Array<String, String>>] demands
|
16
|
+
attr_reader :demands_array
|
72
17
|
|
18
|
+
# @example Basic use:
|
19
|
+
# graph = Solve::Graph.new
|
20
|
+
# graph.artifacts("mysql", "1.2.0")
|
21
|
+
# demands = [["mysql"]]
|
22
|
+
# Solver.new(graph, demands)
|
73
23
|
# @param [Solve::Graph] graph
|
74
24
|
# @param [Array<String>, Array<Array<String, String>>] demands
|
75
25
|
# @param [#say] ui
|
76
|
-
def initialize(graph, demands
|
26
|
+
def initialize(graph, demands, ui = nil)
|
27
|
+
@ds_graph = DepSelector::DependencyGraph.new
|
77
28
|
@graph = graph
|
78
|
-
@
|
79
|
-
@
|
80
|
-
|
81
|
-
@domain = Hash.new
|
82
|
-
@possible_values = Hash.new
|
83
|
-
@constraint_table = ConstraintTable.new
|
84
|
-
@variable_table = VariableTable.new
|
29
|
+
@demands_array = demands
|
30
|
+
@timeout_ms = 1_000
|
31
|
+
end
|
85
32
|
|
86
|
-
|
87
|
-
|
33
|
+
# The problem demands given as Demand model objects
|
34
|
+
# @return [Array<Solve::Demand>]
|
35
|
+
def demands
|
36
|
+
demands_array.map do |name, constraint|
|
37
|
+
Demand.new(self, name, constraint)
|
88
38
|
end
|
89
39
|
end
|
90
40
|
|
@@ -92,227 +42,149 @@ module Solve
|
|
92
42
|
# return the solution as a sorted list instead of a Hash
|
93
43
|
#
|
94
44
|
# @return [Hash, List] Returns a hash like { "Artifact Name" => "Version",... }
|
95
|
-
# unless
|
45
|
+
# unless the :sorted option is true, then it returns a list like [["Artifact Name", "Version],...]
|
46
|
+
# @raise [Errors::NoSolutionError] when the demands cannot be met for the
|
47
|
+
# given graph.
|
48
|
+
# @raise [Errors::UnsortableSolutionError] when the :sorted option is true
|
49
|
+
# and the demands have a solution, but the solution contains a cyclic
|
50
|
+
# dependency
|
96
51
|
def resolve(options = {})
|
97
|
-
|
98
|
-
seed_demand_dependencies
|
99
|
-
|
100
|
-
while unbound_variable = variable_table.first_unbound
|
101
|
-
possible_values_for_unbound = possible_values_for(unbound_variable)
|
102
|
-
constraints = constraint_table.constraints_on_artifact(unbound_variable.artifact)
|
103
|
-
Solve.tracer.searching_for(unbound_variable, constraints, possible_values)
|
104
|
-
|
105
|
-
while possible_value = possible_values_for_unbound.shift
|
106
|
-
possible_artifact = graph.get_artifact(unbound_variable.artifact, possible_value.version)
|
107
|
-
possible_dependencies = possible_artifact.dependencies
|
108
|
-
all_ok = possible_dependencies.all? { |dependency| can_add_new_constraint?(dependency) }
|
109
|
-
if all_ok
|
110
|
-
Solve.tracer.trying(possible_artifact)
|
111
|
-
add_dependencies(possible_dependencies, possible_artifact)
|
112
|
-
unbound_variable.bind(possible_value)
|
113
|
-
break
|
114
|
-
end
|
115
|
-
end
|
52
|
+
solution = solve_demands(demands_as_constraints)
|
116
53
|
|
117
|
-
|
118
|
-
|
119
|
-
|
54
|
+
unsorted_solution = solution.inject({}) do |stringified_soln, (name, version)|
|
55
|
+
stringified_soln[name] = version.to_s
|
56
|
+
stringified_soln
|
120
57
|
end
|
121
58
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
solution
|
127
|
-
end
|
128
|
-
|
129
|
-
def build_unsorted_solution
|
130
|
-
{}.tap do |solution|
|
131
|
-
variable_table.rows.each do |variable|
|
132
|
-
solution[variable.artifact] = variable.value.version.to_s
|
133
|
-
end
|
59
|
+
if options[:sorted]
|
60
|
+
build_sorted_solution(unsorted_solution)
|
61
|
+
else
|
62
|
+
unsorted_solution
|
134
63
|
end
|
135
64
|
end
|
136
65
|
|
137
|
-
|
138
|
-
unsorted_solution = build_unsorted_solution
|
139
|
-
nodes = Hash.new
|
140
|
-
unsorted_solution.each do |name, version|
|
141
|
-
nodes[name] = @graph.get_artifact(name, version).dependencies.map(&:name)
|
142
|
-
end
|
66
|
+
private
|
143
67
|
|
144
|
-
#
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
68
|
+
# DepSelector::DependencyGraph object representing the problem.
|
69
|
+
attr_reader :ds_graph
|
70
|
+
|
71
|
+
# Timeout in milliseconds. Hardcoded to 1s for now.
|
72
|
+
attr_reader :timeout_ms
|
73
|
+
|
74
|
+
# Runs the solver with the set of demands given. If any DepSelector
|
75
|
+
# exceptions are raised, they are rescued and re-raised
|
76
|
+
def solve_demands(demands_as_constraints)
|
77
|
+
selector = DepSelector::Selector.new(ds_graph, (timeout_ms / 1000.0))
|
78
|
+
selector.find_solution(demands_as_constraints, all_artifacts)
|
79
|
+
rescue DepSelector::Exceptions::InvalidSolutionConstraints => e
|
80
|
+
report_invalid_constraints_error(e)
|
81
|
+
rescue DepSelector::Exceptions::NoSolutionExists => e
|
82
|
+
report_no_solution_error(e)
|
83
|
+
rescue DepSelector::Exceptions::TimeBoundExceeded
|
84
|
+
# DepSelector timed out trying to find the solution. There may or may
|
85
|
+
# not be a solution.
|
86
|
+
raise Solve::Errors::NoSolutionError.new(
|
87
|
+
"The dependency constraints could not be solved in the time allotted.")
|
88
|
+
rescue DepSelector::Exceptions::TimeBoundExceededNoSolution
|
89
|
+
# DepSelector determined there wasn't a solution to the problem, then
|
90
|
+
# timed out trying to determine which constraints cause the conflict.
|
91
|
+
raise Solve::Errors::NoSolutionCauseUnknown.new(
|
92
|
+
"There is a dependency conflict, but the solver could not determine the precise cause in the time allotted.")
|
93
|
+
end
|
94
|
+
|
95
|
+
# Maps demands to corresponding DepSelector::SolutionConstraint objects.
|
96
|
+
def demands_as_constraints
|
97
|
+
@demands_as_constraints ||= demands_array.map do |demands_item|
|
98
|
+
item_name, constraint_with_operator = demands_item
|
99
|
+
version_constraint = Semverse::Constraint.new(constraint_with_operator)
|
100
|
+
DepSelector::SolutionConstraint.new(ds_graph.package(item_name), version_constraint)
|
150
101
|
end
|
151
102
|
end
|
152
|
-
begin
|
153
|
-
sorted_names = nodes.tsort
|
154
|
-
rescue TSort::Cyclic => e
|
155
|
-
raise Solve::Errors::UnsortableSolutionError.new(e, unsorted_solution)
|
156
|
-
end
|
157
|
-
|
158
|
-
sorted_names.map do |artifact|
|
159
|
-
[artifact, unsorted_solution[artifact]]
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
|
-
# @overload demands(name, constraint)
|
164
|
-
# Return the Solve::Demand from the collection of demands
|
165
|
-
# with the given name and constraint.
|
166
|
-
#
|
167
|
-
# @param [#to_s]
|
168
|
-
# @param [Solve::Constraint, #to_s]
|
169
|
-
#
|
170
|
-
# @return [Solve::Demand]
|
171
|
-
# @overload demands(name)
|
172
|
-
# Return the Solve::Demand from the collection of demands
|
173
|
-
# with the given name.
|
174
|
-
#
|
175
|
-
# @param [#to_s]
|
176
|
-
#
|
177
|
-
# @return [Solve::Demand]
|
178
|
-
# @overload demands
|
179
|
-
# Return the collection of demands
|
180
|
-
#
|
181
|
-
# @return [Array<Solve::Demand>]
|
182
|
-
def demands(*args)
|
183
|
-
if args.empty?
|
184
|
-
return demand_collection
|
185
|
-
end
|
186
|
-
if args.length > 2
|
187
|
-
raise ArgumentError, "Unexpected number of arguments. You gave: #{args.length}. Expected: 2 or less."
|
188
|
-
end
|
189
103
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
104
|
+
# Maps all artifacts in the graph to DepSelector::Package objects. If not
|
105
|
+
# already done, artifacts are added to the ds_graph as a necessary side effect.
|
106
|
+
def all_artifacts
|
107
|
+
return @all_artifacts if @all_artifacts
|
108
|
+
populate_ds_graph!
|
109
|
+
@all_artifacts
|
195
110
|
end
|
196
111
|
|
197
|
-
|
198
|
-
|
199
|
-
|
112
|
+
# Converts artifacts to DepSelector::Package objects and adds them to the
|
113
|
+
# DepSelector graph. This should only be called once; use #all_artifacts
|
114
|
+
# to safely get the set of all artifacts.
|
115
|
+
def populate_ds_graph!
|
116
|
+
@all_artifacts = Set.new
|
200
117
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
# @param [Solve::Demand] demand
|
206
|
-
#
|
207
|
-
# @return [Solve::Demand]
|
208
|
-
def add_demand(demand)
|
209
|
-
unless has_demand?(demand)
|
210
|
-
@demands[self.class.demand_key(demand)] = demand
|
118
|
+
graph.artifacts.each do |artifact|
|
119
|
+
add_artifact_to_ds_graph(artifact)
|
120
|
+
@all_artifacts << ds_graph.package(artifact.name)
|
121
|
+
end
|
211
122
|
end
|
212
123
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
@demands.delete(self.class.demand_key(demand))
|
124
|
+
def add_artifact_to_ds_graph(artifact)
|
125
|
+
package_version = ds_graph.package(artifact.name).add_version(artifact.version)
|
126
|
+
artifact.dependencies.each do |dependency|
|
127
|
+
dependency = DepSelector::Dependency.new(ds_graph.package(dependency.name), dependency.constraint)
|
128
|
+
package_version.dependencies << dependency
|
129
|
+
end
|
130
|
+
package_version
|
221
131
|
end
|
222
|
-
end
|
223
|
-
|
224
|
-
# @param [Solve::Demand] demand
|
225
|
-
#
|
226
|
-
# @return [Boolean]
|
227
|
-
def has_demand?(demand)
|
228
|
-
@demands.has_key?(self.class.demand_key(demand))
|
229
|
-
end
|
230
132
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
@demands.collect { |name, demand| demand }
|
236
|
-
end
|
133
|
+
def report_invalid_constraints_error(e)
|
134
|
+
non_existent_cookbooks = e.non_existent_packages.inject([]) do |list, constraint|
|
135
|
+
list << constraint.package.name
|
136
|
+
end
|
237
137
|
|
238
|
-
|
239
|
-
|
240
|
-
|
138
|
+
constrained_to_no_versions = e.constrained_to_no_versions.inject([]) do |list, constraint|
|
139
|
+
list << constraint.to_s
|
140
|
+
end
|
241
141
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
142
|
+
raise Solve::Errors::NoSolutionError.new(
|
143
|
+
"Required artifacts do not exist at the desired version",
|
144
|
+
missing_artifacts: non_existent_cookbooks,
|
145
|
+
constraints_excluding_all_artifacts: constrained_to_no_versions
|
146
|
+
)
|
246
147
|
end
|
247
148
|
|
248
|
-
def
|
249
|
-
|
250
|
-
|
251
|
-
constraints_for_variable = constraint_table.constraints_on_artifact(variable.artifact)
|
252
|
-
all_values_for_variable = domain[variable.artifact]
|
253
|
-
possible_values_for_variable = constraints_for_variable.inject(all_values_for_variable) do |remaining_values, constraint|
|
254
|
-
remaining_values.reject { |value| !constraint.satisfies?(value.version) }
|
255
|
-
end
|
256
|
-
possible_values[variable.artifact] = possible_values_for_variable
|
149
|
+
def report_no_solution_error(e)
|
150
|
+
most_constrained_cookbooks = e.disabled_most_constrained_packages.inject([]) do |list, package|
|
151
|
+
list << "#{package.name} = #{package.versions.first.to_s}"
|
257
152
|
end
|
258
|
-
possible_values_for_variable
|
259
|
-
end
|
260
|
-
|
261
|
-
def add_dependencies(dependencies, source)
|
262
|
-
dependencies.each do |dependency|
|
263
|
-
next if (source.respond_to?(:name) && dependency.name == source.name)
|
264
|
-
Solve.tracer.add_constraint(dependency, source)
|
265
|
-
variable_table.add(dependency.name, source)
|
266
|
-
constraint_table.add(dependency, source)
|
267
|
-
dependency_domain = graph.versions(dependency.name, dependency.constraint)
|
268
|
-
domain[dependency.name] = [(domain[dependency.name] || []), dependency_domain]
|
269
|
-
.flatten
|
270
|
-
.uniq
|
271
|
-
.sort { |left, right| right.version <=> left.version }
|
272
|
-
|
273
|
-
#if the variable we are constraining is still unbound, we want to filter
|
274
|
-
#its possible values, if its already bound, we know its ok to add this constraint because
|
275
|
-
#we can never change a previously bound value without removing this constraint and we check above
|
276
|
-
#whether or not its ok to add this constraint given the current value
|
277
|
-
|
278
|
-
variable = variable_table.find_artifact(dependency.name)
|
279
|
-
if variable.value.nil?
|
280
|
-
reset_possible_values_for(variable)
|
281
|
-
end
|
282
153
|
|
154
|
+
non_existent_cookbooks = e.disabled_non_existent_packages.inject([]) do |list, package|
|
155
|
+
list << package.name
|
283
156
|
end
|
284
|
-
end
|
285
157
|
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
158
|
+
raise Solve::Errors::NoSolutionError.new(
|
159
|
+
e.message,
|
160
|
+
unsatisfiable_demand: e.unsatisfiable_solution_constraint.to_s,
|
161
|
+
missing_artifacts: non_existent_cookbooks,
|
162
|
+
artifacts_with_no_satisfactory_version: most_constrained_cookbooks
|
163
|
+
)
|
290
164
|
end
|
291
165
|
|
292
|
-
def
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
if previous_variable.nil?
|
297
|
-
Solve.tracer.cannot_backtrack
|
298
|
-
raise Errors::NoSolutionError
|
166
|
+
def build_sorted_solution(unsorted_solution)
|
167
|
+
nodes = Hash.new
|
168
|
+
unsorted_solution.each do |name, version|
|
169
|
+
nodes[name] = @graph.artifact(name, version).dependencies.map(&:name)
|
299
170
|
end
|
300
171
|
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
172
|
+
# Modified from http://ruby-doc.org/stdlib-1.9.3/libdoc/tsort/rdoc/TSort.html
|
173
|
+
class << nodes
|
174
|
+
include TSort
|
175
|
+
alias tsort_each_node each_key
|
176
|
+
def tsort_each_child(node, &block)
|
177
|
+
fetch(node).each(&block)
|
178
|
+
end
|
308
179
|
end
|
309
|
-
|
310
|
-
|
311
|
-
|
180
|
+
begin
|
181
|
+
sorted_names = nodes.tsort
|
182
|
+
rescue TSort::Cyclic => e
|
183
|
+
raise Solve::Errors::UnsortableSolutionError.new(e, unsorted_solution)
|
312
184
|
end
|
313
|
-
|
314
|
-
|
315
|
-
|
185
|
+
|
186
|
+
sorted_names.map do |artifact|
|
187
|
+
[artifact, unsorted_solution[artifact]]
|
316
188
|
end
|
317
189
|
end
|
318
190
|
end
|