aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorV1K1NGbg <victor@ilchev.com>2024-08-12 17:23:42 +0300
committerV1K1NGbg <victor@ilchev.com>2024-08-12 17:23:42 +0300
commita121c775f9a08e76ed9cb7f2313668f38ac9c67b (patch)
tree11b4c001138f8d110c5afc80cd7387b78faaa5c9
parente62cb8540e458c65146513d0e02af22c83a3bfea (diff)
downloadluarocks-a121c775f9a08e76ed9cb7f2313668f38ac9c67b.tar.gz
luarocks-a121c775f9a08e76ed9cb7f2313668f38ac9c67b.tar.bz2
luarocks-a121c775f9a08e76ed9cb7f2313668f38ac9c67b.zip
test
-rw-r--r--src/luarocks/manif/writer.tl499
-rw-r--r--src/luarocks/repos.tl695
-rw-r--r--src/luarocks/test.tl24
-rw-r--r--src/luarocks/test/busted.tl2
-rw-r--r--src/luarocks/test/command.tl15
5 files changed, 1220 insertions, 15 deletions
diff --git a/src/luarocks/manif/writer.tl b/src/luarocks/manif/writer.tl
new file mode 100644
index 00000000..625fba79
--- /dev/null
+++ b/src/luarocks/manif/writer.tl
@@ -0,0 +1,499 @@
1
2local record writer
3end
4
5local cfg = require("luarocks.core.cfg")
6local search = require("luarocks.search")
7local repos = require("luarocks.repos")
8local deps = require("luarocks.deps")
9local vers = require("luarocks.core.vers")
10local fs = require("luarocks.fs")
11local util = require("luarocks.util")
12local dir = require("luarocks.dir")
13local fetch = require("luarocks.fetch")
14local path = require("luarocks.path")
15local persist = require("luarocks.persist")
16local manif = require("luarocks.manif")
17local queries = require("luarocks.queries")
18
19--- Update storage table to account for items provided by a package.
20-- @param storage table: a table storing items in the following format:
21-- keys are item names and values are arrays of packages providing each item,
22-- where a package is specified as string `name/version`.
23-- @param items table: a table mapping item names to paths.
24-- @param name string: package name.
25-- @param version string: package version.
26local function store_package_items(storage, name, version, items)
27 assert(type(storage) == "table")
28 assert(type(items) == "table")
29 assert(type(name) == "string" and not name:match("/"))
30 assert(type(version) == "string")
31
32 local package_identifier = name.."/"..version
33
34 for item_name, path in pairs(items) do -- luacheck: ignore 431
35 if not storage[item_name] then
36 storage[item_name] = {}
37 end
38
39 table.insert(storage[item_name], package_identifier)
40 end
41end
42
43--- Update storage table removing items provided by a package.
44-- @param storage table: a table storing items in the following format:
45-- keys are item names and values are arrays of packages providing each item,
46-- where a package is specified as string `name/version`.
47-- @param items table: a table mapping item names to paths.
48-- @param name string: package name.
49-- @param version string: package version.
50local function remove_package_items(storage, name, version, items)
51 assert(type(storage) == "table")
52 assert(type(items) == "table")
53 assert(type(name) == "string" and not name:match("/"))
54 assert(type(version) == "string")
55
56 local package_identifier = name.."/"..version
57
58 for item_name, path in pairs(items) do -- luacheck: ignore 431
59 local key = item_name
60 local all_identifiers = storage[key]
61 if not all_identifiers then
62 key = key .. ".init"
63 all_identifiers = storage[key]
64 end
65
66 if all_identifiers then
67 for i, identifier in ipairs(all_identifiers) do
68 if identifier == package_identifier then
69 table.remove(all_identifiers, i)
70 break
71 end
72 end
73
74 if #all_identifiers == 0 then
75 storage[key] = nil
76 end
77 else
78 util.warning("Cannot find entry for " .. item_name .. " in manifest -- corrupted manifest?")
79 end
80 end
81end
82
83--- Process the dependencies of a manifest table to determine its dependency
84-- chains for loading modules. The manifest dependencies information is filled
85-- and any dependency inconsistencies or missing dependencies are reported to
86-- standard error.
87-- @param manifest table: a manifest table.
88-- @param deps_mode string: Dependency mode: "one" for the current default tree,
89-- "all" for all trees, "order" for all trees with priority >= the current default,
90-- "none" for no trees.
91local function update_dependencies(manifest, deps_mode)
92 assert(type(manifest) == "table")
93 assert(type(deps_mode) == "string")
94
95 if not manifest.dependencies then manifest.dependencies = {} end
96 local mdeps = manifest.dependencies
97
98 for pkg, versions in pairs(manifest.repository) do
99 for version, repositories in pairs(versions) do
100 for _, repo in ipairs(repositories) do
101 if repo.arch == "installed" then
102 local rd = {}
103 repo.dependencies = rd
104 deps.scan_deps(rd, mdeps, pkg, version, deps_mode)
105 rd[pkg] = nil
106 end
107 end
108 end
109 end
110end
111
112
113
114--- Sort function for ordering rock identifiers in a manifest's
115-- modules table. Rocks are ordered alphabetically by name, and then
116-- by version which greater first.
117-- @param a string: Version to compare.
118-- @param b string: Version to compare.
119-- @return boolean: The comparison result, according to the
120-- rule outlined above.
121local function sort_pkgs(a, b)
122 assert(type(a) == "string")
123 assert(type(b) == "string")
124
125 local na, va = a:match("(.*)/(.*)$")
126 local nb, vb = b:match("(.*)/(.*)$")
127
128 return (na == nb) and vers.compare_versions(va, vb) or na < nb
129end
130
131--- Sort items of a package matching table by version number (higher versions first).
132-- @param tbl table: the package matching table: keys should be strings
133-- and values arrays of strings with packages names in "name/version" format.
134local function sort_package_matching_table(tbl)
135 assert(type(tbl) == "table")
136
137 if next(tbl) then
138 for item, pkgs in pairs(tbl) do
139 if #pkgs > 1 then
140 table.sort(pkgs, sort_pkgs)
141 -- Remove duplicates from the sorted array.
142 local prev = nil
143 local i = 1
144 while pkgs[i] do
145 local curr = pkgs[i]
146 if curr == prev then
147 table.remove(pkgs, i)
148 else
149 prev = curr
150 i = i + 1
151 end
152 end
153 end
154 end
155 end
156end
157
158--- Filter manifest table by Lua version, removing rockspecs whose Lua version
159-- does not match.
160-- @param manifest table: a manifest table.
161-- @param lua_version string or nil: filter by Lua version
162-- @param repodir string: directory of repository being scanned
163-- @param cache table: temporary rockspec cache table
164local function filter_by_lua_version(manifest, lua_version, repodir, cache)
165 assert(type(manifest) == "table")
166 assert(type(repodir) == "string")
167 assert((not cache) or type(cache) == "table")
168
169 cache = cache or {}
170 lua_version = vers.parse_version(lua_version)
171 for pkg, versions in pairs(manifest.repository) do
172 local to_remove = {}
173 for version, repositories in pairs(versions) do
174 for _, repo in ipairs(repositories) do
175 if repo.arch == "rockspec" then
176 local pathname = dir.path(repodir, pkg.."-"..version..".rockspec")
177 local rockspec, err = cache[pathname]
178 if not rockspec then
179 rockspec, err = fetch.load_local_rockspec(pathname, true)
180 end
181 if rockspec then
182 cache[pathname] = rockspec
183 for _, dep in ipairs(rockspec.dependencies) do
184 if dep.name == "lua" then
185 if not vers.match_constraints(lua_version, dep.constraints) then
186 table.insert(to_remove, version)
187 end
188 break
189 end
190 end
191 else
192 util.printerr("Error loading rockspec for "..pkg.." "..version..": "..err)
193 end
194 end
195 end
196 end
197 if next(to_remove) then
198 for _, incompat in ipairs(to_remove) do
199 versions[incompat] = nil
200 end
201 if not next(versions) then
202 manifest.repository[pkg] = nil
203 end
204 end
205 end
206end
207
208--- Store search results in a manifest table.
209-- @param results table: The search results as returned by search.disk_search.
210-- @param manifest table: A manifest table (must contain repository, modules, commands tables).
211-- It will be altered to include the search results.
212-- @return boolean or (nil, string): true in case of success, or nil followed by an error message.
213local function store_results(results, manifest)
214 assert(type(results) == "table")
215 assert(type(manifest) == "table")
216
217 for name, versions in pairs(results) do
218 local pkgtable = manifest.repository[name] or {}
219 for version, entries in pairs(versions) do
220 local versiontable = {}
221 for _, entry in ipairs(entries) do
222 local entrytable = {}
223 entrytable.arch = entry.arch
224 if entry.arch == "installed" then
225 local rock_manifest, err = manif.load_rock_manifest(name, version)
226 if not rock_manifest then return nil, err end
227
228 entrytable.modules = repos.package_modules(name, version)
229 store_package_items(manifest.modules, name, version, entrytable.modules)
230 entrytable.commands = repos.package_commands(name, version)
231 store_package_items(manifest.commands, name, version, entrytable.commands)
232 end
233 table.insert(versiontable, entrytable)
234 end
235 pkgtable[version] = versiontable
236 end
237 manifest.repository[name] = pkgtable
238 end
239 sort_package_matching_table(manifest.modules)
240 sort_package_matching_table(manifest.commands)
241 return true
242end
243
244--- Commit a table to disk in given local path.
245-- @param where string: The directory where the table should be saved.
246-- @param name string: The filename.
247-- @param tbl table: The table to be saved.
248-- @return boolean or (nil, string): true if successful, or nil and a
249-- message in case of errors.
250local function save_table(where, name, tbl)
251 assert(type(where) == "string")
252 assert(type(name) == "string" and not name:match("/"))
253 assert(type(tbl) == "table")
254
255 local filename = dir.path(where, name)
256 local ok, err = persist.save_from_table(filename..".tmp", tbl)
257 if ok then
258 ok, err = fs.replace_file(filename, filename..".tmp")
259 end
260 return ok, err
261end
262
263function writer.make_rock_manifest(name, version)
264 local install_dir = path.install_dir(name, version)
265 local tree = {}
266 for _, file in ipairs(fs.find(install_dir)) do
267 local full_path = dir.path(install_dir, file)
268 local walk = tree
269 local last
270 local last_name
271 for filename in file:gmatch("[^\\/]+") do
272 local next = walk[filename]
273 if not next then
274 next = {}
275 walk[filename] = next
276 end
277 last = walk
278 last_name = filename
279 walk = next
280 end
281 if fs.is_file(full_path) then
282 local sum, err = fs.get_md5(full_path)
283 if not sum then
284 return nil, "Failed producing checksum: "..tostring(err)
285 end
286 last[last_name] = sum
287 end
288 end
289 local rock_manifest = { rock_manifest=tree }
290 manif.rock_manifest_cache[name.."/"..version] = rock_manifest
291 save_table(install_dir, "rock_manifest", rock_manifest )
292 return true
293end
294
295-- Writes a 'rock_namespace' file in a locally installed rock directory.
296-- @param name string: the rock name, without a namespace
297-- @param version string: the rock version
298-- @param namespace string?: the namespace
299-- @return true if successful (or unnecessary, if there is no namespace),
300-- or nil and an error message.
301function writer.make_namespace_file(name, version, namespace)
302 assert(type(name) == "string" and not name:match("/"))
303 assert(type(version) == "string")
304 assert(type(namespace) == "string" or not namespace)
305 if not namespace then
306 return true
307 end
308 local fd, err = io.open(path.rock_namespace_file(name, version), "w")
309 if not fd then
310 return nil, err
311 end
312 local ok, err = fd:write(namespace)
313 if not ok then
314 return nil, err
315 end
316 fd:close()
317 return true
318end
319
320--- Scan a LuaRocks repository and output a manifest file.
321-- A file called 'manifest' will be written in the root of the given
322-- repository directory.
323-- @param repo A local repository directory.
324-- @param deps_mode string: Dependency mode: "one" for the current default tree,
325-- "all" for all trees, "order" for all trees with priority >= the current default,
326-- "none" for the default dependency mode from the configuration.
327-- @param remote boolean: 'true' if making a manifest for a rocks server.
328-- @return boolean or (nil, string): True if manifest was generated,
329-- or nil and an error message.
330function writer.make_manifest(repo, deps_mode, remote)
331 assert(type(repo) == "string")
332 assert(type(deps_mode) == "string")
333
334 if deps_mode == "none" then deps_mode = cfg.deps_mode end
335
336 if not fs.is_dir(repo) then
337 return nil, "Cannot access repository at "..repo
338 end
339
340 local query = queries.all("any")
341 local results = search.disk_search(repo, query)
342 local manifest = { repository = {}, modules = {}, commands = {} }
343
344 manif.cache_manifest(repo, nil, manifest)
345
346 local ok, err = store_results(results, manifest)
347 if not ok then return nil, err end
348
349 if remote then
350 local cache = {}
351 for luaver in util.lua_versions() do
352 local vmanifest = { repository = {}, modules = {}, commands = {} }
353 local ok, err = store_results(results, vmanifest)
354 filter_by_lua_version(vmanifest, luaver, repo, cache)
355 if not cfg.no_manifest then
356 save_table(repo, "manifest-"..luaver, vmanifest)
357 end
358 end
359 else
360 update_dependencies(manifest, deps_mode)
361 end
362
363 if cfg.no_manifest then
364 -- We want to have cache updated; but exit before save_table is called
365 return true
366 end
367 return save_table(repo, "manifest", manifest)
368end
369
370--- Update manifest file for a local repository
371-- adding information about a version of a package installed in that repository.
372-- @param name string: Name of a package from the repository.
373-- @param version string: Version of a package from the repository.
374-- @param repo string or nil: Pathname of a local repository. If not given,
375-- the default local repository is used.
376-- @param deps_mode string: Dependency mode: "one" for the current default tree,
377-- "all" for all trees, "order" for all trees with priority >= the current default,
378-- "none" for using the default dependency mode from the configuration.
379-- @return boolean or (nil, string): True if manifest was updated successfully,
380-- or nil and an error message.
381function writer.add_to_manifest(name, version, repo, deps_mode)
382 assert(type(name) == "string" and not name:match("/"))
383 assert(type(version) == "string")
384 local rocks_dir = path.rocks_dir(repo or cfg.root_dir)
385 assert(type(deps_mode) == "string")
386
387 if deps_mode == "none" then deps_mode = cfg.deps_mode end
388
389 local manifest, err = manif.load_manifest(rocks_dir)
390 if not manifest then
391 util.printerr("No existing manifest. Attempting to rebuild...")
392 -- Manifest built by `writer.make_manifest` should already
393 -- include information about given name and version,
394 -- no need to update it.
395 return writer.make_manifest(rocks_dir, deps_mode)
396 end
397
398 local results = {[name] = {[version] = {{arch = "installed", repo = rocks_dir}}}}
399
400 local ok, err = store_results(results, manifest)
401 if not ok then return nil, err end
402
403 update_dependencies(manifest, deps_mode)
404
405 if cfg.no_manifest then
406 return true
407 end
408 return save_table(rocks_dir, "manifest", manifest)
409end
410
411--- Update manifest file for a local repository
412-- removing information about a version of a package.
413-- @param name string: Name of a package removed from the repository.
414-- @param version string: Version of a package removed from the repository.
415-- @param repo string or nil: Pathname of a local repository. If not given,
416-- the default local repository is used.
417-- @param deps_mode string: Dependency mode: "one" for the current default tree,
418-- "all" for all trees, "order" for all trees with priority >= the current default,
419-- "none" for using the default dependency mode from the configuration.
420-- @return boolean or (nil, string): True if manifest was updated successfully,
421-- or nil and an error message.
422function writer.remove_from_manifest(name, version, repo, deps_mode)
423 assert(type(name) == "string" and not name:match("/"))
424 assert(type(version) == "string")
425 local rocks_dir = path.rocks_dir(repo or cfg.root_dir)
426 assert(type(deps_mode) == "string")
427
428 if deps_mode == "none" then deps_mode = cfg.deps_mode end
429
430 local manifest, err = manif.load_manifest(rocks_dir)
431 if not manifest then
432 util.printerr("No existing manifest. Attempting to rebuild...")
433 -- Manifest built by `writer.make_manifest` should already
434 -- include up-to-date information, no need to update it.
435 return writer.make_manifest(rocks_dir, deps_mode)
436 end
437
438 local package_entry = manifest.repository[name]
439 if package_entry == nil or package_entry[version] == nil then
440 -- entry is already missing from repository, no need to do anything
441 return true
442 end
443
444 local version_entry = package_entry[version][1]
445 if not version_entry then
446 -- manifest looks corrupted, rebuild
447 return writer.make_manifest(rocks_dir, deps_mode)
448 end
449
450 remove_package_items(manifest.modules, name, version, version_entry.modules)
451 remove_package_items(manifest.commands, name, version, version_entry.commands)
452
453 package_entry[version] = nil
454 manifest.dependencies[name][version] = nil
455
456 if not next(package_entry) then
457 -- No more versions of this package.
458 manifest.repository[name] = nil
459 manifest.dependencies[name] = nil
460 end
461
462 update_dependencies(manifest, deps_mode)
463
464 if cfg.no_manifest then
465 return true
466 end
467 return save_table(rocks_dir, "manifest", manifest)
468end
469
470--- Report missing dependencies for all rocks installed in a repository.
471-- @param repo string or nil: Pathname of a local repository. If not given,
472-- the default local repository is used.
473-- @param deps_mode string: Dependency mode: "one" for the current default tree,
474-- "all" for all trees, "order" for all trees with priority >= the current default,
475-- "none" for using the default dependency mode from the configuration.
476function writer.check_dependencies(repo, deps_mode)
477 local rocks_dir = path.rocks_dir(repo or cfg.root_dir)
478 assert(type(deps_mode) == "string")
479 if deps_mode == "none" then deps_mode = cfg.deps_mode end
480
481 local manifest = manif.load_manifest(rocks_dir)
482 if not manifest then
483 return
484 end
485
486 for name, versions in util.sortedpairs(manifest.repository) do
487 for version, version_entries in util.sortedpairs(versions, vers.compare_versions) do
488 for _, entry in ipairs(version_entries) do
489 if entry.arch == "installed" then
490 if manifest.dependencies[name] and manifest.dependencies[name][version] then
491 deps.report_missing_dependencies(name, version, manifest.dependencies[name][version], deps_mode, util.get_rocks_provided())
492 end
493 end
494 end
495 end
496 end
497end
498
499return writer
diff --git a/src/luarocks/repos.tl b/src/luarocks/repos.tl
new file mode 100644
index 00000000..14f1658c
--- /dev/null
+++ b/src/luarocks/repos.tl
@@ -0,0 +1,695 @@
1
2--- Functions for managing the repository on disk.
3local record repos
4end
5
6local fs = require("luarocks.fs")
7local path = require("luarocks.path")
8local cfg = require("luarocks.core.cfg")
9local util = require("luarocks.util")
10local dir = require("luarocks.dir")
11local manif = require("luarocks.manif")
12local vers = require("luarocks.core.vers")
13
14local unpack = unpack or table.unpack -- luacheck: ignore 211
15
16--- Get type and name of an item (a module or a command) provided by a file.
17-- @param deploy_type string: rock manifest subtree the file comes from ("bin", "lua", or "lib").
18-- @param file_path string: path to the file relatively to deploy_type subdirectory.
19-- @return (string, string): item type ("module" or "command") and name.
20local function get_provided_item(deploy_type: string, file_path: string): string, string
21 local item_type = deploy_type == "bin" and "command" or "module"
22 local item_name = item_type == "command" and file_path or path.path_to_module(file_path)
23 return item_type, item_name
24end
25
26-- Tree of files installed by a package are stored
27-- in its rock manifest. Some of these files have to
28-- be deployed to locations where Lua can load them as
29-- modules or where they can be used as commands.
30-- These files are characterised by pair
31-- (deploy_type, file_path), where deploy_type is the first
32-- component of the file path and file_path is the rest of the
33-- path. Only files with deploy_type in {"lua", "lib", "bin"}
34-- are deployed somewhere.
35-- Each deployed file provides an "item". An item is
36-- characterised by pair (item_type, item_name).
37-- item_type is "command" for files with deploy_type
38-- "bin" and "module" for deploy_type in {"lua", "lib"}.
39-- item_name is same as file_path for commands
40-- and is produced using path.path_to_module(file_path)
41-- for modules.
42
43--- Get all installed versions of a package.
44-- @param name string: a package name.
45-- @return table or nil: An array of strings listing installed
46-- versions of a package, or nil if none is available.
47local function get_installed_versions(name: string): {string}
48 assert(not name:match("/"))
49
50 local dirs = fs.list_dir(path.versions_dir(name))
51 return (dirs and #dirs > 0) and dirs or nil
52end
53
54--- Check if a package exists in a local repository.
55-- Version numbers are compared as exact string comparison.
56-- @param name string: name of package
57-- @param version string: package version in string format
58-- @return boolean: true if a package is installed,
59-- false otherwise.
60function repos.is_installed(name: string, version: string): boolean
61 assert(not name:match("/"))
62
63 return fs.is_dir(path.install_dir(name, version))
64end
65
66function repos.recurse_rock_manifest_entry(entry, action): boolean, string
67 assert(type(action) == "function")
68
69 if entry == nil then
70 return true
71 end
72
73 local function do_recurse_rock_manifest_entry(tree, parent_path)
74
75 for file, sub in pairs(tree) do
76 local sub_path = (parent_path and (parent_path .. "/") or "") .. file
77 local ok, err -- luacheck: ignore 231
78
79 if type(sub) == "table" then
80 ok, err = do_recurse_rock_manifest_entry(sub, sub_path)
81 else
82 ok, err = action(sub_path)
83 end
84
85 if err then return nil, err end
86 end
87 return true
88 end
89 return do_recurse_rock_manifest_entry(entry)
90end
91
92local function store_package_data(result, rock_manifest, deploy_type)
93 if rock_manifest[deploy_type] then
94 repos.recurse_rock_manifest_entry(rock_manifest[deploy_type], function(file_path)
95 local _, item_name = get_provided_item(deploy_type, file_path)
96 result[item_name] = file_path
97 return true
98 end)
99 end
100end
101
102--- Obtain a table of modules within an installed package.
103-- @param name string: The package name; for example "luasocket"
104-- @param version string: The exact version number including revision;
105-- for example "2.0.1-1".
106-- @return table: A table of modules where keys are module names
107-- and values are file paths of files providing modules
108-- relative to "lib" or "lua" rock manifest subtree.
109-- If no modules are found or if package name or version
110-- are invalid, an empty table is returned.
111function repos.package_modules(name, version)
112 assert(type(name) == "string" and not name:match("/"))
113 assert(type(version) == "string")
114
115 local result = {}
116 local rock_manifest = manif.load_rock_manifest(name, version)
117 if not rock_manifest then return result end
118 store_package_data(result, rock_manifest, "lib")
119 store_package_data(result, rock_manifest, "lua")
120 return result
121end
122
123--- Obtain a table of command-line scripts within an installed package.
124-- @param name string: The package name; for example "luasocket"
125-- @param version string: The exact version number including revision;
126-- for example "2.0.1-1".
127-- @return table: A table of commands where keys and values are command names
128-- as strings - file paths of files providing commands
129-- relative to "bin" rock manifest subtree.
130-- If no commands are found or if package name or version
131-- are invalid, an empty table is returned.
132function repos.package_commands(name, version)
133 assert(type(name) == "string" and not name:match("/"))
134 assert(type(version) == "string")
135
136 local result = {}
137 local rock_manifest = manif.load_rock_manifest(name, version)
138 if not rock_manifest then return result end
139 store_package_data(result, rock_manifest, "bin")
140 return result
141end
142
143
144--- Check if a rock contains binary executables.
145-- @param name string: name of an installed rock
146-- @param version string: version of an installed rock
147-- @return boolean: returns true if rock contains platform-specific
148-- binary executables, or false if it is a pure-Lua rock.
149function repos.has_binaries(name, version)
150 assert(type(name) == "string" and not name:match("/"))
151 assert(type(version) == "string")
152
153 local rock_manifest = manif.load_rock_manifest(name, version)
154 if rock_manifest and rock_manifest.bin then
155 for bin_name, md5 in pairs(rock_manifest.bin) do
156 -- TODO verify that it is the same file. If it isn't, find the actual command.
157 if fs.is_actual_binary(dir.path(cfg.deploy_bin_dir, bin_name)) then
158 return true
159 end
160 end
161 end
162 return false
163end
164
165function repos.run_hook(rockspec, hook_name)
166 assert(rockspec:type() == "rockspec")
167 assert(type(hook_name) == "string")
168
169 local hooks = rockspec.hooks
170 if not hooks then
171 return true
172 end
173
174 if cfg.hooks_enabled == false then
175 return nil, "This rockspec contains hooks, which are blocked by the 'hooks_enabled' setting in your LuaRocks configuration."
176 end
177
178 if not hooks.substituted_variables then
179 util.variable_substitutions(hooks, rockspec.variables)
180 hooks.substituted_variables = true
181 end
182 local hook = hooks[hook_name]
183 if hook then
184 util.printout(hook)
185 if not fs.execute(hook) then
186 return nil, "Failed running "..hook_name.." hook."
187 end
188 end
189 return true
190end
191
192function repos.should_wrap_bin_scripts(rockspec)
193 assert(rockspec:type() == "rockspec")
194
195 if cfg.wrap_bin_scripts ~= nil then
196 return cfg.wrap_bin_scripts
197 end
198 if rockspec.deploy and rockspec.deploy.wrap_bin_scripts == false then
199 return false
200 end
201 return true
202end
203
204local function find_suffixed(file, suffix)
205 local filenames = {file}
206 if suffix and suffix ~= "" then
207 table.insert(filenames, 1, file .. suffix)
208 end
209
210 for _, filename in ipairs(filenames) do
211 if fs.exists(filename) then
212 return filename
213 end
214 end
215
216 return nil, table.concat(filenames, ", ") .. " not found"
217end
218
219local function check_suffix(filename, suffix)
220 local suffixed_filename, err = find_suffixed(filename, suffix)
221 if not suffixed_filename then
222 return ""
223 end
224 return suffixed_filename:sub(#filename + 1)
225end
226
227-- Files can be deployed using versioned and non-versioned names.
228-- Several items with same type and name can exist if they are
229-- provided by different packages or versions. In any case
230-- item from the newest version of lexicographically smallest package
231-- is deployed using non-versioned name and others use versioned names.
232
233local function get_deploy_paths(name, version, deploy_type, file_path, repo)
234 assert(type(name) == "string")
235 assert(type(version) == "string")
236 assert(type(deploy_type) == "string")
237 assert(type(file_path) == "string")
238
239 repo = repo or cfg.root_dir
240 local deploy_dir = path["deploy_" .. deploy_type .. "_dir"](repo)
241 local non_versioned = dir.path(deploy_dir, file_path)
242 local versioned = path.versioned_name(non_versioned, deploy_dir, name, version)
243 return { nv = non_versioned, v = versioned }
244end
245
246local function check_spot_if_available(name, version, deploy_type, file_path)
247 local item_type, item_name = get_provided_item(deploy_type, file_path)
248 local cur_name, cur_version = manif.get_current_provider(item_type, item_name)
249
250 -- older versions of LuaRocks (< 3) registered "foo.init" files as "foo"
251 -- (which caused problems, so that behavior was changed). But look for that
252 -- in the manifest anyway for backward compatibility.
253 if not cur_name and deploy_type == "lua" and item_name:match("%.init$") then
254 cur_name, cur_version = manif.get_current_provider(item_type, (item_name:gsub("%.init$", "")))
255 end
256
257 if (not cur_name)
258 or (name < cur_name)
259 or (name == cur_name and (version == cur_version
260 or vers.compare_versions(version, cur_version))) then
261 return "nv", cur_name, cur_version, item_name
262 else
263 -- Existing version has priority, deploy new version using versioned name.
264 return "v", cur_name, cur_version, item_name
265 end
266end
267
268local function backup_existing(should_backup, target)
269 if not should_backup then
270 fs.delete(target)
271 return
272 end
273 if fs.exists(target) then
274 local backup = target
275 repeat
276 backup = backup.."~"
277 until not fs.exists(backup) -- Slight race condition here, but shouldn't be a problem.
278
279 util.warning(target.." is not tracked by this installation of LuaRocks. Moving it to "..backup)
280 local move_ok, move_err = os.rename(target, backup)
281 if not move_ok then
282 return nil, move_err
283 end
284 return backup
285 end
286end
287
288local function prepare_op_install()
289 local mkdirs = {}
290 local rmdirs = {}
291
292 local function memoize_mkdir(d)
293 if mkdirs[d] then
294 return true
295 end
296 local ok, err = fs.make_dir(d)
297 if not ok then
298 return nil, err
299 end
300 mkdirs[d] = true
301 return true
302 end
303
304 local function op_install(op)
305 local ok, err = memoize_mkdir(dir.dir_name(op.dst))
306 if not ok then
307 return nil, err
308 end
309
310 local backup, err = backup_existing(op.backup, op.dst)
311 if err then
312 return nil, err
313 end
314 if backup then
315 op.backup_file = backup
316 end
317
318 ok, err = op.fn(op.src, op.dst, op.backup)
319 if not ok then
320 return nil, err
321 end
322
323 rmdirs[dir.dir_name(op.src)] = true
324 return true
325 end
326
327 local function done_op_install()
328 for d, _ in pairs(rmdirs) do
329 fs.remove_dir_tree_if_empty(d)
330 end
331 end
332
333 return op_install, done_op_install
334end
335
336local function rollback_install(op)
337 fs.delete(op.dst)
338 if op.backup_file then
339 os.rename(op.backup_file, op.dst)
340 end
341 fs.remove_dir_tree_if_empty(dir.dir_name(op.dst))
342 return true
343end
344
345local function op_rename(op)
346 if op.suffix then
347 local suffix = check_suffix(op.src, op.suffix)
348 op.src = op.src .. suffix
349 op.dst = op.dst .. suffix
350 end
351
352 if fs.exists(op.src) then
353 fs.make_dir(dir.dir_name(op.dst))
354 fs.delete(op.dst)
355 local ok, err = os.rename(op.src, op.dst)
356 fs.remove_dir_tree_if_empty(dir.dir_name(op.src))
357 return ok, err
358 else
359 return true
360 end
361end
362
363local function rollback_rename(op)
364 return op_rename({ src = op.dst, dst = op.src })
365end
366
367local function prepare_op_delete()
368 local deletes = {}
369 local rmdirs = {}
370
371 local function done_op_delete()
372 for _, f in ipairs(deletes) do
373 os.remove(f)
374 end
375
376 for d, _ in pairs(rmdirs) do
377 fs.remove_dir_tree_if_empty(d)
378 end
379 end
380
381 local function op_delete(op)
382 if op.suffix then
383 local suffix = check_suffix(op.name, op.suffix)
384 op.name = op.name .. suffix
385 end
386
387 table.insert(deletes, op.name)
388
389 rmdirs[dir.dir_name(op.name)] = true
390 end
391
392 return op_delete, done_op_delete
393end
394
395local function rollback_ops(ops, op_fn, n)
396 for i = 1, n do
397 op_fn(ops[i])
398 end
399end
400
401--- Double check that all files referenced in `rock_manifest` are installed in `repo`.
402function repos.check_everything_is_installed(name, version, rock_manifest, repo, accept_versioned)
403 local missing = {}
404 local suffix = cfg.wrapper_suffix or ""
405 for _, category in ipairs({"bin", "lua", "lib"}) do
406 if rock_manifest[category] then
407 repos.recurse_rock_manifest_entry(rock_manifest[category], function(file_path)
408 local paths = get_deploy_paths(name, version, category, file_path, repo)
409 if category == "bin" then
410 if (fs.exists(paths.nv) or fs.exists(paths.nv .. suffix))
411 or (accept_versioned and (fs.exists(paths.v) or fs.exists(paths.v .. suffix))) then
412 return
413 end
414 else
415 if fs.exists(paths.nv) or (accept_versioned and fs.exists(paths.v)) then
416 return
417 end
418 end
419 table.insert(missing, paths.nv)
420 end)
421 end
422 end
423 if #missing > 0 then
424 return nil, "failed deploying files. " ..
425 "The following files were not installed:\n" ..
426 table.concat(missing, "\n")
427 end
428 return true
429end
430
431--- Deploy a package from the rocks subdirectory.
432-- @param name string: name of package
433-- @param version string: exact package version in string format
434-- @param wrap_bin_scripts bool: whether commands written in Lua should be wrapped.
435-- @param deps_mode: string: Which trees to check dependencies for:
436-- "one" for the current default tree, "all" for all trees,
437-- "order" for all trees with priority >= the current default, "none" for no trees.
438function repos.deploy_files(name, version, wrap_bin_scripts, deps_mode)
439 assert(type(name) == "string" and not name:match("/"))
440 assert(type(version) == "string")
441 assert(type(wrap_bin_scripts) == "boolean")
442
443 local rock_manifest, load_err = manif.load_rock_manifest(name, version)
444 if not rock_manifest then return nil, load_err end
445
446 local repo = cfg.root_dir
447 local renames = {}
448 local installs = {}
449
450 local function install_binary(source, target)
451 if wrap_bin_scripts and fs.is_lua(source) then
452 return fs.wrap_script(source, target, deps_mode, name, version)
453 else
454 return fs.copy_binary(source, target)
455 end
456 end
457
458 local function move_lua(source, target)
459 return fs.move(source, target, "read")
460 end
461
462 local function move_lib(source, target)
463 return fs.move(source, target, "exec")
464 end
465
466 if rock_manifest.bin then
467 local source_dir = path.bin_dir(name, version)
468 repos.recurse_rock_manifest_entry(rock_manifest.bin, function(file_path)
469 local source = dir.path(source_dir, file_path)
470 local paths = get_deploy_paths(name, version, "bin", file_path, repo)
471 local mode, cur_name, cur_version = check_spot_if_available(name, version, "bin", file_path)
472
473 if mode == "nv" and cur_name then
474 local cur_paths = get_deploy_paths(cur_name, cur_version, "bin", file_path, repo)
475 table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v, suffix = cfg.wrapper_suffix })
476 end
477 local target = mode == "nv" and paths.nv or paths.v
478 local backup = name ~= cur_name or version ~= cur_version
479 if wrap_bin_scripts and fs.is_lua(source) then
480 target = target .. (cfg.wrapper_suffix or "")
481 end
482 table.insert(installs, { fn = install_binary, src = source, dst = target, backup = backup })
483 end)
484 end
485
486 if rock_manifest.lua then
487 local source_dir = path.lua_dir(name, version)
488 repos.recurse_rock_manifest_entry(rock_manifest.lua, function(file_path)
489 local source = dir.path(source_dir, file_path)
490 local paths = get_deploy_paths(name, version, "lua", file_path, repo)
491 local mode, cur_name, cur_version = check_spot_if_available(name, version, "lua", file_path)
492
493 if mode == "nv" and cur_name then
494 local cur_paths = get_deploy_paths(cur_name, cur_version, "lua", file_path, repo)
495 table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v })
496 cur_paths = get_deploy_paths(cur_name, cur_version, "lib", file_path:gsub("%.lua$", "." .. cfg.lib_extension), repo)
497 table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v })
498 end
499 local target = mode == "nv" and paths.nv or paths.v
500 local backup = name ~= cur_name or version ~= cur_version
501 table.insert(installs, { fn = move_lua, src = source, dst = target, backup = backup })
502 end)
503 end
504
505 if rock_manifest.lib then
506 local source_dir = path.lib_dir(name, version)
507 repos.recurse_rock_manifest_entry(rock_manifest.lib, function(file_path)
508 local source = dir.path(source_dir, file_path)
509 local paths = get_deploy_paths(name, version, "lib", file_path, repo)
510 local mode, cur_name, cur_version = check_spot_if_available(name, version, "lib", file_path)
511
512 if mode == "nv" and cur_name then
513 local cur_paths = get_deploy_paths(cur_name, cur_version, "lua", file_path:gsub("%.[^.]+$", ".lua"), repo)
514 table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v })
515 cur_paths = get_deploy_paths(cur_name, cur_version, "lib", file_path, repo)
516 table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v })
517 end
518 local target = mode == "nv" and paths.nv or paths.v
519 local backup = name ~= cur_name or version ~= cur_version
520 table.insert(installs, { fn = move_lib, src = source, dst = target, backup = backup })
521 end)
522 end
523
524 for i, op in ipairs(renames) do
525 local ok, err = op_rename(op)
526 if not ok then
527 rollback_ops(renames, rollback_rename, i - 1)
528 return nil, err
529 end
530 end
531 local op_install, done_op_install = prepare_op_install()
532 for i, op in ipairs(installs) do
533 local ok, err = op_install(op)
534 if not ok then
535 rollback_ops(installs, rollback_install, i - 1)
536 rollback_ops(renames, rollback_rename, #renames)
537 return nil, err
538 end
539 end
540 done_op_install()
541
542 local ok, err = repos.check_everything_is_installed(name, version, rock_manifest, repo, true)
543 if not ok then
544 return nil, err
545 end
546
547 local writer = require("luarocks.manif.writer")
548 return writer.add_to_manifest(name, version, nil, deps_mode)
549end
550
551local function add_to_double_checks(double_checks, name, version)
552 double_checks[name] = double_checks[name] or {}
553 double_checks[name][version] = true
554end
555
556local function double_check_all(double_checks, repo)
557 local errs = {}
558 for next_name, versions in pairs(double_checks) do
559 for next_version in pairs(versions) do
560 local rock_manifest, load_err = manif.load_rock_manifest(next_name, next_version)
561 local ok, err = repos.check_everything_is_installed(next_name, next_version, rock_manifest, repo, true)
562 if not ok then
563 table.insert(errs, err)
564 end
565 end
566 end
567 if next(errs) then
568 return nil, table.concat(errs, "\n")
569 end
570 return true
571end
572
573--- Delete a package from the local repository.
574-- @param name string: name of package
575-- @param version string: exact package version in string format
576-- @param deps_mode: string: Which trees to check dependencies for:
577-- "one" for the current default tree, "all" for all trees,
578-- "order" for all trees with priority >= the current default, "none" for no trees.
579-- @param quick boolean: do not try to fix the versioned name
580-- of another version that provides the same module that
581-- was deleted. This is used during 'purge', as every module
582-- will be eventually deleted.
583function repos.delete_version(name, version, deps_mode, quick)
584 assert(type(name) == "string" and not name:match("/"))
585 assert(type(version) == "string")
586 assert(type(deps_mode) == "string")
587
588 local rock_manifest, load_err = manif.load_rock_manifest(name, version)
589 if not rock_manifest then
590 if not quick then
591 local writer = require("luarocks.manif.writer")
592 writer.remove_from_manifest(name, version, nil, deps_mode)
593 return nil, "rock_manifest file not found for "..name.." "..version.." - removed entry from the manifest"
594 end
595 return nil, load_err
596 end
597
598 local repo = cfg.root_dir
599 local renames = {}
600 local deletes = {}
601
602 local double_checks = {}
603
604 if rock_manifest.bin then
605 repos.recurse_rock_manifest_entry(rock_manifest.bin, function(file_path)
606 local paths = get_deploy_paths(name, version, "bin", file_path, repo)
607 local mode, cur_name, cur_version, item_name = check_spot_if_available(name, version, "bin", file_path)
608 if mode == "v" then
609 table.insert(deletes, { name = paths.v, suffix = cfg.wrapper_suffix })
610 else
611 table.insert(deletes, { name = paths.nv, suffix = cfg.wrapper_suffix })
612
613 local next_name, next_version = manif.get_next_provider("command", item_name)
614 if next_name then
615 add_to_double_checks(double_checks, next_name, next_version)
616 local next_paths = get_deploy_paths(next_name, next_version, "bin", file_path, repo)
617 table.insert(renames, { src = next_paths.v, dst = next_paths.nv, suffix = cfg.wrapper_suffix })
618 end
619 end
620 end)
621 end
622
623 if rock_manifest.lua then
624 repos.recurse_rock_manifest_entry(rock_manifest.lua, function(file_path)
625 local paths = get_deploy_paths(name, version, "lua", file_path, repo)
626 local mode, cur_name, cur_version, item_name = check_spot_if_available(name, version, "lua", file_path)
627 if mode == "v" then
628 table.insert(deletes, { name = paths.v })
629 else
630 table.insert(deletes, { name = paths.nv })
631
632 local next_name, next_version = manif.get_next_provider("module", item_name)
633 if next_name then
634 add_to_double_checks(double_checks, next_name, next_version)
635 local next_lua_paths = get_deploy_paths(next_name, next_version, "lua", file_path, repo)
636 table.insert(renames, { src = next_lua_paths.v, dst = next_lua_paths.nv })
637 local next_lib_paths = get_deploy_paths(next_name, next_version, "lib", file_path:gsub("%.[^.]+$", ".lua"), repo)
638 table.insert(renames, { src = next_lib_paths.v, dst = next_lib_paths.nv })
639 end
640 end
641 end)
642 end
643
644 if rock_manifest.lib then
645 repos.recurse_rock_manifest_entry(rock_manifest.lib, function(file_path)
646 local paths = get_deploy_paths(name, version, "lib", file_path, repo)
647 local mode, cur_name, cur_version, item_name = check_spot_if_available(name, version, "lib", file_path)
648 if mode == "v" then
649 table.insert(deletes, { name = paths.v })
650 else
651 table.insert(deletes, { name = paths.nv })
652
653 local next_name, next_version = manif.get_next_provider("module", item_name)
654 if next_name then
655 add_to_double_checks(double_checks, next_name, next_version)
656 local next_lua_paths = get_deploy_paths(next_name, next_version, "lua", file_path:gsub("%.[^.]+$", ".lua"), repo)
657 table.insert(renames, { src = next_lua_paths.v, dst = next_lua_paths.nv })
658 local next_lib_paths = get_deploy_paths(next_name, next_version, "lib", file_path, repo)
659 table.insert(renames, { src = next_lib_paths.v, dst = next_lib_paths.nv })
660 end
661 end
662 end)
663 end
664
665 local op_delete, done_op_delete = prepare_op_delete()
666 for _, op in ipairs(deletes) do
667 op_delete(op)
668 end
669 done_op_delete()
670
671 if not quick then
672 for _, op in ipairs(renames) do
673 op_rename(op)
674 end
675
676 local ok, err = double_check_all(double_checks, repo)
677 if not ok then
678 return nil, err
679 end
680 end
681
682 fs.delete(path.install_dir(name, version))
683 if not get_installed_versions(name) then
684 fs.delete(dir.path(cfg.rocks_dir, name))
685 end
686
687 if quick then
688 return true
689 end
690
691 local writer = require("luarocks.manif.writer")
692 return writer.remove_from_manifest(name, version, nil, deps_mode)
693end
694
695return repos
diff --git a/src/luarocks/test.tl b/src/luarocks/test.tl
index 56bc8c74..b1a97495 100644
--- a/src/luarocks/test.tl
+++ b/src/luarocks/test.tl
@@ -17,7 +17,12 @@ local test_types = {
17local test_modules: {string | Test} = {} 17local test_modules: {string | Test} = {}
18 18
19for _, test_type in ipairs(test_types) do 19for _, test_type in ipairs(test_types) do
20 local mod: Test = require("luarocks.test." .. test_type) 20 local mod: Test
21 if test_type == "command" then
22 mod = require("luarocks.test.command")
23 elseif test_type == "busted" then
24 mod = require("luarocks.test.busted")
25 end
21 table.insert(test_modules, mod) 26 table.insert(test_modules, mod)
22 test_modules[test_type] = mod --! 27 test_modules[test_type] = mod --!
23 test_modules[mod] = test_type 28 test_modules[mod] = test_type
@@ -65,7 +70,7 @@ function test.run_test_suite(rockspec_arg: string | Rockspec, test_type: string,
65 "test_dependencies", 70 "test_dependencies",
66 } 71 }
67 for _, dep_kind in ipairs(all_deps) do 72 for _, dep_kind in ipairs(all_deps) do
68 if rockspec[dep_kind] and next(rockspec[dep_kind]) then 73 if rockspec[dep_kind] and next(rockspec[dep_kind]) then --! rockspec as {atring: any}
69 local _, err, errcode = deps.fulfill_dependencies(rockspec, dep_kind, "all") 74 local _, err, errcode = deps.fulfill_dependencies(rockspec, dep_kind, "all")
70 if err then 75 if err then
71 return nil, err, errcode 76 return nil, err, errcode
@@ -73,10 +78,17 @@ function test.run_test_suite(rockspec_arg: string | Rockspec, test_type: string,
73 end 78 end
74 end 79 end
75 80
76 local mod_name = "luarocks.test." .. test_type 81 local pok, test_mod: boolean --! Test type
77 local pok, test_mod = pcall(require, mod_name) 82 if test_type == "command" then
78 if not pok then 83 pok, test_mod = pcall(require, "luarocks.test.command")
79 return nil, "failed loading test execution module " .. mod_name 84 if not pok then
85 return nil, "failed loading test execution module luarocks.test.command"
86 end
87 elseif test_type == "busted" then
88 pok, test_mod = pcall(require, "luarocks.test.busted")
89 if not pok then
90 return nil, "failed loading test execution module luarocks.test.busted"
91 end
80 end 92 end
81 93
82 if prepare then 94 if prepare then
diff --git a/src/luarocks/test/busted.tl b/src/luarocks/test/busted.tl
index e2663ffa..0870d178 100644
--- a/src/luarocks/test/busted.tl
+++ b/src/luarocks/test/busted.tl
@@ -15,7 +15,7 @@ function busted.detect_type(): boolean
15 return false 15 return false
16end 16end
17 17
18function busted.run_tests(test, args: {string}): boolean, string 18function busted.run_tests(test, args: {string}): boolean, string --! Test type
19 if not test then 19 if not test then
20 test = {} 20 test = {}
21 end 21 end
diff --git a/src/luarocks/test/command.tl b/src/luarocks/test/command.tl
index bed6744e..d7781580 100644
--- a/src/luarocks/test/command.tl
+++ b/src/luarocks/test/command.tl
@@ -1,19 +1,18 @@
1 1
2local command = {} 2local record command
3end
3 4
4local fs = require("luarocks.fs") 5local fs = require("luarocks.fs")
5local cfg = require("luarocks.core.cfg") 6local cfg = require("luarocks.core.cfg")
6 7
7local unpack = table.unpack or unpack 8function command.detect_type(): boolean
8
9function command.detect_type()
10 if fs.exists("test.lua") then 9 if fs.exists("test.lua") then
11 return true 10 return true
12 end 11 end
13 return false 12 return false
14end 13end
15 14
16function command.run_tests(test, args) 15function command.run_tests(test, args): boolean, string --! Test type
17 if not test then 16 if not test then
18 test = { 17 test = {
19 script = "test.lua" 18 script = "test.lua"
@@ -24,7 +23,7 @@ function command.run_tests(test, args)
24 test.script = "test.lua" 23 test.script = "test.lua"
25 end 24 end
26 25
27 local ok 26 local ok: boolean
28 27
29 if test.script then 28 if test.script then
30 if type(test.script) ~= "string" then 29 if type(test.script) ~= "string" then
@@ -34,12 +33,12 @@ function command.run_tests(test, args)
34 return nil, "Test script " .. test.script .. " does not exist" 33 return nil, "Test script " .. test.script .. " does not exist"
35 end 34 end
36 local lua = fs.Q(cfg.variables["LUA"]) -- get lua interpreter configured 35 local lua = fs.Q(cfg.variables["LUA"]) -- get lua interpreter configured
37 ok = fs.execute(lua, test.script, unpack(args)) 36 ok = fs.execute(lua, test.script, table.unpack(args))
38 elseif test.command then 37 elseif test.command then
39 if type(test.command) ~= "string" then 38 if type(test.command) ~= "string" then
40 return nil, "Malformed rockspec: 'command' expects a string" 39 return nil, "Malformed rockspec: 'command' expects a string"
41 end 40 end
42 ok = fs.execute(test.command, unpack(args)) 41 ok = fs.execute(test.command, table.unpack(args))
43 end 42 end
44 43
45 if ok then 44 if ok then