dep_selector 0.0.1
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/ext/dep_gecode/dep_selector_swig.i +64 -0
- data/ext/dep_gecode/dep_selector_swig_wrap.cxx +2462 -0
- data/ext/dep_gecode/dep_selector_to_gecode.cpp +557 -0
- data/ext/dep_gecode/dep_selector_to_gecode.h +136 -0
- data/ext/dep_gecode/dep_selector_to_gecode_interface.cpp +122 -0
- data/ext/dep_gecode/dep_selector_to_gecode_interface.h +70 -0
- data/ext/dep_gecode/extconf.rb +36 -0
- data/ext/dep_gecode/lib/dep_selector_to_gecode.rb +11 -0
- data/lib/dep_selector.rb +32 -0
- data/lib/dep_selector/densely_packed_set.rb +59 -0
- data/lib/dep_selector/dependency.rb +46 -0
- data/lib/dep_selector/dependency_graph.rb +83 -0
- data/lib/dep_selector/error_reporter.rb +28 -0
- data/lib/dep_selector/error_reporter/simple_tree_traverser.rb +183 -0
- data/lib/dep_selector/exceptions.rb +67 -0
- data/lib/dep_selector/gecode_wrapper.rb +151 -0
- data/lib/dep_selector/package.rb +116 -0
- data/lib/dep_selector/package_version.rb +68 -0
- data/lib/dep_selector/selector.rb +264 -0
- data/lib/dep_selector/solution_constraint.rb +22 -0
- data/lib/dep_selector/version.rb +74 -0
- data/lib/dep_selector/version_constraint.rb +120 -0
- metadata +81 -0
@@ -0,0 +1,116 @@
|
|
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_version'
|
21
|
+
require 'dep_selector/densely_packed_set'
|
22
|
+
|
23
|
+
module DepSelector
|
24
|
+
class Package
|
25
|
+
attr_reader :dependency_graph, :name, :versions
|
26
|
+
|
27
|
+
def initialize(dependency_graph, name)
|
28
|
+
@dependency_graph = dependency_graph
|
29
|
+
@name = name
|
30
|
+
@versions = []
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_version(version)
|
34
|
+
versions << (pv = PackageVersion.new(self, version))
|
35
|
+
pv
|
36
|
+
end
|
37
|
+
|
38
|
+
# Note: only invoke this method after all PackageVersions have been added
|
39
|
+
def densely_packed_versions
|
40
|
+
@densely_packed_versions ||= DenselyPackedSet.new(versions.map{|pkg_version| pkg_version.version})
|
41
|
+
end
|
42
|
+
|
43
|
+
# Given a version, this method returns the corresonding
|
44
|
+
# PackageVersion. Given a version constraint, this method returns
|
45
|
+
# an array of matching PackageVersions.
|
46
|
+
#--
|
47
|
+
# TODO [cw,2011/2/4]: rationalize this with DenselyPackedSet#[]
|
48
|
+
def [](version_or_constraint)
|
49
|
+
# version constraints must abide the include? contract
|
50
|
+
if version_or_constraint.respond_to?(:include?)
|
51
|
+
versions.select do |ver|
|
52
|
+
version_or_constraint.include?(ver)
|
53
|
+
end
|
54
|
+
else
|
55
|
+
versions.find{|pkg_version| pkg_version.version == version_or_constraint}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# A Package is considered valid if it has at least one version
|
60
|
+
def valid?
|
61
|
+
versions.any?
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_s(incl_densely_packed_versions = false)
|
65
|
+
components = []
|
66
|
+
components << "Package #{name}"
|
67
|
+
if incl_densely_packed_versions
|
68
|
+
components << " (#{densely_packed_versions.range})"
|
69
|
+
end
|
70
|
+
versions.each{|version| components << "\n #{version.to_s(incl_densely_packed_versions)}"}
|
71
|
+
components.flatten.join
|
72
|
+
end
|
73
|
+
|
74
|
+
# Note: only invoke this method after all PackageVersions have been added
|
75
|
+
def gecode_package_id
|
76
|
+
# Note: gecode does naive bounds propagation at every post,
|
77
|
+
# which means that any package with exactly one version is
|
78
|
+
# considered bound and its dependencies propagated even though
|
79
|
+
# there might not be a solution constraint that requires that
|
80
|
+
# package to be bound, which means that otherwise-irrelevant
|
81
|
+
# constraints (e.g. A1->B1 when the solution constraint is B=2
|
82
|
+
# and there is nothing to induce a dependency on A) can cause
|
83
|
+
# unsatisfiability. Therefore, we want every package to have at
|
84
|
+
# least two versions, one of which is neither the target of
|
85
|
+
# other packages' dependencies nor induces other
|
86
|
+
# dependencies. Package version id -1 serves this purpose.
|
87
|
+
#
|
88
|
+
# TODO [cw, 2011/2/22]: Do we likewise want to leave packages
|
89
|
+
# with no versions (the target of an invalid dependency) with
|
90
|
+
# two versions in order to allow the solver to explore the
|
91
|
+
# invalid portion of the state space instead of naively limiting
|
92
|
+
# it for the purposes of having failure count heuristics?
|
93
|
+
max = densely_packed_versions.range.max || -1
|
94
|
+
@gecode_package_id ||= dependency_graph.gecode_wrapper.add_package(-1, max, 0)
|
95
|
+
end
|
96
|
+
|
97
|
+
def generate_gecode_wrapper_constraints
|
98
|
+
# if this is a valid package (has versions), we don't need to
|
99
|
+
# explicitly call gecode_package_id, because they will do it for
|
100
|
+
# us; however, if this package isn't valid (it only exists as an
|
101
|
+
# invalid dependency and thus has no versions), the solver gets
|
102
|
+
# confused, because we won't have generated its package id by
|
103
|
+
# the time it starts solving.
|
104
|
+
gecode_package_id
|
105
|
+
versions.each{|version| version.generate_gecode_wrapper_constraints }
|
106
|
+
end
|
107
|
+
|
108
|
+
def eql?(o)
|
109
|
+
# TODO [cw,2011/2/7]: this is really shallow. should implement
|
110
|
+
# == for DependencyGraph
|
111
|
+
self.class == o.class && name == o.name
|
112
|
+
end
|
113
|
+
alias :== :eql?
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,68 @@
|
|
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 PackageVersion
|
22
|
+
attr_accessor :package, :version, :dependencies
|
23
|
+
|
24
|
+
def initialize(package, version)
|
25
|
+
@package = package
|
26
|
+
@version = version
|
27
|
+
@dependencies = []
|
28
|
+
end
|
29
|
+
|
30
|
+
def generate_gecode_wrapper_constraints
|
31
|
+
pkg_densely_packed_version = package.densely_packed_versions.index(version)
|
32
|
+
|
33
|
+
dependencies.each do |dep|
|
34
|
+
dep_pkg_range = dep.package.densely_packed_versions[dep.constraint]
|
35
|
+
package.dependency_graph.gecode_wrapper.add_version_constraint(package.gecode_package_id, pkg_densely_packed_version, dep.package.gecode_package_id, dep_pkg_range.min, dep_pkg_range.max)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s(incl_densely_packed_versions = false)
|
40
|
+
components = []
|
41
|
+
components << "#{version}"
|
42
|
+
if incl_densely_packed_versions
|
43
|
+
components << " (#{package.densely_packed_versions.index(version)})"
|
44
|
+
end
|
45
|
+
components << " -> [#{dependencies.map{|d|d.to_s(incl_densely_packed_versions)}.join(', ')}]"
|
46
|
+
components.join
|
47
|
+
end
|
48
|
+
|
49
|
+
def hash
|
50
|
+
# Didn't put any thought or research into this, probably can be
|
51
|
+
# done better
|
52
|
+
to_s.hash
|
53
|
+
end
|
54
|
+
|
55
|
+
def eql?(o)
|
56
|
+
o.class == self.class &&
|
57
|
+
package == o.package &&
|
58
|
+
version == o.version &&
|
59
|
+
dependencies == o.dependencies
|
60
|
+
end
|
61
|
+
alias :== :eql?
|
62
|
+
|
63
|
+
def to_hash
|
64
|
+
{ :package_name => package.name, :version => version }
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,264 @@
|
|
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/dependency_graph'
|
21
|
+
require 'dep_selector/exceptions'
|
22
|
+
require 'dep_selector/error_reporter'
|
23
|
+
require 'dep_selector/error_reporter/simple_tree_traverser'
|
24
|
+
|
25
|
+
# A Selector contains the a DependencyGraph, which is populated with
|
26
|
+
# the dependency relationships, and an array of solution
|
27
|
+
# constraints. When a solution is asked for (via #find_solution),
|
28
|
+
# either a valid assignment is returned or the first solution
|
29
|
+
# constraint that makes a solution impossible.
|
30
|
+
module DepSelector
|
31
|
+
class Selector
|
32
|
+
attr_accessor :dep_graph, :error_reporter
|
33
|
+
|
34
|
+
DEFAULT_ERROR_REPORTER = ErrorReporter::SimpleTreeTraverser.new
|
35
|
+
|
36
|
+
def initialize(dep_graph, error_reporter = DEFAULT_ERROR_REPORTER)
|
37
|
+
@dep_graph = dep_graph
|
38
|
+
@error_reporter = error_reporter
|
39
|
+
end
|
40
|
+
|
41
|
+
# Based on solution_constraints, this method tries to find an
|
42
|
+
# assignment of PackageVersions that is compatible with the
|
43
|
+
# DependencyGraph. If one cannot be found, the constraints are
|
44
|
+
# added one at a time until the first unsatisfiable constraint is
|
45
|
+
# detected. Once the unsatisfiable solution constraint is
|
46
|
+
# identified, required non-existent packages and the most
|
47
|
+
# constrained packages are identified and thrown in a
|
48
|
+
# NoSolutionExists exception.
|
49
|
+
#
|
50
|
+
# If a solution constraint refers to a package that doesn't exist
|
51
|
+
# or the constraint matches no versions, it is considered
|
52
|
+
# invalid. All invalid solution constraints are collected and
|
53
|
+
# raised in an InvalidSolutionConstraints exception. If
|
54
|
+
# valid_packages is non-nil, it is considered the authoritative
|
55
|
+
# list of extant Packages; otherwise, Package#valid? is used. This
|
56
|
+
# is useful if the dependency graph represents an already filtered
|
57
|
+
# set of packages such that a Package actually exists in your
|
58
|
+
# domain but is added to the dependency graph with no versions, in
|
59
|
+
# which case Package#valid? would return false even though we
|
60
|
+
# don't want to report that the package is non-existent.
|
61
|
+
def find_solution(solution_constraints, valid_packages = nil)
|
62
|
+
# this is a performance optimization so that packages that are
|
63
|
+
# completely unreachable by the solution constraints don't get
|
64
|
+
# added to the CSP
|
65
|
+
packages_to_include_in_solve = trim_unreachable_packages(dep_graph, solution_constraints)
|
66
|
+
|
67
|
+
begin
|
68
|
+
# first, try to solve the whole set of constraints
|
69
|
+
solve(dep_graph.clone, solution_constraints, valid_packages, packages_to_include_in_solve)
|
70
|
+
rescue Exceptions::NoSolutionFound
|
71
|
+
# since we're here, solving the whole system failed, so add
|
72
|
+
# the solution_constraints one-by-one and try to solve in
|
73
|
+
# order to find the constraint that breaks the system in order
|
74
|
+
# to give helpful debugging info
|
75
|
+
#
|
76
|
+
# TODO [cw,2010/11/28]: for an efficiency gain, instead of
|
77
|
+
# continually re-building the problem and looking for a
|
78
|
+
# solution, turn solution_constraints into a Generator and
|
79
|
+
# iteratively add and solve in order to re-use
|
80
|
+
# propagations. This will require separating setting up the
|
81
|
+
# constraints from searching for the solution.
|
82
|
+
solution_constraints.each_index do |idx|
|
83
|
+
workspace = dep_graph.clone
|
84
|
+
begin
|
85
|
+
solve(workspace, solution_constraints[0..idx], valid_packages, packages_to_include_in_solve)
|
86
|
+
rescue Exceptions::NoSolutionFound => nsf
|
87
|
+
disabled_packages =
|
88
|
+
packages_to_include_in_solve.inject([]) do |acc, elt|
|
89
|
+
pkg = workspace.package(elt.name)
|
90
|
+
acc << pkg if nsf.unsatisfiable_problem.is_package_disabled?(pkg.gecode_package_id)
|
91
|
+
acc
|
92
|
+
end
|
93
|
+
# disambiguate between packages disabled becuase they
|
94
|
+
# don't exist and those that have otherwise problematic
|
95
|
+
# constraints
|
96
|
+
disabled_non_existent_packages = []
|
97
|
+
disabled_most_constrained_packages = []
|
98
|
+
disabled_packages.each do |disabled_pkg|
|
99
|
+
disabled_collection =
|
100
|
+
if disabled_pkg.valid? || (valid_packages && valid_packages.include?(disabled_pkg))
|
101
|
+
disabled_most_constrained_packages
|
102
|
+
else
|
103
|
+
disabled_non_existent_packages
|
104
|
+
end
|
105
|
+
disabled_collection << disabled_pkg
|
106
|
+
end
|
107
|
+
|
108
|
+
# Pick the first non-existent or most-constrained package
|
109
|
+
# that was required or the package whose constraints had
|
110
|
+
# to be disabled in order to find a solution and generate
|
111
|
+
# feedback for it. We only report feedback for one
|
112
|
+
# package, because it is in fact actionable and dispalying
|
113
|
+
# feedback for every disabled package would probably be
|
114
|
+
# too long. The full set of disabled packages is
|
115
|
+
# accessible in the NoSolutionExists exception.
|
116
|
+
disabled_package_to_report_on = disabled_non_existent_packages.first ||
|
117
|
+
disabled_most_constrained_packages.first
|
118
|
+
feedback = error_reporter.give_feedback(dep_graph, solution_constraints, idx,
|
119
|
+
disabled_package_to_report_on)
|
120
|
+
|
121
|
+
raise Exceptions::NoSolutionExists.new(feedback, solution_constraints[idx],
|
122
|
+
disabled_non_existent_packages,
|
123
|
+
disabled_most_constrained_packages)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
# Given a workspace (a clone of the dependency graph) and an array
|
132
|
+
# of SolutionConstraints, this method attempts to find a
|
133
|
+
# satisfiable set of <Package, Version> pairs.
|
134
|
+
def solve(workspace, solution_constraints, valid_packages, packages_to_include_in_solve)
|
135
|
+
# map packages in packages_to_include_in_solve into
|
136
|
+
# corresponding workspace packages and generate constraints
|
137
|
+
# imposed by the dependency graph
|
138
|
+
packages_in_solve = packages_to_include_in_solve.map{|pkg| workspace.package(pkg.name)}
|
139
|
+
workspace.generate_gecode_wrapper_constraints(packages_in_solve)
|
140
|
+
|
141
|
+
# validate solution_constraints and generate its constraints
|
142
|
+
process_soln_constraints(workspace, solution_constraints, valid_packages)
|
143
|
+
|
144
|
+
# solve and trim the solution down to only the
|
145
|
+
soln = workspace.gecode_wrapper.solve
|
146
|
+
trim_solution(solution_constraints, soln, workspace)
|
147
|
+
end
|
148
|
+
|
149
|
+
# This method validates SolutionConstraints and adds their
|
150
|
+
# corresponding constraints to the workspace.
|
151
|
+
def process_soln_constraints(workspace, solution_constraints, valid_packages)
|
152
|
+
gecode = workspace.gecode_wrapper
|
153
|
+
|
154
|
+
# create shadow package whose dependencies are the solution constraints
|
155
|
+
soln_constraints_pkg_id = gecode.add_package(0, 0, 0)
|
156
|
+
|
157
|
+
soln_constraints_on_non_existent_packages = []
|
158
|
+
soln_constraints_that_match_no_versions = []
|
159
|
+
|
160
|
+
# generate constraints imposed by solution_constraints
|
161
|
+
solution_constraints.each do |soln_constraint|
|
162
|
+
# look up the package in the cloned dep_graph that corresponds to soln_constraint
|
163
|
+
pkg_name = soln_constraint.package.name
|
164
|
+
pkg = workspace.package(pkg_name)
|
165
|
+
constraint = soln_constraint.constraint
|
166
|
+
|
167
|
+
# record invalid solution constraints and raise an exception
|
168
|
+
# afterwards
|
169
|
+
unless pkg.valid? || (valid_packages && valid_packages.include?(pkg))
|
170
|
+
soln_constraints_on_non_existent_packages << soln_constraint
|
171
|
+
next
|
172
|
+
end
|
173
|
+
if pkg[constraint].empty?
|
174
|
+
soln_constraints_that_match_no_versions << soln_constraint
|
175
|
+
next
|
176
|
+
end
|
177
|
+
|
178
|
+
pkg_id = pkg.gecode_package_id
|
179
|
+
gecode.mark_preferred_to_be_at_latest(pkg_id, 10)
|
180
|
+
gecode.mark_required(pkg_id)
|
181
|
+
|
182
|
+
if constraint
|
183
|
+
acceptable_versions = pkg.densely_packed_versions[constraint]
|
184
|
+
gecode.add_version_constraint(soln_constraints_pkg_id, 0, pkg_id, acceptable_versions.min, acceptable_versions.max)
|
185
|
+
else
|
186
|
+
# this restricts the domain of the variable to >= 0, which
|
187
|
+
# means -1, the shadow package, cannot be assigned, meaning
|
188
|
+
# the package must be bound to an actual version
|
189
|
+
gecode.add_version_constraint(soln_constraints_pkg_id, 0, pkg_id, 0, pkg.densely_packed_versions.range.max)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
if soln_constraints_on_non_existent_packages.any? || soln_constraints_that_match_no_versions.any?
|
194
|
+
raise Exceptions::InvalidSolutionConstraints.new(soln_constraints_on_non_existent_packages,
|
195
|
+
soln_constraints_that_match_no_versions)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Given an assignment of versions to packages, filter down to only
|
200
|
+
# the required assignments
|
201
|
+
def trim_solution(soln_constraints, soln, workspace)
|
202
|
+
trimmed_soln = {}
|
203
|
+
soln_constraints.each do |soln_constraint|
|
204
|
+
package = workspace.package(soln_constraint.package.name)
|
205
|
+
expand_package(trimmed_soln, package, soln)
|
206
|
+
end
|
207
|
+
|
208
|
+
trimmed_soln
|
209
|
+
end
|
210
|
+
|
211
|
+
def expand_package(trimmed_soln, package, soln)
|
212
|
+
# don't expand packages that we've already expanded
|
213
|
+
return if trimmed_soln.has_key?(package.name)
|
214
|
+
|
215
|
+
# add the package's assignment to the trimmed solution
|
216
|
+
densely_packed_version = soln.get_package_version(package.gecode_package_id)
|
217
|
+
version = package.densely_packed_versions.sorted_elements[densely_packed_version]
|
218
|
+
trimmed_soln[package.name] = version
|
219
|
+
|
220
|
+
# expand the package's dependencies
|
221
|
+
pkg_version = package[version]
|
222
|
+
pkg_version.dependencies.each do |pkg_dep|
|
223
|
+
expand_package(trimmed_soln, pkg_dep.package, soln)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# Given a workspace and solution constraints, this method returns
|
228
|
+
# an array that includes only packages that can be induced by the
|
229
|
+
# solution constraints.
|
230
|
+
def trim_unreachable_packages(workspace, soln_constraints)
|
231
|
+
reachable_packages = []
|
232
|
+
soln_constraints.each do |soln_constraint|
|
233
|
+
find_reachable_packages(workspace,
|
234
|
+
soln_constraint.package,
|
235
|
+
soln_constraint.constraint,
|
236
|
+
reachable_packages)
|
237
|
+
end
|
238
|
+
|
239
|
+
reachable_packages
|
240
|
+
end
|
241
|
+
|
242
|
+
def find_reachable_packages(workspace, curr_pkg, version_constraint, reachable_packages)
|
243
|
+
# TODO [cw, 2011/3/11]: if we really want to minimize the number
|
244
|
+
# of packages we add to reachable_packages, we should go back to
|
245
|
+
# expanding only the versions of curr_pkg that meet
|
246
|
+
# version_constraint and modify the early exit in this method to
|
247
|
+
# trigger on PackageVersions, not just Package.
|
248
|
+
|
249
|
+
# don't follow circular paths or duplicate work
|
250
|
+
return if reachable_packages.include?(curr_pkg)
|
251
|
+
|
252
|
+
# add curr_pkg to reachable_packages and expand its versions'
|
253
|
+
# dependencies
|
254
|
+
reachable_packages << curr_pkg
|
255
|
+
curr_pkg.versions.each do |curr_pkg_ver|
|
256
|
+
curr_pkg_ver.dependencies.each do |dep|
|
257
|
+
find_reachable_packages(workspace, dep.package, dep.constraint, reachable_packages)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
end
|
262
|
+
|
263
|
+
end
|
264
|
+
end
|