lyp-win 0.2.2

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