lyp 0.0.1

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/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
+