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