/src/luarocks/search.lua

http://github.com/keplerproject/luarocks · Lua · 394 lines · 283 code · 47 blank · 64 comment · 119 complexity · 703af29ff276ed897c82582fc68210b0 MD5 · raw file

  1. local search = {}
  2. local dir = require("luarocks.dir")
  3. local path = require("luarocks.path")
  4. local manif = require("luarocks.manif")
  5. local vers = require("luarocks.core.vers")
  6. local cfg = require("luarocks.core.cfg")
  7. local util = require("luarocks.util")
  8. local queries = require("luarocks.queries")
  9. local results = require("luarocks.results")
  10. --- Store a search result (a rock or rockspec) in the result tree.
  11. -- @param result_tree table: The result tree, where keys are package names and
  12. -- values are tables matching version strings to arrays of
  13. -- tables with fields "arch" and "repo".
  14. -- @param result table: A result.
  15. function search.store_result(result_tree, result)
  16. assert(type(result_tree) == "table")
  17. assert(result:type() == "result")
  18. local name = result.name
  19. local version = result.version
  20. if not result_tree[name] then result_tree[name] = {} end
  21. if not result_tree[name][version] then result_tree[name][version] = {} end
  22. table.insert(result_tree[name][version], {
  23. arch = result.arch,
  24. repo = result.repo,
  25. namespace = result.namespace,
  26. })
  27. end
  28. --- Store a match in a result tree if version matches query.
  29. -- Name, version, arch and repository path are stored in a given
  30. -- table, optionally checking if version and arch (if given) match
  31. -- a query.
  32. -- @param result_tree table: The result tree, where keys are package names and
  33. -- values are tables matching version strings to arrays of
  34. -- tables with fields "arch" and "repo".
  35. -- @param result table: a result object.
  36. -- @param query table: a query object.
  37. local function store_if_match(result_tree, result, query)
  38. assert(result:type() == "result")
  39. assert(query:type() == "query")
  40. if result:satisfies(query) then
  41. search.store_result(result_tree, result)
  42. end
  43. end
  44. --- Perform search on a local repository.
  45. -- @param repo string: The pathname of the local repository.
  46. -- @param query table: a query object.
  47. -- @param result_tree table or nil: If given, this table will store the
  48. -- result tree; if not given, a new table will be created.
  49. -- @return table: The result tree, where keys are package names and
  50. -- values are tables matching version strings to arrays of
  51. -- tables with fields "arch" and "repo".
  52. -- If a table was given in the "result_tree" parameter, that is the result value.
  53. function search.disk_search(repo, query, result_tree)
  54. assert(type(repo) == "string")
  55. assert(query:type() == "query")
  56. assert(type(result_tree) == "table" or not result_tree)
  57. local fs = require("luarocks.fs")
  58. if not result_tree then
  59. result_tree = {}
  60. end
  61. for name in fs.dir(repo) do
  62. local pathname = dir.path(repo, name)
  63. local rname, rversion, rarch = path.parse_name(name)
  64. if rname and (pathname:match(".rockspec$") or pathname:match(".rock$")) then
  65. local result = results.new(rname, rversion, repo, rarch)
  66. store_if_match(result_tree, result, query)
  67. elseif fs.is_dir(pathname) then
  68. for version in fs.dir(pathname) do
  69. if version:match("-%d+$") then
  70. local namespace = path.read_namespace(name, version, repo)
  71. local result = results.new(name, version, repo, "installed", namespace)
  72. store_if_match(result_tree, result, query)
  73. end
  74. end
  75. end
  76. end
  77. return result_tree
  78. end
  79. --- Perform search on a rocks server or tree.
  80. -- @param result_tree table: The result tree, where keys are package names and
  81. -- values are tables matching version strings to arrays of
  82. -- tables with fields "arch" and "repo".
  83. -- @param repo string: The URL of a rocks server or
  84. -- the pathname of a rocks tree (as returned by path.rocks_dir()).
  85. -- @param query table: a query object.
  86. -- @param lua_version string: Lua version in "5.x" format, defaults to installed version.
  87. -- @param is_local boolean
  88. -- @return true or, in case of errors, nil, an error message and an optional error code.
  89. local function manifest_search(result_tree, repo, query, lua_version, is_local)
  90. assert(type(result_tree) == "table")
  91. assert(type(repo) == "string")
  92. assert(query:type() == "query")
  93. -- FIXME do not add this in local repos
  94. if (not is_local) and query.namespace then
  95. repo = repo .. "/manifests/" .. query.namespace
  96. end
  97. local manifest, err, errcode = manif.load_manifest(repo, lua_version, not is_local)
  98. if not manifest then
  99. return nil, err, errcode
  100. end
  101. for name, versions in pairs(manifest.repository) do
  102. for version, items in pairs(versions) do
  103. local namespace = is_local and path.read_namespace(name, version, repo) or query.namespace
  104. for _, item in ipairs(items) do
  105. local result = results.new(name, version, repo, item.arch, namespace)
  106. store_if_match(result_tree, result, query)
  107. end
  108. end
  109. end
  110. return true
  111. end
  112. local function remote_manifest_search(result_tree, repo, query, lua_version)
  113. return manifest_search(result_tree, repo, query, lua_version, false)
  114. end
  115. function search.local_manifest_search(result_tree, repo, query, lua_version)
  116. return manifest_search(result_tree, repo, query, lua_version, true)
  117. end
  118. --- Search on all configured rocks servers.
  119. -- @param query table: a query object.
  120. -- @param lua_version string: Lua version in "5.x" format, defaults to installed version.
  121. -- @return table: A table where keys are package names
  122. -- and values are tables matching version strings to arrays of
  123. -- tables with fields "arch" and "repo".
  124. function search.search_repos(query, lua_version)
  125. assert(query:type() == "query")
  126. local result_tree = {}
  127. for _, repo in ipairs(cfg.rocks_servers) do
  128. if type(repo) == "string" then
  129. repo = { repo }
  130. end
  131. for _, mirror in ipairs(repo) do
  132. if not cfg.disabled_servers[mirror] then
  133. local protocol, pathname = dir.split_url(mirror)
  134. if protocol == "file" then
  135. mirror = pathname
  136. end
  137. local ok, err, errcode = remote_manifest_search(result_tree, mirror, query, lua_version)
  138. if errcode == "network" then
  139. cfg.disabled_servers[mirror] = true
  140. end
  141. if ok then
  142. break
  143. else
  144. util.warning("Failed searching manifest: "..err)
  145. end
  146. end
  147. end
  148. end
  149. -- search through rocks in rocks_provided
  150. local provided_repo = "provided by VM or rocks_provided"
  151. for name, version in pairs(util.get_rocks_provided()) do
  152. local result = results.new(name, version, provided_repo, "installed")
  153. store_if_match(result_tree, result, query)
  154. end
  155. return result_tree
  156. end
  157. --- Get the URL for the latest in a set of versions.
  158. -- @param name string: The package name to be used in the URL.
  159. -- @param versions table: An array of version informations, as stored
  160. -- in search result trees.
  161. -- @return string or nil: the URL for the latest version if one could
  162. -- be picked, or nil.
  163. local function pick_latest_version(name, versions)
  164. assert(type(name) == "string" and not name:match("/"))
  165. assert(type(versions) == "table")
  166. local vtables = {}
  167. for v, _ in pairs(versions) do
  168. table.insert(vtables, vers.parse_version(v))
  169. end
  170. table.sort(vtables)
  171. local version = vtables[#vtables].string
  172. local items = versions[version]
  173. if items then
  174. local pick = 1
  175. for i, item in ipairs(items) do
  176. if (item.arch == 'src' and items[pick].arch == 'rockspec')
  177. or (item.arch ~= 'src' and item.arch ~= 'rockspec') then
  178. pick = i
  179. end
  180. end
  181. return path.make_url(items[pick].repo, name, version, items[pick].arch)
  182. end
  183. return nil
  184. end
  185. -- Find out which other Lua versions provide rock versions matching a query,
  186. -- @param query table: a query object.
  187. -- @return table: array of Lua versions supported, in "5.x" format.
  188. local function supported_lua_versions(query)
  189. assert(query:type() == "query")
  190. local result_tree = {}
  191. for lua_version in util.lua_versions() do
  192. if lua_version ~= cfg.lua_version then
  193. util.printout("Checking for Lua " .. lua_version .. "...")
  194. if search.search_repos(query, lua_version)[query.name] then
  195. table.insert(result_tree, lua_version)
  196. end
  197. end
  198. end
  199. return result_tree
  200. end
  201. --- Attempt to get a single URL for a given search for a rock.
  202. -- @param query table: a query object.
  203. -- @return string or (nil, string, string): URL for latest matching version
  204. -- of the rock if it was found, or nil followed by an error message
  205. -- and an error code.
  206. function search.find_suitable_rock(query)
  207. assert(query:type() == "query")
  208. local rocks_provided = util.get_rocks_provided()
  209. if rocks_provided[query.name] ~= nil then
  210. -- Do not install versions listed in rocks_provided.
  211. return nil, "Rock "..query.name.." "..rocks_provided[query.name]..
  212. " is already provided by VM or via 'rocks_provided' in the config file.", "provided"
  213. end
  214. local result_tree = search.search_repos(query)
  215. local first_rock = next(result_tree)
  216. if not first_rock then
  217. return nil, "No results matching query were found for Lua " .. cfg.lua_version .. ".", "notfound"
  218. elseif next(result_tree, first_rock) then
  219. -- Shouldn't happen as query must match only one package.
  220. return nil, "Several rocks matched query.", "manyfound"
  221. else
  222. return pick_latest_version(query.name, result_tree[first_rock])
  223. end
  224. end
  225. function search.find_src_or_rockspec(name, namespace, version, check_lua_versions)
  226. local query = queries.new(name, namespace, version, false, "src|rockspec")
  227. local url, err = search.find_rock_checking_lua_versions(query, check_lua_versions)
  228. if not url then
  229. return nil, "Could not find a result named "..tostring(query)..": "..err
  230. end
  231. return url
  232. end
  233. function search.find_rock_checking_lua_versions(query, check_lua_versions)
  234. local url, err, errcode = search.find_suitable_rock(query)
  235. if url then
  236. return url
  237. end
  238. if errcode == "notfound" then
  239. local add
  240. if check_lua_versions then
  241. util.printout(query.name .. " not found for Lua " .. cfg.lua_version .. ".")
  242. util.printout("Checking if available for other Lua versions...")
  243. -- Check if constraints are satisfiable with other Lua versions.
  244. local lua_versions = supported_lua_versions(query)
  245. if #lua_versions ~= 0 then
  246. -- Build a nice message in "only Lua 5.x and 5.y but not 5.z." format
  247. for i, lua_version in ipairs(lua_versions) do
  248. lua_versions[i] = "Lua "..lua_version
  249. end
  250. local versions_message = "only "..table.concat(lua_versions, " and ")..
  251. " but not Lua "..cfg.lua_version.."."
  252. if #query.constraints == 0 then
  253. add = query.name.." supports "..versions_message
  254. elseif #query.constraints == 1 and query.constraints[1].op == "==" then
  255. add = query.name.." "..query.constraints[1].version.string.." supports "..versions_message
  256. else
  257. add = "Matching "..query.name.." versions support "..versions_message
  258. end
  259. else
  260. add = query.name.." is not available for any Lua versions."
  261. end
  262. else
  263. add = "To check if it is available for other Lua versions, use --check-lua-versions."
  264. end
  265. err = err .. "\n" .. add
  266. end
  267. return nil, err
  268. end
  269. --- Print a list of rocks/rockspecs on standard output.
  270. -- @param result_tree table: A result tree.
  271. -- @param porcelain boolean or nil: A flag to force machine-friendly output.
  272. function search.print_result_tree(result_tree, porcelain)
  273. assert(type(result_tree) == "table")
  274. assert(type(porcelain) == "boolean" or not porcelain)
  275. if porcelain then
  276. for package, versions in util.sortedpairs(result_tree) do
  277. for version, repos in util.sortedpairs(versions, vers.compare_versions) do
  278. for _, repo in ipairs(repos) do
  279. local nrepo = dir.normalize(repo.repo)
  280. util.printout(package, version, repo.arch, nrepo, repo.namespace)
  281. end
  282. end
  283. end
  284. return
  285. end
  286. for package, versions in util.sortedpairs(result_tree) do
  287. local namespaces = {}
  288. for version, repos in util.sortedpairs(versions, vers.compare_versions) do
  289. for _, repo in ipairs(repos) do
  290. local key = repo.namespace or ""
  291. local list = namespaces[key] or {}
  292. namespaces[key] = list
  293. repo.repo = dir.normalize(repo.repo)
  294. table.insert(list, " "..version.." ("..repo.arch..") - "..path.root_dir(repo.repo))
  295. end
  296. end
  297. for key, list in util.sortedpairs(namespaces) do
  298. util.printout(key == "" and package or key .. "/" .. package)
  299. for _, line in ipairs(list) do
  300. util.printout(line)
  301. end
  302. util.printout()
  303. end
  304. end
  305. end
  306. function search.pick_installed_rock(query, given_tree)
  307. assert(query:type() == "query")
  308. local result_tree = {}
  309. local tree_map = {}
  310. local trees = cfg.rocks_trees
  311. if given_tree then
  312. trees = { given_tree }
  313. end
  314. for _, tree in ipairs(trees) do
  315. local rocks_dir = path.rocks_dir(tree)
  316. tree_map[rocks_dir] = tree
  317. search.local_manifest_search(result_tree, rocks_dir, query)
  318. end
  319. if not next(result_tree) then
  320. return nil, "cannot find package "..tostring(query).."\nUse 'list' to find installed rocks."
  321. end
  322. if not result_tree[query.name] and next(result_tree, next(result_tree)) then
  323. local out = { "multiple installed packages match the name '"..tostring(query).."':\n\n" }
  324. for name, _ in util.sortedpairs(result_tree) do
  325. table.insert(out, " " .. name .. "\n")
  326. end
  327. table.insert(out, "\nPlease specify a single rock.\n")
  328. return nil, table.concat(out)
  329. end
  330. local version = nil
  331. local repo_url
  332. local name, versions
  333. if result_tree[query.name] then
  334. name, versions = query.name, result_tree[query.name]
  335. else
  336. name, versions = util.sortedpairs(result_tree)()
  337. end
  338. --question: what do we do about multiple versions? This should
  339. --give us the latest version on the last repo (which is usually the global one)
  340. for vs, repositories in util.sortedpairs(versions, vers.compare_versions) do
  341. if not version then version = vs end
  342. for _, rp in ipairs(repositories) do repo_url = rp.repo end
  343. end
  344. local repo = tree_map[repo_url]
  345. return name, version, repo, repo_url
  346. end
  347. return search