aboutsummaryrefslogtreecommitdiff
path: root/src/luarocks/deps.lua
diff options
context:
space:
mode:
Diffstat (limited to 'src/luarocks/deps.lua')
-rw-r--r--src/luarocks/deps.lua618
1 files changed, 618 insertions, 0 deletions
diff --git a/src/luarocks/deps.lua b/src/luarocks/deps.lua
new file mode 100644
index 00000000..d5a64e52
--- /dev/null
+++ b/src/luarocks/deps.lua
@@ -0,0 +1,618 @@
1
2--- Dependency handling functions.
3-- Dependencies are represented in LuaRocks through strings with
4-- a package name followed by a comma-separated list of constraints.
5-- Each constraint consists of an operator and a version number.
6-- In this string format, version numbers are represented as
7-- naturally as possible, like they are used by upstream projects
8-- (e.g. "2.0beta3"). Internally, LuaRocks converts them to a purely
9-- numeric representation, allowing comparison following some
10-- "common sense" heuristics. The precise specification of the
11-- comparison criteria is the source code of this module, but the
12-- test/test_deps.lua file included with LuaRocks provides some
13-- insights on what these criteria are.
14module("luarocks.deps", package.seeall)
15
16local rep = require("luarocks.rep")
17local search = require("luarocks.search")
18local install = require("luarocks.install")
19local cfg = require("luarocks.cfg")
20local manif = require("luarocks.manif")
21local fs = require("luarocks.fs")
22local fetch = require("luarocks.fetch")
23local path = require("luarocks.path")
24
25local operators = {
26 ["=="] = "==",
27 ["~="] = "~=",
28 [">"] = ">",
29 ["<"] = "<",
30 [">="] = ">=",
31 ["<="] = "<=",
32 ["~>"] = "~>",
33 -- plus some convenience translations
34 [""] = "==",
35 ["="] = "==",
36 ["!="] = "~="
37}
38
39local deltas = {
40 scm = 1000,
41 cvs = 1000,
42 rc = -1000,
43 pre = -10000,
44 beta = -100000,
45 alpha = -1000000
46}
47
48local version_mt = {
49 --- Equality comparison for versions.
50 -- All version numbers must be equal.
51 -- If both versions have revision numbers, they must be equal;
52 -- otherwise the revision number is ignored.
53 -- @param v1 table: version table to compare.
54 -- @param v2 table: version table to compare.
55 -- @return boolean: true if they are considered equivalent.
56 __eq = function(v1, v2)
57 if #v1 ~= #v2 then
58 return false
59 end
60 for i = 1, #v1 do
61 if v1[i] ~= v2[i] then
62 return false
63 end
64 end
65 if v1.revision and v2.revision then
66 return (v1.revision == v2.revision)
67 end
68 return true
69 end,
70 --- Size comparison for versions.
71 -- All version numbers are compared.
72 -- If both versions have revision numbers, they are compared;
73 -- otherwise the revision number is ignored.
74 -- @param v1 table: version table to compare.
75 -- @param v2 table: version table to compare.
76 -- @return boolean: true if v1 is considered lower than v2.
77 __lt = function(v1, v2)
78 for i = 1, math.max(#v1, #v2) do
79 local v1i, v2i = v1[i] or 0, v2[i] or 0
80 if v1i ~= v2i then
81 return (v1i < v2i)
82 end
83 end
84 if v1.revision and v2.revision then
85 return (v1.revision < v2.revision)
86 end
87 return false
88 end
89}
90
91local version_cache = {}
92setmetatable(version_cache, {
93 __mode = "kv"
94})
95
96--- Parse a version string, converting to table format.
97-- A version table contains all components of the version string
98-- converted to numeric format, stored in the array part of the table.
99-- If the version contains a revision, it is stored numerically
100-- in the 'revision' field. The original string representation of
101-- the string is preserved in the 'string' field.
102-- Returned version tables use a metatable
103-- allowing later comparison through relational operators.
104-- @param vstring string: A version number in string format.
105-- @return table or nil: A version table or nil
106-- if the input string contains invalid characters.
107function parse_version(vstring)
108 if not vstring then return nil end
109 assert(type(vstring) == "string")
110
111 local cached = version_cache[vstring]
112 if cached then
113 return cached
114 end
115
116 local version = {}
117 local i = 1
118
119 local function add_token(number)
120 version[i] = version[i] and version[i] + number/100000 or number
121 i = i + 1
122 end
123
124 -- trim leading and trailing spaces
125 vstring = vstring:match("^%s*(.*)%s*$")
126 version.string = vstring
127 -- store revision separately if any
128 local main, revision = vstring:match("(.*)%-(%d+)$")
129 if revision then
130 vstring = main
131 version.revision = tonumber(revision)
132 end
133 while #vstring > 0 do
134 -- extract a number
135 local token, rest = vstring:match("^(%d+)[%.%-%_]*(.*)")
136 if token then
137 add_token(tonumber(token))
138 else
139 -- extract a word
140 token, rest = vstring:match("^(%a+)[%.%-%_]*(.*)")
141 if not token then
142 return nil
143 end
144 local last = #version
145 version[i] = deltas[token] or (token:byte() / 1000)
146 end
147 vstring = rest
148 end
149 setmetatable(version, version_mt)
150 version_cache[vstring] = version
151 return version
152end
153
154--- Utility function to compare version numbers given as strings.
155-- @param a string: one version.
156-- @param b string: another version.
157-- @return boolean: True if a > b.
158function compare_versions(a, b)
159 return parse_version(a) > parse_version(b)
160end
161
162--- Consumes a constraint from a string, converting it to table format.
163-- For example, a string ">= 1.0, > 2.0" is converted to a table in the
164-- format {op = ">=", version={1,0}} and the rest, "> 2.0", is returned
165-- back to the caller.
166-- @param input string: A list of constraints in string format.
167-- @return (table, string) or nil: A table representing the same
168-- constraints and the string with the unused input, or nil if the
169-- input string is invalid.
170local function parse_constraint(input)
171 assert(type(input) == "string")
172
173 local no_upgrade, op, version, rest = input:match("^(@?)([<>=~!]*)%s*([%w%.%_%-]+)[%s,]*(.*)")
174 op = operators[op]
175 version = parse_version(version)
176 if not op or not version then return nil end
177 return { op = op, version = version, no_upgrade = no_upgrade=="@" and true or nil }, rest
178end
179
180--- Convert a list of constraints from string to table format.
181-- For example, a string ">= 1.0, < 2.0" is converted to a table in the format
182-- {{op = ">=", version={1,0}}, {op = "<", version={2,0}}}.
183-- Version tables use a metatable allowing later comparison through
184-- relational operators.
185-- @param input string: A list of constraints in string format.
186-- @return table or nil: A table representing the same constraints,
187-- or nil if the input string is invalid.
188function parse_constraints(input)
189 assert(type(input) == "string")
190
191 local constraints, constraint = {}, nil
192 while #input > 0 do
193 constraint, input = parse_constraint(input)
194 if constraint then
195 table.insert(constraints, constraint)
196 else
197 return nil
198 end
199 end
200 return constraints
201end
202
203--- Convert a dependency from string to table format.
204-- For example, a string "foo >= 1.0, < 2.0"
205-- is converted to a table in the format
206-- {name = "foo", constraints = {{op = ">=", version={1,0}},
207-- {op = "<", version={2,0}}}}. Version tables use a metatable
208-- allowing later comparison through relational operators.
209-- @param dep string: A dependency in string format
210-- as entered in rockspec files.
211-- @return table or nil: A table representing the same dependency relation,
212-- or nil if the input string is invalid.
213function parse_dep(dep)
214 assert(type(dep) == "string")
215
216 local name, rest = dep:match("^%s*(%a[%w%-]*%w)%s*(.*)")
217 if not name then return nil end
218 local constraints = parse_constraints(rest)
219 if not constraints then return nil end
220 return { name = name, constraints = constraints }
221end
222
223--- Convert a version table to a string.
224-- @param v table: The version table
225-- @param internal boolean or nil: Whether to display versions in their
226-- internal representation format or how they were specified.
227-- @return string: The dependency information pretty-printed as a string.
228function show_version(v, internal)
229 assert(type(v) == "table")
230 assert(type(internal) == "boolean" or not internal)
231
232 return (internal
233 and table.concat(v, ":")..(v.revision and tostring(v.revision) or "")
234 or v.string)
235end
236
237--- Convert a dependency in table format to a string.
238-- @param dep table: The dependency in table format
239-- @param internal boolean or nil: Whether to display versions in their
240-- internal representation format or how they were specified.
241-- @return string: The dependency information pretty-printed as a string.
242function show_dep(dep, internal)
243 assert(type(dep) == "table")
244 assert(type(internal) == "boolean" or not internal)
245
246 local pretty = {}
247 for _, c in ipairs(dep.constraints) do
248 table.insert(pretty, c.op .. " " .. show_version(c.version, internal))
249 end
250 return dep.name.." "..table.concat(pretty, ", ")
251end
252
253--- A more lenient check for equivalence between versions.
254-- This returns true if the requested components of a version
255-- match and ignore the ones that were not given. For example,
256-- when requesting "2", then "2", "2.1", "2.3.5-9"... all match.
257-- When requesting "2.1", then "2.1", "2.1.3" match, but "2.2"
258-- doesn't.
259-- @param version string or table: Version to be tested; may be
260-- in string format or already parsed into a table.
261-- @param requested string or table: Version requested; may be
262-- in string format or already parsed into a table.
263-- @return boolean: True if the tested version matches the requested
264-- version, false otherwise.
265local function partial_match(version, requested)
266 assert(type(version) == "string" or type(version) == "table")
267 assert(type(requested) == "string" or type(version) == "table")
268
269 if type(version) ~= "table" then version = parse_version(version) end
270 if type(requested) ~= "table" then requested = parse_version(requested) end
271 if not version or not requested then return false end
272
273 for i, ri in ipairs(requested) do
274 local vi = version[i] or 0
275 if ri ~= vi then return false end
276 end
277 if requested.revision then
278 return requested.revision == version.revision
279 end
280 return true
281end
282
283--- Check if a version satisfies a set of constraints.
284-- @param version table: A version in table format
285-- @param constraints table: An array of constraints in table format.
286-- @return boolean: True if version satisfies all constraints,
287-- false otherwise.
288function match_constraints(version, constraints)
289 assert(type(version) == "table")
290 assert(type(constraints) == "table")
291 local ok = true
292 setmetatable(version, version_mt)
293 for _, constr in pairs(constraints) do
294 local constr_version = constr.version
295 setmetatable(constr.version, version_mt)
296 if constr.op == "==" then ok = version == constr_version
297 elseif constr.op == "~=" then ok = version ~= constr_version
298 elseif constr.op == ">" then ok = version > constr_version
299 elseif constr.op == "<" then ok = version < constr_version
300 elseif constr.op == ">=" then ok = version >= constr_version
301 elseif constr.op == "<=" then ok = version <= constr_version
302 elseif constr.op == "~>" then ok = partial_match(version, constr_version)
303 end
304 if not ok then break end
305 end
306 return ok
307end
308
309--- Attempt to match a dependency to an installed rock.
310-- @param dep table: A dependency parsed in table format.
311-- @param blacklist table: Versions that can't be accepted. Table where keys
312-- are program versions and values are 'true'.
313-- @return table or nil: A table containing fields 'name' and 'version'
314-- representing an installed rock which matches the given dependency,
315-- or nil if it could not be matched.
316local function match_dep(dep, blacklist)
317 assert(type(dep) == "table")
318
319 local versions
320 if dep.name == "lua" then
321 versions = { "5.1" }
322 else
323 versions = manif.get_versions(dep.name)
324 end
325 if not versions then
326 return nil
327 end
328 if blacklist then
329 local i = 1
330 while versions[i] do
331 if blacklist[versions[i]] then
332 table.remove(versions, i)
333 else
334 i = i + 1
335 end
336 end
337 end
338 local candidates = {}
339 for _, vstring in ipairs(versions) do
340 local version = parse_version(vstring)
341 if match_constraints(version, dep.constraints) then
342 table.insert(candidates, version)
343 end
344 end
345 if #candidates == 0 then
346 return nil
347 else
348 table.sort(candidates)
349 return {
350 name = dep.name,
351 version = candidates[#candidates].string
352 }
353 end
354end
355
356--- Attempt to match dependencies of a rockspec to installed rocks.
357-- @param rockspec table: The rockspec loaded as a table.
358-- @param blacklist table or nil: Program versions to not use as valid matches.
359-- Table where keys are program names and values are tables where keys
360-- are program versions and values are 'true'.
361-- @return table, table: A table where keys are dependencies parsed
362-- in table format and values are tables containing fields 'name' and
363-- version' representing matches, and a table of missing dependencies
364-- parsed as tables.
365function match_deps(rockspec, blacklist)
366 assert(type(rockspec) == "table")
367 assert(type(blacklist) == "table" or not blacklist)
368 local matched, missing, no_upgrade = {}, {}, {}
369
370 for _, dep in ipairs(rockspec.dependencies) do
371 local found = match_dep(dep, blacklist and blacklist[dep.name] or nil)
372 if found then
373 if dep.name ~= "lua" then
374 matched[dep] = found
375 end
376 else
377 if dep.constraints[1] and dep.constraints[1].no_upgrade then
378 no_upgrade[dep.name] = dep
379 else
380 missing[dep.name] = dep
381 end
382 end
383 end
384 return matched, missing, no_upgrade
385end
386
387--- Return a set of values of a table.
388-- @param tbl table: The input table.
389-- @return table: The array of keys.
390local function values_set(tbl)
391 local set = {}
392 for _, v in pairs(tbl) do
393 set[v] = true
394 end
395 return set
396end
397
398--- Check dependencies of a rock and attempt to install any missing ones.
399-- Packages are installed using the LuaRocks "install" command.
400-- Aborts the program if a dependency could not be fulfilled.
401-- @param rockspec table: A rockspec in table format.
402-- @return boolean or (nil, string): True if no errors occurred, or
403-- nil and an error message if any test failed.
404function fulfill_dependencies(rockspec)
405
406 if rockspec.supported_platforms then
407 if not platforms_set then
408 platforms_set = values_set(cfg.platforms)
409 end
410 local supported = nil
411 for _, plat in pairs(rockspec.supported_platforms) do
412 local neg, plat = plat:match("^(!?)(.*)")
413 if neg == "!" then
414 if platforms_set[plat] then
415 return nil, "This rockspec for "..rockspec.package.." does not support "..plat.." platforms."
416 end
417 else
418 if platforms_set[plat] then
419 supported = true
420 else
421 if supported == nil then
422 supported = false
423 end
424 end
425 end
426 end
427 if supported == false then
428 local plats = table.concat(cfg.platforms, ", ")
429 return nil, "This rockspec for "..rockspec.package.." does not support "..plats.." platforms."
430 end
431 end
432
433 local matched, missing, no_upgrade = match_deps(rockspec)
434
435 if next(no_upgrade) then
436 print("Missing dependencies for "..rockspec.name.." "..rockspec.version..":")
437 for _, dep in pairs(no_upgrade) do
438 print(show_dep(dep))
439 end
440 if next(missing) then
441 for _, dep in pairs(missing) do
442 print(show_dep(dep))
443 end
444 end
445 print()
446 for _, dep in pairs(no_upgrade) do
447 print("This version of "..rockspec.name.." is designed for use with")
448 print(show_dep(dep)..", but is configured to avoid upgrading it")
449 print("automatically. Please upgrade "..dep.name.." with")
450 print(" luarocks install "..dep.name)
451 print("or choose an older version of "..rockspec.name.." with")
452 print(" luarocks search "..rockspec.name)
453 end
454 return nil, "Failed matching dependencies."
455 end
456
457 if next(missing) then
458 print()
459 print("Missing dependencies for "..rockspec.name..":")
460 for _, dep in pairs(missing) do
461 print(show_dep(dep))
462 end
463 print()
464
465 for _, dep in pairs(missing) do
466 -- Double-check in case dependency was filled during recursion.
467 if not match_dep(dep) then
468 local rock = search.find_suitable_rock(dep)
469 if not rock then
470 return nil, "Could not find a rock to satisfy dependency: "..show_dep(dep)
471 end
472 local ok, err = install.run(rock)
473 if not ok then
474 return nil, "Failed installing dependency: "..rock.." - "..err
475 end
476 end
477 end
478 end
479 return true
480end
481
482--- Set up path-related variables for external dependencies.
483-- For each key in the external_dependencies table in the
484-- rockspec file, four variables are created: <key>_DIR, <key>_BINDIR,
485-- <key>_INCDIR and <key>_LIBDIR. These are not overwritten
486-- if already set (e.g. by the LuaRocks config file or through the
487-- command-line). Values in the external_dependencies table
488-- are tables that may contain a "header" or a "library" field,
489-- with filenames to be tested for existence.
490-- @param rockspec table: The rockspec table.
491-- @param mode string: if "build" is given, checks all files;
492-- if "install" is given, do not scan for headers.
493-- @return boolean or (nil, string): True if no errors occurred, or
494-- nil and an error message if any test failed.
495function check_external_deps(rockspec, mode)
496 assert(type(rockspec) == "table")
497
498 local vars = rockspec.variables
499 local patterns = cfg.external_deps_patterns
500 local subdirs = cfg.external_deps_subdirs
501 if mode == "install" then
502 patterns = cfg.runtime_external_deps_patterns
503 subdirs = cfg.runtime_external_deps_subdirs
504 end
505 local dirs = {
506 BINDIR = { subdir = subdirs.bin, testfile = "program", pattern = patterns.bin },
507 INCDIR = { subdir = subdirs.include, testfile = "header", pattern = patterns.include },
508 LIBDIR = { subdir = subdirs.lib, testfile = "library", pattern = patterns.lib }
509 }
510 if mode == "install" then
511 dirs.INCDIR = nil
512 end
513 if rockspec.external_dependencies then
514 for name, files in pairs(rockspec.external_dependencies) do
515 local ok = true
516 local failed_file = nil
517 for _, extdir in ipairs(cfg.external_deps_dirs) do
518 ok = true
519 local prefix = vars[name.."_DIR"]
520 if not prefix then
521 prefix = extdir
522 end
523 for dirname, dirdata in pairs(dirs) do
524 dirdata.dir = vars[name.."_"..dirname] or fs.make_path(prefix, dirdata.subdir)
525 local file = files[dirdata.testfile]
526 if file then
527 local files = {}
528 if not file:match("%.") then
529 for _, pattern in ipairs(dirdata.pattern) do
530 table.insert(files, pattern:gsub("?", file))
531 end
532 else
533 table.insert(files, file)
534 end
535 local found = false
536 failed_file = nil
537 for _, f in pairs(files) do
538 if f:match("%.so$") or f:match("%.dylib$") or f:match("%.dll$") then
539 f = f:gsub("%.[^.]+$", "."..cfg.external_lib_extension)
540 end
541 local testfile = fs.make_path(dirdata.dir, f)
542 if fs.exists(testfile) then
543 found = true
544 break
545 else
546 if failed_file then
547 failed_file = failed_file .. ", or " .. f
548 else
549 failed_file = f
550 end
551 end
552 end
553 if not found then
554 ok = false
555 break
556 end
557 end
558 end
559 if ok then
560 for dirname, dirdata in pairs(dirs) do
561 vars[name.."_"..dirname] = dirdata.dir
562 end
563 vars[name.."_DIR"] = prefix
564 break
565 end
566 end
567 if not ok then
568 return nil, "Could not find expected file "..failed_file.." for "..name.." -- you may have to install "..name.." in your system and/or set the "..name.."_DIR variable"
569 end
570 end
571 end
572 return true
573end
574
575--- Recursively scan dependencies, to build a transitive closure of all
576-- dependent packages.
577-- @param results table: The results table being built.
578-- @param name string: Package name.
579-- @param version string: Package version.
580-- @return (table, table): The results and a table of missing dependencies.
581function scan_deps(results, missing, manifest, name, version)
582 assert(type(results) == "table")
583 assert(type(missing) == "table")
584 assert(type(name) == "string")
585 assert(type(version) == "string")
586
587 local err
588 if results[name] then
589 return results, missing
590 end
591 if not manifest.dependencies then manifest.dependencies = {} end
592 local dependencies = manifest.dependencies
593 if not dependencies[name] then dependencies[name] = {} end
594 local dependencies_name = dependencies[name]
595 local deplist = dependencies_name[version]
596 local rockspec, err
597 if not deplist then
598 rockspec, err = fetch.load_local_rockspec(path.rockspec_file(name, version))
599 if err then
600 missing[name.." "..version] = true
601 return results, missing
602 end
603 dependencies_name[version] = rockspec.dependencies
604 else
605 rockspec = { dependencies = deplist }
606 end
607 local matched, failures = match_deps(rockspec)
608 for _, match in pairs(matched) do
609 results, missing = scan_deps(results, missing, manifest, match.name, match.version)
610 end
611 if next(failures) then
612 for _, failure in pairs(failures) do
613 missing[show_dep(failure)] = true
614 end
615 end
616 results[name] = version
617 return results, missing
618end