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.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +9 -0
- data/bin/lilypond +27 -0
- data/bin/lyp +4 -0
- data/lib/lyp.rb +16 -0
- data/lib/lyp/cli.rb +32 -0
- data/lib/lyp/cli/commands.rb +146 -0
- data/lib/lyp/directories.rb +26 -0
- data/lib/lyp/env.rb +32 -0
- data/lib/lyp/lilypond.rb +377 -0
- data/lib/lyp/output.rb +18 -0
- data/lib/lyp/package.rb +28 -0
- data/lib/lyp/resolver.rb +411 -0
- data/lib/lyp/settings.rb +37 -0
- data/lib/lyp/template.rb +59 -0
- data/lib/lyp/templates/deps_wrapper.rb +58 -0
- data/lib/lyp/version.rb +3 -0
- data/lib/lyp/wrapper.rb +15 -0
- metadata +163 -0
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
|
data/lib/lyp/package.rb
ADDED
@@ -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
|
data/lib/lyp/resolver.rb
ADDED
@@ -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
|
+
|