lyp 0.3.7 → 0.3.8

Sign up to get free protection for your applications and to get access to all the features.
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
-