summaryrefslogtreecommitdiff
path: root/src/luarocks/search.lua
blob: 180f8f4524c13db24b451159b62f6f69d73f498a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
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
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
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
389
390
391
392
393
local search = {}

local dir = require("luarocks.dir")
local path = require("luarocks.path")
local manif = require("luarocks.manif")
local vers = require("luarocks.core.vers")
local cfg = require("luarocks.core.cfg")
local util = require("luarocks.util")
local queries = require("luarocks.queries")
local results = require("luarocks.results")

--- Store a search result (a rock or rockspec) in the result tree.
-- @param result_tree table: The result tree, where keys are package names and
-- values are tables matching version strings to arrays of
-- tables with fields "arch" and "repo".
-- @param result table: A result.
function search.store_result(result_tree, result)
   assert(type(result_tree) == "table")
   assert(result:type() == "result")

   local name = result.name
   local version = result.version

   if not result_tree[name] then result_tree[name] = {} end
   if not result_tree[name][version] then result_tree[name][version] = {} end
   table.insert(result_tree[name][version], {
      arch = result.arch,
      repo = result.repo,
      namespace = result.namespace,
   })
end

--- Store a match in a result tree if version matches query.
-- Name, version, arch and repository path are stored in a given
-- table, optionally checking if version and arch (if given) match
-- a query.
-- @param result_tree table: The result tree, where keys are package names and
-- values are tables matching version strings to arrays of
-- tables with fields "arch" and "repo".
-- @param result table: a result object.
-- @param query table: a query object.
local function store_if_match(result_tree, result, query)
   assert(result:type() == "result")
   assert(query:type() == "query")

   if result:satisfies(query) then
      search.store_result(result_tree, result)
   end
end

--- Perform search on a local repository.
-- @param repo string: The pathname of the local repository.
-- @param query table: a query object.
-- @param result_tree table or nil: If given, this table will store the
-- result tree; if not given, a new table will be created.
-- @return table: The result tree, where keys are package names and
-- values are tables matching version strings to arrays of
-- tables with fields "arch" and "repo".
-- If a table was given in the "result_tree" parameter, that is the result value.
function search.disk_search(repo, query, result_tree)
   assert(type(repo) == "string")
   assert(query:type() == "query")
   assert(type(result_tree) == "table" or not result_tree)

   local fs = require("luarocks.fs")

   if not result_tree then
      result_tree = {}
   end

   for name in fs.dir(repo) do
      local pathname = dir.path(repo, name)
      local rname, rversion, rarch = path.parse_name(name)

      if rname and (pathname:match(".rockspec$") or pathname:match(".rock$")) then
         local result = results.new(rname, rversion, repo, rarch)
         store_if_match(result_tree, result, query)
      elseif fs.is_dir(pathname) then
         for version in fs.dir(pathname) do
            if version:match("-%d+$") then
               local namespace = path.read_namespace(name, version, repo)
               local result = results.new(name, version, repo, "installed", namespace)
               store_if_match(result_tree, result, query)
            end
         end
      end
   end
   return result_tree
end

--- Perform search on a rocks server or tree.
-- @param result_tree table: The result tree, where keys are package names and
-- values are tables matching version strings to arrays of
-- tables with fields "arch" and "repo".
-- @param repo string: The URL of a rocks server or
-- the pathname of a rocks tree (as returned by path.rocks_dir()).
-- @param query table: a query object.
-- @param lua_version string: Lua version in "5.x" format, defaults to installed version.
-- @param is_local boolean
-- @return true or, in case of errors, nil, an error message and an optional error code.
local function manifest_search(result_tree, repo, query, lua_version, is_local)
   assert(type(result_tree) == "table")
   assert(type(repo) == "string")
   assert(query:type() == "query")

   -- FIXME do not add this in local repos
   if (not is_local) and query.namespace then
      repo = repo .. "/manifests/" .. query.namespace
   end

   local manifest, err, errcode = manif.load_manifest(repo, lua_version, not is_local)
   if not manifest then
      return nil, err, errcode
   end
   for name, versions in pairs(manifest.repository) do
      for version, items in pairs(versions) do
         local namespace = is_local and path.read_namespace(name, version, repo) or query.namespace
         for _, item in ipairs(items) do
            local result = results.new(name, version, repo, item.arch, namespace)
            store_if_match(result_tree, result, query)
         end
      end
   end
   return true
end

local function remote_manifest_search(result_tree, repo, query, lua_version)
   return manifest_search(result_tree, repo, query, lua_version, false)
end

function search.local_manifest_search(result_tree, repo, query, lua_version)
   return manifest_search(result_tree, repo, query, lua_version, true)
end

--- Search on all configured rocks servers.
-- @param query table: a query object.
-- @param lua_version string: Lua version in "5.x" format, defaults to installed version.
-- @return table: A table where keys are package names
-- and values are tables matching version strings to arrays of
-- tables with fields "arch" and "repo".
function search.search_repos(query, lua_version)
   assert(query:type() == "query")

   local result_tree = {}
   for _, repo in ipairs(cfg.rocks_servers) do
      if type(repo) == "string" then
         repo = { repo }
      end
      for _, mirror in ipairs(repo) do
         if not cfg.disabled_servers[mirror] then
            local protocol, pathname = dir.split_url(mirror)
            if protocol == "file" then
               mirror = pathname
            end
            local ok, err, errcode = remote_manifest_search(result_tree, mirror, query, lua_version)
            if errcode == "network" then
               cfg.disabled_servers[mirror] = true
            end
            if ok then
               break
            else
               util.warning("Failed searching manifest: "..err)
               if errcode == "downloader" then
                  break
               end
            end
         end
      end
   end
   -- search through rocks in rocks_provided
   local provided_repo = "provided by VM or rocks_provided"
   for name, version in pairs(util.get_rocks_provided()) do
      local result = results.new(name, version, provided_repo, "installed")
      store_if_match(result_tree, result, query)
   end
   return result_tree
end

--- Get the URL for the latest in a set of versions.
-- @param name string: The package name to be used in the URL.
-- @param versions table: An array of version informations, as stored
-- in search result trees.
-- @return string or nil: the URL for the latest version if one could
-- be picked, or nil.
local function pick_latest_version(name, versions)
   assert(type(name) == "string" and not name:match("/"))
   assert(type(versions) == "table")

   local vtables = {}
   for v, _ in pairs(versions) do
      table.insert(vtables, vers.parse_version(v))
   end
   table.sort(vtables)
   local version = vtables[#vtables].string
   local items = versions[version]
   if items then
      local pick = 1
      for i, item in ipairs(items) do
         if (item.arch == 'src' and items[pick].arch == 'rockspec')
         or (item.arch ~= 'src' and item.arch ~= 'rockspec') then
            pick = i
         end
      end
      return path.make_url(items[pick].repo, name, version, items[pick].arch)
   end
   return nil
end

-- Find out which other Lua versions provide rock versions matching a query,
-- @param query table: a query object.
-- @return table: array of Lua versions supported, in "5.x" format.
local function supported_lua_versions(query)
   assert(query:type() == "query")
   local result_tree = {}

   for lua_version in util.lua_versions() do
      if lua_version ~= cfg.lua_version then
         util.printout("Checking for Lua " .. lua_version .. "...")
         if search.search_repos(query, lua_version)[query.name] then
            table.insert(result_tree, lua_version)
         end
      end
   end

   return result_tree
end

--- Attempt to get a single URL for a given search for a rock.
-- @param query table: a query object.
-- @return string or (nil, string, string): URL for latest matching version
-- of the rock if it was found, or nil followed by an error message
-- and an error code.
function search.find_suitable_rock(query)
   assert(query:type() == "query")

   local rocks_provided = util.get_rocks_provided()

   if rocks_provided[query.name] ~= nil then
      -- Do not install versions listed in rocks_provided.
      return nil, "Rock "..query.name.." "..rocks_provided[query.name]..
         " is already provided by VM or via 'rocks_provided' in the config file.", "provided"
   end

   local result_tree = search.search_repos(query)
   local first_rock = next(result_tree)
   if not first_rock then
      return nil, "No results matching query were found for Lua " .. cfg.lua_version .. ".", "notfound"
   elseif next(result_tree, first_rock) then
      -- Shouldn't happen as query must match only one package.
      return nil, "Several rocks matched query.", "manyfound"
   else
      return pick_latest_version(query.name, result_tree[first_rock])
   end
end

function search.find_src_or_rockspec(name, namespace, version, check_lua_versions)
   local query = queries.new(name, namespace, version, false, "src|rockspec")
   local url, err = search.find_rock_checking_lua_versions(query, check_lua_versions)
   if not url then
      return nil, "Could not find a result named "..tostring(query)..": "..err
   end
   return url
end

function search.find_rock_checking_lua_versions(query, check_lua_versions)
   local url, err, errcode = search.find_suitable_rock(query)
   if url then
      return url
   end

   if errcode == "notfound" then
      local add
      if check_lua_versions then
         util.printout(query.name .. " not found for Lua " .. cfg.lua_version .. ".")
         util.printout("Checking if available for other Lua versions...")

         -- Check if constraints are satisfiable with other Lua versions.
         local lua_versions = supported_lua_versions(query)

         if #lua_versions ~= 0 then
            -- Build a nice message in "only Lua 5.x and 5.y but not 5.z." format
            for i, lua_version in ipairs(lua_versions) do
               lua_versions[i] = "Lua "..lua_version
            end

            local versions_message = "only "..table.concat(lua_versions, " and ")..
               " but not Lua "..cfg.lua_version.."."

            if #query.constraints == 0 then
               add = query.name.." supports "..versions_message
            elseif #query.constraints == 1 and query.constraints[1].op == "==" then
               add = query.name.." "..query.constraints[1].version.string.." supports "..versions_message
            else
               add = "Matching "..query.name.." versions support "..versions_message
            end
         else
            add = query.name.." is not available for any Lua versions."
         end
      else
         add = "To check if it is available for other Lua versions, use --check-lua-versions."
      end
      err = err .. "\n" .. add
   end

   return nil, err
end

--- Print a list of rocks/rockspecs on standard output.
-- @param result_tree table: A result tree.
-- @param porcelain boolean or nil: A flag to force machine-friendly output.
function search.print_result_tree(result_tree, porcelain)
   assert(type(result_tree) == "table")
   assert(type(porcelain) == "boolean" or not porcelain)

   if porcelain then
      for package, versions in util.sortedpairs(result_tree) do
         for version, repos in util.sortedpairs(versions, vers.compare_versions) do
            for _, repo in ipairs(repos) do
               local nrepo = dir.normalize(repo.repo)
               util.printout(package, version, repo.arch, nrepo, repo.namespace)
            end
         end
      end
      return
   end

   for package, versions in util.sortedpairs(result_tree) do
      local namespaces = {}
      for version, repos in util.sortedpairs(versions, vers.compare_versions) do
         for _, repo in ipairs(repos) do
            local key = repo.namespace or ""
            local list = namespaces[key] or {}
            namespaces[key] = list

            repo.repo = dir.normalize(repo.repo)
            table.insert(list, "   "..version.." ("..repo.arch..") - "..path.root_dir(repo.repo))
         end
      end
      for key, list in util.sortedpairs(namespaces) do
         util.printout(key == "" and package or key .. "/" .. package)
         for _, line in ipairs(list) do
            util.printout(line)
         end
         util.printout()
      end
   end
end

function search.pick_installed_rock(query, given_tree)
   assert(query:type() == "query")

   local result_tree = {}
   local tree_map = {}
   local trees = cfg.rocks_trees
   if given_tree then
      trees = { given_tree }
   end
   for _, tree in ipairs(trees) do
      local rocks_dir = path.rocks_dir(tree)
      tree_map[rocks_dir] = tree
      search.local_manifest_search(result_tree, rocks_dir, query)
   end
   if not next(result_tree) then
      return nil, "cannot find package "..tostring(query).."\nUse 'list' to find installed rocks."
   end

   if not result_tree[query.name] and next(result_tree, next(result_tree)) then
      local out = { "multiple installed packages match the name '"..tostring(query).."':\n\n" }
      for name, _ in util.sortedpairs(result_tree) do
         table.insert(out, "   " .. name .. "\n")
      end
      table.insert(out, "\nPlease specify a single rock.\n")
      return nil, table.concat(out)
   end

   local repo_url

   local name, versions
   if result_tree[query.name] then
      name, versions = query.name, result_tree[query.name]
   else
      name, versions = util.sortedpairs(result_tree)()
   end

   local version, repositories = util.sortedpairs(versions, vers.compare_versions)()
   for _, rp in ipairs(repositories) do repo_url = rp.repo end

   local repo = tree_map[repo_url]
   return name, version, repo, repo_url
end

return search