PageRenderTime 40ms CodeModel.GetById 15ms app.highlight 18ms RepoModel.GetById 1ms app.codeStats 1ms

/src/luarocks/search.lua

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