lyp 0.0.1

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