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.lua378
1 files changed, 38 insertions, 340 deletions
diff --git a/src/luarocks/deps.lua b/src/luarocks/deps.lua
index dcebec9b..015e2527 100644
--- a/src/luarocks/deps.lua
+++ b/src/luarocks/deps.lua
@@ -1,344 +1,40 @@
1 1
2--- Dependency handling functions. 2--- High-level dependency related 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.
14local deps = {} 3local deps = {}
15package.loaded["luarocks.deps"] = deps
16 4
17local cfg = require("luarocks.cfg") 5local cfg = require("luarocks.core.cfg")
18local manif_core = require("luarocks.manif_core") 6local manif = require("luarocks.manif")
19local path = require("luarocks.path") 7local path = require("luarocks.path")
20local dir = require("luarocks.dir") 8local dir = require("luarocks.dir")
21local util = require("luarocks.util") 9local util = require("luarocks.util")
22 10local vers = require("luarocks.vers")
23local operators = {
24 ["=="] = "==",
25 ["~="] = "~=",
26 [">"] = ">",
27 ["<"] = "<",
28 [">="] = ">=",
29 ["<="] = "<=",
30 ["~>"] = "~>",
31 -- plus some convenience translations
32 [""] = "==",
33 ["="] = "==",
34 ["!="] = "~="
35}
36
37local deltas = {
38 scm = 1100,
39 cvs = 1000,
40 rc = -1000,
41 pre = -10000,
42 beta = -100000,
43 alpha = -1000000
44}
45
46local version_mt = {
47 --- Equality comparison for versions.
48 -- All version numbers must be equal.
49 -- If both versions have revision numbers, they must be equal;
50 -- otherwise the revision number is ignored.
51 -- @param v1 table: version table to compare.
52 -- @param v2 table: version table to compare.
53 -- @return boolean: true if they are considered equivalent.
54 __eq = function(v1, v2)
55 if #v1 ~= #v2 then
56 return false
57 end
58 for i = 1, #v1 do
59 if v1[i] ~= v2[i] then
60 return false
61 end
62 end
63 if v1.revision and v2.revision then
64 return (v1.revision == v2.revision)
65 end
66 return true
67 end,
68 --- Size comparison for versions.
69 -- All version numbers are compared.
70 -- If both versions have revision numbers, they are compared;
71 -- otherwise the revision number is ignored.
72 -- @param v1 table: version table to compare.
73 -- @param v2 table: version table to compare.
74 -- @return boolean: true if v1 is considered lower than v2.
75 __lt = function(v1, v2)
76 for i = 1, math.max(#v1, #v2) do
77 local v1i, v2i = v1[i] or 0, v2[i] or 0
78 if v1i ~= v2i then
79 return (v1i < v2i)
80 end
81 end
82 if v1.revision and v2.revision then
83 return (v1.revision < v2.revision)
84 end
85 return false
86 end
87}
88
89local version_cache = {}
90setmetatable(version_cache, {
91 __mode = "kv"
92})
93
94--- Parse a version string, converting to table format.
95-- A version table contains all components of the version string
96-- converted to numeric format, stored in the array part of the table.
97-- If the version contains a revision, it is stored numerically
98-- in the 'revision' field. The original string representation of
99-- the string is preserved in the 'string' field.
100-- Returned version tables use a metatable
101-- allowing later comparison through relational operators.
102-- @param vstring string: A version number in string format.
103-- @return table or nil: A version table or nil
104-- if the input string contains invalid characters.
105function deps.parse_version(vstring)
106 if not vstring then return nil end
107 assert(type(vstring) == "string")
108
109 local cached = version_cache[vstring]
110 if cached then
111 return cached
112 end
113
114 local version = {}
115 local i = 1
116
117 local function add_token(number)
118 version[i] = version[i] and version[i] + number/100000 or number
119 i = i + 1
120 end
121
122 -- trim leading and trailing spaces
123 vstring = vstring:match("^%s*(.*)%s*$")
124 version.string = vstring
125 -- store revision separately if any
126 local main, revision = vstring:match("(.*)%-(%d+)$")
127 if revision then
128 vstring = main
129 version.revision = tonumber(revision)
130 end
131 while #vstring > 0 do
132 -- extract a number
133 local token, rest = vstring:match("^(%d+)[%.%-%_]*(.*)")
134 if token then
135 add_token(tonumber(token))
136 else
137 -- extract a word
138 token, rest = vstring:match("^(%a+)[%.%-%_]*(.*)")
139 if not token then
140 util.printerr("Warning: version number '"..vstring.."' could not be parsed.")
141 version[i] = 0
142 break
143 end
144 version[i] = deltas[token] or (token:byte() / 1000)
145 end
146 vstring = rest
147 end
148 setmetatable(version, version_mt)
149 version_cache[vstring] = version
150 return version
151end
152
153--- Utility function to compare version numbers given as strings.
154-- @param a string: one version.
155-- @param b string: another version.
156-- @return boolean: True if a > b.
157function deps.compare_versions(a, b)
158 return deps.parse_version(a) > deps.parse_version(b)
159end
160
161--- Consumes a constraint from a string, converting it to table format.
162-- For example, a string ">= 1.0, > 2.0" is converted to a table in the
163-- format {op = ">=", version={1,0}} and the rest, "> 2.0", is returned
164-- back to the caller.
165-- @param input string: A list of constraints in string format.
166-- @return (table, string) or nil: A table representing the same
167-- constraints and the string with the unused input, or nil if the
168-- input string is invalid.
169local function parse_constraint(input)
170 assert(type(input) == "string")
171
172 local no_upgrade, op, version, rest = input:match("^(@?)([<>=~!]*)%s*([%w%.%_%-]+)[%s,]*(.*)")
173 local _op = operators[op]
174 version = deps.parse_version(version)
175 if not _op then
176 return nil, "Encountered bad constraint operator: '"..tostring(op).."' in '"..input.."'"
177 end
178 if not version then
179 return nil, "Could not parse version from constraint: '"..input.."'"
180 end
181 return { op = _op, version = version, no_upgrade = no_upgrade=="@" and true or nil }, rest
182end
183
184--- Convert a list of constraints from string to table format.
185-- For example, a string ">= 1.0, < 2.0" is converted to a table in the format
186-- {{op = ">=", version={1,0}}, {op = "<", version={2,0}}}.
187-- Version tables use a metatable allowing later comparison through
188-- relational operators.
189-- @param input string: A list of constraints in string format.
190-- @return table or nil: A table representing the same constraints,
191-- or nil if the input string is invalid.
192function deps.parse_constraints(input)
193 assert(type(input) == "string")
194
195 local constraints, constraint, oinput = {}, nil, input
196 while #input > 0 do
197 constraint, input = parse_constraint(input)
198 if constraint then
199 table.insert(constraints, constraint)
200 else
201 return nil, "Failed to parse constraint '"..tostring(oinput).."' with error: ".. input
202 end
203 end
204 return constraints
205end
206
207--- Convert a dependency from string to table format.
208-- For example, a string "foo >= 1.0, < 2.0"
209-- is converted to a table in the format
210-- {name = "foo", constraints = {{op = ">=", version={1,0}},
211-- {op = "<", version={2,0}}}}. Version tables use a metatable
212-- allowing later comparison through relational operators.
213-- @param dep string: A dependency in string format
214-- as entered in rockspec files.
215-- @return table or nil: A table representing the same dependency relation,
216-- or nil if the input string is invalid.
217function deps.parse_dep(dep)
218 assert(type(dep) == "string")
219
220 local name, rest = dep:match("^%s*([a-zA-Z0-9][a-zA-Z0-9%.%-%_]*)%s*(.*)")
221 if not name then return nil, "failed to extract dependency name from '"..tostring(dep).."'" end
222 local constraints, err = deps.parse_constraints(rest)
223 if not constraints then return nil, err end
224 return { name = name, constraints = constraints }
225end
226
227--- Convert a version table to a string.
228-- @param v table: The version table
229-- @param internal boolean or nil: Whether to display versions in their
230-- internal representation format or how they were specified.
231-- @return string: The dependency information pretty-printed as a string.
232function deps.show_version(v, internal)
233 assert(type(v) == "table")
234 assert(type(internal) == "boolean" or not internal)
235
236 return (internal
237 and table.concat(v, ":")..(v.revision and tostring(v.revision) or "")
238 or v.string)
239end
240
241--- Convert a dependency in table format to a string.
242-- @param dep table: The dependency in table format
243-- @param internal boolean or nil: Whether to display versions in their
244-- internal representation format or how they were specified.
245-- @return string: The dependency information pretty-printed as a string.
246function deps.show_dep(dep, internal)
247 assert(type(dep) == "table")
248 assert(type(internal) == "boolean" or not internal)
249
250 if #dep.constraints > 0 then
251 local pretty = {}
252 for _, c in ipairs(dep.constraints) do
253 table.insert(pretty, c.op .. " " .. deps.show_version(c.version, internal))
254 end
255 return dep.name.." "..table.concat(pretty, ", ")
256 else
257 return dep.name
258 end
259end
260
261--- A more lenient check for equivalence between versions.
262-- This returns true if the requested components of a version
263-- match and ignore the ones that were not given. For example,
264-- when requesting "2", then "2", "2.1", "2.3.5-9"... all match.
265-- When requesting "2.1", then "2.1", "2.1.3" match, but "2.2"
266-- doesn't.
267-- @param version string or table: Version to be tested; may be
268-- in string format or already parsed into a table.
269-- @param requested string or table: Version requested; may be
270-- in string format or already parsed into a table.
271-- @return boolean: True if the tested version matches the requested
272-- version, false otherwise.
273local function partial_match(version, requested)
274 assert(type(version) == "string" or type(version) == "table")
275 assert(type(requested) == "string" or type(version) == "table")
276
277 if type(version) ~= "table" then version = deps.parse_version(version) end
278 if type(requested) ~= "table" then requested = deps.parse_version(requested) end
279 if not version or not requested then return false end
280
281 for i, ri in ipairs(requested) do
282 local vi = version[i] or 0
283 if ri ~= vi then return false end
284 end
285 if requested.revision then
286 return requested.revision == version.revision
287 end
288 return true
289end
290
291--- Check if a version satisfies a set of constraints.
292-- @param version table: A version in table format
293-- @param constraints table: An array of constraints in table format.
294-- @return boolean: True if version satisfies all constraints,
295-- false otherwise.
296function deps.match_constraints(version, constraints)
297 assert(type(version) == "table")
298 assert(type(constraints) == "table")
299 local ok = true
300 setmetatable(version, version_mt)
301 for _, constr in pairs(constraints) do
302 if type(constr.version) == "string" then
303 constr.version = deps.parse_version(constr.version)
304 end
305 local constr_version, constr_op = constr.version, constr.op
306 setmetatable(constr_version, version_mt)
307 if constr_op == "==" then ok = version == constr_version
308 elseif constr_op == "~=" then ok = version ~= constr_version
309 elseif constr_op == ">" then ok = version > constr_version
310 elseif constr_op == "<" then ok = version < constr_version
311 elseif constr_op == ">=" then ok = version >= constr_version
312 elseif constr_op == "<=" then ok = version <= constr_version
313 elseif constr_op == "~>" then ok = partial_match(version, constr_version)
314 end
315 if not ok then break end
316 end
317 return ok
318end
319 11
320--- Attempt to match a dependency to an installed rock. 12--- Attempt to match a dependency to an installed rock.
321-- @param dep table: A dependency parsed in table format. 13-- @param dep table: A dependency parsed in table format.
322-- @param blacklist table: Versions that can't be accepted. Table where keys 14-- @param blacklist table: Versions that can't be accepted. Table where keys
323-- are program versions and values are 'true'. 15-- are program versions and values are 'true'.
16-- @param rocks_provided table: A table of auto-dependencies provided
17-- by this Lua implementation for the given dependency.
324-- @return string or nil: latest installed version of the rock matching the dependency 18-- @return string or nil: latest installed version of the rock matching the dependency
325-- or nil if it could not be matched. 19-- or nil if it could not be matched.
326local function match_dep(dep, blacklist, deps_mode) 20local function match_dep(dep, blacklist, deps_mode, rocks_provided)
327 assert(type(dep) == "table") 21 assert(type(dep) == "table")
328 22 assert(type(rocks_provided) == "table")
23
329 local versions 24 local versions
330 if cfg.rocks_provided[dep.name] then 25 local provided = rocks_provided[dep.name]
331 -- provided rocks have higher priority than manifest's rocks 26 if provided then
332 versions = { cfg.rocks_provided[dep.name] } 27 -- Provided rocks have higher priority than manifest's rocks.
28 versions = { provided }
333 else 29 else
334 versions = manif_core.get_versions(dep.name, deps_mode) 30 versions = manif.get_versions(dep.name, deps_mode)
335 end 31 end
336 32
337 local latest_version 33 local latest_version
338 for _, vstring in ipairs(versions) do 34 for _, vstring in ipairs(versions) do
339 if not blacklist or not blacklist[vstring] then 35 if not blacklist or not blacklist[vstring] then
340 local version = deps.parse_version(vstring) 36 local version = vers.parse_version(vstring)
341 if deps.match_constraints(version, dep.constraints) then 37 if vers.match_constraints(version, dep.constraints) then
342 if not latest_version or version > latest_version then 38 if not latest_version or version > latest_version then
343 latest_version = version 39 latest_version = version
344 end 40 end
@@ -366,9 +62,9 @@ function deps.match_deps(rockspec, blacklist, deps_mode)
366 local matched, missing, no_upgrade = {}, {}, {} 62 local matched, missing, no_upgrade = {}, {}, {}
367 63
368 for _, dep in ipairs(rockspec.dependencies) do 64 for _, dep in ipairs(rockspec.dependencies) do
369 local found = match_dep(dep, blacklist and blacklist[dep.name] or nil, deps_mode) 65 local found = match_dep(dep, blacklist and blacklist[dep.name] or nil, deps_mode, rockspec.rocks_provided)
370 if found then 66 if found then
371 if not cfg.rocks_provided[dep.name] then 67 if not rockspec.rocks_provided[dep.name] then
372 matched[dep] = {name = dep.name, version = found} 68 matched[dep] = {name = dep.name, version = found}
373 end 69 end
374 else 70 else
@@ -393,10 +89,10 @@ local function values_set(tbl)
393 return set 89 return set
394end 90end
395 91
396local function rock_status(name, deps_mode) 92local function rock_status(name, deps_mode, rocks_provided)
397 local search = require("luarocks.search") 93 local search = require("luarocks.search")
398 local installed = match_dep(search.make_query(name), nil, deps_mode) 94 local installed = match_dep(search.make_query(name), nil, deps_mode, rocks_provided)
399 local installation_type = cfg.rocks_provided[name] and "provided by VM" or "installed" 95 local installation_type = rocks_provided[name] and "provided by VM" or "installed"
400 return installed and installed.." "..installation_type or "not installed" 96 return installed and installed.." "..installation_type or "not installed"
401end 97end
402 98
@@ -405,19 +101,21 @@ end
405-- @param version string: package version. 101-- @param version string: package version.
406-- @param dependencies table: array of dependencies. 102-- @param dependencies table: array of dependencies.
407-- @param deps_mode string: Which trees to check dependencies for: 103-- @param deps_mode string: Which trees to check dependencies for:
104-- @param rocks_provided table: A table of auto-dependencies provided
105-- by this Lua implementation for the given dependency.
408-- "one" for the current default tree, "all" for all trees, 106-- "one" for the current default tree, "all" for all trees,
409-- "order" for all trees with priority >= the current default, "none" for no trees. 107-- "order" for all trees with priority >= the current default, "none" for no trees.
410function deps.report_missing_dependencies(name, version, dependencies, deps_mode) 108function deps.report_missing_dependencies(name, version, dependencies, deps_mode, rocks_provided)
411 local first_missing_dep = true 109 local first_missing_dep = true
412 110
413 for _, dep in ipairs(dependencies) do 111 for _, dep in ipairs(dependencies) do
414 if not match_dep(dep, nil, deps_mode) then 112 if not match_dep(dep, nil, deps_mode, rocks_provided) then
415 if first_missing_dep then 113 if first_missing_dep then
416 util.printout(("Missing dependencies for %s %s:"):format(name, version)) 114 util.printout(("Missing dependencies for %s %s:"):format(name, version))
417 first_missing_dep = false 115 first_missing_dep = false
418 end 116 end
419 117
420 util.printout((" %s (%s)"):format(deps.show_dep(dep), rock_status(dep.name, deps_mode))) 118 util.printout((" %s (%s)"):format(vers.show_dep(dep), rock_status(dep.name, deps_mode, rocks_provided)))
421 end 119 end
422 end 120 end
423end 121end
@@ -432,7 +130,7 @@ end
432function deps.fulfill_dependencies(rockspec, deps_mode) 130function deps.fulfill_dependencies(rockspec, deps_mode)
433 131
434 local search = require("luarocks.search") 132 local search = require("luarocks.search")
435 local install = require("luarocks.install") 133 local install = require("luarocks.cmd.install")
436 134
437 if rockspec.supported_platforms then 135 if rockspec.supported_platforms then
438 if not deps.platforms_set then 136 if not deps.platforms_set then
@@ -440,7 +138,8 @@ function deps.fulfill_dependencies(rockspec, deps_mode)
440 end 138 end
441 local supported = nil 139 local supported = nil
442 for _, plat in pairs(rockspec.supported_platforms) do 140 for _, plat in pairs(rockspec.supported_platforms) do
443 local neg, plat = plat:match("^(!?)(.*)") 141 local neg
142 neg, plat = plat:match("^(!?)(.*)")
444 if neg == "!" then 143 if neg == "!" then
445 if deps.platforms_set[plat] then 144 if deps.platforms_set[plat] then
446 return nil, "This rockspec for "..rockspec.package.." does not support "..plat.." platforms." 145 return nil, "This rockspec for "..rockspec.package.." does not support "..plat.." platforms."
@@ -461,23 +160,23 @@ function deps.fulfill_dependencies(rockspec, deps_mode)
461 end 160 end
462 end 161 end
463 162
464 deps.report_missing_dependencies(rockspec.name, rockspec.version, rockspec.dependencies, deps_mode) 163 deps.report_missing_dependencies(rockspec.name, rockspec.version, rockspec.dependencies, deps_mode, rockspec.rocks_provided)
465 164
466 local first_missing_dep = true 165 local first_missing_dep = true
467 166
468 for _, dep in ipairs(rockspec.dependencies) do 167 for _, dep in ipairs(rockspec.dependencies) do
469 if not match_dep(dep, nil, deps_mode) then 168 if not match_dep(dep, nil, deps_mode, rockspec.rocks_provided) then
470 if first_missing_dep then 169 if first_missing_dep then
471 util.printout() 170 util.printout()
472 first_missing_dep = false 171 first_missing_dep = false
473 end 172 end
474 173
475 util.printout(("%s %s depends on %s (%s)"):format( 174 util.printout(("%s %s depends on %s (%s)"):format(
476 rockspec.name, rockspec.version, deps.show_dep(dep), rock_status(dep.name, deps_mode))) 175 rockspec.name, rockspec.version, vers.show_dep(dep), rock_status(dep.name, deps_mode, rockspec.rocks_provided)))
477 176
478 if dep.constraints[1] and dep.constraints[1].no_upgrade then 177 if dep.constraints[1] and dep.constraints[1].no_upgrade then
479 util.printerr("This version of "..rockspec.name.." is designed for use with") 178 util.printerr("This version of "..rockspec.name.." is designed for use with")
480 util.printerr(deps.show_dep(dep)..", but is configured to avoid upgrading it") 179 util.printerr(vers.show_dep(dep)..", but is configured to avoid upgrading it")
481 util.printerr("automatically. Please upgrade "..dep.name.." with") 180 util.printerr("automatically. Please upgrade "..dep.name.." with")
482 util.printerr(" luarocks install "..dep.name) 181 util.printerr(" luarocks install "..dep.name)
483 util.printerr("or choose an older version of "..rockspec.name.." with") 182 util.printerr("or choose an older version of "..rockspec.name.." with")
@@ -487,7 +186,7 @@ function deps.fulfill_dependencies(rockspec, deps_mode)
487 186
488 local url, search_err = search.find_suitable_rock(dep) 187 local url, search_err = search.find_suitable_rock(dep)
489 if not url then 188 if not url then
490 return nil, "Could not satisfy dependency "..deps.show_dep(dep)..": "..search_err 189 return nil, "Could not satisfy dependency "..vers.show_dep(dep)..": "..search_err
491 end 190 end
492 util.printout("Installing "..url) 191 util.printout("Installing "..url)
493 local ok, install_err, errcode = install.command({deps_mode = deps_mode}, url) 192 local ok, install_err, errcode = install.command({deps_mode = deps_mode}, url)
@@ -717,7 +416,10 @@ function deps.scan_deps(results, manifest, name, version, deps_mode)
717 end 416 end
718 dependencies_name[version] = rockspec.dependencies 417 dependencies_name[version] = rockspec.dependencies
719 else 418 else
720 rockspec = { dependencies = deplist } 419 rockspec = {
420 dependencies = deplist,
421 rocks_provided = setmetatable({}, { __index = cfg.rocks_provided_3_0 })
422 }
721 end 423 end
722 local matched = deps.match_deps(rockspec, nil, deps_mode) 424 local matched = deps.match_deps(rockspec, nil, deps_mode)
723 results[name] = version 425 results[name] = version
@@ -745,8 +447,4 @@ function deps.get_deps_mode(flags)
745 end 447 end
746end 448end
747 449
748function deps.deps_mode_to_flag(deps_mode)
749 return "--deps-mode="..deps_mode
750end
751
752return deps 450return deps