lyp 0.3.7 → 0.3.8

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/lib/lyp/resolver.rb CHANGED
@@ -1,499 +1,541 @@
1
- class Lyp::Resolver
2
- def initialize(user_file, opts = {})
3
- @user_file = user_file
4
- @opts = opts
5
- @ext_require = @opts[:ext_require]
6
- end
7
-
8
- # Resolving package dependencies involves two stages:
9
- # 1. Create a dependency tree from user files and packages
10
- # 2. Resolve the dependency tree into a list of specific package versions
11
- def resolve_package_dependencies
12
- tree = get_dependency_tree
13
-
14
- definite_versions = resolve_tree(tree)
15
- specifier_map = map_specifiers_to_versions(tree)
16
-
17
- refs, dirs = {}, {}
18
- definite_versions.each do |v|
19
- package = v =~ Lyp::PACKAGE_RE && $1
20
-
21
- specifier_map[package].each_key {|s| refs[s] = package}
22
- dirs[package] = File.dirname(tree[:available_packages][v][:path])
23
- end
24
-
25
- {
26
- user_file: @user_file,
27
- definite_versions: definite_versions,
28
- package_refs: refs,
29
- package_dirs: dirs,
30
- preload: @opts[:ext_require]
31
- }
32
- end
33
-
34
- DEP_RE = /\\(require|include|pinclude|pincludeOnce) "([^"]+)"/.freeze
35
- INCLUDE = "include".freeze
36
- PINCLUDE = "pinclude".freeze
37
- PINCLUDE_ONCE = "pincludeOnce".freeze
38
- REQUIRE = "require".freeze
39
-
40
- # Each "leaf" on the dependency tree is a hash of the following structure:
41
- # {
42
- # dependencies: {
43
- # "<package_name>" => {
44
- # clause: "<package>@<version_specifier>",
45
- # versions: {
46
- # "<version>" => {...}
47
- # ...
48
- # }
49
- # }
50
- # }
51
- # }
52
- #
53
- # Since files to be processed are added to a queue, this method loops through
54
- # the queue until it's empty.
55
- def get_dependency_tree(opts = {})
56
- tree = {
57
- dependencies: {},
58
- queue: [],
59
- processed_files: {}
60
- }
61
-
62
- queue_file_for_processing(@user_file, tree, tree)
63
-
64
- while job = pull_file_from_queue(tree)
65
- process_lilypond_file(job[:path], tree, job[:leaf], opts)
66
- end
67
-
68
- unless opts[:ignore_missing]
69
- squash_old_versions(tree)
70
- remove_unfulfilled_dependencies(tree)
71
- end
72
-
73
- tree
74
- end
75
-
76
- # Scans a lilypond file for \require and \(p)include statements. An included
77
- # file is queued for processing. For required packages, search for suitable
78
- # versions of the package and add them to the tree.
79
- #
80
- # The leaf argument is a pointer to the current leaf on the tree on which to
81
- # add dependencies. This is how transitive dependencies are represented.
82
- def process_lilypond_file(path, tree, leaf, opts)
83
- # path is expected to be absolute
84
- return if tree[:processed_files][path]
85
-
86
- ly_content = IO.read(path)
87
- dir = File.dirname(path)
88
-
89
- # Parse lilypond file for \include and \require
90
- ly_content.scan(DEP_RE) do |type, ref|
91
- case type
92
- when INCLUDE, PINCLUDE, PINCLUDE_ONCE
93
- process_include_command(ref, dir, tree, leaf, opts)
94
- when REQUIRE
95
- process_require_command(ref, dir, tree, leaf, opts)
96
- end
1
+ module Lyp
2
+ class DependencySpec
3
+ attr_reader :clause, :versions
4
+
5
+ def initialize(clause, versions = {})
6
+ @clause = clause
7
+ @versions = versions.inject({}) {|m, kv| m[kv[0].to_s] = kv[1]; m}
97
8
  end
98
9
 
99
- # process any external requires (supplied using the -r command line option)
100
- if @ext_require
101
- @ext_require.each do |p|
102
- process_require_command(p, dir, tree, leaf, opts)
103
- end
104
- @ext_require = nil
10
+ def add_version(version, leaf)
11
+ @versions[version.to_s] = leaf
12
+ end
13
+
14
+ def eql?(o)
15
+ @clause.eql?(o.clause) && @versions.eql?(o.versions)
16
+ end
17
+
18
+ def hash
19
+ {clase: clause, versions: versions}.hash
105
20
  end
106
-
107
- tree[:processed_files][path] = true
108
- rescue Errno::ENOENT
109
- raise "Cannot find file #{path}"
110
- end
111
-
112
- def process_include_command(ref, dir, tree, leaf, opts)
113
- # a package would normally use a plain \pinclude or \pincludeOnce
114
- # command to include package files, e.g. \pinclude "inc/init.ly".
115
- #
116
- # But a package can also qualify the file reference with the package
117
- # name, in order to be able to load files after the package has already
118
- # been loaded, e.g. \pinclude "mypack:inc/init.ly"
119
- if ref =~ /^([^\:]+)\:(.+)$/
120
- # ignore null package (used for testing purposes only)
121
- return if $1 == 'null'
122
- ref = $2
123
- end
124
- qualified_path = File.expand_path(ref, dir)
125
- queue_file_for_processing(qualified_path, tree, leaf)
126
21
  end
127
-
128
- def process_require_command(ref, dir, tree, leaf, opts)
129
- forced_path = nil
130
- if ref =~ /^([^\:]+)\:(.+)$/
131
- ref = $1
132
- forced_path = File.expand_path($2, dir)
22
+
23
+ class DependencyLeaf
24
+ attr_reader :dependencies
25
+
26
+ def initialize(dependencies = {})
27
+ @dependencies = dependencies.inject({}) {|m, kv| m[kv[0].to_s] = kv[1]; m}
133
28
  end
134
29
 
135
- ref =~ Lyp::PACKAGE_RE
136
- package, version = $1, $2
137
- return if package == 'null'
30
+ def add_dependency(name, spec)
31
+ @dependencies[name.to_s] = spec
32
+ end
138
33
 
139
- # set forced path if applicable
140
- if forced_path
141
- set_forced_package_path(tree, package, forced_path)
34
+ def resolve(opts = {})
35
+ DependencyResolver.new(self, opts).resolve_tree
142
36
  end
143
37
 
144
- find_package_versions(ref, tree, leaf, opts)
145
- end
146
-
147
- def queue_file_for_processing(path, tree, leaf)
148
- (tree[:queue] ||= []) << {path: path, leaf: leaf}
38
+ def eql?(o)
39
+ @dependencies.eql?(o.dependencies)
40
+ end
41
+
42
+ def hash
43
+ @dependencies.hash
44
+ end
149
45
  end
150
-
151
- def pull_file_from_queue(tree)
152
- tree[:queue].shift
46
+
47
+ class DependencyPackage < DependencyLeaf
48
+ attr_reader :path
49
+
50
+ def initialize(path, dependencies = {})
51
+ @path = path
52
+ super(dependencies)
53
+ end
153
54
  end
154
-
155
- # Find available packaging matching the package specifier, and queue them for
156
- # processing any include files or transitive dependencies.
157
- def find_package_versions(ref, tree, leaf, opts)
158
- return {} unless ref =~ Lyp::PACKAGE_RE
159
- ref_package = $1
160
- version_clause = $2
161
-
162
- matches = find_matching_packages(ref, tree)
163
-
164
- # Raise if no match found and we're at top of the tree
165
- if matches.empty? && (tree == leaf) && !opts[:ignore_missing]
166
- raise "No package found for requirement #{ref}"
167
- end
168
-
169
- matches.each do |p, subtree|
170
- if subtree[:path]
171
- queue_file_for_processing(subtree[:path], tree, subtree)
55
+
56
+ class DependencyResolver
57
+ attr_reader :tree, :opts
58
+
59
+ def initialize(tree, opts = {})
60
+ if tree.is_a?(String)
61
+ @user_file = tree
62
+ @tree = DependencyLeaf.new
63
+ else
64
+ @tree = tree
172
65
  end
66
+ @opts = opts
67
+ @ext_require = @opts[:ext_require]
68
+ @queue = []
69
+ @processed_files = {}
173
70
  end
174
71
 
175
- # Setup up dependency leaf
176
- (leaf[:dependencies] ||= {}).merge!({
177
- ref_package => {
178
- clause: ref,
179
- versions: matches
72
+ # Resolving package dependencies involves two stages:
73
+ # 1. Create a dependency tree from user files and packages
74
+ # 2. Resolve the dependency tree into a list of specific package versions
75
+ def resolve_package_dependencies
76
+ compile_dependency_tree
77
+ definite_versions = resolve_tree
78
+ specifier_map = map_specifiers_to_versions
79
+
80
+ refs, dirs = {}, {}
81
+ definite_versions.each do |v|
82
+ package = v =~ Lyp::PACKAGE_RE && $1
83
+
84
+ specifier_map[package].each_key {|s| refs[s] = package}
85
+ dirs[package] = File.dirname(available_packages[v].path)
86
+ end
87
+
88
+ {
89
+ user_file: @user_file,
90
+ definite_versions: definite_versions,
91
+ package_refs: refs,
92
+ package_dirs: dirs,
93
+ preload: @opts[:ext_require]
180
94
  }
181
- })
182
- end
183
-
184
- # Find packages meeting the version requirement
185
- def find_matching_packages(req, tree)
186
- return {} unless req =~ Lyp::PACKAGE_RE
187
-
188
- req_package = $1
189
- req_version = $2
190
-
191
- req = nil
192
- if @opts[:forced_package_paths] && @opts[:forced_package_paths][req_package]
193
- req_version = 'forced'
194
- end
195
-
196
- req = Gem::Requirement.new(req_version || '>=0') rescue nil
197
- available_packages(tree).select do |package, sub_tree|
198
- if (package =~ Lyp::PACKAGE_RE) && (req_package == $1)
199
- version = Gem::Version.new($2 || '0') rescue nil
200
- if version.nil? || req.nil?
201
- req_version.nil? || (req_version == $2)
202
- else
203
- req =~ version
204
- end
95
+ end
96
+
97
+ # Resolve the given dependency tree and return a list of concrete packages
98
+ # that meet all dependency requirements.
99
+ #
100
+ # The following stages are involved:
101
+ # - Create permutations of possible version combinations for all dependencies
102
+ # - Remove invalid permutations
103
+ # - Select the permutation with the highest versions
104
+ def resolve_tree
105
+ permutations = permutate_simplified_tree
106
+ permutations = filter_invalid_permutations(permutations)
107
+
108
+ # select highest versioned dependencies (for those specified by user)
109
+ user_deps = tree.dependencies.keys
110
+ result = select_highest_versioned_permutation(permutations, user_deps).flatten
111
+
112
+ if result.empty? && !tree.dependencies.empty?
113
+ raise "Failed to satisfy dependency requirements"
205
114
  else
206
- nil
115
+ result
207
116
  end
208
117
  end
209
- end
210
-
211
- MAIN_PACKAGE_FILE = 'package.ly'
212
-
213
- # Memoize and return a hash of available packages
214
- def available_packages(tree)
215
- tree[:available_packages] ||= get_available_packages(Lyp.packages_dir)
216
- end
217
-
218
- def set_forced_package_path(tree, package, path)
219
- @opts[:forced_package_paths] ||= {}
220
- @opts[:forced_package_paths][package] = path
221
-
222
- available_packages(tree)["#{package}@forced"] = {
223
- path: File.join(path, MAIN_PACKAGE_FILE),
224
- dependencies: {}
225
- }
226
- end
227
-
228
- # Return a hash of all packages found in the packages directory, creating a
229
- # leaf for each package
230
- def get_available_packages(dir)
231
- packages = Dir["#{Lyp.packages_dir}/*"].inject({}) do |m, p|
232
- name = File.basename(p)
233
-
234
- m[name] = {
235
- path: File.join(p, MAIN_PACKAGE_FILE),
236
- dependencies: {}
237
- }
238
- m
239
- end
240
118
 
241
- forced_paths = @opts[:forced_package_paths] || {}
242
-
243
- if @opts[:forced_package_paths]
244
- @opts[:forced_package_paths].each do |package, path|
245
- packages["#{package}@forced"] = {
246
- path: File.join(path, MAIN_PACKAGE_FILE),
247
- dependencies: {}
248
- }
119
+ DEP_RE = /\\(require|include|pinclude|pincludeOnce) "([^"]+)"/.freeze
120
+ INCLUDE = "include".freeze
121
+ PINCLUDE = "pinclude".freeze
122
+ PINCLUDE_ONCE = "pincludeOnce".freeze
123
+ REQUIRE = "require".freeze
124
+
125
+ # Each "leaf" on the dependency tree is a hash of the following structure:
126
+ # {
127
+ # dependencies: {
128
+ # "<package_name>" => {
129
+ # clause: "<package>@<version_specifier>",
130
+ # versions: {
131
+ # "<version>" => {...}
132
+ # ...
133
+ # }
134
+ # }
135
+ # }
136
+ # }
137
+ #
138
+ # Since files to be processed are added to a queue, this method loops through
139
+ # the queue until it's empty.
140
+ def compile_dependency_tree(opts = {})
141
+ old_opts = @opts
142
+ @opts = @opts.merge(opts)
143
+ @queue = []
144
+ @processed_files = {}
145
+ @tree ||= DependencyLeaf.new
146
+
147
+ queue_file_for_processing(@user_file, @tree)
148
+
149
+ while job = pull_file_from_queue
150
+ process_lilypond_file(job[:path], job[:leaf], opts)
249
151
  end
152
+
153
+ unless @opts[:ignore_missing]
154
+ squash_old_versions
155
+ remove_unfulfilled_dependencies(tree)
156
+ end
157
+
158
+ @tree
159
+ ensure
160
+ @opts = old_opts
250
161
  end
251
-
252
- packages
253
- end
254
162
 
255
- # Recursively remove any dependency for which no version is locally
256
- # available. If no version is found for any of the dependencies specified
257
- # by the user, an error is raised.
258
- #
259
- # The processed hash is used for keeping track of dependencies that were
260
- # already processed, and thus deal with circular dependencies.
261
- def remove_unfulfilled_dependencies(tree, raise_on_missing = true, processed = {})
262
- return unless tree[:dependencies]
263
-
264
- tree[:dependencies].each do |package, dependency|
265
- dependency[:versions].select! do |version, version_subtree|
266
- if processed[version]
267
- true
268
- else
269
- processed[version] = true
163
+ # Scans a lilypond file for \require and \(p)include statements. An included
164
+ # file is queued for processing. For required packages, search for suitable
165
+ # versions of the package and add them to the tree.
166
+ #
167
+ # The leaf argument is a pointer to the current leaf on the tree on which to
168
+ # add dependencies. This is how transitive dependencies are represented.
169
+ def process_lilypond_file(path, leaf, opts)
170
+ # path is expected to be absolute
171
+ return if @processed_files[path]
270
172
 
271
- # Remove unfulfilled transitive dependencies
272
- remove_unfulfilled_dependencies(version_subtree, false, processed)
273
- valid = true
274
- version_subtree[:dependencies].each do |k, v|
275
- valid = false if v[:versions].empty?
276
- end
277
- valid
173
+ ly_content = IO.read(path)
174
+ dir = File.dirname(path)
175
+
176
+ # Parse lilypond file for \include and \require
177
+ ly_content.scan(DEP_RE) do |type, ref|
178
+ case type
179
+ when INCLUDE, PINCLUDE, PINCLUDE_ONCE
180
+ process_include_command(ref, dir, leaf, opts)
181
+ when REQUIRE
182
+ process_require_command(ref, dir, leaf, opts)
278
183
  end
279
184
  end
280
- if dependency[:versions].empty? && raise_on_missing
281
- raise "No valid version found for package #{package}"
185
+
186
+ # process any external requires (supplied using the -r command line option)
187
+ if @ext_require
188
+ @ext_require.each do |p|
189
+ process_require_command(p, dir, leaf, opts)
190
+ end
191
+ @ext_require = nil
282
192
  end
193
+
194
+ @processed_files[path] = true
195
+ rescue Errno::ENOENT
196
+ raise "Cannot find file #{path}"
283
197
  end
284
- end
285
-
286
- # Remove redundant older versions of dependencies by collating package
287
- # versions by package specifiers, then removing older versions for any
288
- # package for which a single package specifier exists.
289
- def squash_old_versions(tree)
290
- specifiers = map_specifiers_to_versions(tree)
291
-
292
- compare_versions = lambda do |x, y|
293
- v_x = x =~ Lyp::PACKAGE_RE && Gem::Version.new($2)
294
- v_y = y =~ Lyp::PACKAGE_RE && Gem::Version.new($2)
295
- x <=> y
296
- end
297
-
298
- # Remove old versions for anything but
299
- specifiers.each do |package, specifiers|
300
- # Remove old versions only if the package is referenced from a single
301
- # specifier
302
- if specifiers.size == 1
303
- specifier = specifiers.values.first
304
- specifier.each do |version_tree|
305
- # check if all versions have same dependencies. Older versions can be
306
- # safely removed only if their dependencies are identical
307
- deps = version_tree.map {|k, v| v[:dependencies]}
308
- if deps.uniq.size == 1
309
- versions = version_tree.keys.sort(&compare_versions)
310
- latest = versions.last
311
- version_tree.select! {|v| v == latest}
198
+
199
+ def process_include_command(ref, dir, leaf, opts)
200
+ # a package would normally use a plain \pinclude or \pincludeOnce
201
+ # command to include package files, e.g. \pinclude "inc/init.ly".
202
+ #
203
+ # But a package can also qualify the file reference with the package
204
+ # name, in order to be able to load files after the package has already
205
+ # been loaded, e.g. \pinclude "mypack:inc/init.ly"
206
+ if ref =~ /^([^\:]+)\:(.+)$/
207
+ # ignore null package (used for testing purposes only)
208
+ return if $1 == 'null'
209
+ ref = $2
210
+ end
211
+ qualified_path = File.expand_path(ref, dir)
212
+ queue_file_for_processing(qualified_path, leaf)
213
+ end
214
+
215
+ def process_require_command(ref, dir, leaf, opts)
216
+ forced_path = nil
217
+ if ref =~ /^([^\:]+)\:(.+)$/
218
+ ref = $1
219
+ forced_path = File.expand_path($2, dir)
220
+ end
221
+
222
+ ref =~ Lyp::PACKAGE_RE
223
+ package, version = $1, $2
224
+ return if package == 'null'
225
+
226
+ # set forced path if applicable
227
+ if forced_path
228
+ set_forced_package_path(package, forced_path)
229
+ end
230
+
231
+ find_package_versions(ref, leaf)
232
+ end
233
+
234
+ def queue_file_for_processing(path, leaf)
235
+ @queue << {path: path, leaf: leaf}
236
+ end
237
+
238
+ def pull_file_from_queue
239
+ @queue.shift
240
+ end
241
+
242
+ # Create permutations of package versions for the given dependency tree. The
243
+ # tree is first simplified (superfluous information removed), then turned into
244
+ # an array of dependencies, from which version permutations are generated.
245
+ def permutate_simplified_tree
246
+ deps = dependencies_array(simplified_deps_tree(tree))
247
+ return deps if deps.empty?
248
+
249
+ # Return a cartesian product of dependencies
250
+ deps[0].product(*deps[1..-1]).map(&:flatten)
251
+ end
252
+
253
+ # Converts the dependency tree into a simplified dependency tree of the form
254
+ # {
255
+ # <package name> =>
256
+ # <version> =>
257
+ # <package name> =>
258
+ # <version> => ...
259
+ # ...
260
+ # ...
261
+ # ...
262
+ # ...
263
+ # }
264
+ # The processed hash is used to deal with circular dependencies
265
+ def simplified_deps_tree(leaf, processed = {})
266
+ return {} unless leaf.dependencies
267
+
268
+ return processed[leaf] if processed[leaf]
269
+ processed[leaf] = dep_versions = {}
270
+
271
+ # For each dependency, generate a deps tree for each available version
272
+ leaf.dependencies.each do |p, spec|
273
+ dep_versions[p] = {}
274
+ spec.versions.each do |v, subleaf|
275
+ dep_versions[p][v] =
276
+ simplified_deps_tree(subleaf, processed)
277
+ end
278
+ end
279
+
280
+ dep_versions
281
+ end
282
+
283
+ # Converts a simplified dependency tree into an array of dependencies,
284
+ # containing a sub-array for each top-level dependency. Each such sub-array
285
+ # contains, in its turn, version permutations for the top-level dependency
286
+ # and any transitive dependencies.
287
+ def dependencies_array(leaf, processed = {})
288
+ return processed[leaf] if processed[leaf]
289
+
290
+ deps_array = []
291
+ processed[leaf] = deps_array
292
+
293
+ leaf.each do |pack, versions|
294
+ a = []
295
+ versions.each do |version, deps|
296
+ perms = []
297
+ sub_perms = dependencies_array(deps, processed)
298
+ if sub_perms == []
299
+ perms += [version]
300
+ else
301
+ sub_perms[0].each do |perm|
302
+ perms << [version] + [perm].flatten
303
+ end
312
304
  end
305
+ a += perms
313
306
  end
307
+ deps_array << a
314
308
  end
309
+
310
+ deps_array
315
311
  end
316
- end
317
-
318
- # Return a hash mapping packages to package specifiers to version trees, to
319
- # be used to eliminate older versions from the dependency tree
320
- def map_specifiers_to_versions(tree)
321
- specifiers = {}
322
- processed = {}
323
-
324
- l = lambda do |t|
325
- return if processed[t]
326
- processed[t] = true
327
- t[:dependencies].each do |package, subtree|
328
- versions = subtree[:versions]
329
- clause = subtree[:clause]
330
-
331
- specifiers[package] ||= {}
332
- specifiers[package][clause] ||= []
333
- specifiers[package][clause] << versions
334
-
335
- versions.each_value {|v| l[v]}
312
+
313
+ # Remove invalid permutations, that is permutations that contain multiple
314
+ # versions of the same package, a scenario which could arrive in the case of
315
+ # circular dependencies, or when different dependencies rely on different
316
+ # versions of the same transitive dependency.
317
+ def filter_invalid_permutations(permutations)
318
+ valid = []
319
+ permutations.each do |perm|
320
+ versions = {}; invalid = false
321
+ perm.each do |ref|
322
+ if ref =~ /(.+)@(.+)/
323
+ name, version = $1, $2
324
+ if versions[name] && versions[name] != version
325
+ invalid = true
326
+ break
327
+ else
328
+ versions[name] = version
329
+ end
330
+ end
331
+ end
332
+ valid << perm.uniq unless invalid
336
333
  end
334
+
335
+ valid
337
336
  end
338
-
339
- l[tree]
340
- specifiers
341
- end
342
-
343
- # Resolve the given dependency tree and return a list of concrete packages
344
- # that meet all dependency requirements.
345
- #
346
- # The following stages are involved:
347
- # - Create permutations of possible version combinations for all dependencies
348
- # - Remove invalid permutations
349
- # - Select the permutation with the highest versions
350
- def resolve_tree(tree)
351
- permutations = permutate_simplified_tree(tree)
352
- permutations = filter_invalid_permutations(permutations)
353
-
354
- user_deps = tree[:dependencies].keys
355
- result = select_highest_versioned_permutation(permutations, user_deps).flatten
356
-
357
- if result.empty? && !tree[:dependencies].empty?
358
- raise "Failed to satisfy dependency requirements"
359
- else
360
- result
337
+
338
+ # Select the highest versioned permutation of package versions
339
+ def select_highest_versioned_permutation(permutations, user_deps)
340
+ sorted = sort_permutations(permutations, user_deps)
341
+ sorted.empty? ? [] : sorted.last
361
342
  end
362
- end
363
-
364
- # Create permutations of package versions for the given dependency tree. The
365
- # tree is first simplified (superfluous information removed), then turned into
366
- # an array of dependencies, from which version permutations are generated.
367
- def permutate_simplified_tree(tree)
368
- deps = dependencies_array(simplified_deps_tree(tree))
369
- return deps if deps.empty?
370
-
371
- # Return a cartesian product of dependencies
372
- deps[0].product(*deps[1..-1]).map(&:flatten)
373
- end
374
-
375
- # Converts a simplified dependency tree into an array of dependencies,
376
- # containing a sub-array for each top-level dependency. Each such sub-array
377
- # contains, in its turn, version permutations for the top-level dependency
378
- # and any transitive dependencies.
379
- def dependencies_array(tree, processed = {})
380
- return processed[tree] if processed[tree]
381
-
382
- deps_array = []
383
- processed[tree] = deps_array
384
-
385
- tree.each do |pack, versions|
386
- a = []
387
- versions.each do |version, deps|
388
- perms = []
389
- sub_perms = dependencies_array(deps, processed)
390
- if sub_perms == []
391
- perms += [version]
392
- else
393
- sub_perms[0].each do |perm|
394
- perms << [version] + [perm].flatten
343
+
344
+ # Sort permutations by version numbers
345
+ def sort_permutations(permutations, user_deps)
346
+ # Cache for versions converted to Gem::Version instances
347
+ versions = {}
348
+
349
+ map = lambda do |m, p|
350
+ if p =~ Lyp::PACKAGE_RE
351
+ m[$1] = versions[p] ||= (Gem::Version.new($2 || '0.0') rescue nil)
352
+ end
353
+ m
354
+ end
355
+
356
+ compare = lambda do |x, y|
357
+ x_versions = x.inject({}, &map)
358
+ y_versions = y.inject({}, &map)
359
+
360
+ # If the dependency is direct (not transitive), just compare its versions.
361
+ # Otherwise, add the result of comparison to score.
362
+ x_versions.inject(0) do |score, kv|
363
+ package = kv[0]
364
+ cmp = kv[1] <=> y_versions[package]
365
+ if user_deps.include?(package) && cmp != 0
366
+ return cmp
367
+ else
368
+ score += cmp unless cmp.nil?
395
369
  end
370
+ score
396
371
  end
397
- a += perms
398
372
  end
399
- deps_array << a
373
+
374
+ permutations.sort(&compare)
400
375
  end
401
376
 
402
- deps_array
403
- end
404
-
405
- # Converts the dependency tree into a simplified dependency tree of the form
406
- # {
407
- # <package name> =>
408
- # <version> =>
409
- # <package name> =>
410
- # <version> => ...
411
- # ...
412
- # ...
413
- # ...
414
- # ...
415
- # }
416
- # The processed hash is used to deal with circular dependencies
417
- def simplified_deps_tree(version, processed = {})
418
- return {} unless version[:dependencies]
419
-
420
- return processed[version] if processed[version]
421
- processed[version] = dep_versions = {}
422
-
423
- # For each dependency, generate a deps tree for each available version
424
- version[:dependencies].each do |p, subtree|
425
- dep_versions[p] = {}
426
- subtree[:versions].each do |v, version_subtree|
427
- dep_versions[p][v] =
428
- simplified_deps_tree(version_subtree, processed)
377
+ # Memoize and return a hash of available packages
378
+ def available_packages
379
+ @available_packages ||= get_available_packages(Lyp.packages_dir)
380
+ end
381
+
382
+ MAIN_PACKAGE_FILE = 'package.ly'
383
+
384
+ # Return a hash of all packages found in the packages directory, creating a
385
+ # leaf for each package
386
+ def get_available_packages(dir)
387
+ packages = Dir["#{Lyp.packages_dir}/*"].inject({}) do |m, p|
388
+ name = File.basename(p)
389
+ m[name] = DependencyPackage.new(File.join(p, MAIN_PACKAGE_FILE))
390
+ m
391
+ end
392
+
393
+ forced_paths = @opts[:forced_package_paths] || {}
394
+
395
+ if @opts[:forced_package_paths]
396
+ @opts[:forced_package_paths].each do |package, path|
397
+ packages["#{package}@forced"] = DependencyPackage.new(File.join(path, MAIN_PACKAGE_FILE))
398
+ end
429
399
  end
400
+
401
+ packages
430
402
  end
431
403
 
432
- dep_versions
433
- end
434
-
435
- # Remove invalid permutations, that is permutations that contain multiple
436
- # versions of the same package, a scenario which could arrive in the case of
437
- # circular dependencies, or when different dependencies rely on different
438
- # versions of the same transitive dependency.
439
- def filter_invalid_permutations(permutations)
440
- valid = []
441
- permutations.each do |perm|
442
- versions = {}; invalid = false
443
- perm.each do |ref|
444
- if ref =~ /(.+)@(.+)/
445
- name, version = $1, $2
446
- if versions[name] && versions[name] != version
447
- invalid = true
448
- break
404
+ # Find packages meeting the version requirement
405
+ def find_matching_packages(req)
406
+ return {} unless req =~ Lyp::PACKAGE_RE
407
+
408
+ req_package = $1
409
+ req_version = $2
410
+
411
+ req = nil
412
+ if @opts[:forced_package_paths] && @opts[:forced_package_paths][req_package]
413
+ req_version = 'forced'
414
+ end
415
+
416
+ req = Gem::Requirement.new(req_version || '>=0') rescue nil
417
+ available_packages.select do |package, leaf|
418
+ if (package =~ Lyp::PACKAGE_RE) && (req_package == $1)
419
+ version = Gem::Version.new($2 || '0') rescue nil
420
+ if version.nil? || req.nil?
421
+ req_version.nil? || (req_version == $2)
449
422
  else
450
- versions[name] = version
423
+ req =~ version
451
424
  end
425
+ else
426
+ nil
452
427
  end
453
428
  end
454
- valid << perm.uniq unless invalid
455
429
  end
456
430
 
457
- valid
458
- end
459
-
460
- # Select the highest versioned permutation of package versions
461
- def select_highest_versioned_permutation(permutations, user_deps)
462
- sorted = sort_permutations(permutations, user_deps)
463
- sorted.empty? ? [] : sorted.last
464
- end
431
+ # Find available packaging matching the package specifier, and queue them for
432
+ # processing any include files or transitive dependencies.
433
+ def find_package_versions(ref, leaf)
434
+ return {} unless ref =~ Lyp::PACKAGE_RE
435
+ ref_package = $1
436
+ version_clause = $2
465
437
 
466
- # Sort permutations by version numbers
467
- def sort_permutations(permutations, user_deps)
468
- # Cache for versions converted to Gem::Version instances
469
- versions = {}
470
-
471
- map = lambda do |m, p|
472
- if p =~ Lyp::PACKAGE_RE
473
- m[$1] = versions[p] ||= (Gem::Version.new($2 || '0.0') rescue nil)
438
+ matches = find_matching_packages(ref)
439
+
440
+ # Raise if no match found and we're at top of the tree
441
+ if matches.empty? && (leaf == tree) && !opts[:ignore_missing]
442
+ raise "No package found for requirement #{ref}"
474
443
  end
475
- m
476
- end
477
-
478
- compare = lambda do |x, y|
479
- x_versions = x.inject({}, &map)
480
- y_versions = y.inject({}, &map)
481
-
482
- # If the dependency is direct (not transitive), just compare its versions.
483
- # Otherwise, add the result of comparison to score.
484
- x_versions.inject(0) do |score, kv|
485
- package = kv[0]
486
- cmp = kv[1] <=> y_versions[package]
487
- if user_deps.include?(package) && cmp != 0
488
- return cmp
489
- else
490
- score += cmp unless cmp.nil?
444
+
445
+ matches.each do |p, package_leaf|
446
+ if package_leaf.path
447
+ queue_file_for_processing(package_leaf.path, package_leaf)
448
+ end
449
+ end
450
+
451
+ # Setup up dependency leaf
452
+ leaf.add_dependency(ref_package, DependencySpec.new(ref, matches))
453
+ end
454
+
455
+ # Remove redundant older versions of dependencies by collating package
456
+ # versions by package specifiers, then removing older versions for any
457
+ # package for which a single package specifier exists.
458
+ def squash_old_versions
459
+ specifiers = map_specifiers_to_versions
460
+
461
+ compare_versions = lambda do |x, y|
462
+ v_x = x =~ Lyp::PACKAGE_RE && Gem::Version.new($2)
463
+ v_y = y =~ Lyp::PACKAGE_RE && Gem::Version.new($2)
464
+ x <=> y
465
+ end
466
+
467
+ specifiers.each do |package, clauses|
468
+ # Remove old versions only if the package is referenced using a single
469
+ # specifier clause
470
+ next unless clauses.size == 1
471
+
472
+ specs = clauses.values.first
473
+ specs.each do |s|
474
+ if s.versions.values.uniq.size == 1
475
+ versions = s.versions.keys.sort(&compare_versions)
476
+ latest = versions.last
477
+ s.versions.select! {|k, v| k == latest}
478
+ end
491
479
  end
492
- score
493
480
  end
494
481
  end
495
-
496
- permutations.sort(&compare)
497
- end
482
+
483
+ # Return a hash mapping packages to package specifiers to spec objects, to
484
+ # be used to eliminate older versions from the dependency tree
485
+ def map_specifiers_to_versions
486
+ specifiers = {}
487
+ processed = {}
488
+
489
+ l = lambda do |t|
490
+ return if processed[t.object_id]
491
+ processed[t.object_id] = true
492
+ t.dependencies.each do |package, spec|
493
+ specifiers[package] ||= {}
494
+ specifiers[package][spec.clause] ||= []
495
+ specifiers[package][spec.clause] << spec
496
+ spec.versions.each_value {|v| l[v]}
497
+ end
498
+ end
499
+
500
+ l[@tree]
501
+ specifiers
502
+ end
503
+
504
+ # Recursively remove any dependency for which no version is locally
505
+ # available. If no version is found for any of the dependencies specified
506
+ # by the user, an error is raised.
507
+ #
508
+ # The processed hash is used for keeping track of dependencies that were
509
+ # already processed, and thus deal with circular dependencies.
510
+ def remove_unfulfilled_dependencies(leaf, raise_on_missing = true, processed = {})
511
+ tree.dependencies.each do |package, dependency|
512
+ dependency.versions.select! do |version, leaf|
513
+ if processed[version]
514
+ true
515
+ else
516
+ processed[version] = true
517
+
518
+ # Remove unfulfilled transitive dependencies
519
+ remove_unfulfilled_dependencies(leaf, false, processed)
520
+ valid = true
521
+ leaf.dependencies.each do |k, v|
522
+ valid = false if v.versions.empty?
523
+ end
524
+ valid
525
+ end
526
+ end
527
+ if dependency.versions.empty? && raise_on_missing
528
+ raise "No valid version found for package #{package}"
529
+ end
530
+ end
531
+ end
532
+
533
+ def set_forced_package_path(package, path)
534
+ @opts[:forced_package_paths] ||= {}
535
+ @opts[:forced_package_paths][package] = path
536
+
537
+ available_packages["#{package}@forced"] = DependencyPackage.new(
538
+ File.join(path, MAIN_PACKAGE_FILE))
539
+ end
540
+ end
498
541
  end
499
-