dep_selector 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,46 @@
1
+ #
2
+ # Author:: Christopher Walters (<cw@opscode.com>)
3
+ # Author:: Mark Anderson (<mark@opscode.com>)
4
+ # Copyright:: Copyright (c) 2010-2011 Opscode, Inc.
5
+ # License:: Apache License, Version 2.0
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ require 'dep_selector/version_constraint'
21
+
22
+ module DepSelector
23
+ class Dependency
24
+ attr_reader :package, :constraint
25
+
26
+ def initialize(package, constraint=nil)
27
+ @package = package
28
+ @constraint = constraint || VersionConstraint.new
29
+ end
30
+
31
+ def to_s(incl_densely_packed_versions = false)
32
+ range = package.densely_packed_versions[constraint]
33
+ "(#{package.name} #{constraint.to_s}#{incl_densely_packed_versions ? " (#{range})" : ''})"
34
+ end
35
+
36
+ def ==(o)
37
+ o.respond_to?(:package) && package == o.package &&
38
+ o.respond_to?(:constraint) && constraint == o.constraint
39
+ end
40
+
41
+ def eql?(o)
42
+ self.class == o.class && self == o
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,83 @@
1
+ #
2
+ # Author:: Christopher Walters (<cw@opscode.com>)
3
+ # Author:: Mark Anderson (<mark@opscode.com>)
4
+ # Copyright:: Copyright (c) 2010-2011 Opscode, Inc.
5
+ # License:: Apache License, Version 2.0
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ require 'dep_selector/package'
21
+ require 'dep_selector/gecode_wrapper'
22
+
23
+ # DependencyGraphs contain Packages, which in turn contain
24
+ # PackageVersions. Packages are created at access-time through
25
+ # #package
26
+ module DepSelector
27
+ class DependencyGraph
28
+
29
+ attr_reader :packages
30
+
31
+ def initialize
32
+ @packages = {}
33
+ end
34
+
35
+ def package(name)
36
+ packages.has_key?(name) ? packages[name] : (packages[name]=Package.new(self, name))
37
+ end
38
+
39
+ def each_package
40
+ packages.each do |name, pkg|
41
+ yield pkg
42
+ end
43
+ end
44
+
45
+ def gecode_wrapper
46
+ raise "Must invoke generate_gecode_wrapper_constraints before attempting to access gecode_wrapper" unless @gecode_wrapper
47
+ @gecode_wrapper
48
+ end
49
+
50
+ # Note: only invoke this method once all Packages and
51
+ # PackageVersions have been added.
52
+ def generate_gecode_wrapper_constraints(packages_to_include_in_solve=nil)
53
+ unless @gecode_wrapper
54
+ packages_in_solve =
55
+ if packages_to_include_in_solve
56
+ packages_to_include_in_solve
57
+ else
58
+ packages.map{ |name, pkg| pkg }
59
+ end
60
+
61
+ # In addition to all the packages that the user specified,
62
+ # there is a "ghost" package that contains the solution
63
+ # constraints. See Selector#solve for more information.
64
+ @gecode_wrapper = GecodeWrapper.new(packages_in_solve.size + 1)
65
+ packages_in_solve.each{ |pkg| pkg.generate_gecode_wrapper_constraints }
66
+ end
67
+ end
68
+
69
+ def gecode_model_vars
70
+ packages.inject({}){|acc, elt| acc[elt.first] = elt.last.gecode_model_var ; acc }
71
+ end
72
+
73
+ def to_s(incl_densely_packed_versions = false)
74
+ packages.keys.sort.map{|name| packages[name].to_s(incl_densely_packed_versions)}.join("\n")
75
+ end
76
+
77
+ # TODO [cw,2010/11/23]: this is a simple but inefficient impl. Do
78
+ # it for realz.
79
+ def clone
80
+ Marshal.load(Marshal.dump(self))
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,28 @@
1
+ #
2
+ # Author:: Christopher Walters (<cw@opscode.com>)
3
+ # Author:: Mark Anderson (<mark@opscode.com>)
4
+ # Copyright:: Copyright (c) 2010-2011 Opscode, Inc.
5
+ # License:: Apache License, Version 2.0
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ module DepSelector
21
+ class ErrorReporter
22
+
23
+ def give_feedback(workspace, solution_constraints, unsatisfiable_constraint_idx, most_constrained_package)
24
+ raise "Sub-class must implement"
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,183 @@
1
+ #
2
+ # Author:: Christopher Walters (<cw@opscode.com>)
3
+ # Author:: Mark Anderson (<mark@opscode.com>)
4
+ # Copyright:: Copyright (c) 2010-2011 Opscode, Inc.
5
+ # License:: Apache License, Version 2.0
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ require 'dep_selector/error_reporter'
21
+
22
+ # This error reporter simply maps the versions of packages explicitly
23
+ # included in the list of solution constraints to the restrictions
24
+ # placed on the most constrained package.
25
+ module DepSelector
26
+ class ErrorReporter
27
+ class SimpleTreeTraverser < ErrorReporter
28
+
29
+ def give_feedback(dep_graph, soln_constraints, unsatisfiable_constraint_idx, most_constrained_pkg)
30
+ unsatisfiable_soln_constraint = soln_constraints[unsatisfiable_constraint_idx]
31
+ feedback = "Unable to satisfy constraints on package #{most_constrained_pkg.name}"
32
+ feedback << ", which does not exist," unless most_constrained_pkg.valid?
33
+ feedback << " due to solution constraint #{unsatisfiable_soln_constraint}. "
34
+
35
+ all_paths = paths_from_soln_constraints_to_pkg_constraints(dep_graph, soln_constraints, most_constrained_pkg)
36
+ collapsed_paths = collapse_adjacent_paths(all_paths).map{|collapsed_path| "[#{print_path(collapsed_path).join(' -> ')}]"}
37
+
38
+ feedback << "Solution constraints that may result in a constraint on #{most_constrained_pkg.name}: #{collapsed_paths.join(', ')}"
39
+ end
40
+
41
+ private
42
+
43
+ def paths_from_soln_constraints_to_pkg_constraints(dep_graph, soln_constraints, most_constrained_pkg)
44
+ all_paths = []
45
+ soln_constraints.each do |soln_constraint|
46
+ paths_to_pkg(dep_graph,
47
+ soln_constraint.package,
48
+ soln_constraint.constraint,
49
+ most_constrained_pkg,
50
+ [],
51
+ all_paths)
52
+ end
53
+
54
+ all_paths
55
+ end
56
+
57
+ def paths_to_pkg(dep_graph, curr_pkg, version_constraint, target_pkg, curr_path, all_paths)
58
+ if curr_pkg == target_pkg
59
+ # register the culminating constraint
60
+ all_paths.push(Array.new(curr_path).push(SolutionConstraint.new(curr_pkg, version_constraint)))
61
+ return
62
+ end
63
+
64
+ # curr_pkg has no versions, it is invalid so don't recurse
65
+ if curr_pkg.versions.empty?
66
+ # TODO [cw, 2011/2/17]: find a way to track these invalid
67
+ # packages and return as potential conflict-causing
68
+ # constraints.
69
+ return
70
+ end
71
+
72
+ if curr_path.select{|elt| elt.package == curr_pkg}.any?
73
+ # TODO [cw, 2011/2/18]: this indicates a circular dependency
74
+ # in the dependency graph. This might be useful warning
75
+ # information to report to the user.
76
+ return
77
+ end
78
+
79
+ # determine all versions of curr_pkg that match
80
+ # version_constraint and recurse into them
81
+ curr_pkg[version_constraint].each do |curr_pkg_ver|
82
+ curr_path.push(curr_pkg_ver)
83
+ curr_pkg_ver.dependencies.each do |dep|
84
+ paths_to_pkg(dep_graph, dep.package, dep.constraint, target_pkg, curr_path, all_paths)
85
+ end
86
+ curr_path.pop
87
+ end
88
+ end
89
+
90
+ # This is a simple collapsing function. For each adjacent path,
91
+ # if there is only one element different between the two paths
92
+ # and their packages are the same (meaning only the version
93
+ # binding is different), then the elements are considered
94
+ # collasable. The merged path has all the common elements and a
95
+ # set containing the two version bindings in place of the
96
+ # contentious path item.
97
+ def collapse_adjacent_paths(paths)
98
+ return paths if paths.length < 2
99
+
100
+ paths.inject([]) do |collapsed_paths, path|
101
+ merge_path_into_collapsed_paths(collapsed_paths, path)
102
+ end
103
+ end
104
+
105
+ def print_path(path)
106
+ path.map do |step|
107
+ if step.respond_to? :version
108
+ "(#{step.package.name} = #{step.version})"
109
+ elsif step.respond_to? :constraint
110
+ step.to_s
111
+ elsif step.kind_of?(Array)
112
+ # TODO [cw, 2011/2/23]: consider detecting complete
113
+ # ranges here instead of calling each out individually
114
+ "(#{step.first.package.name} = {#{step.map{|elt| "#{elt.version}"}.join(',')}})"
115
+ else
116
+ raise "don't know how to print step"
117
+ end
118
+ end
119
+ end
120
+
121
+ # collapses path_under_consideration onto the end of
122
+ # collapsed_paths or adds a new path to be used in the next
123
+ # round(s) of collapsing.
124
+ #
125
+ # Note: collapsed_paths is side-effected
126
+ def merge_path_into_collapsed_paths(collapsed_paths, path_under_consideration)
127
+ curr_collapsed_path = collapsed_paths.last
128
+
129
+ # if there is no curr_collapsed_path or it isn't the same
130
+ # length as path_under_consideration, then they cannot
131
+ # possibly be mergeable
132
+ if curr_collapsed_path.nil? || curr_collapsed_path.length != path_under_consideration.length
133
+ # TODO [cw.2011/2/7]: do we need this to be a new array, or
134
+ # can we save ourselves a little memory and work by just
135
+ # pushing the reference to path_under_consideration?
136
+ return collapsed_paths << Array.new(path_under_consideration)
137
+ end
138
+
139
+ # lengths are equal, so find the first path element where
140
+ # curr_collapsed_path and path_under_consideration diverge, if
141
+ # that is the only unequal element, it's for the same package,
142
+ # and they are both PackageVersion objects then merge;
143
+ # otherwise, this is a new path
144
+ #
145
+ # TODO [cw,2011/2/7]: should we merge even if they're not for
146
+ # the same package?
147
+ unequal_idx = nil
148
+ merged_set = nil
149
+ mergeable = true
150
+ path_under_consideration.each_with_index do |path_element, curr_idx|
151
+ if path_element != curr_collapsed_path[curr_idx]
152
+ unless unequal_idx
153
+ merged_set = [curr_collapsed_path[curr_idx]].flatten
154
+ if merged_set.first.package == path_element.package &&
155
+ merged_set.first.is_a?(PackageVersion) &&
156
+ path_element.is_a?(PackageVersion)
157
+ merged_set << path_element
158
+ else
159
+ mergeable = false
160
+ break
161
+ end
162
+ unequal_idx = curr_idx
163
+ else
164
+ # this is the second place they are unequal. fast-fail,
165
+ # because we know we can't merge the paths.
166
+ mergeable = false
167
+ break
168
+ end
169
+ end
170
+ end
171
+
172
+ if unequal_idx && mergeable
173
+ curr_collapsed_path[unequal_idx] = merged_set
174
+ else
175
+ collapsed_paths << Array.new(path_under_consideration)
176
+ end
177
+
178
+ collapsed_paths
179
+ end
180
+
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,67 @@
1
+ #
2
+ # Author:: Christopher Walters (<cw@opscode.com>)
3
+ # Author:: Mark Anderson (<mark@opscode.com>)
4
+ # Copyright:: Copyright (c) 2010-2011 Opscode, Inc.
5
+ # License:: Apache License, Version 2.0
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ module DepSelector
21
+ module Exceptions
22
+
23
+ # This exception is what the client of dep_selector should
24
+ # catch. It contains the solution constraint that introduces
25
+ # unsatisfiability, as well as the set of packages that are
26
+ # required to be disabled due to
27
+ class NoSolutionExists < StandardError
28
+ attr_reader :message, :unsatisfiable_solution_constraint,
29
+ :disabled_non_existent_packages,
30
+ :disabled_most_constrained_packages
31
+ def initialize(message, unsatisfiable_solution_constraint,
32
+ disabled_non_existent_packages = [],
33
+ disabled_most_constrained_packages = [])
34
+ @message = message
35
+ @unsatisfiable_solution_constraint = unsatisfiable_solution_constraint
36
+ @disabled_non_existent_packages = disabled_non_existent_packages
37
+ @disabled_most_constrained_packages = disabled_most_constrained_packages
38
+ end
39
+ end
40
+
41
+ # This exception is thrown by gecode_wrapper and only used
42
+ # internally
43
+ class NoSolutionFound < StandardError
44
+ attr_reader :unsatisfiable_problem
45
+ def initialize(unsatisfiable_problem=nil)
46
+ @unsatisfiable_problem = unsatisfiable_problem
47
+ end
48
+ end
49
+
50
+ # This exception is thrown during Selector#find_solution if any of
51
+ # the solution constraints make finding solution impossible. The
52
+ # two cases are if a solution constraint references a package that
53
+ # doesn't exist or if the constraint on an extant package matches
54
+ # no versions.
55
+ class InvalidSolutionConstraints < ArgumentError
56
+ attr_reader :non_existent_packages, :constrained_to_no_versions
57
+ def initialize(non_existent_packages, constrained_to_no_versions)
58
+ @non_existent_packages = non_existent_packages
59
+ @constrained_to_no_versions = constrained_to_no_versions
60
+ end
61
+ end
62
+
63
+ class InvalidVersion < ArgumentError ; end
64
+ class InvalidVersionConstraint < ArgumentError; end
65
+
66
+ end
67
+ end
@@ -0,0 +1,151 @@
1
+ #
2
+ # Author:: Christopher Walters (<cw@opscode.com>)
3
+ # Author:: Mark Anderson (<mark@opscode.com>)
4
+ # Copyright:: Copyright (c) 2010-2011 Opscode, Inc.
5
+ # License:: Apache License, Version 2.0
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ require "dep_gecode"
21
+ require 'dep_selector/exceptions'
22
+
23
+ module DepSelector
24
+ class GecodeWrapper
25
+ attr_reader :gecode_problem
26
+ DontCareConstraint = -1
27
+ NoMatchConstraint = -2
28
+
29
+ # This insures that we properly deallocate the c++ class at the heart of dep_gecode.
30
+ # modeled after http://www.mikeperham.com/2010/02/24/the-trouble-with-ruby-finalizers/
31
+ def initialize(problem_or_package_count)
32
+ if (problem_or_package_count.is_a?(Numeric))
33
+ @gecode_problem = Dep_gecode.VersionProblemCreate(problem_or_package_count)
34
+ else
35
+ @gecode_problem = problem_or_package_count
36
+ end
37
+ ObjectSpace.define_finalizer(self, self.class.finalize(@gecode_problem))
38
+ end
39
+ def self.finalize(gecode_problem)
40
+ proc { Dep_gecode.VersionProblemDestroy(gecode_problem) }
41
+ end
42
+
43
+ def check_package_id(package_id, param_name)
44
+ raise "Gecode #{param_name} is out of range #{package_id}" unless (package_id >= 0 && package_id < self.size())
45
+ end
46
+
47
+ def size()
48
+ raise "Gecode internal failure" if gecode_problem.nil?
49
+ Dep_gecode.VersionProblemSize(gecode_problem)
50
+ end
51
+ def package_count()
52
+ raise "Gecode internal failure" if gecode_problem.nil?
53
+ Dep_gecode.VersionProblemPackageCount(gecode_problem)
54
+ end
55
+ def add_package(min, max, current_version)
56
+ raise "Gecode internal failure" if gecode_problem.nil?
57
+ Dep_gecode.AddPackage(gecode_problem, min, max, current_version)
58
+ end
59
+
60
+ def add_version_constraint(package_id, version, dependent_package_id, min_dependent_version, max_dependent_version)
61
+ raise "Gecode internal failure" if gecode_problem.nil?
62
+ check_package_id(package_id, "package_id")
63
+ check_package_id(dependent_package_id, "dependent_package_id")
64
+
65
+ # Valid package versions are between -1 and its max (-1 means
66
+ # don't care, meaning it doesn't need to be assigned). To
67
+ # indicate constraints that match no versions, -2 is used, since
68
+ # it's not a valid assignment of the variable; thus, any branch
69
+ # that assigns -2 will fail.
70
+ #
71
+ # This mechanism is also used when a dependent package has no
72
+ # versions, which only happens if the dependency's package is
73
+ # auto-vivified when creating the parent PackageVersion's
74
+ # dependency but with no corresponding set of PackageVersions
75
+ # (i.e. it's an invalid deendency, because it does not exist in
76
+ # the dependency graph). Again, we won't abort immediately, but
77
+ # we'll add a constraint to the package that makes exploring
78
+ # that portion of the solution space unsatisfiable. Thus it is
79
+ # impossible to find solutions dependent on non-existent
80
+ # packages.
81
+
82
+ min = min_dependent_version || NoMatchConstraint
83
+ max = max_dependent_version || NoMatchConstraint
84
+ Dep_gecode.AddVersionConstraint(gecode_problem, package_id, version, dependent_package_id, min, max)
85
+
86
+ # if the package was constrained to no versions, hint to the
87
+ # solver that in the event of failure, it should prefer to
88
+ # indicate constraints on dependent_package_id as the culprit
89
+ if min == NoMatchConstraint && max == NoMatchConstraint
90
+ Dep_gecode.MarkPackageSuspicious(gecode_problem, dependent_package_id)
91
+ end
92
+ end
93
+ def get_package_version(package_id)
94
+ raise "Gecode internal failure" if gecode_problem.nil?
95
+ check_package_id(package_id, "package_id")
96
+ Dep_gecode.GetPackageVersion(gecode_problem, package_id)
97
+ end
98
+ def is_package_disabled?(package_id)
99
+ raise "Gecode internal failure" if gecode_problem.nil?
100
+ check_package_id(package_id, "package_id")
101
+ Dep_gecode.GetPackageDisabledState(gecode_problem, package_id);
102
+ end
103
+
104
+ def get_package_max(package_id)
105
+ raise "Gecode internal failure" if gecode_problem.nil?
106
+ check_package_id(package_id, "package_id")
107
+ Dep_gecode.GetPackageMax(gecode_problem, package_id)
108
+ end
109
+ def get_package_min(package_id)
110
+ raise "Gecode internal failure" if gecode_problem.nil?
111
+ check_package_id(package_id, "package_id")
112
+ Dep_gecode.GetPackageMin(gecode_problem, package_id)
113
+ end
114
+ def dump()
115
+ raise "Gecode internal failure" if gecode_problem.nil?
116
+ Dep_gecode.VersionProblemDump(gecode_problem)
117
+ end
118
+ def dump_package_var(package_id)
119
+ raise "Gecode internal failure" if gecode_problem.nil?
120
+ check_package_id(package_id, "package_id")
121
+ Dep_gecode.VersionProblemPrintPackageVar(gecode_problem, package_id)
122
+ end
123
+
124
+ def package_disabled_count
125
+ raise "Gecode internal failure (package disabled count)" if gecode_problem.nil?
126
+ Dep_gecode.GetDisabledVariableCount(gecode_problem)
127
+ end
128
+
129
+ def mark_required(package_id)
130
+ raise "Gecode internal failure (mark_required)" if gecode_problem.nil?
131
+ check_package_id(package_id, "package_id")
132
+ Dep_gecode.MarkPackageRequired(gecode_problem, package_id);
133
+ end
134
+
135
+ def mark_preferred_to_be_at_latest(package_id, weight)
136
+ raise "Gecode internal failure (mark_preferred_to_be_at_latest)" if gecode_problem.nil?
137
+ check_package_id(package_id, "package_id")
138
+ Dep_gecode.MarkPackagePreferredToBeAtLatest(gecode_problem, package_id, weight);
139
+ end
140
+
141
+ def solve()
142
+ raise "Gecode internal failure (solve)" if gecode_problem.nil?
143
+ solution = GecodeWrapper.new(Dep_gecode.Solve(gecode_problem))
144
+ raise "Gecode internal failure (no solution found)" if (solution.nil?)
145
+
146
+ raise Exceptions::NoSolutionFound.new(solution) if solution.package_disabled_count > 0
147
+ solution
148
+ end
149
+
150
+ end
151
+ end