lyp-win 0.2.2

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.
@@ -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
+