summaryrefslogtreecommitdiff
path: root/src/luarocks
diff options
context:
space:
mode:
Diffstat (limited to 'src/luarocks')
-rw-r--r--src/luarocks/admin/cache.lua88
-rw-r--r--src/luarocks/admin/cmd/add.lua134
-rw-r--r--src/luarocks/admin/cmd/make_manifest.lua50
-rw-r--r--src/luarocks/admin/cmd/refresh_cache.lua31
-rw-r--r--src/luarocks/admin/cmd/remove.lua95
-rw-r--r--src/luarocks/admin/index.lua185
-rw-r--r--src/luarocks/build.lua495
-rw-r--r--src/luarocks/build/builtin.lua395
-rw-r--r--src/luarocks/build/cmake.lua78
-rw-r--r--src/luarocks/build/command.lua41
-rw-r--r--src/luarocks/build/make.lua98
-rw-r--r--src/luarocks/cmd.lua781
-rw-r--r--src/luarocks/cmd/build.lua198
-rw-r--r--src/luarocks/cmd/config.lua392
-rw-r--r--src/luarocks/cmd/doc.lua153
-rw-r--r--src/luarocks/cmd/download.lua51
-rw-r--r--src/luarocks/cmd/init.lua219
-rw-r--r--src/luarocks/cmd/install.lua271
-rw-r--r--src/luarocks/cmd/lint.lua50
-rw-r--r--src/luarocks/cmd/list.lua96
-rw-r--r--src/luarocks/cmd/make.lua163
-rw-r--r--src/luarocks/cmd/new_version.lua228
-rw-r--r--src/luarocks/cmd/pack.lua36
-rw-r--r--src/luarocks/cmd/path.lua83
-rw-r--r--src/luarocks/cmd/purge.lua73
-rw-r--r--src/luarocks/cmd/remove.lua72
-rw-r--r--src/luarocks/cmd/search.lua84
-rw-r--r--src/luarocks/cmd/show.lua314
-rw-r--r--src/luarocks/cmd/test.lua48
-rw-r--r--src/luarocks/cmd/unpack.lua169
-rw-r--r--src/luarocks/cmd/upload.lua128
-rw-r--r--src/luarocks/cmd/which.lua40
-rw-r--r--src/luarocks/cmd/write_rockspec.lua408
-rw-r--r--src/luarocks/config.lua36
-rw-r--r--src/luarocks/core/cfg.lua940
-rw-r--r--src/luarocks/core/dir.lua98
-rw-r--r--src/luarocks/core/manif.lua114
-rw-r--r--src/luarocks/core/path.lua157
-rw-r--r--src/luarocks/core/persist.lua81
-rw-r--r--src/luarocks/core/sysdetect.lua419
-rw-r--r--src/luarocks/core/util.lua322
-rw-r--r--src/luarocks/core/vers.lua207
-rw-r--r--src/luarocks/deplocks.lua106
-rw-r--r--src/luarocks/deps.lua831
-rw-r--r--src/luarocks/dir.lua63
-rw-r--r--src/luarocks/download.lua68
-rw-r--r--src/luarocks/fetch.lua610
-rw-r--r--src/luarocks/fetch/cvs.lua55
-rw-r--r--src/luarocks/fetch/git.lua165
-rw-r--r--src/luarocks/fetch/git_file.lua19
-rw-r--r--src/luarocks/fetch/git_http.lua26
-rw-r--r--src/luarocks/fetch/git_https.lua7
-rw-r--r--src/luarocks/fetch/git_ssh.lua32
-rw-r--r--src/luarocks/fetch/hg.lua65
-rw-r--r--src/luarocks/fetch/hg_http.lua24
-rw-r--r--src/luarocks/fetch/hg_https.lua8
-rw-r--r--src/luarocks/fetch/hg_ssh.lua8
-rw-r--r--src/luarocks/fetch/sscm.lua44
-rw-r--r--src/luarocks/fetch/svn.lua64
-rw-r--r--src/luarocks/fs.lua148
-rw-r--r--src/luarocks/fs/linux.lua50
-rw-r--r--src/luarocks/fs/lua.lua1307
-rw-r--r--src/luarocks/fs/macosx.lua50
-rw-r--r--src/luarocks/fs/tools.lua222
-rw-r--r--src/luarocks/fs/unix.lua266
-rw-r--r--src/luarocks/fs/unix/tools.lua353
-rw-r--r--src/luarocks/fs/win32.lua384
-rw-r--r--src/luarocks/fs/win32/tools.lua330
-rw-r--r--src/luarocks/fun.lua143
-rw-r--r--src/luarocks/loader.lua269
-rw-r--r--src/luarocks/manif.lua225
-rw-r--r--src/luarocks/manif/writer.lua498
-rw-r--r--src/luarocks/pack.lua184
-rw-r--r--src/luarocks/path.lua263
-rw-r--r--src/luarocks/persist.lua259
-rw-r--r--src/luarocks/queries.lua217
-rw-r--r--src/luarocks/remove.lua135
-rw-r--r--src/luarocks/repos.lua697
-rw-r--r--src/luarocks/require.lua2
-rw-r--r--src/luarocks/results.lua62
-rw-r--r--src/luarocks/rockspecs.lua183
-rw-r--r--src/luarocks/search.lua393
-rw-r--r--src/luarocks/signing.lua48
-rw-r--r--src/luarocks/test.lua100
-rw-r--r--src/luarocks/test/busted.lua53
-rw-r--r--src/luarocks/test/command.lua52
-rw-r--r--src/luarocks/tools/patch.lua716
-rw-r--r--src/luarocks/tools/tar.lua191
-rw-r--r--src/luarocks/tools/zip.lua531
-rw-r--r--src/luarocks/type/manifest.lua80
-rw-r--r--src/luarocks/type/rockspec.lua199
-rw-r--r--src/luarocks/type_check.lua213
-rw-r--r--src/luarocks/upload/api.lua265
-rw-r--r--src/luarocks/upload/multipart.lua109
-rw-r--r--src/luarocks/util.lua634
-rw-r--r--src/luarocks/vendor/argparse.lua2103
-rw-r--r--src/luarocks/vendor/dkjson.lua749
97 files changed, 22999 insertions, 0 deletions
diff --git a/src/luarocks/admin/cache.lua b/src/luarocks/admin/cache.lua
new file mode 100644
index 0000000..7a4e4af
--- /dev/null
+++ b/src/luarocks/admin/cache.lua
@@ -0,0 +1,88 @@
+
+--- Module handling the LuaRocks local cache.
+-- Adds a rock or rockspec to a rocks server.
+local cache = {}
+
+local fs = require("luarocks.fs")
+local cfg = require("luarocks.core.cfg")
+local dir = require("luarocks.dir")
+local util = require("luarocks.util")
+
+function cache.get_upload_server(server)
+ if not server then server = cfg.upload_server end
+ if not server then
+ return nil, "No server specified and no default configured with upload_server."
+ end
+ return server, cfg.upload_servers and cfg.upload_servers[server]
+end
+
+function cache.get_server_urls(server, upload_server)
+ local download_url = server
+ local login_url = nil
+ if upload_server then
+ if upload_server.rsync then download_url = "rsync://"..upload_server.rsync
+ elseif upload_server.http then download_url = "http://"..upload_server.http
+ elseif upload_server.ftp then download_url = "ftp://"..upload_server.ftp
+ end
+
+ if upload_server.ftp then login_url = "ftp://"..upload_server.ftp
+ elseif upload_server.sftp then login_url = "sftp://"..upload_server.sftp
+ end
+ end
+ return download_url, login_url
+end
+
+function cache.split_server_url(url, user, password)
+ local protocol, server_path = dir.split_url(url)
+ if protocol == "file" then
+ server_path = fs.absolute_name(server_path)
+ elseif server_path:match("@") then
+ local credentials
+ credentials, server_path = server_path:match("([^@]*)@(.*)")
+ if credentials:match(":") then
+ user, password = credentials:match("([^:]*):(.*)")
+ else
+ user = credentials
+ end
+ end
+ local local_cache = dir.path(cfg.local_cache, (server_path:gsub("[\\/]", "_")))
+ return local_cache, protocol, server_path, user, password
+end
+
+local function download_cache(protocol, server_path, user, password)
+ os.remove("index.html")
+ -- TODO abstract away explicit 'wget' call
+ if protocol == "rsync" then
+ local srv, path = server_path:match("([^/]+)(/.+)")
+ return fs.execute(cfg.variables.RSYNC.." "..cfg.variables.RSYNCFLAGS.." -e ssh "..user.."@"..srv..":"..path.."/ ./")
+ elseif protocol == "file" then
+ return fs.copy_contents(server_path, ".")
+ else
+ local login_info = ""
+ if user then login_info = " --user="..user end
+ if password then login_info = login_info .. " --password="..password end
+ return fs.execute(cfg.variables.WGET.." --no-cache -q -m -np -nd "..protocol.."://"..server_path..login_info)
+ end
+end
+
+function cache.refresh_local_cache(url, given_user, given_password)
+ local local_cache, protocol, server_path, user, password = cache.split_server_url(url, given_user, given_password)
+
+ local ok, err = fs.make_dir(local_cache)
+ if not ok then
+ return nil, "Failed creating local cache dir: "..err
+ end
+
+ fs.change_dir(local_cache)
+
+ util.printout("Refreshing cache "..local_cache.."...")
+
+ ok = download_cache(protocol, server_path, user, password)
+ if not ok then
+ return nil, "Failed downloading cache."
+ end
+
+ return local_cache, protocol, server_path, user, password
+end
+
+return cache
diff --git a/src/luarocks/admin/cmd/add.lua b/src/luarocks/admin/cmd/add.lua
new file mode 100644
index 0000000..aa444c5
--- /dev/null
+++ b/src/luarocks/admin/cmd/add.lua
@@ -0,0 +1,134 @@
+
+--- Module implementing the luarocks-admin "add" command.
+-- Adds a rock or rockspec to a rocks server.
+local add = {}
+
+local cfg = require("luarocks.core.cfg")
+local util = require("luarocks.util")
+local dir = require("luarocks.dir")
+local writer = require("luarocks.manif.writer")
+local fs = require("luarocks.fs")
+local cache = require("luarocks.admin.cache")
+local index = require("luarocks.admin.index")
+
+function add.add_to_parser(parser)
+ local cmd = parser:command("add", "Add a rock or rockspec to a rocks server.", util.see_also())
+
+ cmd:argument("rock", "A local rockspec or rock file.")
+ :args("+")
+
+ cmd:option("--server", "The server to use. If not given, the default server "..
+ "set in the upload_server variable from the configuration file is used instead.")
+ :target("add_server")
+ cmd:flag("--no-refresh", "Do not refresh the local cache prior to "..
+ "generation of the updated manifest.")
+ cmd:flag("--index", "Produce an index.html file for the manifest. This "..
+ "flag is automatically set if an index.html file already exists.")
+end
+
+local function zip_manifests()
+ for ver in util.lua_versions() do
+ local file = "manifest-"..ver
+ local zip = file..".zip"
+ fs.delete(dir.path(fs.current_dir(), zip))
+ fs.zip(zip, file)
+ end
+end
+
+local function add_files_to_server(refresh, rockfiles, server, upload_server, do_index)
+ assert(type(refresh) == "boolean" or not refresh)
+ assert(type(rockfiles) == "table")
+ assert(type(server) == "string")
+ assert(type(upload_server) == "table" or not upload_server)
+
+ local download_url, login_url = cache.get_server_urls(server, upload_server)
+ local at = fs.current_dir()
+ local refresh_fn = refresh and cache.refresh_local_cache or cache.split_server_url
+
+ local local_cache, protocol, server_path, user, password = refresh_fn(download_url, cfg.upload_user, cfg.upload_password)
+ if not local_cache then
+ return nil, protocol
+ end
+
+ if not login_url then
+ login_url = protocol.."://"..server_path
+ end
+
+ local ok, err = fs.change_dir(at)
+ if not ok then return nil, err end
+
+ local files = {}
+ for _, rockfile in ipairs(rockfiles) do
+ if fs.exists(rockfile) then
+ util.printout("Copying file "..rockfile.." to "..local_cache.."...")
+ local absolute = fs.absolute_name(rockfile)
+ fs.copy(absolute, local_cache, "read")
+ table.insert(files, dir.base_name(absolute))
+ else
+ util.printerr("File "..rockfile.." not found")
+ end
+ end
+ if #files == 0 then
+ return nil, "No files found"
+ end
+
+ local ok, err = fs.change_dir(local_cache)
+ if not ok then return nil, err end
+
+ util.printout("Updating manifest...")
+ writer.make_manifest(local_cache, "one", true)
+
+ zip_manifests()
+
+ if fs.exists("index.html") then
+ do_index = true
+ end
+
+ if do_index then
+ util.printout("Updating index.html...")
+ index.make_index(local_cache)
+ end
+
+ local login_info = ""
+ if user then login_info = " -u "..user end
+ if password then login_info = login_info..":"..password end
+ if not login_url:match("/$") then
+ login_url = login_url .. "/"
+ end
+
+ if do_index then
+ table.insert(files, "index.html")
+ end
+ table.insert(files, "manifest")
+ for ver in util.lua_versions() do
+ table.insert(files, "manifest-"..ver)
+ table.insert(files, "manifest-"..ver..".zip")
+ end
+
+ -- TODO abstract away explicit 'curl' call
+
+ local cmd
+ if protocol == "rsync" then
+ local srv, path = server_path:match("([^/]+)(/.+)")
+ cmd = cfg.variables.RSYNC.." "..cfg.variables.RSYNCFLAGS.." -e ssh "..local_cache.."/ "..user.."@"..srv..":"..path.."/"
+ elseif protocol == "file" then
+ return fs.copy_contents(local_cache, server_path)
+ elseif upload_server and upload_server.sftp then
+ local part1, part2 = upload_server.sftp:match("^([^/]*)/(.*)$")
+ cmd = cfg.variables.SCP.." "..table.concat(files, " ").." "..user.."@"..part1..":/"..part2
+ else
+ cmd = cfg.variables.CURL.." "..login_info.." -T '{"..table.concat(files, ",").."}' "..login_url
+ end
+
+ util.printout(cmd)
+ return fs.execute(cmd)
+end
+
+function add.command(args)
+ local server, server_table = cache.get_upload_server(args.add_server or args.server)
+ if not server then return nil, server_table end
+ return add_files_to_server(not args.no_refresh, args.rock, server, server_table, args.index)
+end
+
+
+return add
diff --git a/src/luarocks/admin/cmd/make_manifest.lua b/src/luarocks/admin/cmd/make_manifest.lua
new file mode 100644
index 0000000..18f74b5
--- /dev/null
+++ b/src/luarocks/admin/cmd/make_manifest.lua
@@ -0,0 +1,50 @@
+
+--- Module implementing the luarocks-admin "make_manifest" command.
+-- Compile a manifest file for a repository.
+local make_manifest = {}
+
+local writer = require("luarocks.manif.writer")
+local index = require("luarocks.admin.index")
+local cfg = require("luarocks.core.cfg")
+local util = require("luarocks.util")
+local deps = require("luarocks.deps")
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+
+function make_manifest.add_to_parser(parser)
+ local cmd = parser:command("make_manifest", "Compile a manifest file for a repository.", util.see_also())
+
+ cmd:argument("repository", "Local repository pathname.")
+ :args("?")
+
+ cmd:flag("--local-tree", "If given, do not write versioned versions of the manifest file.\n"..
+ "Use this when rebuilding the manifest of a local rocks tree.")
+ util.deps_mode_option(cmd)
+end
+
+--- Driver function for "make_manifest" command.
+-- @return boolean or (nil, string): True if manifest was generated,
+-- or nil and an error message.
+function make_manifest.command(args)
+ local repo = args.repository or cfg.rocks_dir
+
+ util.printout("Making manifest for "..repo)
+
+ if repo:match("/lib/luarocks") and not args.local_tree then
+ util.warning("This looks like a local rocks tree, but you did not pass --local-tree.")
+ end
+
+ local ok, err = writer.make_manifest(repo, deps.get_deps_mode(args), not args.local_tree)
+ if ok and not args.local_tree then
+ util.printout("Generating index.html for "..repo)
+ index.make_index(repo)
+ end
+ if args.local_tree then
+ for luaver in util.lua_versions() do
+ fs.delete(dir.path(repo, "manifest-"..luaver))
+ end
+ end
+ return ok, err
+end
+
+return make_manifest
diff --git a/src/luarocks/admin/cmd/refresh_cache.lua b/src/luarocks/admin/cmd/refresh_cache.lua
new file mode 100644
index 0000000..f8d5189
--- /dev/null
+++ b/src/luarocks/admin/cmd/refresh_cache.lua
@@ -0,0 +1,31 @@
+
+--- Module implementing the luarocks-admin "refresh_cache" command.
+local refresh_cache = {}
+
+local cfg = require("luarocks.core.cfg")
+local util = require("luarocks.util")
+local cache = require("luarocks.admin.cache")
+
+function refresh_cache.add_to_parser(parser)
+ local cmd = parser:command("refresh_cache", "Refresh local cache of a remote rocks server.", util.see_also())
+
+ cmd:option("--from", "The server to use. If not given, the default server "..
+ "set in the upload_server variable from the configuration file is used instead.")
+ :argname("<server>")
+end
+
+function refresh_cache.command(args)
+ local server, upload_server = cache.get_upload_server(args.server)
+ if not server then return nil, upload_server end
+ local download_url = cache.get_server_urls(server, upload_server)
+
+ local ok, err = cache.refresh_local_cache(download_url, cfg.upload_user, cfg.upload_password)
+ if not ok then
+ return nil, err
+ else
+ return true
+ end
+end
+
+
+return refresh_cache
diff --git a/src/luarocks/admin/cmd/remove.lua b/src/luarocks/admin/cmd/remove.lua
new file mode 100644
index 0000000..ed7644e
--- /dev/null
+++ b/src/luarocks/admin/cmd/remove.lua
@@ -0,0 +1,95 @@
+
+--- Module implementing the luarocks-admin "remove" command.
+-- Removes a rock or rockspec from a rocks server.
+local admin_remove = {}
+
+local cfg = require("luarocks.core.cfg")
+local util = require("luarocks.util")
+local dir = require("luarocks.dir")
+local writer = require("luarocks.manif.writer")
+local fs = require("luarocks.fs")
+local cache = require("luarocks.admin.cache")
+local index = require("luarocks.admin.index")
+
+function admin_remove.add_to_parser(parser)
+ local cmd = parser:command("remove", "Remove a rock or rockspec from a rocks server.", util.see_also())
+
+ cmd:argument("rock", "A local rockspec or rock file.")
+ :args("+")
+
+ cmd:option("--server", "The server to use. If not given, the default server "..
+ "set in the upload_server variable from the configuration file is used instead.")
+ cmd:flag("--no-refresh", "Do not refresh the local cache prior to "..
+ "generation of the updated manifest.")
+end
+
+local function remove_files_from_server(refresh, rockfiles, server, upload_server)
+ assert(type(refresh) == "boolean" or not refresh)
+ assert(type(rockfiles) == "table")
+ assert(type(server) == "string")
+ assert(type(upload_server) == "table" or not upload_server)
+
+ local download_url, login_url = cache.get_server_urls(server, upload_server)
+ local at = fs.current_dir()
+ local refresh_fn = refresh and cache.refresh_local_cache or cache.split_server_url
+
+ local local_cache, protocol, server_path, user, password = refresh_fn(download_url, cfg.upload_user, cfg.upload_password)
+ if not local_cache then
+ return nil, protocol
+ end
+
+ local ok, err = fs.change_dir(at)
+ if not ok then return nil, err end
+
+ local nr_files = 0
+ for _, rockfile in ipairs(rockfiles) do
+ local basename = dir.base_name(rockfile)
+ local file = dir.path(local_cache, basename)
+ util.printout("Removing file "..file.."...")
+ fs.delete(file)
+ if not fs.exists(file) then
+ nr_files = nr_files + 1
+ else
+ util.printerr("Failed removing "..file)
+ end
+ end
+ if nr_files == 0 then
+ return nil, "No files removed."
+ end
+
+ local ok, err = fs.change_dir(local_cache)
+ if not ok then return nil, err end
+
+ util.printout("Updating manifest...")
+ writer.make_manifest(local_cache, "one", true)
+ util.printout("Updating index.html...")
+ index.make_index(local_cache)
+
+ if protocol == "file" then
+ local cmd = cfg.variables.RSYNC.." "..cfg.variables.RSYNCFLAGS.." --delete "..local_cache.."/ ".. server_path.."/"
+ util.printout(cmd)
+ fs.execute(cmd)
+ return true
+ end
+
+ if protocol ~= "rsync" then
+ return nil, "This command requires 'rsync', check your configuration."
+ end
+
+ local srv, path = server_path:match("([^/]+)(/.+)")
+ local cmd = cfg.variables.RSYNC.." "..cfg.variables.RSYNCFLAGS.." --delete -e ssh "..local_cache.."/ "..user.."@"..srv..":"..path.."/"
+
+ util.printout(cmd)
+ fs.execute(cmd)
+
+ return true
+end
+
+function admin_remove.command(args)
+ local server, server_table = cache.get_upload_server(args.server)
+ if not server then return nil, server_table end
+ return remove_files_from_server(not args.no_refresh, args.rock, server, server_table)
+end
+
+
+return admin_remove
diff --git a/src/luarocks/admin/index.lua b/src/luarocks/admin/index.lua
new file mode 100644
index 0000000..64c8c1e
--- /dev/null
+++ b/src/luarocks/admin/index.lua
@@ -0,0 +1,185 @@
+
+--- Module which builds the index.html page to be used in rocks servers.
+local index = {}
+
+local util = require("luarocks.util")
+local fs = require("luarocks.fs")
+local vers = require("luarocks.core.vers")
+local persist = require("luarocks.persist")
+local dir = require("luarocks.dir")
+local manif = require("luarocks.manif")
+
+local ext_url_target = ' target="_blank"'
+
+local index_header = [[
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+<title>Available rocks</title>
+<meta http-equiv="content-type" content="text/html; charset=iso-8859-1">
+<style>
+body {
+ background-color: white;
+ font-family: "bitstream vera sans", "verdana", "sans";
+ font-size: 14px;
+}
+a {
+ color: #0000c0;
+ text-decoration: none;
+}
+a.pkg {
+ color: black;
+}
+a:hover {
+ text-decoration: underline;
+}
+td.main {
+ border-style: none;
+}
+blockquote {
+ font-size: 12px;
+}
+td.package {
+ background-color: #f0f0f0;
+ vertical-align: top;
+}
+td.spacer {
+ height: 5px;
+}
+td.version {
+ background-color: #d0d0d0;
+ vertical-align: top;
+ text-align: left;
+ padding: 5px;
+ width: 100px;
+}
+p.manifest {
+ font-size: 8px;
+}
+</style>
+</head>
+<body>
+<h1>Available rocks</h1>
+<p>
+Lua modules available from this location for use with <a href="http://www.luarocks.org">LuaRocks</a>:
+</p>
+<table class="main">
+]]
+
+local index_package_begin = [[
+<td class="package">
+<p><a name="$anchor"></a><a href="#$anchor" class="pkg"><b>$package</b></a> - $summary<br/>
+</p><blockquote><p>$detailed<br/>
+$externaldependencies
+<font size="-1"><a href="$original">latest sources</a> $homepage | License: $license</font></p>
+</blockquote></a></td>
+<td class="version">
+]]
+
+local index_package_end = [[
+</td></tr>
+<tr><td colspan="2" class="spacer"></td></tr>
+]]
+
+local index_footer_begin = [[
+</table>
+<p class="manifest">
+<a href="manifest">manifest file</a>
+]]
+local index_manifest_ver = [[
+&bull; <a href="manifest-$VER">Lua $VER manifest file</a> (<a href="manifest-$VER.zip">zip</a>)
+]]
+local index_footer_end = [[
+</p>
+</body>
+</html>
+]]
+
+function index.format_external_dependencies(rockspec)
+ if rockspec.external_dependencies then
+ local deplist = {}
+ local listed_set = {}
+ local plats = nil
+ for name, desc in util.sortedpairs(rockspec.external_dependencies) do
+ if name ~= "platforms" then
+ table.insert(deplist, name:lower())
+ listed_set[name] = true
+ else
+ plats = desc
+ end
+ end
+ if plats then
+ for plat, entries in util.sortedpairs(plats) do
+ for name, desc in util.sortedpairs(entries) do
+ if not listed_set[name] then
+ table.insert(deplist, name:lower() .. " (on "..plat..")")
+ end
+ end
+ end
+ end
+ return '<p><b>External dependencies:</b> ' .. table.concat(deplist, ',&nbsp;').. '</p>'
+ else
+ return ""
+ end
+end
+
+function index.make_index(repo)
+ if not fs.is_dir(repo) then
+ return nil, "Cannot access repository at "..repo
+ end
+ local manifest = manif.load_manifest(repo)
+ local out = io.open(dir.path(repo, "index.html"), "w")
+
+ out:write(index_header)
+ for package, version_list in util.sortedpairs(manifest.repository) do
+ local latest_rockspec = nil
+ local output = index_package_begin
+ for version, data in util.sortedpairs(version_list, vers.compare_versions) do
+ local versions = {}
+ output = output..version..':&nbsp;'
+ table.sort(data, function(a,b) return a.arch < b.arch end)
+ for _, item in ipairs(data) do
+ local file
+ if item.arch == 'rockspec' then
+ file = ("%s-%s.rockspec"):format(package, version)
+ if not latest_rockspec then latest_rockspec = file end
+ else
+ file = ("%s-%s.%s.rock"):format(package, version, item.arch)
+ end
+ table.insert(versions, '<a href="'..file..'">'..item.arch..'</a>')
+ end
+ output = output .. table.concat(versions, ',&nbsp;') .. '<br/>'
+ end
+ output = output .. index_package_end
+ if latest_rockspec then
+ local rockspec = persist.load_into_table(dir.path(repo, latest_rockspec))
+ local descript = rockspec.description or {}
+ local vars = {
+ anchor = package,
+ package = rockspec.package,
+ original = rockspec.source.url,
+ summary = descript.summary or "",
+ detailed = descript.detailed or "",
+ license = descript.license or "N/A",
+ homepage = descript.homepage and ('| <a href="'..descript.homepage..'"'..ext_url_target..'>project homepage</a>') or "",
+ externaldependencies = index.format_external_dependencies(rockspec)
+ }
+ vars.detailed = vars.detailed:gsub("\n\n", "</p><p>"):gsub("%s+", " ")
+ vars.detailed = vars.detailed:gsub("(https?://[a-zA-Z0-9%.%%-_%+%[%]=%?&/$@;:]+)", '<a href="%1"'..ext_url_target..'>%1</a>')
+ output = output:gsub("$(%w+)", vars)
+ else
+ output = output:gsub("$anchor", package)
+ output = output:gsub("$package", package)
+ output = output:gsub("$(%w+)", "")
+ end
+ out:write(output)
+ end
+ out:write(index_footer_begin)
+ for ver in util.lua_versions() do
+ out:write((index_manifest_ver:gsub("$VER", ver)))
+ end
+ out:write(index_footer_end)
+ out:close()
+end
+
+return index
diff --git a/src/luarocks/build.lua b/src/luarocks/build.lua
new file mode 100644
index 0000000..c6b3388
--- /dev/null
+++ b/src/luarocks/build.lua
@@ -0,0 +1,495 @@
+
+local build = {}
+
+local path = require("luarocks.path")
+local util = require("luarocks.util")
+local fun = require("luarocks.fun")
+local fetch = require("luarocks.fetch")
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+local deps = require("luarocks.deps")
+local cfg = require("luarocks.core.cfg")
+local vers = require("luarocks.core.vers")
+local repos = require("luarocks.repos")
+local writer = require("luarocks.manif.writer")
+local deplocks = require("luarocks.deplocks")
+
+build.opts = util.opts_table("build.opts", {
+ need_to_fetch = "boolean",
+ minimal_mode = "boolean",
+ deps_mode = "string",
+ build_only_deps = "boolean",
+ namespace = "string?",
+ branch = "string?",
+ verify = "boolean",
+ check_lua_versions = "boolean",
+ pin = "boolean",
+ rebuild = "boolean",
+ no_install = "boolean"
+})
+
+do
+ --- Write to the current directory the contents of a table,
+ -- where each key is a file name and its value is the file content.
+ -- @param files table: The table of files to be written.
+ local function extract_from_rockspec(files)
+ for name, content in pairs(files) do
+ local fd = io.open(dir.path(fs.current_dir(), name), "w+")
+ fd:write(content)
+ fd:close()
+ end
+ end
+
+ --- Applies patches inlined in the build.patches section
+ -- and extracts files inlined in the build.extra_files section
+ -- of a rockspec.
+ -- @param rockspec table: A rockspec table.
+ -- @return boolean or (nil, string): True if succeeded or
+ -- nil and an error message.
+ function build.apply_patches(rockspec)
+ assert(rockspec:type() == "rockspec")
+
+ if not (rockspec.build.extra_files or rockspec.build.patches) then
+ return true
+ end
+
+ local fd = io.open(fs.absolute_name(".luarocks.patches.applied"), "r")
+ if fd then
+ fd:close()
+ return true
+ end
+
+ if rockspec.build.extra_files then
+ extract_from_rockspec(rockspec.build.extra_files)
+ end
+ if rockspec.build.patches then
+ extract_from_rockspec(rockspec.build.patches)
+ for patch, patchdata in util.sortedpairs(rockspec.build.patches) do
+ util.printout("Applying patch "..patch.."...")
+ local create_delete = rockspec:format_is_at_least("3.0")
+ local ok, err = fs.apply_patch(tostring(patch), patchdata, create_delete)
+ if not ok then
+ return nil, "Failed applying patch "..patch
+ end
+ end
+ end
+
+ fd = io.open(fs.absolute_name(".luarocks.patches.applied"), "w")
+ if fd then
+ fd:close()
+ end
+ return true
+ end
+end
+
+local function check_macosx_deployment_target(rockspec)
+ local target = rockspec.build.macosx_deployment_target
+ local function patch_variable(var)
+ if rockspec.variables[var]:match("MACOSX_DEPLOYMENT_TARGET") then
+ rockspec.variables[var] = (rockspec.variables[var]):gsub("MACOSX_DEPLOYMENT_TARGET=[^ ]*", "MACOSX_DEPLOYMENT_TARGET="..target)
+ else
+ rockspec.variables[var] = "env MACOSX_DEPLOYMENT_TARGET="..target.." "..rockspec.variables[var]
+ end
+ end
+ if cfg.is_platform("macosx") and rockspec:format_is_at_least("3.0") and target then
+ local version = util.popen_read("sw_vers -productVersion")
+ if version:match("^%d+%.%d+%.%d+$") or version:match("^%d+%.%d+$") then
+ if vers.compare_versions(target, version) then
+ return nil, ("This rock requires Mac OSX %s, and you are running %s."):format(target, version)
+ end
+ end
+ patch_variable("CC")
+ patch_variable("LD")
+ end
+ return true
+end
+
+local function process_dependencies(rockspec, opts)
+ if not opts.build_only_deps then
+ local ok, err, errcode = deps.check_external_deps(rockspec, "build")
+ if err then
+ return nil, err, errcode
+ end
+ end
+
+ if opts.deps_mode == "none" then
+ return true
+ end
+
+ if not opts.build_only_deps then
+ if next(rockspec.build_dependencies) then
+
+ local user_lua_version = cfg.lua_version
+ local running_lua_version = _VERSION:sub(5)
+
+ if running_lua_version ~= user_lua_version then
+ -- Temporarily flip the user-selected Lua version,
+ -- so that we install build dependencies for the
+ -- Lua version on which the LuaRocks program is running.
+
+ -- HACK: we have to do this by flipping a bunch of
+ -- global config settings, and this list may not be complete.
+ cfg.lua_version = running_lua_version
+ cfg.lua_modules_path = cfg.lua_modules_path:gsub(user_lua_version:gsub("%.", "%%."), running_lua_version)
+ cfg.lib_modules_path = cfg.lib_modules_path:gsub(user_lua_version:gsub("%.", "%%."), running_lua_version)
+ cfg.rocks_subdir = cfg.rocks_subdir:gsub(user_lua_version:gsub("%.", "%%."), running_lua_version)
+ path.use_tree(cfg.root_dir)
+ end
+
+ local ok, err, errcode = deps.fulfill_dependencies(rockspec, "build_dependencies", "all", opts.verify)
+
+ path.add_to_package_paths(cfg.root_dir)
+
+ if running_lua_version ~= user_lua_version then
+ -- flip the settings back
+ cfg.lua_version = user_lua_version
+ cfg.lua_modules_path = cfg.lua_modules_path:gsub(running_lua_version:gsub("%.", "%%."), user_lua_version)
+ cfg.lib_modules_path = cfg.lib_modules_path:gsub(running_lua_version:gsub("%.", "%%."), user_lua_version)
+ cfg.rocks_subdir = cfg.rocks_subdir:gsub(running_lua_version:gsub("%.", "%%."), user_lua_version)
+ path.use_tree(cfg.root_dir)
+ end
+
+ if err then
+ return nil, err, errcode
+ end
+ end
+ end
+
+ return deps.fulfill_dependencies(rockspec, "dependencies", opts.deps_mode, opts.verify)
+end
+
+local function fetch_and_change_to_source_dir(rockspec, opts)
+ if opts.minimal_mode or opts.build_only_deps then
+ return true
+ end
+ if opts.need_to_fetch then
+ if opts.branch then
+ rockspec.source.branch = opts.branch
+ end
+ local ok, source_dir, errcode = fetch.fetch_sources(rockspec, true)
+ if not ok then
+ return nil, source_dir, errcode
+ end
+ local err
+ ok, err = fs.change_dir(source_dir)
+ if not ok then
+ return nil, err
+ end
+ else
+ if rockspec.source.file then
+ local ok, err = fs.unpack_archive(rockspec.source.file)
+ if not ok then
+ return nil, err
+ end
+ end
+ local ok, err = fetch.find_rockspec_source_dir(rockspec, ".")
+ if not ok then
+ return nil, err
+ end
+ end
+ fs.change_dir(rockspec.source.dir)
+ return true
+end
+
+local function prepare_install_dirs(name, version)
+ local dirs = {
+ lua = { name = path.lua_dir(name, version), is_module_path = true, perms = "read" },
+ lib = { name = path.lib_dir(name, version), is_module_path = true, perms = "exec" },
+ bin = { name = path.bin_dir(name, version), is_module_path = false, perms = "exec" },
+ conf = { name = path.conf_dir(name, version), is_module_path = false, perms = "read" },
+ }
+
+ for _, d in pairs(dirs) do
+ local ok, err = fs.make_dir(d.name)
+ if not ok then
+ return nil, err
+ end
+ end
+
+ return dirs
+end
+
+local function run_build_driver(rockspec, no_install)
+ local btype = rockspec.build.type
+ if btype == "none" then
+ return true
+ end
+ -- Temporary compatibility
+ if btype == "module" then
+ util.printout("Do not use 'module' as a build type. Use 'builtin' instead.")
+ btype = "builtin"
+ rockspec.build.type = btype
+ end
+ if cfg.accepted_build_types and not fun.contains(cfg.accepted_build_types, btype) then
+ return nil, "This rockspec uses the '"..btype.."' build type, which is blocked by the 'accepted_build_types' setting in your LuaRocks configuration."
+ end
+ local pok, driver = pcall(require, "luarocks.build." .. btype)
+ if not pok or type(driver) ~= "table" then
+ return nil, "Failed initializing build back-end for build type '"..btype.."': "..driver
+ end
+
+ if not driver.skip_lua_inc_lib_check then
+ local ok, err, errcode = deps.check_lua_incdir(rockspec.variables)
+ if not ok then
+ return nil, err, errcode
+ end
+
+ if cfg.link_lua_explicitly then
+ local ok, err, errcode = deps.check_lua_libdir(rockspec.variables)
+ if not ok then
+ return nil, err, errcode
+ end
+ end
+ end
+
+ local ok, err = driver.run(rockspec, no_install)
+ if not ok then
+ return nil, "Build error: " .. err
+ end
+ return true
+end
+
+local install_files
+do
+ --- Install files to a given location.
+ -- Takes a table where the array part is a list of filenames to be copied.
+ -- In the hash part, other keys, if is_module_path is set, are identifiers
+ -- in Lua module format, to indicate which subdirectory the file should be
+ -- copied to. For example, install_files({["foo.bar"] = "src/bar.lua"}, "boo")
+ -- will copy src/bar.lua to boo/foo.
+ -- @param files table or nil: A table containing a list of files to copy in
+ -- the format described above. If nil is passed, this function is a no-op.
+ -- Directories should be delimited by forward slashes as in internet URLs.
+ -- @param location string: The base directory files should be copied to.
+ -- @param is_module_path boolean: True if string keys in files should be
+ -- interpreted as dotted module paths.
+ -- @param perms string ("read" or "exec"): Permissions of the newly created
+ -- files installed.
+ -- Directories are always created with the default permissions.
+ -- @return boolean or (nil, string): True if succeeded or
+ -- nil and an error message.
+ local function install_to(files, location, is_module_path, perms)
+ assert(type(files) == "table" or not files)
+ assert(type(location) == "string")
+ if not files then
+ return true
+ end
+ for k, file in pairs(files) do
+ local dest = location
+ local filename = dir.base_name(file)
+ if type(k) == "string" then
+ local modname = k
+ if is_module_path then
+ dest = dir.path(location, path.module_to_path(modname))
+ local ok, err = fs.make_dir(dest)
+ if not ok then return nil, err end
+ if filename:match("%.lua$") then
+ local basename = modname:match("([^.]+)$")
+ filename = basename..".lua"
+ end
+ else
+ dest = dir.path(location, dir.dir_name(modname))
+ local ok, err = fs.make_dir(dest)
+ if not ok then return nil, err end
+ filename = dir.base_name(modname)
+ end
+ else
+ local ok, err = fs.make_dir(dest)
+ if not ok then return nil, err end
+ end
+ local ok = fs.copy(file, dir.path(dest, filename), perms)
+ if not ok then
+ return nil, "Failed copying "..file
+ end
+ end
+ return true
+ end
+
+ local function install_default_docs(name, version)
+ local patterns = { "readme", "license", "copying", ".*%.md" }
+ local dest = dir.path(path.install_dir(name, version), "doc")
+ local has_dir = false
+ for file in fs.dir() do
+ for _, pattern in ipairs(patterns) do
+ if file:lower():match("^"..pattern) then
+ if not has_dir then
+ fs.make_dir(dest)
+ has_dir = true
+ end
+ fs.copy(file, dest, "read")
+ break
+ end
+ end
+ end
+ end
+
+ install_files = function(rockspec, dirs)
+ local name, version = rockspec.name, rockspec.version
+
+ if rockspec.build.install then
+ for k, d in pairs(dirs) do
+ local ok, err = install_to(rockspec.build.install[k], d.name, d.is_module_path, d.perms)
+ if not ok then return nil, err end
+ end
+ end
+
+ local copy_directories = rockspec.build.copy_directories
+ local copying_default = false
+ if not copy_directories then
+ copy_directories = {"doc"}
+ copying_default = true
+ end
+
+ local any_docs = false
+ for _, copy_dir in pairs(copy_directories) do
+ if fs.is_dir(copy_dir) then
+ local dest = dir.path(path.install_dir(name, version), copy_dir)
+ fs.make_dir(dest)
+ fs.copy_contents(copy_dir, dest)
+ any_docs = true
+ else
+ if not copying_default then
+ return nil, "Directory '"..copy_dir.."' not found"
+ end
+ end
+ end
+ if not any_docs then
+ install_default_docs(name, version)
+ end
+
+ return true
+ end
+end
+
+local function write_rock_dir_files(rockspec, opts)
+ local name, version = rockspec.name, rockspec.version
+
+ fs.copy(rockspec.local_abs_filename, path.rockspec_file(name, version), "read")
+
+ local deplock_file = deplocks.get_abs_filename(rockspec.name)
+ if deplock_file then
+ fs.copy(deplock_file, dir.path(path.install_dir(name, version), "luarocks.lock"), "read")
+ end
+
+ local ok, err = writer.make_rock_manifest(name, version)
+ if not ok then return nil, err end
+
+ ok, err = writer.make_namespace_file(name, version, opts.namespace)
+ if not ok then return nil, err end
+
+ return true
+end
+
+--- Build and install a rock given a rockspec.
+-- @param opts table: build options table
+-- @return (string, string) or (nil, string, [string]): Name and version of
+-- installed rock if succeeded or nil and an error message followed by an error code.
+function build.build_rockspec(rockspec, opts)
+ assert(rockspec:type() == "rockspec")
+ assert(opts:type() == "build.opts")
+
+ if not rockspec.build then
+ if rockspec:format_is_at_least("3.0") then
+ rockspec.build = {
+ type = "builtin"
+ }
+ else
+ return nil, "Rockspec error: build table not specified"
+ end
+ end
+
+ if not rockspec.build.type then
+ if rockspec:format_is_at_least("3.0") then
+ rockspec.build.type = "builtin"
+ else
+ return nil, "Rockspec error: build type not specified"
+ end
+ end
+
+ local ok, err = fetch_and_change_to_source_dir(rockspec, opts)
+ if not ok then return nil, err end
+
+ if opts.pin then
+ deplocks.init(rockspec.name, ".")
+ end
+
+ ok, err = process_dependencies(rockspec, opts)
+ if not ok then return nil, err end
+
+ local name, version = rockspec.name, rockspec.version
+ if opts.build_only_deps then
+ if opts.pin then
+ deplocks.write_file()
+ end
+ return name, version
+ end
+
+ local dirs, err
+ local rollback
+ if not opts.no_install then
+ if repos.is_installed(name, version) then
+ repos.delete_version(name, version, opts.deps_mode)
+ end
+
+ dirs, err = prepare_install_dirs(name, version)
+ if not dirs then return nil, err end
+
+ rollback = util.schedule_function(function()
+ fs.delete(path.install_dir(name, version))
+ fs.remove_dir_if_empty(path.versions_dir(name))
+ end)
+ end
+
+ ok, err = build.apply_patches(rockspec)
+ if not ok then return nil, err end
+
+ ok, err = check_macosx_deployment_target(rockspec)
+ if not ok then return nil, err end
+
+ ok, err = run_build_driver(rockspec, opts.no_install)
+ if not ok then return nil, err end
+
+ if opts.no_install then
+ fs.pop_dir()
+ if opts.need_to_fetch then
+ fs.pop_dir()
+ end
+ return name, version
+ end
+
+ ok, err = install_files(rockspec, dirs)
+ if not ok then return nil, err end
+
+ for _, d in pairs(dirs) do
+ fs.remove_dir_if_empty(d.name)
+ end
+
+ fs.pop_dir()
+ if opts.need_to_fetch then
+ fs.pop_dir()
+ end
+
+ if opts.pin then
+ deplocks.write_file()
+ end
+
+ ok, err = write_rock_dir_files(rockspec, opts)
+ if not ok then return nil, err end
+
+ ok, err = repos.deploy_files(name, version, repos.should_wrap_bin_scripts(rockspec), opts.deps_mode)
+ if not ok then return nil, err end
+
+ util.remove_scheduled_function(rollback)
+ rollback = util.schedule_function(function()
+ repos.delete_version(name, version, opts.deps_mode)
+ end)
+
+ ok, err = repos.run_hook(rockspec, "post_install")
+ if not ok then return nil, err end
+
+ util.announce_install(rockspec)
+ util.remove_scheduled_function(rollback)
+ return name, version
+end
+
+return build
diff --git a/src/luarocks/build/builtin.lua b/src/luarocks/build/builtin.lua
new file mode 100644
index 0000000..4c15d2b
--- /dev/null
+++ b/src/luarocks/build/builtin.lua
@@ -0,0 +1,395 @@
+
+--- A builtin build system: back-end to provide a portable way of building C-based Lua modules.
+local builtin = {}
+
+-- This build driver checks LUA_INCDIR and LUA_LIBDIR on demand,
+-- so that pure-Lua rocks don't need to have development headers
+-- installed.
+builtin.skip_lua_inc_lib_check = true
+
+local unpack = unpack or table.unpack
+local dir_sep = package.config:sub(1, 1)
+
+local fs = require("luarocks.fs")
+local path = require("luarocks.path")
+local util = require("luarocks.util")
+local cfg = require("luarocks.core.cfg")
+local dir = require("luarocks.dir")
+local deps = require("luarocks.deps")
+
+local function autoextract_libs(external_dependencies, variables)
+ if not external_dependencies then
+ return nil, nil, nil
+ end
+ local libs = {}
+ local incdirs = {}
+ local libdirs = {}
+ for name, data in pairs(external_dependencies) do
+ if data.library then
+ table.insert(libs, data.library)
+ table.insert(incdirs, variables[name .. "_INCDIR"])
+ table.insert(libdirs, variables[name .. "_LIBDIR"])
+ end
+ end
+ return libs, incdirs, libdirs
+end
+
+do
+ local function get_cmod_name(file)
+ local fd = io.open(dir.path(fs.current_dir(), file), "r")
+ if not fd then return nil end
+ local data = fd:read("*a")
+ fd:close()
+ return (data:match("int%s+luaopen_([a-zA-Z0-9_]+)"))
+ end
+
+ local skiplist = {
+ ["spec"] = true,
+ [".luarocks"] = true,
+ ["lua_modules"] = true,
+ ["test.lua"] = true,
+ ["tests.lua"] = true,
+ }
+
+ function builtin.autodetect_modules(libs, incdirs, libdirs)
+ local modules = {}
+ local install
+ local copy_directories
+
+ local prefix = ""
+ for _, parent in ipairs({"src", "lua", "lib"}) do
+ if fs.is_dir(parent) then
+ fs.change_dir(parent)
+ prefix = parent .. dir_sep
+ break
+ end
+ end
+
+ for _, file in ipairs(fs.find()) do
+ local base = file:match("^([^\\/]*)")
+ if not skiplist[base] then
+ local luamod = file:match("(.*)%.lua$")
+ if luamod then
+ modules[path.path_to_module(file)] = prefix .. file
+ else
+ local cmod = file:match("(.*)%.c$")
+ if cmod then
+ local modname = get_cmod_name(file) or path.path_to_module(file:gsub("%.c$", ".lua"))
+ modules[modname] = {
+ sources = prefix..file,
+ libraries = libs,
+ incdirs = incdirs,
+ libdirs = libdirs,
+ }
+ end
+ end
+ end
+ end
+
+ if prefix ~= "" then
+ fs.pop_dir()
+ end
+
+ local bindir = (fs.is_dir(dir.path("src", "bin")) and dir.path("src", "bin"))
+ or (fs.is_dir("bin") and "bin")
+ if bindir then
+ install = { bin = {} }
+ for _, file in ipairs(fs.list_dir(bindir)) do
+ table.insert(install.bin, dir.path(bindir, file))
+ end
+ end
+
+ for _, directory in ipairs({ "doc", "docs", "samples", "tests" }) do
+ if fs.is_dir(directory) then
+ if not copy_directories then
+ copy_directories = {}
+ end
+ table.insert(copy_directories, directory)
+ end
+ end
+
+ return modules, install, copy_directories
+ end
+end
+
+--- Run a command displaying its execution on standard output.
+-- @return boolean: true if command succeeds (status code 0), false
+-- otherwise.
+local function execute(...)
+ io.stdout:write(table.concat({...}, " ").."\n")
+ return fs.execute(...)
+end
+
+--- Driver function for the builtin build back-end.
+-- @param rockspec table: the loaded rockspec.
+-- @return boolean or (nil, string): true if no errors occurred,
+-- nil and an error message otherwise.
+function builtin.run(rockspec, no_install)
+ assert(rockspec:type() == "rockspec")
+ local compile_object, compile_library, compile_static_library
+
+ local build = rockspec.build
+ local variables = rockspec.variables
+ local checked_lua_h = false
+
+ for _, var in ipairs{ "CC", "CFLAGS", "LDFLAGS" } do
+ variables[var] = variables[var] or os.getenv(var) or ""
+ end
+
+ local function add_flags(extras, flag, flags)
+ if flags then
+ if type(flags) ~= "table" then
+ flags = { tostring(flags) }
+ end
+ util.variable_substitutions(flags, variables)
+ for _, v in ipairs(flags) do
+ table.insert(extras, flag:format(v))
+ end
+ end
+ end
+
+ if cfg.is_platform("mingw32") then
+ compile_object = function(object, source, defines, incdirs)
+ local extras = {}
+ add_flags(extras, "-D%s", defines)
+ add_flags(extras, "-I%s", incdirs)
+ return execute(variables.CC.." "..variables.CFLAGS, "-c", "-o", object, "-I"..variables.LUA_INCDIR, source, unpack(extras))
+ end
+ compile_library = function(library, objects, libraries, libdirs, name)
+ local extras = { unpack(objects) }
+ add_flags(extras, "-L%s", libdirs)
+ add_flags(extras, "-l%s", libraries)
+ extras[#extras+1] = dir.path(variables.LUA_LIBDIR, variables.LUALIB)
+
+ if variables.CC == "clang" or variables.CC == "clang-cl" then
+ local exported_name = name:gsub("%.", "_")
+ exported_name = exported_name:match('^[^%-]+%-(.+)$') or exported_name
+ extras[#extras+1] = string.format("-Wl,-export:luaopen_%s", exported_name)
+ else
+ extras[#extras+1] = "-l" .. (variables.MSVCRT or "m")
+ end
+
+ local ok = execute(variables.LD.." "..variables.LDFLAGS.." "..variables.LIBFLAG, "-o", library, unpack(extras))
+ return ok
+ end
+ --[[ TODO disable static libs until we fix the conflict in the manifest, which will take extending the manifest format.
+ compile_static_library = function(library, objects, libraries, libdirs, name)
+ local ok = execute(variables.AR, "rc", library, unpack(objects))
+ if ok then
+ ok = execute(variables.RANLIB, library)
+ end
+ return ok
+ end
+ ]]
+ elseif cfg.is_platform("win32") then
+ compile_object = function(object, source, defines, incdirs)
+ local extras = {}
+ add_flags(extras, "-D%s", defines)
+ add_flags(extras, "-I%s", incdirs)
+ return execute(variables.CC.." "..variables.CFLAGS, "-c", "-Fo"..object, "-I"..variables.LUA_INCDIR, source, unpack(extras))
+ end
+ compile_library = function(library, objects, libraries, libdirs, name)
+ local extras = { unpack(objects) }
+ add_flags(extras, "-libpath:%s", libdirs)
+ add_flags(extras, "%s.lib", libraries)
+ local basename = dir.base_name(library):gsub(".[^.]*$", "")
+ local deffile = basename .. ".def"
+ local def = io.open(dir.path(fs.current_dir(), deffile), "w+")
+ local exported_name = name:gsub("%.", "_")
+ exported_name = exported_name:match('^[^%-]+%-(.+)$') or exported_name
+ def:write("EXPORTS\n")
+ def:write("luaopen_"..exported_name.."\n")
+ def:close()
+ local ok = execute(variables.LD, "-dll", "-def:"..deffile, "-out:"..library, dir.path(variables.LUA_LIBDIR, variables.LUALIB), unpack(extras))
+ local basedir = ""
+ if name:find("%.") ~= nil then
+ basedir = name:gsub("%.%w+$", "\\")
+ basedir = basedir:gsub("%.", "\\")
+ end
+ local manifestfile = basedir .. basename..".dll.manifest"
+
+ if ok and fs.exists(manifestfile) then
+ ok = execute(variables.MT, "-manifest", manifestfile, "-outputresource:"..basedir..basename..".dll;2")
+ end
+ return ok
+ end
+ --[[ TODO disable static libs until we fix the conflict in the manifest, which will take extending the manifest format.
+ compile_static_library = function(library, objects, libraries, libdirs, name)
+ local ok = execute(variables.AR, "-out:"..library, unpack(objects))
+ return ok
+ end
+ ]]
+ else
+ compile_object = function(object, source, defines, incdirs)
+ local extras = {}
+ add_flags(extras, "-D%s", defines)
+ add_flags(extras, "-I%s", incdirs)
+ return execute(variables.CC.." "..variables.CFLAGS, "-I"..variables.LUA_INCDIR, "-c", source, "-o", object, unpack(extras))
+ end
+ compile_library = function (library, objects, libraries, libdirs)
+ local extras = { unpack(objects) }
+ add_flags(extras, "-L%s", libdirs)
+ if cfg.gcc_rpath then
+ add_flags(extras, "-Wl,-rpath,%s", libdirs)
+ end
+ add_flags(extras, "-l%s", libraries)
+ if cfg.link_lua_explicitly then
+ extras[#extras+1] = "-L"..variables.LUA_LIBDIR
+ extras[#extras+1] = "-llua"
+ end
+ return execute(variables.LD.." "..variables.LDFLAGS.." "..variables.LIBFLAG, "-o", library, unpack(extras))
+ end
+ compile_static_library = function(library, objects, libraries, libdirs, name) -- luacheck: ignore 211
+ local ok = execute(variables.AR, "rc", library, unpack(objects))
+ if ok then
+ ok = execute(variables.RANLIB, library)
+ end
+ return ok
+ end
+ end
+
+ local ok, err
+ local lua_modules = {}
+ local lib_modules = {}
+ local luadir = path.lua_dir(rockspec.name, rockspec.version)
+ local libdir = path.lib_dir(rockspec.name, rockspec.version)
+
+ local autolibs, autoincdirs, autolibdirs = autoextract_libs(rockspec.external_dependencies, rockspec.variables)
+
+ if not build.modules then
+ if rockspec:format_is_at_least("3.0") then
+ local install, copy_directories
+ build.modules, install, copy_directories = builtin.autodetect_modules(autolibs, autoincdirs, autolibdirs)
+ build.install = build.install or install
+ build.copy_directories = build.copy_directories or copy_directories
+ else
+ return nil, "Missing build.modules table"
+ end
+ end
+
+ local compile_temp_dir
+
+ local mkdir_cache = {}
+ local function cached_make_dir(name)
+ if name == "" or mkdir_cache[name] then
+ return true
+ end
+ mkdir_cache[name] = true
+ return fs.make_dir(name)
+ end
+
+ for name, info in pairs(build.modules) do
+ local moddir = path.module_to_path(name)
+ if type(info) == "string" then
+ local ext = info:match("%.([^.]+)$")
+ if ext == "lua" then
+ local filename = dir.base_name(info)
+ if filename == "init.lua" and not name:match("%.init$") then
+ moddir = path.module_to_path(name..".init")
+ else
+ local basename = name:match("([^.]+)$")
+ filename = basename..".lua"
+ end
+ local dest = dir.path(luadir, moddir, filename)
+ lua_modules[info] = dest
+ else
+ info = {info}
+ end
+ end
+ if type(info) == "table" then
+ if not checked_lua_h then
+ local ok, err, errcode = deps.check_lua_incdir(rockspec.variables)
+ if not ok then
+ return nil, err, errcode
+ end
+
+ if cfg.link_lua_explicitly then
+ local ok, err, errcode = deps.check_lua_libdir(rockspec.variables)
+ if not ok then
+ return nil, err, errcode
+ end
+ end
+ checked_lua_h = true
+ end
+ local objects = {}
+ local sources = info.sources
+ if info[1] then sources = info end
+ if type(sources) == "string" then sources = {sources} end
+ if type(sources) ~= "table" then
+ return nil, "error in rockspec: module '" .. name .. "' entry has no 'sources' list"
+ end
+ for _, source in ipairs(sources) do
+ if type(source) ~= "string" then
+ return nil, "error in rockspec: module '" .. name .. "' does not specify source correctly."
+ end
+ local object = source:gsub("%.[^.]*$", "."..cfg.obj_extension)
+ if not object then
+ object = source.."."..cfg.obj_extension
+ end
+ ok = compile_object(object, source, info.defines, info.incdirs or autoincdirs)
+ if not ok then
+ return nil, "Failed compiling object "..object
+ end
+ table.insert(objects, object)
+ end
+
+ if not compile_temp_dir then
+ compile_temp_dir = fs.make_temp_dir("build-" .. rockspec.package .. "-" .. rockspec.version)
+ util.schedule_function(fs.delete, compile_temp_dir)
+ end
+
+ local module_name = name:match("([^.]*)$").."."..util.matchquote(cfg.lib_extension)
+ if moddir ~= "" then
+ module_name = dir.path(moddir, module_name)
+ end
+
+ local build_name = dir.path(compile_temp_dir, module_name)
+ local build_dir = dir.dir_name(build_name)
+ cached_make_dir(build_dir)
+
+ lib_modules[build_name] = dir.path(libdir, module_name)
+ ok = compile_library(build_name, objects, info.libraries, info.libdirs or autolibdirs, name)
+ if not ok then
+ return nil, "Failed compiling module "..module_name
+ end
+
+ -- for backwards compatibility, try keeping a copy of the module
+ -- in the old location (luasec-1.3.2-1 rockspec breaks otherwise)
+ if cached_make_dir(dir.dir_name(module_name)) then
+ fs.copy(build_name, module_name)
+ end
+
+ --[[ TODO disable static libs until we fix the conflict in the manifest, which will take extending the manifest format.
+ module_name = name:match("([^.]*)$").."."..util.matchquote(cfg.static_lib_extension)
+ if moddir ~= "" then
+ module_name = dir.path(moddir, module_name)
+ end
+ lib_modules[module_name] = dir.path(libdir, module_name)
+ ok = compile_static_library(module_name, objects, info.libraries, info.libdirs, name)
+ if not ok then
+ return nil, "Failed compiling static library "..module_name
+ end
+ ]]
+ end
+ end
+ if not no_install then
+ for _, mods in ipairs({{ tbl = lua_modules, perms = "read" }, { tbl = lib_modules, perms = "exec" }}) do
+ for name, dest in pairs(mods.tbl) do
+ cached_make_dir(dir.dir_name(dest))
+ ok, err = fs.copy(name, dest, mods.perms)
+ if not ok then
+ return nil, "Failed installing "..name.." in "..dest..": "..err
+ end
+ end
+ end
+ if fs.is_dir("lua") then
+ ok, err = fs.copy_contents("lua", luadir)
+ if not ok then
+ return nil, "Failed copying contents of 'lua' directory: "..err
+ end
+ end
+ end
+ return true
+end
+
+return builtin
diff --git a/src/luarocks/build/cmake.lua b/src/luarocks/build/cmake.lua
new file mode 100644
index 0000000..b7a4786
--- /dev/null
+++ b/src/luarocks/build/cmake.lua
@@ -0,0 +1,78 @@
+
+--- Build back-end for CMake-based modules.
+local cmake = {}
+
+local fs = require("luarocks.fs")
+local util = require("luarocks.util")
+local cfg = require("luarocks.core.cfg")
+
+--- Driver function for the "cmake" build back-end.
+-- @param rockspec table: the loaded rockspec.
+-- @return boolean or (nil, string): true if no errors occurred,
+-- nil and an error message otherwise.
+function cmake.run(rockspec, no_install)
+ assert(rockspec:type() == "rockspec")
+ local build = rockspec.build
+ local variables = build.variables or {}
+
+ -- Pass Env variables
+ variables.CMAKE_MODULE_PATH=os.getenv("CMAKE_MODULE_PATH")
+ variables.CMAKE_LIBRARY_PATH=os.getenv("CMAKE_LIBRARY_PATH")
+ variables.CMAKE_INCLUDE_PATH=os.getenv("CMAKE_INCLUDE_PATH")
+
+ util.variable_substitutions(variables, rockspec.variables)
+
+ local ok, err_msg = fs.is_tool_available(rockspec.variables.CMAKE, "CMake")
+ if not ok then
+ return nil, err_msg
+ end
+
+ -- If inline cmake is present create CMakeLists.txt from it.
+ if type(build.cmake) == "string" then
+ local cmake_handler = assert(io.open(fs.current_dir().."/CMakeLists.txt", "w"))
+ cmake_handler:write(build.cmake)
+ cmake_handler:close()
+ end
+
+ -- Execute cmake with variables.
+ local args = ""
+
+ -- Try to pick the best generator. With msvc and x64, CMake does not select it by default so we need to be explicit.
+ if cfg.cmake_generator then
+ args = args .. ' -G"'..cfg.cmake_generator.. '"'
+ elseif cfg.is_platform("windows") and cfg.target_cpu:match("x86_64$") then
+ args = args .. " -DCMAKE_GENERATOR_PLATFORM=x64"
+ end
+
+ for k,v in pairs(variables) do
+ args = args .. ' -D' ..k.. '="' ..tostring(v).. '"'
+ end
+
+ if not fs.execute_string(rockspec.variables.CMAKE.." -H. -Bbuild.luarocks "..args) then
+ return nil, "Failed cmake."
+ end
+
+ local do_build, do_install
+ if rockspec:format_is_at_least("3.0") then
+ do_build = (build.build_pass == nil) and true or build.build_pass
+ do_install = (build.install_pass == nil) and true or build.install_pass
+ else
+ do_build = true
+ do_install = true
+ end
+
+ if do_build then
+ if not fs.execute_string(rockspec.variables.CMAKE.." --build build.luarocks --config Release") then
+ return nil, "Failed building."
+ end
+ end
+ if do_install and not no_install then
+ if not fs.execute_string(rockspec.variables.CMAKE.." --build build.luarocks --target install --config Release") then
+ return nil, "Failed installing."
+ end
+ end
+
+ return true
+end
+
+return cmake
diff --git a/src/luarocks/build/command.lua b/src/luarocks/build/command.lua
new file mode 100644
index 0000000..b0c4aa7
--- /dev/null
+++ b/src/luarocks/build/command.lua
@@ -0,0 +1,41 @@
+
+--- Build back-end for raw listing of commands in rockspec files.
+local command = {}
+
+local fs = require("luarocks.fs")
+local util = require("luarocks.util")
+local cfg = require("luarocks.core.cfg")
+
+--- Driver function for the "command" build back-end.
+-- @param rockspec table: the loaded rockspec.
+-- @return boolean or (nil, string): true if no errors occurred,
+-- nil and an error message otherwise.
+function command.run(rockspec, not_install)
+ assert(rockspec:type() == "rockspec")
+
+ local build = rockspec.build
+
+ util.variable_substitutions(build, rockspec.variables)
+
+ local env = {
+ CC = cfg.variables.CC,
+ --LD = cfg.variables.LD,
+ --CFLAGS = cfg.variables.CFLAGS,
+ }
+
+ if build.build_command then
+ util.printout(build.build_command)
+ if not fs.execute_env(env, build.build_command) then
+ return nil, "Failed building."
+ end
+ end
+ if build.install_command and not not_install then
+ util.printout(build.install_command)
+ if not fs.execute_env(env, build.install_command) then
+ return nil, "Failed installing."
+ end
+ end
+ return true
+end
+
+return command
diff --git a/src/luarocks/build/make.lua b/src/luarocks/build/make.lua
new file mode 100644
index 0000000..4345ddf
--- /dev/null
+++ b/src/luarocks/build/make.lua
@@ -0,0 +1,98 @@
+
+--- Build back-end for using Makefile-based packages.
+local make = {}
+
+local unpack = unpack or table.unpack
+
+local fs = require("luarocks.fs")
+local util = require("luarocks.util")
+local cfg = require("luarocks.core.cfg")
+
+--- Call "make" with given target and variables
+-- @param make_cmd string: the make command to be used (typically
+-- configured through variables.MAKE in the config files, or
+-- the appropriate platform-specific default).
+-- @param pass boolean: If true, run make; if false, do nothing.
+-- @param target string: The make target; an empty string indicates
+-- the default target.
+-- @param variables table: A table containing string-string key-value
+-- pairs representing variable assignments to be passed to make.
+-- @return boolean: false if any errors occurred, true otherwise.
+local function make_pass(make_cmd, pass, target, variables)
+ assert(type(pass) == "boolean")
+ assert(type(target) == "string")
+ assert(type(variables) == "table")
+
+ local assignments = {}
+ for k,v in pairs(variables) do
+ table.insert(assignments, k.."="..v)
+ end
+ if pass then
+ return fs.execute(make_cmd.." "..target, unpack(assignments))
+ else
+ return true
+ end
+end
+
+--- Driver function for the "make" build back-end.
+-- @param rockspec table: the loaded rockspec.
+-- @return boolean or (nil, string): true if no errors occurred,
+-- nil and an error message otherwise.
+function make.run(rockspec, not_install)
+ assert(rockspec:type() == "rockspec")
+
+ local build = rockspec.build
+
+ if build.build_pass == nil then build.build_pass = true end
+ if build.install_pass == nil then build.install_pass = true end
+ build.build_variables = build.build_variables or {}
+ build.install_variables = build.install_variables or {}
+ build.build_target = build.build_target or ""
+ build.install_target = build.install_target or "install"
+ local makefile = build.makefile or cfg.makefile
+ if makefile then
+ -- Assumes all make's accept -f. True for POSIX make, GNU make and Microsoft nmake.
+ build.build_target = "-f "..makefile.." "..build.build_target
+ build.install_target = "-f "..makefile.." "..build.install_target
+ end
+
+ if build.variables then
+ for var, val in pairs(build.variables) do
+ build.build_variables[var] = val
+ build.install_variables[var] = val
+ end
+ end
+
+ util.warn_if_not_used(build.build_variables, { CFLAGS=true }, "variable %s was not passed in build_variables")
+
+ util.variable_substitutions(build.build_variables, rockspec.variables)
+ util.variable_substitutions(build.install_variables, rockspec.variables)
+
+ local auto_variables = { "CC" }
+
+ for _, variable in pairs(auto_variables) do
+ if not build.build_variables[variable] then
+ build.build_variables[variable] = rockspec.variables[variable]
+ end
+ if not build.install_variables[variable] then
+ build.install_variables[variable] = rockspec.variables[variable]
+ end
+ end
+
+ -- backwards compatibility
+ local make_cmd = cfg.make or rockspec.variables.MAKE
+
+ local ok = make_pass(make_cmd, build.build_pass, build.build_target, build.build_variables)
+ if not ok then
+ return nil, "Failed building."
+ end
+ if not not_install then
+ ok = make_pass(make_cmd, build.install_pass, build.install_target, build.install_variables)
+ if not ok then
+ return nil, "Failed installing."
+ end
+ end
+ return true
+end
+
+return make
diff --git a/src/luarocks/cmd.lua b/src/luarocks/cmd.lua
new file mode 100644
index 0000000..7e0abe5
--- /dev/null
+++ b/src/luarocks/cmd.lua
@@ -0,0 +1,781 @@
+
+--- Functions for command-line scripts.
+local cmd = {}
+
+local manif = require("luarocks.manif")
+local config = require("luarocks.config")
+local util = require("luarocks.util")
+local path = require("luarocks.path")
+local cfg = require("luarocks.core.cfg")
+local dir = require("luarocks.dir")
+local fun = require("luarocks.fun")
+local fs = require("luarocks.fs")
+local argparse = require("luarocks.vendor.argparse")
+
+local unpack = table.unpack or unpack
+local pack = table.pack or function(...) return { n = select("#", ...), ... } end
+
+local hc_ok, hardcoded = pcall(require, "luarocks.core.hardcoded")
+if not hc_ok then
+ hardcoded = {}
+end
+
+local program = util.this_program("luarocks")
+
+cmd.errorcodes = {
+ OK = 0,
+ UNSPECIFIED = 1,
+ PERMISSIONDENIED = 2,
+ CONFIGFILE = 3,
+ LOCK = 4,
+ CRASH = 99
+}
+
+local function check_popen()
+ local popen_ok, popen_result = pcall(io.popen, "")
+ if popen_ok then
+ if popen_result then
+ popen_result:close()
+ end
+ else
+ io.stderr:write("Your version of Lua does not support io.popen,\n")
+ io.stderr:write("which is required by LuaRocks. Please check your Lua installation.\n")
+ os.exit(cmd.errorcodes.UNSPECIFIED)
+ end
+end
+
+local process_tree_args
+do
+ local function replace_tree(args, root, tree)
+ root = dir.normalize(root)
+ args.tree = root
+ path.use_tree(tree or root)
+ end
+
+ local function strip_trailing_slashes()
+ if type(cfg.root_dir) == "string" then
+ cfg.root_dir = cfg.root_dir:gsub("/+$", "")
+ else
+ cfg.root_dir.root = cfg.root_dir.root:gsub("/+$", "")
+ end
+ cfg.rocks_dir = cfg.rocks_dir:gsub("/+$", "")
+ cfg.deploy_bin_dir = cfg.deploy_bin_dir:gsub("/+$", "")
+ cfg.deploy_lua_dir = cfg.deploy_lua_dir:gsub("/+$", "")
+ cfg.deploy_lib_dir = cfg.deploy_lib_dir:gsub("/+$", "")
+ end
+
+ local function set_named_tree(args, name)
+ for _, tree in ipairs(cfg.rocks_trees) do
+ if type(tree) == "table" and name == tree.name then
+ if not tree.root then
+ return nil, "Configuration error: tree '"..tree.name.."' has no 'root' field."
+ end
+ replace_tree(args, tree.root, tree)
+ return true
+ end
+ end
+ return false
+ end
+
+ process_tree_args = function(args, project_dir)
+
+ if args.global then
+ local ok, err = set_named_tree(args, "system")
+ if not ok then
+ return nil, err
+ end
+ elseif args.tree then
+ local named = set_named_tree(args, args.tree)
+ if not named then
+ local root_dir = fs.absolute_name(args.tree)
+ replace_tree(args, root_dir)
+ if (args.deps_mode or cfg.deps_mode) ~= "order" then
+ table.insert(cfg.rocks_trees, 1, { name = "arg", root = root_dir } )
+ end
+ end
+ elseif args["local"] then
+ if fs.is_superuser() then
+ return nil, "The --local flag is meant for operating in a user's home directory.\n"..
+ "You are running as a superuser, which is intended for system-wide operation.\n"..
+ "To force using the superuser's home, use --tree explicitly."
+ else
+ local ok, err = set_named_tree(args, "user")
+ if not ok then
+ return nil, err
+ end
+ end
+ elseif args.project_tree then
+ local tree = args.project_tree
+ table.insert(cfg.rocks_trees, 1, { name = "project", root = tree } )
+ manif.load_rocks_tree_manifests()
+ path.use_tree(tree)
+ elseif cfg.local_by_default then
+ local ok, err = set_named_tree(args, "user")
+ if not ok then
+ return nil, err
+ end
+ elseif project_dir then
+ local project_tree = project_dir .. "/lua_modules"
+ table.insert(cfg.rocks_trees, 1, { name = "project", root = project_tree } )
+ manif.load_rocks_tree_manifests()
+ path.use_tree(project_tree)
+ else
+ local trees = cfg.rocks_trees
+ path.use_tree(trees[#trees])
+ end
+
+ strip_trailing_slashes()
+
+ cfg.variables.ROCKS_TREE = cfg.rocks_dir
+ cfg.variables.SCRIPTS_DIR = cfg.deploy_bin_dir
+
+ return true
+ end
+end
+
+local function process_server_args(args)
+ if args.server then
+ local protocol, pathname = dir.split_url(args.server)
+ table.insert(cfg.rocks_servers, 1, protocol.."://"..pathname)
+ end
+
+ if args.dev then
+ local append_dev = function(s) return dir.path(s, "dev") end
+ local dev_servers = fun.traverse(cfg.rocks_servers, append_dev)
+ cfg.rocks_servers = fun.concat(dev_servers, cfg.rocks_servers)
+ end
+
+ if args.only_server then
+ if args.dev then
+ return nil, "--only-server cannot be used with --dev"
+ end
+ if args.server then
+ return nil, "--only-server cannot be used with --server"
+ end
+ cfg.rocks_servers = { args.only_server }
+ end
+
+ return true
+end
+
+local function error_handler(err)
+ if not debug then
+ return err
+ end
+ local mode = "Arch.: " .. (cfg and cfg.arch or "unknown")
+ if package.config:sub(1, 1) == "\\" then
+ if cfg and cfg.fs_use_modules then
+ mode = mode .. " (fs_use_modules = true)"
+ end
+ end
+ if cfg and cfg.is_binary then
+ mode = mode .. " (binary)"
+ end
+ return debug.traceback("LuaRocks "..cfg.program_version..
+ " bug (please report at https://github.com/luarocks/luarocks/issues).\n"..
+ mode.."\n"..err, 2)
+end
+
+--- Display an error message and exit.
+-- @param message string: The error message.
+-- @param exitcode number: the exitcode to use
+local function die(message, exitcode)
+ assert(type(message) == "string", "bad error, expected string, got: " .. type(message))
+ assert(exitcode == nil or type(exitcode) == "number", "bad error, expected number, got: " .. type(exitcode) .. " - " .. tostring(exitcode))
+ util.printerr("\nError: "..message)
+
+ local ok, err = xpcall(util.run_scheduled_functions, error_handler)
+ if not ok then
+ util.printerr("\nError: "..err)
+ exitcode = cmd.errorcodes.CRASH
+ end
+
+ os.exit(exitcode or cmd.errorcodes.UNSPECIFIED)
+end
+
+local function search_lua(lua_version, verbose, search_at)
+ if search_at then
+ return util.find_lua(search_at, lua_version, verbose)
+ end
+
+ local path_sep = (package.config:sub(1, 1) == "\\" and ";" or ":")
+ local all_tried = {}
+ for bindir in (os.getenv("PATH") or ""):gmatch("[^"..path_sep.."]+") do
+ local searchdir = (bindir:gsub("[\\/]+bin[\\/]?$", ""))
+ local detected, tried = util.find_lua(searchdir, lua_version)
+ if detected then
+ return detected
+ else
+ table.insert(all_tried, tried)
+ end
+ end
+ return nil, "Could not find " ..
+ (lua_version and "Lua " .. lua_version or "Lua") ..
+ " in PATH." ..
+ (verbose and " Tried:\n" .. table.concat(all_tried, "\n") or "")
+end
+
+local init_config
+do
+ local detect_config_via_args
+ do
+ local function find_project_dir(project_tree)
+ if project_tree then
+ return project_tree:gsub("[/\\][^/\\]+$", ""), true
+ else
+ local try = "."
+ for _ = 1, 10 do -- FIXME detect when root dir was hit instead
+ if util.exists(try .. "/.luarocks") and util.exists(try .. "/lua_modules") then
+ return dir.normalize(try), false
+ elseif util.exists(try .. "/.luarocks-no-project") then
+ break
+ end
+ try = try .. "/.."
+ end
+ end
+ return nil
+ end
+
+ local function find_default_lua_version(args, project_dir)
+ if hardcoded.FORCE_CONFIG then
+ return nil
+ end
+
+ local dirs = {}
+ if project_dir then
+ table.insert(dirs, dir.path(project_dir, ".luarocks"))
+ end
+ if cfg.homeconfdir then
+ table.insert(dirs, cfg.homeconfdir)
+ end
+ table.insert(dirs, cfg.sysconfdir)
+ for _, d in ipairs(dirs) do
+ local f = dir.path(d, "default-lua-version.lua")
+ local mod, err = loadfile(f, "t")
+ if mod then
+ local pok, ver = pcall(mod)
+ if pok and type(ver) == "string" and ver:match("%d+.%d+") then
+ if args.verbose then
+ util.printout("Defaulting to Lua " .. ver .. " based on " .. f .. " ...")
+ end
+ return ver
+ end
+ end
+ end
+ return nil
+ end
+
+ local function find_version_from_config(dirname)
+ return fun.find(util.lua_versions("descending"), function(v)
+ if util.exists(dir.path(dirname, ".luarocks", "config-"..v..".lua")) then
+ return v
+ end
+ end)
+ end
+
+ local function detect_lua_via_args(args, project_dir)
+ local lua_version = args.lua_version
+ or find_default_lua_version(args, project_dir)
+ or (project_dir and find_version_from_config(project_dir))
+
+ if args.lua_dir then
+ local detected, err = util.find_lua(args.lua_dir, lua_version)
+ if not detected then
+ local suggestion = (not args.lua_version)
+ and "\nYou may want to specify a different Lua version with --lua-version\n"
+ or ""
+ die(err .. suggestion)
+ end
+ return detected
+ end
+
+ if lua_version then
+ local detected = search_lua(lua_version)
+ if detected then
+ return detected
+ end
+ return {
+ lua_version = lua_version,
+ }
+ end
+
+ return {}
+ end
+
+ detect_config_via_args = function(args)
+ local project_dir, given
+ if not args.no_project then
+ project_dir, given = find_project_dir(args.project_tree)
+ end
+
+ local detected = detect_lua_via_args(args, project_dir)
+ if args.lua_version then
+ detected.given_lua_version = args.lua_version
+ end
+ if args.lua_dir then
+ detected.given_lua_dir = args.lua_dir
+ end
+ if given then
+ detected.given_project_dir = project_dir
+ end
+ detected.project_dir = project_dir
+ return detected
+ end
+ end
+
+ init_config = function(args)
+ local detected = detect_config_via_args(args)
+
+ local ok, err = cfg.init(detected, util.warning)
+ if not ok then
+ return nil, err
+ end
+
+ return (detected.lua_dir ~= nil)
+ end
+end
+
+local variables_help = [[
+Variables:
+ Variables from the "variables" table of the configuration file can be
+ overridden with VAR=VALUE assignments.
+
+]]
+
+local lua_example = package.config:sub(1, 1) == "\\"
+ and "<d:\\path\\lua.exe>"
+ or "</path/lua>"
+
+local function show_status(file, status, err)
+ return (file and file .. " " or "") .. (status and "(ok)" or ("(" .. (err or "not found") ..")"))
+end
+
+local function use_to_fix_location(key, what)
+ local buf = " ****************************************\n"
+ buf = buf .. " Use the command\n\n"
+ buf = buf .. " luarocks config " .. key .. " " .. (what or "<dir>") .. "\n\n"
+ buf = buf .. " to fix the location\n"
+ buf = buf .. " ****************************************\n"
+ return buf
+end
+
+local function get_config_text(cfg) -- luacheck: ignore 431
+ local deps = require("luarocks.deps")
+
+ local libdir_ok = deps.check_lua_libdir(cfg.variables)
+ local incdir_ok = deps.check_lua_incdir(cfg.variables)
+ local lua_ok = cfg.variables.LUA and fs.exists(cfg.variables.LUA)
+
+ local buf = "Configuration:\n"
+ buf = buf.." Lua:\n"
+ buf = buf.." Version : "..cfg.lua_version.."\n"
+ if cfg.luajit_version then
+ buf = buf.." LuaJIT : "..cfg.luajit_version.."\n"
+ end
+ buf = buf.." LUA : "..show_status(cfg.variables.LUA, lua_ok, "interpreter not found").."\n"
+ if not lua_ok then
+ buf = buf .. use_to_fix_location("variables.LUA", lua_example)
+ end
+ buf = buf.." LUA_INCDIR : "..show_status(cfg.variables.LUA_INCDIR, incdir_ok, "lua.h not found").."\n"
+ if lua_ok and not incdir_ok then
+ buf = buf .. use_to_fix_location("variables.LUA_INCDIR")
+ end
+ buf = buf.." LUA_LIBDIR : "..show_status(cfg.variables.LUA_LIBDIR, libdir_ok, "Lua library itself not found").."\n"
+ if lua_ok and not libdir_ok then
+ buf = buf .. use_to_fix_location("variables.LUA_LIBDIR")
+ end
+
+ buf = buf.."\n Configuration files:\n"
+ local conf = cfg.config_files
+ buf = buf.." System : "..show_status(fs.absolute_name(conf.system.file), conf.system.found).."\n"
+ if conf.user.file then
+ buf = buf.." User : "..show_status(fs.absolute_name(conf.user.file), conf.user.found).."\n"
+ else
+ buf = buf.." User : disabled in this LuaRocks installation.\n"
+ end
+ if conf.project then
+ buf = buf.." Project : "..show_status(fs.absolute_name(conf.project.file), conf.project.found).."\n"
+ end
+ buf = buf.."\n Rocks trees in use: \n"
+ for _, tree in ipairs(cfg.rocks_trees) do
+ if type(tree) == "string" then
+ buf = buf.." "..fs.absolute_name(tree)
+ else
+ local name = tree.name and " (\""..tree.name.."\")" or ""
+ buf = buf.." "..fs.absolute_name(tree.root)..name
+ end
+ buf = buf .. "\n"
+ end
+
+ return buf
+end
+
+local function get_parser(description, cmd_modules)
+ local basename = dir.base_name(program)
+ local parser = argparse(
+ basename, "LuaRocks "..cfg.program_version..", the Lua package manager\n\n"..
+ program.." - "..description, variables_help.."Run '"..basename..
+ "' without any arguments to see the configuration.")
+ :help_max_width(80)
+ :add_help_command()
+ :add_complete_command({
+ help_max_width = 100,
+ summary = "Output a shell completion script.",
+ description = [[
+Output a shell completion script.
+
+Enabling completions for Bash:
+
+ Add the following line to your ~/.bashrc:
+ source <(]]..basename..[[ completion bash)
+ or save the completion script to the local completion directory:
+ ]]..basename..[[ completion bash > ~/.local/share/bash-completion/completions/]]..basename..[[
+
+
+Enabling completions for Zsh:
+
+ Save the completion script to a file in your $fpath.
+ You can add a new directory to your $fpath by adding e.g.
+ fpath=(~/.zfunc $fpath)
+ to your ~/.zshrc.
+ Then run:
+ ]]..basename..[[ completion zsh > ~/.zfunc/_]]..basename..[[
+
+
+Enabling completion for Fish:
+
+ Add the following line to your ~/.config/fish/config.fish:
+ ]]..basename..[[ completion fish | source
+ or save the completion script to the local completion directory:
+ ]]..basename..[[ completion fish > ~/.config/fish/completions/]]..basename..[[.fish
+]]})
+ :command_target("command")
+ :require_command(false)
+
+ parser:flag("--version", "Show version info and exit.")
+ :action(function()
+ util.printout(program.." "..cfg.program_version)
+ util.printout(description)
+ util.printout()
+ os.exit(cmd.errorcodes.OK)
+ end)
+ parser:flag("--dev", "Enable the sub-repositories in rocks servers for "..
+ "rockspecs of in-development versions.")
+ parser:option("--server", "Fetch rocks/rockspecs from this server "..
+ "(takes priority over config file).")
+ :hidden_name("--from")
+ parser:option("--only-server", "Fetch rocks/rockspecs from this server only "..
+ "(overrides any entries in the config file).")
+ :argname("<server>")
+ :hidden_name("--only-from")
+ parser:option("--only-sources", "Restrict downloads to paths matching the given URL.")
+ :argname("<url>")
+ :hidden_name("--only-sources-from")
+ parser:option("--namespace", "Specify the rocks server namespace to use.")
+ :convert(string.lower)
+ parser:option("--lua-dir", "Which Lua installation to use.")
+ :argname("<prefix>")
+ parser:option("--lua-version", "Which Lua version to use.")
+ :argname("<ver>")
+ :convert(function(s) return (s:match("^%d+%.%d+$")) end)
+ parser:option("--tree", "Which tree to operate on.")
+ :hidden_name("--to")
+ parser:flag("--local", "Use the tree in the user's home directory.\n"..
+ "To enable it, see '"..program.." help path'.")
+ parser:flag("--global", "Use the system tree when `local_by_default` is `true`.")
+ parser:flag("--no-project", "Do not use project tree even if running from a project folder.")
+ parser:flag("--force-lock", "Attempt to overwrite the lock for commands " ..
+ "that require exclusive access, such as 'install'")
+ parser:flag("--verbose", "Display verbose output of commands executed.")
+ parser:option("--timeout", "Timeout on network operations, in seconds.\n"..
+ "0 means no timeout (wait forever). Default is "..
+ tostring(cfg.connection_timeout)..".")
+ :argname("<seconds>")
+ :convert(tonumber)
+
+ -- Used internally to force the use of a particular project tree
+ parser:option("--project-tree"):hidden(true)
+
+ for _, module in util.sortedpairs(cmd_modules) do
+ module.add_to_parser(parser)
+ end
+
+ return parser
+end
+
+local function get_first_arg()
+ if not arg then
+ return
+ end
+ local first_arg = arg[0]
+ local i = -1
+ while arg[i] do
+ first_arg = arg[i]
+ i = i -1
+ end
+ return first_arg
+end
+
+--- Main command-line processor.
+-- Parses input arguments and calls the appropriate driver function
+-- to execute the action requested on the command-line, forwarding
+-- to it any additional arguments passed by the user.
+-- @param description string: Short summary description of the program.
+-- @param commands table: contains the loaded modules representing commands.
+-- @param external_namespace string: where to look for external commands.
+-- @param ... string: Arguments given on the command-line.
+function cmd.run_command(description, commands, external_namespace, ...)
+
+ check_popen()
+
+ -- Preliminary initialization
+ cfg.init()
+
+ fs.init()
+
+ for _, module_name in ipairs(fs.modules(external_namespace)) do
+ if not commands[module_name] then
+ commands[module_name] = external_namespace.."."..module_name
+ end
+ end
+
+ local cmd_modules = {}
+ for name, module in pairs(commands) do
+ local pok, mod = pcall(require, module)
+ if pok and type(mod) == "table" then
+ local original_command = mod.command
+ if original_command then
+ if not mod.add_to_parser then
+ mod.add_to_parser = function(parser)
+ parser:command(name, mod.help, util.see_also())
+ :summary(mod.help_summary)
+ :handle_options(false)
+ :argument("input")
+ :args("*")
+ end
+
+ mod.command = function(args)
+ return original_command(args, unpack(args.input))
+ end
+ end
+ cmd_modules[name] = mod
+ else
+ util.warning("command module " .. module .. " does not implement command(), skipping")
+ end
+ else
+ util.warning("failed to load command module " .. module .. ": " .. mod)
+ end
+ end
+
+ local function process_cmdline_vars(...)
+ local args = pack(...)
+ local cmdline_vars = {}
+ local last = args.n
+ for i = 1, args.n do
+ if args[i] == "--" then
+ last = i - 1
+ break
+ end
+ end
+ for i = last, 1, -1 do
+ local arg = args[i]
+ if arg:match("^[^-][^=]*=") then
+ local var, val = arg:match("^([A-Z_][A-Z0-9_]*)=(.*)")
+ if val then
+ cmdline_vars[var] = val
+ table.remove(args, i)
+ else
+ die("Invalid assignment: "..arg)
+ end
+ end
+ end
+
+ return args, cmdline_vars
+ end
+
+ local args, cmdline_vars = process_cmdline_vars(...)
+ local parser = get_parser(description, cmd_modules)
+ args = parser:parse(args)
+
+ -- Compatibility for old flag
+ if args.nodeps then
+ args.deps_mode = "none"
+ end
+
+ if args.timeout then -- setting it in the config file will kick-in earlier in the process
+ cfg.connection_timeout = args.timeout
+ end
+
+ if args.command == "config" then
+ if args.key == "lua_version" and args.value then
+ args.lua_version = args.value
+ elseif args.key == "lua_dir" and args.value then
+ args.lua_dir = args.value
+ end
+ end
+
+ -----------------------------------------------------------------------------
+ local lua_found, err = init_config(args)
+ if err then
+ die(err)
+ end
+ -----------------------------------------------------------------------------
+
+ -- Now that the config is fully loaded, reinitialize fs using the full
+ -- feature set.
+ fs.init()
+
+ -- if the Lua interpreter wasn't explicitly found before cfg.init,
+ -- try again now.
+ local tried
+ if not lua_found then
+ local detected
+ detected, tried = search_lua(cfg.lua_version, args.verbose, cfg.variables.LUA_DIR)
+ if detected then
+ lua_found = true
+ cfg.variables.LUA = detected.lua
+ cfg.variables.LUA_DIR = detected.lua_dir
+ cfg.variables.LUA_BINDIR = detected.lua_bindir
+ if args.lua_dir then
+ cfg.variables.LUA_INCDIR = nil
+ cfg.variables.LUA_LIBDIR = nil
+ end
+ else
+ cfg.variables.LUA = nil
+ cfg.variables.LUA_DIR = nil
+ cfg.variables.LUA_BINDIR = nil
+ cfg.variables.LUA_INCDIR = nil
+ cfg.variables.LUA_LIBDIR = nil
+ end
+ end
+
+ if lua_found then
+ assert(cfg.variables.LUA)
+ else
+ -- Fallback producing _some_ Lua configuration based on the running interpreter.
+ -- Most likely won't produce correct results when running from the standalone binary,
+ -- so eventually we need to drop this and outright fail if Lua is not found
+ -- or explictly configured
+ if not cfg.variables.LUA then
+ local first_arg = get_first_arg()
+ local bin_dir = dir.dir_name(fs.absolute_name(first_arg))
+ local exe = dir.base_name(first_arg)
+ exe = exe:match("rocks") and ("lua" .. (cfg.arch:match("win") and ".exe" or "")) or exe
+ local full_path = dir.path(bin_dir, exe)
+ if util.check_lua_version(full_path, cfg.lua_version) then
+ cfg.variables.LUA = dir.path(bin_dir, exe)
+ cfg.variables.LUA_DIR = bin_dir:gsub("[/\\]bin[/\\]?$", "")
+ cfg.variables.LUA_BINDIR = bin_dir
+ cfg.variables.LUA_INCDIR = nil
+ cfg.variables.LUA_LIBDIR = nil
+ end
+ end
+ end
+
+ cfg.lua_found = lua_found
+
+ if cfg.project_dir then
+ cfg.project_dir = fs.absolute_name(cfg.project_dir)
+ end
+
+ if args.verbose then
+ cfg.verbose = true
+ print(("-"):rep(79))
+ print("Current configuration:")
+ print(("-"):rep(79))
+ print(config.to_string(cfg))
+ print(("-"):rep(79))
+ fs.verbose()
+ end
+
+ if (not fs.current_dir()) or fs.current_dir() == "" then
+ die("Current directory does not exist. Please run LuaRocks from an existing directory.")
+ end
+
+ local ok, err = process_tree_args(args, cfg.project_dir)
+ if not ok then
+ die(err)
+ end
+
+ ok, err = process_server_args(args)
+ if not ok then
+ die(err)
+ end
+
+ if args.only_sources then
+ cfg.only_sources_from = args.only_sources
+ end
+
+ for k, v in pairs(cmdline_vars) do
+ cfg.variables[k] = v
+ end
+
+ -- if running as superuser, use system cache dir
+ if fs.is_superuser() then
+ cfg.local_cache = dir.path(fs.system_cache_dir(), "luarocks")
+ end
+
+ if args.no_manifest then
+ cfg.no_manifest = true
+ end
+
+ if not args.command then
+ parser:epilog(variables_help..get_config_text(cfg))
+ util.printout()
+ util.printout(parser:get_help())
+ util.printout()
+ os.exit(cmd.errorcodes.OK)
+ end
+
+ if not cfg.variables["LUA"] and args.command ~= "config" and args.command ~= "help" then
+ local flag = (not cfg.project_tree)
+ and "--local "
+ or ""
+ if args.lua_version then
+ flag = "--lua-version=" .. args.lua_version .. " " .. flag
+ end
+ die((tried or "Lua interpreter not found.") ..
+ "\nPlease set your Lua interpreter with:\n\n" ..
+ " luarocks " .. flag.. "config variables.LUA " .. lua_example .. "\n")
+ end
+
+ local cmd_mod = cmd_modules[args.command]
+
+ local lock
+ if cmd_mod.needs_lock and cmd_mod.needs_lock(args) then
+ local ok, err = fs.check_command_permissions(args)
+ if not ok then
+ die(err, cmd.errorcodes.PERMISSIONDENIED)
+ end
+
+ lock, err = fs.lock_access(path.root_dir(cfg.root_dir), args.force_lock)
+ if not lock then
+ err = args.force_lock
+ and ("failed to force the lock" .. (err and ": " .. err or ""))
+ or (err and err ~= "File exists")
+ and err
+ or "try --force-lock to overwrite the lock"
+
+ die("command '" .. args.command .. "' " ..
+ "requires exclusive write access to " .. path.root_dir(cfg.root_dir) .. " - " ..
+ err, cmd.errorcodes.LOCK)
+ end
+ end
+
+ local call_ok, ok, err, exitcode = xpcall(function()
+ return cmd_mod.command(args)
+ end, error_handler)
+
+ if lock then
+ fs.unlock_access(lock)
+ end
+
+ if not call_ok then
+ die(ok, cmd.errorcodes.CRASH)
+ elseif not ok then
+ die(err, exitcode)
+ end
+ util.run_scheduled_functions()
+end
+
+return cmd
diff --git a/src/luarocks/cmd/build.lua b/src/luarocks/cmd/build.lua
new file mode 100644
index 0000000..3268041
--- /dev/null
+++ b/src/luarocks/cmd/build.lua
@@ -0,0 +1,198 @@
+
+--- Module implementing the LuaRocks "build" command.
+-- Builds a rock, compiling its C parts if any.
+local cmd_build = {}
+
+local pack = require("luarocks.pack")
+local path = require("luarocks.path")
+local util = require("luarocks.util")
+local fetch = require("luarocks.fetch")
+local fs = require("luarocks.fs")
+local deps = require("luarocks.deps")
+local remove = require("luarocks.remove")
+local cfg = require("luarocks.core.cfg")
+local build = require("luarocks.build")
+local writer = require("luarocks.manif.writer")
+local search = require("luarocks.search")
+local make = require("luarocks.cmd.make")
+local repos = require("luarocks.repos")
+
+function cmd_build.add_to_parser(parser)
+ local cmd = parser:command("build", "Build and install a rock, compiling its C parts if any.\n".. -- luacheck: ignore 431
+ "If the sources contain a luarocks.lock file, uses it as an authoritative source for "..
+ "exact version of dependencies.\n"..
+ "If no arguments are given, behaves as luarocks make.", util.see_also())
+ :summary("Build/compile a rock.")
+
+ cmd:argument("rock", "A rockspec file, a source rock file, or the name of "..
+ "a rock to be fetched from a repository.")
+ :args("?")
+ :action(util.namespaced_name_action)
+ cmd:argument("version", "Rock version.")
+ :args("?")
+
+ cmd:flag("--only-deps --deps-only", "Install only the dependencies of the rock.")
+ cmd:option("--branch", "Override the `source.branch` field in the loaded "..
+ "rockspec. Allows to specify a different branch to fetch. Particularly "..
+ 'for "dev" rocks.')
+ :argname("<name>")
+ cmd:flag("--pin", "Create a luarocks.lock file listing the exact "..
+ "versions of each dependency found for this rock (recursively), "..
+ "and store it in the rock's directory. "..
+ "Ignores any existing luarocks.lock file in the rock's sources.")
+ make.cmd_options(cmd)
+end
+
+--- Build and install a rock.
+-- @param rock_filename string: local or remote filename of a rock.
+-- @param opts table: build options
+-- @return boolean or (nil, string, [string]): True if build was successful,
+-- or false and an error message and an optional error code.
+local function build_rock(rock_filename, opts)
+ assert(type(rock_filename) == "string")
+ assert(opts:type() == "build.opts")
+
+ local ok, err, errcode
+
+ local unpack_dir
+ unpack_dir, err, errcode = fetch.fetch_and_unpack_rock(rock_filename, nil, opts.verify)
+ if not unpack_dir then
+ return nil, err, errcode
+ end
+
+ local rockspec_filename = path.rockspec_name_from_rock(rock_filename)
+
+ ok, err = fs.change_dir(unpack_dir)
+ if not ok then return nil, err end
+
+ local rockspec
+ rockspec, err, errcode = fetch.load_rockspec(rockspec_filename)
+ if not rockspec then
+ return nil, err, errcode
+ end
+
+ ok, err, errcode = build.build_rockspec(rockspec, opts)
+
+ fs.pop_dir()
+ return ok, err, errcode
+end
+
+local function do_build(name, namespace, version, opts)
+ assert(type(name) == "string")
+ assert(type(namespace) == "string" or not namespace)
+ assert(version == nil or type(version) == "string")
+ assert(opts:type() == "build.opts")
+
+ local url, err
+ if name:match("%.rockspec$") or name:match("%.rock$") then
+ url = name
+ else
+ url, err = search.find_src_or_rockspec(name, namespace, version, opts.check_lua_versions)
+ if not url then
+ return nil, err
+ end
+ end
+
+ name, version = path.parse_name(url)
+ if name and repos.is_installed(name, version) then
+ if not opts.rebuild then
+ util.printout(name .. " " .. version .. " is already installed in " .. path.root_dir(cfg.root_dir))
+ util.printout("Use --force to reinstall.")
+ return name, version, "skip"
+ end
+ end
+
+ if url:match("%.rockspec$") then
+ local rockspec, err = fetch.load_rockspec(url, nil, opts.verify)
+ if not rockspec then
+ return nil, err
+ end
+ return build.build_rockspec(rockspec, opts)
+ end
+
+ if url:match("%.src%.rock$") then
+ opts.need_to_fetch = false
+ end
+
+ return build_rock(url, opts)
+end
+
+--- Driver function for "build" command.
+-- If a package name is given, forwards the request to "search" and,
+-- if returned a result, installs the matching rock.
+-- When passing a package name, a version number may also be given.
+-- @return boolean or (nil, string, exitcode): True if build was successful; nil and an
+-- error message otherwise. exitcode is optionally returned.
+function cmd_build.command(args)
+ if not args.rock then
+ return make.command(args)
+ end
+
+ local opts = build.opts({
+ need_to_fetch = true,
+ minimal_mode = false,
+ deps_mode = deps.get_deps_mode(args),
+ build_only_deps = not not (args.only_deps and not args.pack_binary_rock),
+ namespace = args.namespace,
+ branch = args.branch,
+ verify = not not args.verify,
+ check_lua_versions = not not args.check_lua_versions,
+ pin = not not args.pin,
+ rebuild = not not (args.force or args.force_fast),
+ no_install = false
+ })
+
+ if args.sign and not args.pack_binary_rock then
+ return nil, "In the build command, --sign is meant to be used only with --pack-binary-rock"
+ end
+
+ if args.pack_binary_rock then
+ return pack.pack_binary_rock(args.rock, args.namespace, args.version, args.sign, function()
+ local name, version = do_build(args.rock, args.namespace, args.version, opts)
+ if name and args.no_doc then
+ util.remove_doc_dir(name, version)
+ end
+ return name, version
+ end)
+ end
+
+ local name, version, skip = do_build(args.rock, args.namespace, args.version, opts)
+ if not name then
+ return nil, version
+ end
+ if skip == "skip" then
+ return name, version
+ end
+
+ if args.no_doc then
+ util.remove_doc_dir(name, version)
+ end
+
+ if opts.build_only_deps then
+ util.printout("Stopping after installing dependencies for " ..name.." "..version)
+ util.printout()
+ else
+ if (not args.keep) and not cfg.keep_other_versions then
+ local ok, err, warn = remove.remove_other_versions(name, version, args.force, args.force_fast)
+ if not ok then
+ return nil, err
+ elseif warn then
+ util.printerr(err)
+ end
+ end
+ end
+
+ if opts.deps_mode ~= "none" then
+ writer.check_dependencies(nil, deps.get_deps_mode(args))
+ end
+ return name, version
+end
+
+cmd_build.needs_lock = function(args)
+ if args.pack_binary_rock then
+ return false
+ end
+ return true
+end
+
+return cmd_build
diff --git a/src/luarocks/cmd/config.lua b/src/luarocks/cmd/config.lua
new file mode 100644
index 0000000..d67711a
--- /dev/null
+++ b/src/luarocks/cmd/config.lua
@@ -0,0 +1,392 @@
+--- Module implementing the LuaRocks "config" command.
+-- Queries information about the LuaRocks configuration.
+local config_cmd = {}
+
+local persist = require("luarocks.persist")
+local config = require("luarocks.config")
+local cfg = require("luarocks.core.cfg")
+local util = require("luarocks.util")
+local deps = require("luarocks.deps")
+local dir = require("luarocks.dir")
+local fs = require("luarocks.fs")
+local json = require("luarocks.vendor.dkjson")
+
+function config_cmd.add_to_parser(parser)
+ local cmd = parser:command("config", [[
+Query information about the LuaRocks configuration.
+
+* When given a configuration key, it prints the value of that key according to
+ the currently active configuration (taking into account all config files and
+ any command-line flags passed)
+
+ Examples:
+ luarocks config variables.LUA_INCDIR
+ luarocks config lua_version
+
+* When given a configuration key and a value, it overwrites the config file (see
+ the --scope option below to determine which) and replaces the value of the
+ given key with the given value.
+
+ * `lua_dir` is a special key as it checks for a valid Lua installation
+ (equivalent to --lua-dir) and sets several keys at once.
+ * `lua_version` is a special key as it changes the default Lua version
+ used by LuaRocks commands (equivalent to passing --lua-version).
+
+ Examples:
+ luarocks config variables.OPENSSL_DIR /usr/local/openssl
+ luarocks config lua_dir /usr/local
+ luarocks config lua_version 5.3
+
+* When given a configuration key and --unset, it overwrites the config file (see
+ the --scope option below to determine which) and deletes that key from the
+ file.
+
+ Example: luarocks config variables.OPENSSL_DIR --unset
+
+* When given no arguments, it prints the entire currently active configuration,
+ resulting from reading the config files from all scopes.
+
+ Example: luarocks config]], util.see_also([[
+ https://github.com/luarocks/luarocks/wiki/Config-file-format
+ for detailed information on the LuaRocks config file format.
+]]))
+ :summary("Query information about the LuaRocks configuration.")
+
+ cmd:argument("key", "The configuration key.")
+ :args("?")
+ cmd:argument("value", "The configuration value.")
+ :args("?")
+
+ cmd:option("--scope", "The scope indicates which config file should be rewritten.\n"..
+ '* Using a wrapper created with `luarocks init`, the default is "project".\n'..
+ '* Using --local (or when `local_by_default` is `true`), the default is "user".\n'..
+ '* Otherwise, the default is "system".')
+ :choices({"system", "user", "project"})
+ cmd:flag("--unset", "Delete the key from the configuration file.")
+ cmd:flag("--json", "Output as JSON.")
+
+ -- Deprecated flags
+ cmd:flag("--lua-incdir"):hidden(true)
+ cmd:flag("--lua-libdir"):hidden(true)
+ cmd:flag("--lua-ver"):hidden(true)
+ cmd:flag("--system-config"):hidden(true)
+ cmd:flag("--user-config"):hidden(true)
+ cmd:flag("--rock-trees"):hidden(true)
+end
+
+local function config_file(conf)
+ print(dir.normalize(conf.file))
+ if conf.found then
+ return true
+ else
+ return nil, "file not found"
+ end
+end
+
+local function traverse_varstring(var, tbl, fn, missing_parent)
+ local k, r = var:match("^%[([0-9]+)%]%.(.*)$")
+ if k then
+ k = tonumber(k)
+ else
+ k, r = var:match("^([^.[]+)%.(.*)$")
+ if not k then
+ k, r = var:match("^([^[]+)(%[.*)$")
+ end
+ end
+
+ if k then
+ if not tbl[k] and missing_parent then
+ missing_parent(tbl, k)
+ end
+
+ if tbl[k] then
+ return traverse_varstring(r, tbl[k], fn, missing_parent)
+ else
+ return nil, "Unknown entry " .. k
+ end
+ end
+
+ local i = var:match("^%[([0-9]+)%]$")
+ if i then
+ var = tonumber(i)
+ end
+
+ return fn(tbl, var)
+end
+
+local function print_json(value)
+ print(json.encode(value))
+ return true
+end
+
+local function print_entry(var, tbl, is_json)
+ return traverse_varstring(var, tbl, function(t, k)
+ if not t[k] then
+ return nil, "Unknown entry " .. k
+ end
+ local val = t[k]
+
+ if not config.should_skip(var, val) then
+ if is_json then
+ return print_json(val)
+ elseif type(val) == "string" then
+ print(val)
+ else
+ persist.write_value(io.stdout, val)
+ end
+ end
+ return true
+ end)
+end
+
+local function infer_type(var)
+ local typ
+ traverse_varstring(var, cfg, function(t, k)
+ if t[k] ~= nil then
+ typ = type(t[k])
+ end
+ end)
+ return typ
+end
+
+local function write_entries(keys, scope, do_unset)
+ if scope == "project" and not cfg.config_files.project then
+ return nil, "Current directory is not part of a project. You may want to run `luarocks init`."
+ end
+
+ local file_name = cfg.config_files[scope].file
+
+ local tbl, err = persist.load_config_file_if_basic(file_name, cfg)
+ if not tbl then
+ return nil, err
+ end
+
+ for var, val in util.sortedpairs(keys) do
+ traverse_varstring(var, tbl, function(t, k)
+ if do_unset then
+ t[k] = nil
+ else
+ local typ = infer_type(var)
+ local v
+ if typ == "number" and tonumber(val) then
+ v = tonumber(val)
+ elseif typ == "boolean" and val == "true" then
+ v = true
+ elseif typ == "boolean" and val == "false" then
+ v = false
+ else
+ v = val
+ end
+ t[k] = v
+ keys[var] = v
+ end
+ return true
+ end, function(p, k)
+ p[k] = {}
+ end)
+ end
+
+ local ok, err = fs.make_dir(dir.dir_name(file_name))
+ if not ok then
+ return nil, err
+ end
+
+ ok, err = persist.save_from_table(file_name, tbl)
+ if ok then
+ print(do_unset and "Removed" or "Wrote")
+ for var, val in util.sortedpairs(keys) do
+ if do_unset then
+ print(("\t%s"):format(var))
+ else
+ if type(val) == "string" then
+ print(("\t%s = %q"):format(var, val))
+ else
+ print(("\t%s = %s"):format(var, tostring(val)))
+ end
+ end
+ end
+ print(do_unset and "from" or "to")
+ print("\t" .. file_name)
+ return true
+ else
+ return nil, err
+ end
+end
+
+local function get_scope(args)
+ return args.scope
+ or (args["local"] and "user")
+ or (args.project_tree and "project")
+ or (cfg.local_by_default and "user")
+ or (fs.is_writable(cfg.config_files["system"].file) and "system")
+ or "user"
+end
+
+local function report_on_lua_incdir_config(value, lua_version)
+ local variables = {
+ ["LUA_DIR"] = cfg.variables.LUA_DIR,
+ ["LUA_BINDIR"] = cfg.variables.LUA_BINDIR,
+ ["LUA_INCDIR"] = value,
+ ["LUA_LIBDIR"] = cfg.variables.LUA_LIBDIR,
+ ["LUA"] = cfg.variables.LUA,
+ }
+
+ local ok, err = deps.check_lua_incdir(variables, lua_version)
+ if not ok then
+ util.printerr()
+ util.warning((err:gsub(" You can use.*", "")))
+ end
+ return ok
+end
+
+local function report_on_lua_libdir_config(value, lua_version)
+ local variables = {
+ ["LUA_DIR"] = cfg.variables.LUA_DIR,
+ ["LUA_BINDIR"] = cfg.variables.LUA_BINDIR,
+ ["LUA_INCDIR"] = cfg.variables.LUA_INCDIR,
+ ["LUA_LIBDIR"] = value,
+ ["LUA"] = cfg.variables.LUA,
+ }
+
+ local ok, err, _, err_files = deps.check_lua_libdir(variables, lua_version)
+ if not ok then
+ util.printerr()
+ util.warning((err:gsub(" You can use.*", "")))
+ util.printerr("Tried:")
+ for _, l in pairs(err_files or {}) do
+ for _, d in ipairs(l) do
+ util.printerr("\t" .. d)
+ end
+ end
+ end
+ return ok
+end
+
+local function warn_bad_c_config()
+ util.printerr()
+ util.printerr("LuaRocks may not work correctly when building C modules using this configuration.")
+ util.printerr()
+end
+
+--- Driver function for "config" command.
+-- @return boolean: True if succeeded, nil on errors.
+function config_cmd.command(args)
+ local lua_version = args.lua_version or cfg.lua_version
+
+ deps.check_lua_incdir(cfg.variables, lua_version)
+ deps.check_lua_libdir(cfg.variables, lua_version)
+
+ -- deprecated flags
+ if args.lua_incdir then
+ print(cfg.variables.LUA_INCDIR)
+ return true
+ end
+ if args.lua_libdir then
+ print(cfg.variables.LUA_LIBDIR)
+ return true
+ end
+ if args.lua_ver then
+ print(cfg.lua_version)
+ return true
+ end
+ if args.system_config then
+ return config_file(cfg.config_files.system)
+ end
+ if args.user_config then
+ return config_file(cfg.config_files.user)
+ end
+ if args.rock_trees then
+ for _, tree in ipairs(cfg.rocks_trees) do
+ if type(tree) == "string" then
+ util.printout(dir.normalize(tree))
+ else
+ local name = tree.name and "\t"..tree.name or ""
+ util.printout(dir.normalize(tree.root)..name)
+ end
+ end
+ return true
+ end
+
+ if args.key == "lua_version" and args.value then
+ local scope = get_scope(args)
+ if scope == "project" and not cfg.config_files.project then
+ return nil, "Current directory is not part of a project. You may want to run `luarocks init`."
+ end
+
+ local location = cfg.config_files[scope]
+ if (not location) or (not location.file) then
+ return nil, "could not get config file location for " .. tostring(scope) .. " scope"
+ end
+
+ local prefix = dir.dir_name(location.file)
+ local ok, err = persist.save_default_lua_version(prefix, args.value)
+ if not ok then
+ return nil, "could not set default Lua version: " .. err
+ end
+ print("Lua version will default to " .. args.value .. " in " .. prefix)
+ end
+
+ if args.key == "lua_dir" and args.value then
+ local scope = get_scope(args)
+ local keys = {
+ ["variables.LUA_DIR"] = cfg.variables.LUA_DIR,
+ ["variables.LUA_BINDIR"] = cfg.variables.LUA_BINDIR,
+ ["variables.LUA_INCDIR"] = cfg.variables.LUA_INCDIR,
+ ["variables.LUA_LIBDIR"] = cfg.variables.LUA_LIBDIR,
+ ["variables.LUA"] = cfg.variables.LUA,
+ }
+ if args.lua_version then
+ local prefix = dir.dir_name(cfg.config_files[scope].file)
+ persist.save_default_lua_version(prefix, args.lua_version)
+ end
+ local ok, err = write_entries(keys, scope, args.unset)
+ if ok then
+ local inc_ok = report_on_lua_incdir_config(cfg.variables.LUA_INCDIR, lua_version)
+ local lib_ok = ok and report_on_lua_libdir_config(cfg.variables.LUA_LIBDIR, lua_version)
+ if not (inc_ok and lib_ok) then
+ warn_bad_c_config()
+ end
+ end
+
+ return ok, err
+ end
+
+ if args.key then
+ if args.key:match("^[A-Z]") then
+ args.key = "variables." .. args.key
+ end
+
+ if args.value or args.unset then
+ local scope = get_scope(args)
+
+ local ok, err = write_entries({ [args.key] = args.value or args.unset }, scope, args.unset)
+
+ if ok then
+ if args.key == "variables.LUA_INCDIR" then
+ local ok = report_on_lua_incdir_config(args.value, lua_version)
+ if not ok then
+ warn_bad_c_config()
+ end
+ elseif args.key == "variables.LUA_LIBDIR" then
+ local ok = report_on_lua_libdir_config(args.value, lua_version)
+ if not ok then
+ warn_bad_c_config()
+ end
+ end
+ end
+
+ return ok, err
+ else
+ return print_entry(args.key, cfg, args.json)
+ end
+ end
+
+ if args.json then
+ return print_json(config.get_config_for_display(cfg))
+ else
+ print(config.to_string(cfg))
+ return true
+ end
+end
+
+return config_cmd
diff --git a/src/luarocks/cmd/doc.lua b/src/luarocks/cmd/doc.lua
new file mode 100644
index 0000000..a311700
--- /dev/null
+++ b/src/luarocks/cmd/doc.lua
@@ -0,0 +1,153 @@
+
+--- Module implementing the LuaRocks "doc" command.
+-- Shows documentation for an installed rock.
+local doc = {}
+
+local util = require("luarocks.util")
+local queries = require("luarocks.queries")
+local search = require("luarocks.search")
+local path = require("luarocks.path")
+local dir = require("luarocks.dir")
+local fetch = require("luarocks.fetch")
+local fs = require("luarocks.fs")
+local download = require("luarocks.download")
+
+function doc.add_to_parser(parser)
+ local cmd = parser:command("doc", "Show documentation for an installed rock.\n\n"..
+ "Without any flags, tries to load the documentation using a series of heuristics.\n"..
+ "With flags, return only the desired information.", util.see_also([[
+ For more information about a rock, see the 'show' command.
+]]))
+ :summary("Show documentation for an installed rock.")
+
+ cmd:argument("rock", "Name of the rock.")
+ :action(util.namespaced_name_action)
+ cmd:argument("version", "Version of the rock.")
+ :args("?")
+
+ cmd:flag("--home", "Open the home page of project.")
+ cmd:flag("--list", "List documentation files only.")
+ cmd:flag("--porcelain", "Produce machine-friendly output.")
+end
+
+local function show_homepage(homepage, name, namespace, version)
+ if not homepage then
+ return nil, "No 'homepage' field in rockspec for "..util.format_rock_name(name, namespace, version)
+ end
+ util.printout("Opening "..homepage.." ...")
+ fs.browser(homepage)
+ return true
+end
+
+local function try_to_open_homepage(name, namespace, version)
+ local temp_dir, err = fs.make_temp_dir("doc-"..name.."-"..(version or ""))
+ if not temp_dir then
+ return nil, "Failed creating temporary directory: "..err
+ end
+ util.schedule_function(fs.delete, temp_dir)
+ local ok, err = fs.change_dir(temp_dir)
+ if not ok then return nil, err end
+ local filename, err = download.download("rockspec", name, namespace, version)
+ if not filename then return nil, err end
+ local rockspec, err = fetch.load_local_rockspec(filename)
+ if not rockspec then return nil, err end
+ fs.pop_dir()
+ local descript = rockspec.description or {}
+ return show_homepage(descript.homepage, name, namespace, version)
+end
+
+--- Driver function for "doc" command.
+-- @return boolean: True if succeeded, nil on errors.
+function doc.command(args)
+ local query = queries.new(args.rock, args.namespace, args.version)
+ local iname, iversion, repo = search.pick_installed_rock(query, args.tree)
+ if not iname then
+ local rock = util.format_rock_name(args.rock, args.namespace, args.version)
+ util.printout(rock.." is not installed. Looking for it in the rocks servers...")
+ return try_to_open_homepage(args.rock, args.namespace, args.version)
+ end
+ local name, version = iname, iversion
+
+ local rockspec, err = fetch.load_local_rockspec(path.rockspec_file(name, version, repo))
+ if not rockspec then return nil,err end
+ local descript = rockspec.description or {}
+
+ if args.home then
+ return show_homepage(descript.homepage, name, args.namespace, version)
+ end
+
+ local directory = path.install_dir(name, version, repo)
+
+ local docdir
+ local directories = { "doc", "docs" }
+ for _, d in ipairs(directories) do
+ local dirname = dir.path(directory, d)
+ if fs.is_dir(dirname) then
+ docdir = dirname
+ break
+ end
+ end
+ if not docdir then
+ if descript.homepage and not args.list then
+ util.printout("Local documentation directory not found -- opening "..descript.homepage.." ...")
+ fs.browser(descript.homepage)
+ return true
+ end
+ return nil, "Documentation directory not found for "..name.." "..version
+ end
+
+ docdir = dir.normalize(docdir)
+ local files = fs.find(docdir)
+ local htmlpatt = "%.html?$"
+ local extensions = { htmlpatt, "%.md$", "%.txt$", "%.textile$", "" }
+ local basenames = { "index", "readme", "manual" }
+
+ local porcelain = args.porcelain
+ if #files > 0 then
+ util.title("Documentation files for "..name.." "..version, porcelain)
+ if porcelain then
+ for _, file in ipairs(files) do
+ util.printout(docdir.."/"..file)
+ end
+ else
+ util.printout(docdir.."/")
+ for _, file in ipairs(files) do
+ util.printout("\t"..file)
+ end
+ end
+ end
+
+ if args.list then
+ return true
+ end
+
+ for _, extension in ipairs(extensions) do
+ for _, basename in ipairs(basenames) do
+ local filename = basename..extension
+ local found
+ for _, file in ipairs(files) do
+ if file:lower():match(filename) and ((not found) or #file < #found) then
+ found = file
+ end
+ end
+ if found then
+ local pathname = dir.path(docdir, found)
+ util.printout()
+ util.printout("Opening "..pathname.." ...")
+ util.printout()
+ local ok = fs.browser(pathname)
+ if not ok and not pathname:match(htmlpatt) then
+ local fd = io.open(pathname, "r")
+ util.printout(fd:read("*a"))
+ fd:close()
+ end
+ return true
+ end
+ end
+ end
+
+ return true
+end
+
+
+return doc
diff --git a/src/luarocks/cmd/download.lua b/src/luarocks/cmd/download.lua
new file mode 100644
index 0000000..eae8243
--- /dev/null
+++ b/src/luarocks/cmd/download.lua
@@ -0,0 +1,51 @@
+
+--- Module implementing the luarocks "download" command.
+-- Download a rock from the repository.
+local cmd_download = {}
+
+local util = require("luarocks.util")
+local download = require("luarocks.download")
+
+function cmd_download.add_to_parser(parser)
+ local cmd = parser:command("download", "Download a specific rock file from a rocks server.", util.see_also())
+
+ cmd:argument("name", "Name of the rock.")
+ :args("?")
+ :action(util.namespaced_name_action)
+ cmd:argument("version", "Version of the rock.")
+ :args("?")
+
+ cmd:flag("--all", "Download all files if there are multiple matches.")
+ cmd:mutex(
+ cmd:flag("--source", "Download .src.rock if available."),
+ cmd:flag("--rockspec", "Download .rockspec if available."),
+ cmd:option("--arch", "Download rock for a specific architecture."))
+ cmd:flag("--check-lua-versions", "If the rock can't be found, check repository "..
+ "and report if it is available for another Lua version.")
+end
+
+--- Driver function for the "download" command.
+-- @return boolean or (nil, string): true if successful or nil followed
+-- by an error message.
+function cmd_download.command(args)
+ if not args.name and not args.all then
+ return nil, "Argument missing. "..util.see_help("download")
+ end
+
+ args.name = args.name or ""
+
+ local arch
+
+ if args.source then
+ arch = "src"
+ elseif args.rockspec then
+ arch = "rockspec"
+ elseif args.arch then
+ arch = args.arch
+ end
+
+ local dl, err = download.download(arch, args.name, args.namespace, args.version, args.all, args.check_lua_versions)
+ return dl and true, err
+end
+
+return cmd_download
diff --git a/src/luarocks/cmd/init.lua b/src/luarocks/cmd/init.lua
new file mode 100644
index 0000000..b5359c9
--- /dev/null
+++ b/src/luarocks/cmd/init.lua
@@ -0,0 +1,219 @@
+
+local init = {}
+
+local cfg = require("luarocks.core.cfg")
+local fs = require("luarocks.fs")
+local path = require("luarocks.path")
+local deps = require("luarocks.deps")
+local dir = require("luarocks.dir")
+local util = require("luarocks.util")
+local persist = require("luarocks.persist")
+local write_rockspec = require("luarocks.cmd.write_rockspec")
+
+function init.add_to_parser(parser)
+ local cmd = parser:command("init", "Initialize a directory for a Lua project using LuaRocks.", util.see_also())
+
+ cmd:argument("name", "The project name.")
+ :args("?")
+ cmd:argument("version", "An optional project version.")
+ :args("?")
+ cmd:option("--wrapper-dir", "Location where the 'lua' and 'luarocks' wrapper scripts " ..
+ "should be generated; if not given, the current directory is used as a default.")
+ cmd:flag("--reset", "Delete any .luarocks/config-5.x.lua and ./lua and generate new ones.")
+ cmd:flag("--no-wrapper-scripts", "Do not generate wrapper ./lua and ./luarocks launcher scripts.")
+ cmd:flag("--no-gitignore", "Do not generate a .gitignore file.")
+
+ cmd:group("Options for specifying rockspec data", write_rockspec.cmd_options(cmd))
+end
+
+local function gitignore_path(pwd, wrapper_dir, filename)
+ local norm_cur = fs.absolute_name(pwd)
+ local norm_file = fs.absolute_name(dir.path(wrapper_dir, filename))
+ if norm_file:sub(1, #norm_cur) == norm_cur then
+ return norm_file:sub(#norm_cur + 2)
+ else
+ return filename
+ end
+end
+
+local function write_gitignore(entries)
+ local gitignore = ""
+ local fd = io.open(".gitignore", "r")
+ if fd then
+ gitignore = fd:read("*a")
+ fd:close()
+ gitignore = "\n" .. gitignore .. "\n"
+ end
+
+ fd = io.open(".gitignore", gitignore and "a" or "w")
+ if fd then
+ for _, entry in ipairs(entries) do
+ entry = "/" .. entry
+ if not gitignore:find("\n"..entry.."\n", 1, true) then
+ fd:write(entry.."\n")
+ end
+ end
+ fd:close()
+ end
+end
+
+local function inject_tree(tree)
+ path.use_tree(tree)
+ local tree_set = false
+ for _, t in ipairs(cfg.rocks_trees) do
+ if type(t) == "table" then
+ if t.name == "project" then
+ t.root = tree
+ tree_set = true
+ end
+ end
+ end
+ if not tree_set then
+ table.insert(cfg.rocks_trees, 1, { name = "project", root = tree })
+ end
+end
+
+local function write_wrapper_scripts(wrapper_dir, luarocks_wrapper, lua_wrapper)
+ local tree = dir.path(fs.current_dir(), "lua_modules")
+
+ fs.make_dir(wrapper_dir)
+
+ luarocks_wrapper = dir.path(wrapper_dir, luarocks_wrapper)
+ if not fs.exists(luarocks_wrapper) then
+ util.printout("Preparing " .. luarocks_wrapper .. " ...")
+ fs.wrap_script(arg[0], luarocks_wrapper, "none", nil, nil, "--project-tree", tree)
+ else
+ util.printout(luarocks_wrapper .. " already exists. Not overwriting it!")
+ end
+
+ lua_wrapper = dir.path(wrapper_dir, lua_wrapper)
+ local write_lua_wrapper = true
+ if fs.exists(lua_wrapper) then
+ if not util.lua_is_wrapper(lua_wrapper) then
+ util.printout(lua_wrapper .. " already exists and does not look like a wrapper script. Not overwriting.")
+ write_lua_wrapper = false
+ end
+ end
+
+ if write_lua_wrapper then
+ if util.check_lua_version(cfg.variables.LUA, cfg.lua_version) then
+ util.printout("Preparing " .. lua_wrapper .. " for version " .. cfg.lua_version .. "...")
+
+ -- Inject tree so it shows up as a lookup path in the wrappers
+ inject_tree(tree)
+
+ fs.wrap_script(nil, lua_wrapper, "all")
+ else
+ util.warning("No Lua interpreter detected for version " .. cfg.lua_version .. ". Not creating " .. lua_wrapper)
+ end
+ end
+end
+
+--- Driver function for "init" command.
+-- @return boolean: True if succeeded, nil on errors.
+function init.command(args)
+ local do_gitignore = not args.no_gitignore
+ local do_wrapper_scripts = not args.no_wrapper_scripts
+ local wrapper_dir = args.wrapper_dir or "."
+
+ local pwd = fs.current_dir()
+
+ if not args.name then
+ args.name = dir.base_name(pwd)
+ if args.name == "/" then
+ return nil, "When running from the root directory, please specify the <name> argument"
+ end
+ end
+
+ util.title("Initializing project '" .. args.name .. "' for Lua " .. cfg.lua_version .. " ...")
+
+ local ok, err = deps.check_lua_incdir(cfg.variables)
+ if not ok then
+ return nil, err
+ end
+
+ local has_rockspec = false
+ for file in fs.dir() do
+ if file:match("%.rockspec$") then
+ has_rockspec = true
+ break
+ end
+ end
+
+ if not has_rockspec then
+ args.version = args.version or "dev"
+ args.location = pwd
+ local ok, err = write_rockspec.command(args)
+ if not ok then
+ util.printerr(err)
+ end
+ end
+
+ local ext = cfg.wrapper_suffix
+ local luarocks_wrapper = "luarocks" .. ext
+ local lua_wrapper = "lua" .. ext
+
+ if do_gitignore then
+ util.printout("Adding entries to .gitignore ...")
+ local ignores = { "lua_modules", ".luarocks" }
+ if do_wrapper_scripts then
+ table.insert(ignores, 1, gitignore_path(pwd, wrapper_dir, luarocks_wrapper))
+ table.insert(ignores, 2, gitignore_path(pwd, wrapper_dir, lua_wrapper))
+ end
+ write_gitignore(ignores)
+ end
+
+ util.printout("Preparing ./.luarocks/ ...")
+ fs.make_dir(".luarocks")
+ local config_file = ".luarocks/config-" .. cfg.lua_version .. ".lua"
+
+ if args.reset then
+ if do_wrapper_scripts then
+ fs.delete(fs.absolute_name(dir.path(wrapper_dir, lua_wrapper)))
+ end
+ fs.delete(fs.absolute_name(config_file))
+ end
+
+ local config_tbl, err = persist.load_config_file_if_basic(config_file, cfg)
+ if config_tbl then
+ local varnames = {
+ "LUA_DIR",
+ "LUA_INCDIR",
+ "LUA_LIBDIR",
+ "LUA_BINDIR",
+ "LUA",
+ }
+ for _, varname in ipairs(varnames) do
+ if cfg.variables[varname] then
+ config_tbl.variables = config_tbl.variables or {}
+ config_tbl.variables[varname] = cfg.variables[varname]
+ end
+ end
+ local ok, err = persist.save_from_table(config_file, config_tbl)
+ if ok then
+ util.printout("Wrote " .. config_file)
+ else
+ util.printout("Failed writing " .. config_file .. ": " .. err)
+ end
+ else
+ util.printout("Will not attempt to overwrite " .. config_file)
+ end
+
+ ok, err = persist.save_default_lua_version(".luarocks", cfg.lua_version)
+ if not ok then
+ util.printout("Failed setting default Lua version: " .. err)
+ end
+
+ util.printout("Preparing ./lua_modules/ ...")
+ fs.make_dir("lua_modules/lib/luarocks/rocks-" .. cfg.lua_version)
+
+ if do_wrapper_scripts then
+ write_wrapper_scripts(wrapper_dir, luarocks_wrapper, lua_wrapper)
+ end
+
+ return true
+end
+
+init.needs_lock = function() return true end
+
+return init
diff --git a/src/luarocks/cmd/install.lua b/src/luarocks/cmd/install.lua
new file mode 100644
index 0000000..05e31fe
--- /dev/null
+++ b/src/luarocks/cmd/install.lua
@@ -0,0 +1,271 @@
+--- Module implementing the LuaRocks "install" command.
+-- Installs binary rocks.
+local install = {}
+
+local dir = require("luarocks.dir")
+local path = require("luarocks.path")
+local repos = require("luarocks.repos")
+local fetch = require("luarocks.fetch")
+local util = require("luarocks.util")
+local fs = require("luarocks.fs")
+local deps = require("luarocks.deps")
+local writer = require("luarocks.manif.writer")
+local remove = require("luarocks.remove")
+local search = require("luarocks.search")
+local queries = require("luarocks.queries")
+local cfg = require("luarocks.core.cfg")
+
+function install.add_to_parser(parser)
+ local cmd = parser:command("install", "Install a rock.", util.see_also()) -- luacheck: ignore 431
+
+ cmd:argument("rock", "The name of a rock to be fetched from a repository "..
+ "or a filename of a locally available rock.")
+ :action(util.namespaced_name_action)
+ cmd:argument("version", "Version of the rock.")
+ :args("?")
+
+ cmd:flag("--keep", "Do not remove previously installed versions of the "..
+ "rock after building a new one. This behavior can be made permanent by "..
+ "setting keep_other_versions=true in the configuration file.")
+ cmd:flag("--force", "If --keep is not specified, force removal of "..
+ "previously installed versions if it would break dependencies. "..
+ "If rock is already installed, reinstall it anyway.")
+ cmd:flag("--force-fast", "Like --force, but performs a forced removal "..
+ "without reporting dependency issues.")
+ cmd:flag("--only-deps --deps-only", "Install only the dependencies of the rock.")
+ cmd:flag("--no-doc", "Install the rock without its documentation.")
+ cmd:flag("--verify", "Verify signature of the rockspec or src.rock being "..
+ "built. If the rockspec or src.rock is being downloaded, LuaRocks will "..
+ "attempt to download the signature as well. Otherwise, the signature "..
+ "file should be already available locally in the same directory.\n"..
+ "You need the signer’s public key in your local keyring for this "..
+ "option to work properly.")
+ cmd:flag("--check-lua-versions", "If the rock can't be found, check repository "..
+ "and report if it is available for another Lua version.")
+ util.deps_mode_option(cmd)
+ cmd:flag("--no-manifest", "Skip creating/updating the manifest")
+ cmd:flag("--pin", "If the installed rock is a Lua module, create a "..
+ "luarocks.lock file listing the exact versions of each dependency found for "..
+ "this rock (recursively), and store it in the rock's directory. "..
+ "Ignores any existing luarocks.lock file in the rock's sources.")
+ -- luarocks build options
+ parser:flag("--pack-binary-rock"):hidden(true)
+ parser:option("--branch"):hidden(true)
+ parser:flag("--sign"):hidden(true)
+end
+
+install.opts = util.opts_table("install.opts", {
+ namespace = "string?",
+ keep = "boolean",
+ force = "boolean",
+ force_fast = "boolean",
+ no_doc = "boolean",
+ deps_mode = "string",
+ verify = "boolean",
+})
+
+--- Install a binary rock.
+-- @param rock_file string: local or remote filename of a rock.
+-- @param opts table: installation options
+-- @return (string, string) or (nil, string, [string]): Name and version of
+-- installed rock if succeeded or nil and an error message followed by an error code.
+function install.install_binary_rock(rock_file, opts)
+ assert(type(rock_file) == "string")
+ assert(opts:type() == "install.opts")
+
+ local namespace = opts.namespace
+ local deps_mode = opts.deps_mode
+
+ local name, version, arch = path.parse_name(rock_file)
+ if not name then
+ return nil, "Filename "..rock_file.." does not match format 'name-version-revision.arch.rock'."
+ end
+
+ if arch ~= "all" and arch ~= cfg.arch then
+ return nil, "Incompatible architecture "..arch, "arch"
+ end
+ if repos.is_installed(name, version) then
+ if not (opts.force or opts.force_fast) then
+ util.printout(name .. " " .. version .. " is already installed in " .. path.root_dir(cfg.root_dir))
+ util.printout("Use --force to reinstall.")
+ return name, version
+ end
+ repos.delete_version(name, version, opts.deps_mode)
+ end
+
+ local install_dir = path.install_dir(name, version)
+
+ local rollback = util.schedule_function(function()
+ fs.delete(install_dir)
+ fs.remove_dir_if_empty(path.versions_dir(name))
+ end)
+
+ local ok, err, errcode = fetch.fetch_and_unpack_rock(rock_file, install_dir, opts.verify)
+ if not ok then return nil, err, errcode end
+
+ local rockspec, err = fetch.load_rockspec(path.rockspec_file(name, version))
+ if err then
+ return nil, "Failed loading rockspec for installed package: "..err, errcode
+ end
+
+ if opts.deps_mode ~= "none" then
+ ok, err, errcode = deps.check_external_deps(rockspec, "install")
+ if err then return nil, err, errcode end
+ end
+
+ -- For compatibility with .rock files built with LuaRocks 1
+ if not fs.exists(path.rock_manifest_file(name, version)) then
+ ok, err = writer.make_rock_manifest(name, version)
+ if err then return nil, err end
+ end
+
+ if namespace then
+ ok, err = writer.make_namespace_file(name, version, namespace)
+ if err then return nil, err end
+ end
+
+ if deps_mode ~= "none" then
+ local deplock_dir = fs.exists(dir.path(".", "luarocks.lock"))
+ and "."
+ or install_dir
+ ok, err, errcode = deps.fulfill_dependencies(rockspec, "dependencies", deps_mode, opts.verify, deplock_dir)
+ if err then return nil, err, errcode end
+ end
+
+ ok, err = repos.deploy_files(name, version, repos.should_wrap_bin_scripts(rockspec), deps_mode)
+ if err then return nil, err end
+
+ util.remove_scheduled_function(rollback)
+ rollback = util.schedule_function(function()
+ repos.delete_version(name, version, deps_mode)
+ end)
+
+ ok, err = repos.run_hook(rockspec, "post_install")
+ if err then return nil, err end
+
+ util.announce_install(rockspec)
+ util.remove_scheduled_function(rollback)
+ return name, version
+end
+
+--- Installs the dependencies of a binary rock.
+-- @param rock_file string: local or remote filename of a rock.
+-- @param opts table: installation options
+-- @return (string, string) or (nil, string, [string]): Name and version of
+-- the rock whose dependencies were installed if succeeded or nil and an error message
+-- followed by an error code.
+function install.install_binary_rock_deps(rock_file, opts)
+ assert(type(rock_file) == "string")
+ assert(opts:type() == "install.opts")
+
+ local name, version, arch = path.parse_name(rock_file)
+ if not name then
+ return nil, "Filename "..rock_file.." does not match format 'name-version-revision.arch.rock'."
+ end
+
+ if arch ~= "all" and arch ~= cfg.arch then
+ return nil, "Incompatible architecture "..arch, "arch"
+ end
+
+ local install_dir = path.install_dir(name, version)
+
+ local ok, err, errcode = fetch.fetch_and_unpack_rock(rock_file, install_dir, opts.verify)
+ if not ok then return nil, err, errcode end
+
+ local rockspec, err = fetch.load_rockspec(path.rockspec_file(name, version))
+ if err then
+ return nil, "Failed loading rockspec for installed package: "..err, errcode
+ end
+
+ ok, err, errcode = deps.fulfill_dependencies(rockspec, "dependencies", opts.deps_mode, opts.verify, install_dir)
+ if err then return nil, err, errcode end
+
+ util.printout()
+ util.printout("Successfully installed dependencies for " ..name.." "..version)
+
+ return name, version
+end
+
+local function install_rock_file_deps(filename, opts)
+ assert(opts:type() == "install.opts")
+
+ local name, version = install.install_binary_rock_deps(filename, opts)
+ if not name then return nil, version end
+
+ writer.check_dependencies(nil, opts.deps_mode)
+ return name, version
+end
+
+local function install_rock_file(filename, opts)
+ assert(type(filename) == "string")
+ assert(opts:type() == "install.opts")
+
+ local name, version = install.install_binary_rock(filename, opts)
+ if not name then return nil, version end
+
+ if opts.no_doc then
+ util.remove_doc_dir(name, version)
+ end
+
+ if (not opts.keep) and not cfg.keep_other_versions then
+ local ok, err, warn = remove.remove_other_versions(name, version, opts.force, opts.force_fast)
+ if not ok then
+ return nil, err
+ elseif warn then
+ util.printerr(err)
+ end
+ end
+
+ writer.check_dependencies(nil, opts.deps_mode)
+ return name, version
+end
+
+--- Driver function for the "install" command.
+-- If an URL or pathname to a binary rock is given, fetches and installs it.
+-- If a rockspec or a source rock is given, forwards the request to the "build"
+-- command.
+-- If a package name is given, forwards the request to "search" and,
+-- if returned a result, installs the matching rock.
+-- @return boolean or (nil, string, exitcode): True if installation was
+-- successful, nil and an error message otherwise. exitcode is optionally returned.
+function install.command(args)
+ if args.rock:match("%.rockspec$") or args.rock:match("%.src%.rock$") then
+ local build = require("luarocks.cmd.build")
+ return build.command(args)
+ elseif args.rock:match("%.rock$") then
+ local deps_mode = deps.get_deps_mode(args)
+ local opts = install.opts({
+ namespace = args.namespace,
+ keep = not not args.keep,
+ force = not not args.force,
+ force_fast = not not args.force_fast,
+ no_doc = not not args.no_doc,
+ deps_mode = deps_mode,
+ verify = not not args.verify,
+ })
+ if args.only_deps then
+ return install_rock_file_deps(args.rock, opts)
+ else
+ return install_rock_file(args.rock, opts)
+ end
+ else
+ local url, err = search.find_rock_checking_lua_versions(
+ queries.new(args.rock, args.namespace, args.version),
+ args.check_lua_versions)
+ if not url then
+ return nil, err
+ end
+ util.printout("Installing "..url)
+ args.rock = url
+ return install.command(args)
+ end
+end
+
+install.needs_lock = function(args)
+ if args.pack_binary_rock then
+ return false
+ end
+ return true
+end
+
+return install
diff --git a/src/luarocks/cmd/lint.lua b/src/luarocks/cmd/lint.lua
new file mode 100644
index 0000000..738503c
--- /dev/null
+++ b/src/luarocks/cmd/lint.lua
@@ -0,0 +1,50 @@
+
+--- Module implementing the LuaRocks "lint" command.
+-- Utility function that checks syntax of the rockspec.
+local lint = {}
+
+local util = require("luarocks.util")
+local download = require("luarocks.download")
+local fetch = require("luarocks.fetch")
+
+function lint.add_to_parser(parser)
+ local cmd = parser:command("lint", "Check syntax of a rockspec.\n\n"..
+ "Returns success if the text of the rockspec is syntactically correct, else failure.",
+ util.see_also())
+ :summary("Check syntax of a rockspec.")
+
+ cmd:argument("rockspec", "The rockspec to check.")
+end
+
+function lint.command(args)
+
+ local filename = args.rockspec
+ if not filename:match(".rockspec$") then
+ local err
+ filename, err = download.download("rockspec", filename:lower())
+ if not filename then
+ return nil, err
+ end
+ end
+
+ local rs, err = fetch.load_local_rockspec(filename)
+ if not rs then
+ return nil, "Failed loading rockspec: "..err
+ end
+
+ local ok = true
+
+ -- This should have been done in the type checker,
+ -- but it would break compatibility of other commands.
+ -- Making 'lint' alone be stricter shouldn't be a problem,
+ -- because extra-strict checks is what lint-type commands
+ -- are all about.
+ if not rs.description or not rs.description.license then
+ util.printerr("Rockspec has no description.license field.")
+ ok = false
+ end
+
+ return ok, ok or filename.." failed consistency checks."
+end
+
+return lint
diff --git a/src/luarocks/cmd/list.lua b/src/luarocks/cmd/list.lua
new file mode 100644
index 0000000..7b2682f
--- /dev/null
+++ b/src/luarocks/cmd/list.lua
@@ -0,0 +1,96 @@
+
+--- Module implementing the LuaRocks "list" command.
+-- Lists currently installed rocks.
+local list = {}
+
+local search = require("luarocks.search")
+local queries = require("luarocks.queries")
+local vers = require("luarocks.core.vers")
+local cfg = require("luarocks.core.cfg")
+local util = require("luarocks.util")
+local path = require("luarocks.path")
+
+function list.add_to_parser(parser)
+ local cmd = parser:command("list", "List currently installed rocks.", util.see_also())
+
+ cmd:argument("filter", "A substring of a rock name to filter by.")
+ :args("?")
+ cmd:argument("version", "Rock version to filter by.")
+ :args("?")
+
+ cmd:flag("--outdated", "List only rocks for which there is a higher "..
+ "version available in the rocks server.")
+ cmd:flag("--porcelain", "Produce machine-friendly output.")
+end
+
+local function check_outdated(trees, query)
+ local results_installed = {}
+ for _, tree in ipairs(trees) do
+ search.local_manifest_search(results_installed, path.rocks_dir(tree), query)
+ end
+ local outdated = {}
+ for name, versions in util.sortedpairs(results_installed) do
+ versions = util.keys(versions)
+ table.sort(versions, vers.compare_versions)
+ local latest_installed = versions[1]
+
+ local query_available = queries.new(name:lower())
+ local results_available, err = search.search_repos(query_available)
+
+ if results_available[name] then
+ local available_versions = util.keys(results_available[name])
+ table.sort(available_versions, vers.compare_versions)
+ local latest_available = available_versions[1]
+ local latest_available_repo = results_available[name][latest_available][1].repo
+
+ if vers.compare_versions(latest_available, latest_installed) then
+ table.insert(outdated, { name = name, installed = latest_installed, available = latest_available, repo = latest_available_repo })
+ end
+ end
+ end
+ return outdated
+end
+
+local function list_outdated(trees, query, porcelain)
+ util.title("Outdated rocks:", porcelain)
+ local outdated = check_outdated(trees, query)
+ for _, item in ipairs(outdated) do
+ if porcelain then
+ util.printout(item.name, item.installed, item.available, item.repo)
+ else
+ util.printout(item.name)
+ util.printout(" "..item.installed.." < "..item.available.." at "..item.repo)
+ util.printout()
+ end
+ end
+ return true
+end
+
+--- Driver function for "list" command.
+-- @return boolean: True if succeeded, nil on errors.
+function list.command(args)
+ local query = queries.new(args.filter and args.filter:lower() or "", args.namespace, args.version, true)
+ local trees = cfg.rocks_trees
+ local title = "Rocks installed for Lua "..cfg.lua_version
+ if args.tree then
+ trees = { args.tree }
+ title = title .. " in " .. args.tree
+ end
+
+ if args.outdated then
+ return list_outdated(trees, query, args.porcelain)
+ end
+
+ local results = {}
+ for _, tree in ipairs(trees) do
+ local ok, err, errcode = search.local_manifest_search(results, path.rocks_dir(tree), query)
+ if not ok and errcode ~= "open" then
+ util.warning(err)
+ end
+ end
+ util.title(title, args.porcelain)
+ search.print_result_tree(results, args.porcelain)
+ return true
+end
+
+return list
diff --git a/src/luarocks/cmd/make.lua b/src/luarocks/cmd/make.lua
new file mode 100644
index 0000000..0b3db27
--- /dev/null
+++ b/src/luarocks/cmd/make.lua
@@ -0,0 +1,163 @@
+
+--- Module implementing the LuaRocks "make" command.
+-- Builds sources in the current directory, but unlike "build",
+-- it does not fetch sources, etc., assuming everything is
+-- available in the current directory.
+local make = {}
+
+local build = require("luarocks.build")
+local util = require("luarocks.util")
+local cfg = require("luarocks.core.cfg")
+local fetch = require("luarocks.fetch")
+local pack = require("luarocks.pack")
+local remove = require("luarocks.remove")
+local deps = require("luarocks.deps")
+local writer = require("luarocks.manif.writer")
+
+function make.cmd_options(parser)
+ parser:flag("--no-install", "Do not install the rock.")
+ parser:flag("--no-doc", "Install the rock without its documentation.")
+ parser:flag("--pack-binary-rock", "Do not install rock. Instead, produce a "..
+ ".rock file with the contents of compilation in the current directory.")
+ parser:flag("--keep", "Do not remove previously installed versions of the "..
+ "rock after building a new one. This behavior can be made permanent by "..
+ "setting keep_other_versions=true in the configuration file.")
+ parser:flag("--force", "If --keep is not specified, force removal of "..
+ "previously installed versions if it would break dependencies. "..
+ "If rock is already installed, reinstall it anyway.")
+ parser:flag("--force-fast", "Like --force, but performs a forced removal "..
+ "without reporting dependency issues.")
+ parser:flag("--verify", "Verify signature of the rockspec or src.rock being "..
+ "built. If the rockspec or src.rock is being downloaded, LuaRocks will "..
+ "attempt to download the signature as well. Otherwise, the signature "..
+ "file should be already available locally in the same directory.\n"..
+ "You need the signer’s public key in your local keyring for this "..
+ "option to work properly.")
+ parser:flag("--sign", "To be used with --pack-binary-rock. Also produce a "..
+ "signature file for the generated .rock file.")
+ parser:flag("--check-lua-versions", "If the rock can't be found, check repository "..
+ "and report if it is available for another Lua version.")
+ parser:flag("--pin", "Pin the exact dependencies used for the rockspec"..
+ "being built into a luarocks.lock file in the current directory.")
+ parser:flag("--no-manifest", "Skip creating/updating the manifest")
+ parser:flag("--only-deps --deps-only", "Install only the dependencies of the rock.")
+ util.deps_mode_option(parser)
+end
+
+function make.add_to_parser(parser)
+ -- luacheck: push ignore 431
+ local cmd = parser:command("make", [[
+Builds sources in the current directory, but unlike "build", it does not fetch
+sources, etc., assuming everything is available in the current directory. If no
+argument is given, it looks for a rockspec in the current directory and in
+"rockspec/" and "rockspecs/" subdirectories, picking the rockspec with newest
+version or without version name. If rockspecs for different rocks are found or
+there are several rockspecs without version, you must specify which to use,
+through the command-line.
+
+This command is useful as a tool for debugging rockspecs.
+To install rocks, you'll normally want to use the "install" and "build"
+commands. See the help on those for details.
+
+If the current directory contains a luarocks.lock file, it is used as the
+authoritative source for exact version of dependencies. The --pin flag
+overrides and recreates this file scanning dependency based on ranges.
+]], util.see_also())
+ :summary("Compile package in current directory using a rockspec.")
+ -- luacheck: pop
+
+ cmd:argument("rockspec", "Rockspec for the rock to build.")
+ :args("?")
+
+ make.cmd_options(cmd)
+end
+
+--- Driver function for "make" command.
+-- @return boolean or (nil, string, exitcode): True if build was successful; nil and an
+-- error message otherwise. exitcode is optionally returned.
+function make.command(args)
+ local rockspec_filename = args.rockspec
+ if not rockspec_filename then
+ local err
+ rockspec_filename, err = util.get_default_rockspec()
+ if not rockspec_filename then
+ return nil, err
+ end
+ end
+ if not rockspec_filename:match("rockspec$") then
+ return nil, "Invalid argument: 'make' takes a rockspec as a parameter. "..util.see_help("make")
+ end
+
+ local rockspec, err, errcode = fetch.load_rockspec(rockspec_filename)
+ if not rockspec then
+ return nil, err
+ end
+
+ local name, namespace = util.split_namespace(rockspec.name)
+ namespace = namespace or args.namespace
+
+ local opts = build.opts({
+ need_to_fetch = false,
+ minimal_mode = true,
+ deps_mode = deps.get_deps_mode(args),
+ build_only_deps = not not (args.only_deps and not args.pack_binary_rock),
+ namespace = namespace,
+ branch = args.branch,
+ verify = not not args.verify,
+ check_lua_versions = not not args.check_lua_versions,
+ pin = not not args.pin,
+ rebuild = true,
+ no_install = not not args.no_install
+ })
+
+ if args.sign and not args.pack_binary_rock then
+ return nil, "In the make command, --sign is meant to be used only with --pack-binary-rock"
+ end
+
+ if args.no_install then
+ return build.build_rockspec(rockspec, opts)
+ elseif args.pack_binary_rock then
+ return pack.pack_binary_rock(name, namespace, rockspec.version, args.sign, function()
+ local name, version = build.build_rockspec(rockspec, opts) -- luacheck: ignore 431
+ if name and args.no_doc then
+ util.remove_doc_dir(name, version)
+ end
+ return name, version
+ end)
+ else
+ local ok, err = build.build_rockspec(rockspec, opts)
+ if not ok then return nil, err end
+ local name, version = ok, err -- luacheck: ignore 421
+
+ if opts.build_only_deps then
+ util.printout("Stopping after installing dependencies for " ..name.." "..version)
+ util.printout()
+ return name, version
+ end
+
+ if args.no_doc then
+ util.remove_doc_dir(name, version)
+ end
+
+ if (not args.keep) and not cfg.keep_other_versions then
+ local ok, err, warn = remove.remove_other_versions(name, version, args.force, args.force_fast)
+ if not ok then
+ return nil, err
+ elseif warn then
+ util.printerr(warn)
+ end
+ end
+
+ writer.check_dependencies(nil, deps.get_deps_mode(args))
+ return name, version
+ end
+end
+
+make.needs_lock = function(args)
+ if args.pack_binary_rock or args.no_install then
+ return false
+ end
+ return true
+end
+
+return make
diff --git a/src/luarocks/cmd/new_version.lua b/src/luarocks/cmd/new_version.lua
new file mode 100644
index 0000000..ccba933
--- /dev/null
+++ b/src/luarocks/cmd/new_version.lua
@@ -0,0 +1,228 @@
+
+--- Module implementing the LuaRocks "new_version" command.
+-- Utility function that writes a new rockspec, updating data from a previous one.
+local new_version = {}
+
+local util = require("luarocks.util")
+local download = require("luarocks.download")
+local fetch = require("luarocks.fetch")
+local persist = require("luarocks.persist")
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+local type_rockspec = require("luarocks.type.rockspec")
+
+function new_version.add_to_parser(parser)
+ local cmd = parser:command("new_version", [[
+This is a utility function that writes a new rockspec, updating data from a
+previous one.
+
+If a package name is given, it downloads the latest rockspec from the default
+server. If a rockspec is given, it uses it instead. If no argument is given, it
+looks for a rockspec same way 'luarocks make' does.
+
+If the version number is not given and tag is passed using --tag, it is used as
+the version, with 'v' removed from beginning. Otherwise, it only increments the
+revision number of the given (or downloaded) rockspec.
+
+If a URL is given, it replaces the one from the old rockspec with the given URL.
+If a URL is not given and a new version is given, it tries to guess the new URL
+by replacing occurrences of the version number in the URL or tag; if the guessed
+URL is invalid, the old URL is restored. It also tries to download the new URL
+to determine the new MD5 checksum.
+
+If a tag is given, it replaces the one from the old rockspec. If there is an old
+tag but no new one passed, it is guessed in the same way URL is.
+
+If a directory is not given, it defaults to the current directory.
+
+WARNING: it writes the new rockspec to the given directory, overwriting the file
+if it already exists.]], util.see_also())
+ :summary("Auto-write a rockspec for a new version of a rock.")
+
+ cmd:argument("rock", "Package name or rockspec.")
+ :args("?")
+ cmd:argument("new_version", "New version of the rock.")
+ :args("?")
+ cmd:argument("new_url", "New URL of the rock.")
+ :args("?")
+
+ cmd:option("--dir", "Output directory for the new rockspec.")
+ cmd:option("--tag", "New SCM tag.")
+end
+
+
+local function try_replace(tbl, field, old, new)
+ if not tbl[field] then
+ return false
+ end
+ local old_field = tbl[field]
+ local new_field = tbl[field]:gsub(old, new)
+ if new_field ~= old_field then
+ util.printout("Guessing new '"..field.."' field as "..new_field)
+ tbl[field] = new_field
+ return true
+ end
+ return false
+end
+
+-- Try to download source file using URL from a rockspec.
+-- If it specified MD5, update it.
+-- @return (true, false) if MD5 was not specified or it stayed same,
+-- (true, true) if MD5 changed, (nil, string) on error.
+local function check_url_and_update_md5(out_rs, invalid_is_error)
+ local file, temp_dir = fetch.fetch_url_at_temp_dir(out_rs.source.url, "luarocks-new-version-"..out_rs.package)
+ if not file then
+ if invalid_is_error then
+ return nil, "invalid URL - "..temp_dir
+ end
+ util.warning("invalid URL - "..temp_dir)
+ return true, false
+ end
+ do
+ local inferred_dir, found_dir = fetch.find_base_dir(file, temp_dir, out_rs.source.url, out_rs.source.dir)
+ if not inferred_dir then
+ return nil, found_dir
+ end
+
+ if found_dir and found_dir ~= inferred_dir then
+ out_rs.source.dir = found_dir
+ end
+ end
+ if file then
+ if out_rs.source.md5 then
+ util.printout("File successfully downloaded. Updating MD5 checksum...")
+ local new_md5, err = fs.get_md5(file)
+ if not new_md5 then
+ return nil, err
+ end
+ local old_md5 = out_rs.source.md5
+ out_rs.source.md5 = new_md5
+ return true, new_md5 ~= old_md5
+ else
+ util.printout("File successfully downloaded.")
+ return true, false
+ end
+ end
+end
+
+local function update_source_section(out_rs, url, tag, old_ver, new_ver)
+ if tag then
+ out_rs.source.tag = tag
+ end
+ if url then
+ out_rs.source.url = url
+ return check_url_and_update_md5(out_rs)
+ end
+ if new_ver == old_ver then
+ return true
+ end
+ if out_rs.source.dir then
+ try_replace(out_rs.source, "dir", old_ver, new_ver)
+ end
+ if out_rs.source.file then
+ try_replace(out_rs.source, "file", old_ver, new_ver)
+ end
+
+ local old_url = out_rs.source.url
+ if try_replace(out_rs.source, "url", old_ver, new_ver) then
+ local ok, md5_changed = check_url_and_update_md5(out_rs, true)
+ if ok then
+ return ok, md5_changed
+ end
+ out_rs.source.url = old_url
+ end
+ if tag or try_replace(out_rs.source, "tag", old_ver, new_ver) then
+ return true
+ end
+ -- Couldn't replace anything significant, use the old URL.
+ local ok, md5_changed = check_url_and_update_md5(out_rs)
+ if not ok then
+ return nil, md5_changed
+ end
+ if md5_changed then
+ util.warning("URL is the same, but MD5 has changed. Old rockspec is broken.")
+ end
+ return true
+end
+
+function new_version.command(args)
+ if not args.rock then
+ local err
+ args.rock, err = util.get_default_rockspec()
+ if not args.rock then
+ return nil, err
+ end
+ end
+
+ local filename, err
+ if args.rock:match("rockspec$") then
+ filename, err = fetch.fetch_url(args.rock)
+ if not filename then
+ return nil, err
+ end
+ else
+ filename, err = download.download("rockspec", args.rock:lower())
+ if not filename then
+ return nil, err
+ end
+ end
+
+ local valid_rs, err = fetch.load_rockspec(filename)
+ if not valid_rs then
+ return nil, err
+ end
+
+ local old_ver, old_rev = valid_rs.version:match("(.*)%-(%d+)$")
+ local new_ver, new_rev
+
+ if args.tag and not args.new_version then
+ args.new_version = args.tag:gsub("^v", "")
+ end
+
+ local out_dir
+ if args.dir then
+ out_dir = dir.normalize(args.dir)
+ end
+
+ if args.new_version then
+ new_ver, new_rev = args.new_version:match("(.*)%-(%d+)$")
+ new_rev = tonumber(new_rev)
+ if not new_rev then
+ new_ver = args.new_version
+ new_rev = 1
+ end
+ else
+ new_ver = old_ver
+ new_rev = tonumber(old_rev) + 1
+ end
+ local new_rockver = new_ver:gsub("-", "")
+
+ local out_rs, err = persist.load_into_table(filename)
+ local out_name = out_rs.package:lower()
+ out_rs.version = new_rockver.."-"..new_rev
+
+ local ok, err = update_source_section(out_rs, args.new_url, args.tag, old_ver, new_ver)
+ if not ok then return nil, err end
+
+ if out_rs.build and out_rs.build.type == "module" then
+ out_rs.build.type = "builtin"
+ end
+
+ local out_filename = out_name.."-"..new_rockver.."-"..new_rev..".rockspec"
+ if out_dir then
+ out_filename = dir.path(out_dir, out_filename)
+ fs.make_dir(out_dir)
+ end
+ persist.save_from_table(out_filename, out_rs, type_rockspec.order)
+
+ util.printout("Wrote "..out_filename)
+
+ local valid_out_rs, err = fetch.load_local_rockspec(out_filename)
+ if not valid_out_rs then
+ return nil, "Failed loading generated rockspec: "..err
+ end
+
+ return true
+end
+
+return new_version
diff --git a/src/luarocks/cmd/pack.lua b/src/luarocks/cmd/pack.lua
new file mode 100644
index 0000000..29a43e7
--- /dev/null
+++ b/src/luarocks/cmd/pack.lua
@@ -0,0 +1,36 @@
+
+--- Module implementing the LuaRocks "pack" command.
+-- Creates a rock, packing sources or binaries.
+local cmd_pack = {}
+
+local util = require("luarocks.util")
+local pack = require("luarocks.pack")
+local queries = require("luarocks.queries")
+
+function cmd_pack.add_to_parser(parser)
+ local cmd = parser:command("pack", "Create a rock, packing sources or binaries.", util.see_also())
+
+ cmd:argument("rock", "A rockspec file, for creating a source rock, or the "..
+ "name of an installed package, for creating a binary rock.")
+ :action(util.namespaced_name_action)
+ cmd:argument("version", "A version may be given if the first argument is a rock name.")
+ :args("?")
+
+ cmd:flag("--sign", "Produce a signature file as well.")
+end
+
+--- Driver function for the "pack" command.
+-- @return boolean or (nil, string): true if successful or nil followed
+-- by an error message.
+function cmd_pack.command(args)
+ local file, err
+ if args.rock:match(".*%.rockspec") then
+ file, err = pack.pack_source_rock(args.rock)
+ else
+ local query = queries.new(args.rock, args.namespace, args.version)
+ file, err = pack.pack_installed_rock(query, args.tree)
+ end
+ return pack.report_and_sign_local_file(file, err, args.sign)
+end
+
+return cmd_pack
diff --git a/src/luarocks/cmd/path.lua b/src/luarocks/cmd/path.lua
new file mode 100644
index 0000000..ba34655
--- /dev/null
+++ b/src/luarocks/cmd/path.lua
@@ -0,0 +1,83 @@
+
+--- @module luarocks.path_cmd
+-- Driver for the `luarocks path` command.
+local path_cmd = {}
+
+local util = require("luarocks.util")
+local cfg = require("luarocks.core.cfg")
+local fs = require("luarocks.fs")
+
+function path_cmd.add_to_parser(parser)
+ local cmd = parser:command("path", [[
+Returns the package path currently configured for this installation
+of LuaRocks, formatted as shell commands to update LUA_PATH and LUA_CPATH.
+
+On Unix systems, you may run:
+ eval `luarocks path`
+And on Windows:
+ luarocks path > "%temp%\_lrp.bat"
+ call "%temp%\_lrp.bat" && del "%temp%\_lrp.bat"]],
+ util.see_also())
+ :summary("Return the currently configured package path.")
+
+ cmd:flag("--no-bin", "Do not export the PATH variable.")
+ cmd:flag("--append", "Appends the paths to the existing paths. Default is "..
+ "to prefix the LR paths to the existing paths.")
+ cmd:flag("--lr-path", "Prints Lua path components defined by the configured rocks trees " ..
+ "(not formatted as a shell command)")
+ cmd:flag("--lr-cpath", "Prints Lua cpath components defined by the configured rocks trees " ..
+ "(not formatted as a shell command)")
+ cmd:flag("--full", "By default, --lr-path and --lr-cpath only include the paths " ..
+ "derived by the LuaRocks rocks_trees. Using --full includes any other components " ..
+ "defined in your system's package.(c)path, either via the running interpreter's " ..
+ "default paths or via LUA_(C)PATH(_5_x) environment variables (in short, using " ..
+ "--full produces the same lists as shown in the shell outputs of 'luarocks path').")
+ cmd:flag("--lr-bin", "Exports the system path (not formatted as shell command).")
+ cmd:flag("--bin"):hidden(true)
+end
+
+--- Driver function for "path" command.
+-- @return boolean This function always succeeds.
+function path_cmd.command(args)
+ local lr_path, lr_cpath, lr_bin = cfg.package_paths(args.tree)
+ local path_sep = cfg.export_path_separator
+
+ local full_list = ((not args.lr_path) and (not args.lr_cpath) and (not args.lr_bin))
+ or args.full
+
+ local clean_path = util.cleanup_path(os.getenv("PATH") or "", path_sep, nil, true)
+
+ if full_list then
+ if args.append then
+ lr_path = package.path .. ";" .. lr_path
+ lr_cpath = package.cpath .. ";" .. lr_cpath
+ lr_bin = clean_path .. path_sep .. lr_bin
+ else
+ lr_path = lr_path.. ";" .. package.path
+ lr_cpath = lr_cpath .. ";" .. package.cpath
+ lr_bin = lr_bin .. path_sep .. clean_path
+ end
+ end
+
+ if args.lr_path then
+ util.printout(util.cleanup_path(lr_path, ';', cfg.lua_version, true))
+ return true
+ elseif args.lr_cpath then
+ util.printout(util.cleanup_path(lr_cpath, ';', cfg.lua_version, true))
+ return true
+ elseif args.lr_bin then
+ util.printout(util.cleanup_path(lr_bin, path_sep, nil, true))
+ return true
+ end
+
+ local lpath_var, lcpath_var = util.lua_path_variables()
+
+ util.printout(fs.export_cmd(lpath_var, util.cleanup_path(lr_path, ';', cfg.lua_version, args.append)))
+ util.printout(fs.export_cmd(lcpath_var, util.cleanup_path(lr_cpath, ';', cfg.lua_version, args.append)))
+ if not args.no_bin then
+ util.printout(fs.export_cmd("PATH", util.cleanup_path(lr_bin, path_sep, nil, args.append)))
+ end
+ return true
+end
+
+return path_cmd
diff --git a/src/luarocks/cmd/purge.lua b/src/luarocks/cmd/purge.lua
new file mode 100644
index 0000000..30811dd
--- /dev/null
+++ b/src/luarocks/cmd/purge.lua
@@ -0,0 +1,73 @@
+
+--- Module implementing the LuaRocks "purge" command.
+-- Remove all rocks from a given tree.
+local purge = {}
+
+local util = require("luarocks.util")
+local path = require("luarocks.path")
+local search = require("luarocks.search")
+local vers = require("luarocks.core.vers")
+local repos = require("luarocks.repos")
+local writer = require("luarocks.manif.writer")
+local cfg = require("luarocks.core.cfg")
+local remove = require("luarocks.remove")
+local queries = require("luarocks.queries")
+
+function purge.add_to_parser(parser)
+ -- luacheck: push ignore 431
+ local cmd = parser:command("purge", [[
+This command removes rocks en masse from a given tree.
+By default, it removes all rocks from a tree.
+
+The --tree option is mandatory: luarocks purge does not assume a default tree.]],
+ util.see_also())
+ :summary("Remove all installed rocks from a tree.")
+ -- luacheck: pop
+
+ cmd:flag("--old-versions", "Keep the highest-numbered version of each "..
+ "rock and remove the other ones. By default it only removes old "..
+ "versions if they are not needed as dependencies. This can be "..
+ "overridden with the flag --force.")
+ cmd:flag("--force", "If --old-versions is specified, force removal of "..
+ "previously installed versions if it would break dependencies.")
+ cmd:flag("--force-fast", "Like --force, but performs a forced removal "..
+ "without reporting dependency issues.")
+end
+
+function purge.command(args)
+ local tree = args.tree
+
+ local results = {}
+ search.local_manifest_search(results, path.rocks_dir(tree), queries.all())
+
+ local sort = function(a,b) return vers.compare_versions(b,a) end
+ if args.old_versions then
+ sort = vers.compare_versions
+ end
+
+ for package, versions in util.sortedpairs(results) do
+ for version, _ in util.sortedpairs(versions, sort) do
+ if args.old_versions then
+ util.printout("Keeping "..package.." "..version.."...")
+ local ok, err, warn = remove.remove_other_versions(package, version, args.force, args.force_fast)
+ if not ok then
+ util.printerr(err)
+ elseif warn then
+ util.printerr(err)
+ end
+ break
+ else
+ util.printout("Removing "..package.." "..version.."...")
+ local ok, err = repos.delete_version(package, version, "none", true)
+ if not ok then
+ util.printerr(err)
+ end
+ end
+ end
+ end
+ return writer.make_manifest(cfg.rocks_dir, "one")
+end
+
+purge.needs_lock = function() return true end
+
+return purge
diff --git a/src/luarocks/cmd/remove.lua b/src/luarocks/cmd/remove.lua
new file mode 100644
index 0000000..8b11bcd
--- /dev/null
+++ b/src/luarocks/cmd/remove.lua
@@ -0,0 +1,72 @@
+
+--- Module implementing the LuaRocks "remove" command.
+-- Uninstalls rocks.
+local cmd_remove = {}
+
+local remove = require("luarocks.remove")
+local util = require("luarocks.util")
+local cfg = require("luarocks.core.cfg")
+local search = require("luarocks.search")
+local path = require("luarocks.path")
+local deps = require("luarocks.deps")
+local writer = require("luarocks.manif.writer")
+local queries = require("luarocks.queries")
+
+function cmd_remove.add_to_parser(parser)
+ -- luacheck: push ignore 431
+ local cmd = parser:command("remove", [[
+Uninstall a rock.
+
+If a version is not given, try to remove all versions at once.
+Will only perform the removal if it does not break dependencies.
+To override this check and force the removal, use --force or --force-fast.]],
+ util.see_also())
+ :summary("Uninstall a rock.")
+ -- luacheck: pop
+
+ cmd:argument("rock", "Name of the rock to be uninstalled.")
+ :action(util.namespaced_name_action)
+ cmd:argument("version", "Version of the rock to uninstall.")
+ :args("?")
+
+ cmd:flag("--force", "Force removal if it would break dependencies.")
+ cmd:flag("--force-fast", "Perform a forced removal without reporting dependency issues.")
+ util.deps_mode_option(cmd)
+end
+
+--- Driver function for the "remove" command.
+-- @return boolean or (nil, string, exitcode): True if removal was
+-- successful, nil and an error message otherwise. exitcode is optionally returned.
+function cmd_remove.command(args)
+ local name = args.rock
+ local deps_mode = deps.get_deps_mode(args)
+
+ local rock_type = name:match("%.(rock)$") or name:match("%.(rockspec)$")
+ local version = args.version
+ local filename = name
+ if rock_type then
+ name, version = path.parse_name(filename)
+ if not name then return nil, "Invalid "..rock_type.." filename: "..filename end
+ end
+
+ name = name:lower()
+
+ local results = {}
+ search.local_manifest_search(results, cfg.rocks_dir, queries.new(name, args.namespace, version))
+ if not results[name] then
+ local rock = util.format_rock_name(name, args.namespace, version)
+ return nil, "Could not find rock '"..rock.."' in "..path.rocks_tree_to_string(cfg.root_dir)
+ end
+
+ local ok, err = remove.remove_search_results(results, name, deps_mode, args.force, args.force_fast)
+ if not ok then
+ return nil, err
+ end
+
+ writer.check_dependencies(nil, deps.get_deps_mode(args))
+ return true
+end
+
+cmd_remove.needs_lock = function() return true end
+
+return cmd_remove
diff --git a/src/luarocks/cmd/search.lua b/src/luarocks/cmd/search.lua
new file mode 100644
index 0000000..6cab6d8
--- /dev/null
+++ b/src/luarocks/cmd/search.lua
@@ -0,0 +1,84 @@
+
+--- Module implementing the LuaRocks "search" command.
+-- Queries LuaRocks servers.
+local cmd_search = {}
+
+local cfg = require("luarocks.core.cfg")
+local util = require("luarocks.util")
+local search = require("luarocks.search")
+local queries = require("luarocks.queries")
+local results = require("luarocks.results")
+
+function cmd_search.add_to_parser(parser)
+ local cmd = parser:command("search", "Query the LuaRocks servers.", util.see_also())
+
+ cmd:argument("name", "Name of the rock to search for.")
+ :args("?")
+ :action(util.namespaced_name_action)
+ cmd:argument("version", "Rock version to search for.")
+ :args("?")
+
+ cmd:flag("--source", "Return only rockspecs and source rocks, to be used "..
+ 'with the "build" command.')
+ cmd:flag("--binary", "Return only pure Lua and binary rocks (rocks that "..
+ 'can be used with the "install" command without requiring a C toolchain).')
+ cmd:flag("--all", "List all contents of the server that are suitable to "..
+ "this platform, do not filter by name.")
+ cmd:flag("--porcelain", "Return a machine readable format.")
+end
+
+--- Splits a list of search results into two lists, one for "source" results
+-- to be used with the "build" command, and one for "binary" results to be
+-- used with the "install" command.
+-- @param result_tree table: A search results table.
+-- @return (table, table): Two tables, one for source and one for binary
+-- results.
+local function split_source_and_binary_results(result_tree)
+ local sources, binaries = {}, {}
+ for name, versions in pairs(result_tree) do
+ for version, repositories in pairs(versions) do
+ for _, repo in ipairs(repositories) do
+ local where = sources
+ if repo.arch == "all" or repo.arch == cfg.arch then
+ where = binaries
+ end
+ local entry = results.new(name, version, repo.repo, repo.arch)
+ search.store_result(where, entry)
+ end
+ end
+ end
+ return sources, binaries
+end
+
+--- Driver function for "search" command.
+-- @return boolean or (nil, string): True if build was successful; nil and an
+-- error message otherwise.
+function cmd_search.command(args)
+ local name = args.name
+
+ if args.all then
+ name, args.version = "", nil
+ end
+
+ if not args.name and not args.all then
+ return nil, "Enter name and version or use --all. "..util.see_help("search")
+ end
+
+ local query = queries.new(name, args.namespace, args.version, true)
+ local result_tree, err = search.search_repos(query)
+ local porcelain = args.porcelain
+ local full_name = util.format_rock_name(name, args.namespace, args.version)
+ util.title(full_name .. " - Search results for Lua "..cfg.lua_version..":", porcelain, "=")
+ local sources, binaries = split_source_and_binary_results(result_tree)
+ if next(sources) and not args.binary then
+ util.title("Rockspecs and source rocks:", porcelain)
+ search.print_result_tree(sources, porcelain)
+ end
+ if next(binaries) and not args.source then
+ util.title("Binary and pure-Lua rocks:", porcelain)
+ search.print_result_tree(binaries, porcelain)
+ end
+ return true
+end
+
+return cmd_search
diff --git a/src/luarocks/cmd/show.lua b/src/luarocks/cmd/show.lua
new file mode 100644
index 0000000..88cbbad
--- /dev/null
+++ b/src/luarocks/cmd/show.lua
@@ -0,0 +1,314 @@
+--- Module implementing the LuaRocks "show" command.
+-- Shows information about an installed rock.
+local show = {}
+
+local queries = require("luarocks.queries")
+local search = require("luarocks.search")
+local dir = require("luarocks.core.dir")
+local fs = require("luarocks.fs")
+local cfg = require("luarocks.core.cfg")
+local util = require("luarocks.util")
+local path = require("luarocks.path")
+local fetch = require("luarocks.fetch")
+local manif = require("luarocks.manif")
+local repos = require("luarocks.repos")
+
+function show.add_to_parser(parser)
+ local cmd = parser:command("show", [[
+Show information about an installed rock.
+
+Without any flags, show all module information.
+With flags, return only the desired information.]], util.see_also())
+ :summary("Show information about an installed rock.")
+
+ cmd:argument("rock", "Name of an installed rock.")
+ :action(util.namespaced_name_action)
+ cmd:argument("version", "Rock version.")
+ :args("?")
+
+ cmd:flag("--home", "Show home page of project.")
+ cmd:flag("--modules", "Show all modules provided by the package as used by require().")
+ cmd:flag("--deps", "Show packages the package depends on.")
+ cmd:flag("--build-deps", "Show build-only dependencies for the package.")
+ cmd:flag("--test-deps", "Show dependencies for testing the package.")
+ cmd:flag("--rockspec", "Show the full path of the rockspec file.")
+ cmd:flag("--mversion", "Show the package version.")
+ cmd:flag("--rock-tree", "Show local tree where rock is installed.")
+ cmd:flag("--rock-namespace", "Show rock namespace.")
+ cmd:flag("--rock-dir", "Show data directory of the installed rock.")
+ cmd:flag("--rock-license", "Show rock license.")
+ cmd:flag("--issues", "Show URL for project's issue tracker.")
+ cmd:flag("--labels", "List the labels of the rock.")
+ cmd:flag("--porcelain", "Produce machine-friendly output.")
+end
+
+local friendly_template = [[
+ :
+?namespace:${namespace}/${package} ${version} - ${summary}
+!namespace:${package} ${version} - ${summary}
+ :
+*detailed :${detailed}
+?detailed :
+?license :License: \t${license}
+?homepage :Homepage: \t${homepage}
+?issues :Issues: \t${issues}
+?labels :Labels: \t${labels}
+?location :Installed in: \t${location}
+?commands :
+?commands :Commands:
+*commands :\t${name} (${file})
+?modules :
+?modules :Modules:
+*modules :\t${name} (${file})
+?bdeps :
+?bdeps :Has build dependency on:
+*bdeps :\t${name} (${label})
+?tdeps :
+?tdeps :Tests depend on:
+*tdeps :\t${name} (${label})
+?deps :
+?deps :Depends on:
+*deps :\t${name} (${label})
+?ideps :
+?ideps :Indirectly pulling:
+*ideps :\t${name} (${label})
+ :
+]]
+
+local porcelain_template = [[
+?namespace:namespace\t${namespace}
+?package :package\t${package}
+?version :version\t${version}
+?summary :summary\t${summary}
+*detailed :detailed\t${detailed}
+?license :license\t${license}
+?homepage :homepage\t${homepage}
+?issues :issues\t${issues}
+?labels :labels\t${labels}
+?location :location\t${location}
+*commands :command\t${name}\t${file}
+*modules :module\t${name}\t${file}
+*bdeps :build_dependency\t${name}\t${label}
+*tdeps :test_dependency\t${name}\t${label}
+*deps :dependency\t${name}\t${label}
+*ideps :indirect_dependency\t${name}\t${label}
+]]
+
+local function keys_as_string(t, sep)
+ local keys = util.keys(t)
+ table.sort(keys)
+ return table.concat(keys, sep or " ")
+end
+
+local function word_wrap(line)
+ local width = tonumber(os.getenv("COLUMNS")) or 80
+ if width > 80 then width = 80 end
+ if #line > width then
+ local brk = width
+ while brk > 0 and line:sub(brk, brk) ~= " " do
+ brk = brk - 1
+ end
+ if brk > 0 then
+ return line:sub(1, brk-1) .. "\n" .. word_wrap(line:sub(brk+1))
+ end
+ end
+ return line
+end
+
+local function format_text(text)
+ text = text:gsub("^%s*",""):gsub("%s$", ""):gsub("\n[ \t]+","\n"):gsub("([^\n])\n([^\n])","%1 %2")
+ local paragraphs = util.split_string(text, "\n\n")
+ for n, line in ipairs(paragraphs) do
+ paragraphs[n] = word_wrap(line)
+ end
+ return (table.concat(paragraphs, "\n\n"):gsub("%s$", ""))
+end
+
+local function installed_rock_label(dep, tree)
+ local installed, version
+ local rocks_provided = util.get_rocks_provided()
+ if rocks_provided[dep.name] then
+ installed, version = true, rocks_provided[dep.name]
+ else
+ installed, version = search.pick_installed_rock(dep, tree)
+ end
+ return installed and "using "..version or "missing"
+end
+
+local function render(template, data)
+ local out = {}
+ for cmd, var, line in template:gmatch("(.)([a-z]*)%s*:([^\n]*)\n") do
+ line = line:gsub("\\t", "\t")
+ local d = data[var]
+ if cmd == " " then
+ table.insert(out, line)
+ elseif cmd == "?" or cmd == "*" or cmd == "!" then
+ if (cmd == "!" and d == nil)
+ or (cmd ~= "!" and (type(d) == "string"
+ or (type(d) == "table" and next(d)))) then
+ local n = cmd == "*" and #d or 1
+ for i = 1, n do
+ local tbl = cmd == "*" and d[i] or data
+ if type(tbl) == "string" then
+ tbl = tbl:gsub("%%", "%%%%")
+ end
+ table.insert(out, (line:gsub("${([a-z]+)}", tbl)))
+ end
+ end
+ end
+ end
+ return table.concat(out, "\n")
+end
+
+local function adjust_path(name, version, basedir, pathname, suffix)
+ pathname = dir.path(basedir, pathname)
+ local vpathname = path.versioned_name(pathname, basedir, name, version)
+ return (fs.exists(vpathname)
+ and vpathname
+ or pathname) .. (suffix or "")
+end
+
+local function modules_to_list(name, version, repo)
+ local ret = {}
+ local rock_manifest = manif.load_rock_manifest(name, version, repo)
+
+ local lua_dir = path.deploy_lua_dir(repo)
+ local lib_dir = path.deploy_lib_dir(repo)
+ repos.recurse_rock_manifest_entry(rock_manifest.lua, function(pathname)
+ table.insert(ret, {
+ name = path.path_to_module(pathname),
+ file = adjust_path(name, version, lua_dir, pathname),
+ })
+ end)
+ repos.recurse_rock_manifest_entry(rock_manifest.lib, function(pathname)
+ table.insert(ret, {
+ name = path.path_to_module(pathname),
+ file = adjust_path(name, version, lib_dir, pathname),
+ })
+ end)
+ table.sort(ret, function(a, b)
+ if a.name == b.name then
+ return a.file < b.file
+ end
+ return a.name < b.name
+ end)
+ return ret
+end
+
+local function commands_to_list(name, version, repo)
+ local ret = {}
+ local rock_manifest = manif.load_rock_manifest(name, version, repo)
+
+ local bin_dir = path.deploy_bin_dir(repo)
+ repos.recurse_rock_manifest_entry(rock_manifest.bin, function(pathname)
+ table.insert(ret, {
+ name = name,
+ file = adjust_path(name, version, bin_dir, pathname, cfg.wrapper_suffix),
+ })
+ end)
+ table.sort(ret, function(a, b)
+ if a.name == b.name then
+ return a.file < b.file
+ end
+ return a.name < b.name
+ end)
+ return ret
+end
+
+local function deps_to_list(dependencies, tree)
+ local ret = {}
+ for _, dep in ipairs(dependencies or {}) do
+ table.insert(ret, { name = tostring(dep), label = installed_rock_label(dep, tree) })
+ end
+ return ret
+end
+
+local function indirect_deps(mdeps, rdeps, tree)
+ local ret = {}
+ local direct_deps = {}
+ for _, dep in ipairs(rdeps) do
+ direct_deps[dep] = true
+ end
+ for dep_name in util.sortedpairs(mdeps or {}) do
+ if not direct_deps[dep_name] then
+ table.insert(ret, { name = tostring(dep_name), label = installed_rock_label(queries.new(dep_name), tree) })
+ end
+ end
+ return ret
+end
+
+local function show_rock(template, namespace, name, version, rockspec, repo, minfo, tree)
+ local desc = rockspec.description or {}
+ local data = {
+ namespace = namespace,
+ package = rockspec.package,
+ version = rockspec.version,
+ summary = desc.summary or "",
+ detailed = desc.detailed and util.split_string(format_text(desc.detailed), "\n"),
+ license = desc.license,
+ homepage = desc.homepage,
+ issues = desc.issues_url,
+ labels = desc.labels and table.concat(desc.labels, ", "),
+ location = path.rocks_tree_to_string(repo),
+ commands = commands_to_list(name, version, repo),
+ modules = modules_to_list(name, version, repo),
+ bdeps = deps_to_list(rockspec.build_dependencies, tree),
+ tdeps = deps_to_list(rockspec.test_dependencies, tree),
+ deps = deps_to_list(rockspec.dependencies, tree),
+ ideps = indirect_deps(minfo.dependencies, rockspec.dependencies, tree),
+ }
+ util.printout(render(template, data))
+end
+
+--- Driver function for "show" command.
+-- @return boolean: True if succeeded, nil on errors.
+function show.command(args)
+ local query = queries.new(args.rock, args.namespace, args.version, true)
+
+ local name, version, repo, repo_url = search.pick_installed_rock(query, args.tree)
+ if not name then
+ return nil, version
+ end
+ local tree = path.rocks_tree_to_string(repo)
+ local directory = path.install_dir(name, version, repo)
+ local namespace = path.read_namespace(name, version, tree)
+ local rockspec_file = path.rockspec_file(name, version, repo)
+ local rockspec, err = fetch.load_local_rockspec(rockspec_file)
+ if not rockspec then return nil,err end
+
+ local descript = rockspec.description or {}
+ local manifest, err = manif.load_manifest(repo_url)
+ if not manifest then return nil,err end
+ local minfo = manifest.repository[name][version][1]
+
+ if args.rock_tree then util.printout(tree)
+ elseif args.rock_namespace then util.printout(namespace)
+ elseif args.rock_dir then util.printout(directory)
+ elseif args.home then util.printout(descript.homepage)
+ elseif args.rock_license then util.printout(descript.license)
+ elseif args.issues then util.printout(descript.issues_url)
+ elseif args.labels then util.printout(descript.labels and table.concat(descript.labels, "\n"))
+ elseif args.modules then util.printout(keys_as_string(minfo.modules, "\n"))
+ elseif args.deps then
+ for _, dep in ipairs(rockspec.dependencies) do
+ util.printout(tostring(dep))
+ end
+ elseif args.build_deps then
+ for _, dep in ipairs(rockspec.build_dependencies) do
+ util.printout(tostring(dep))
+ end
+ elseif args.test_deps then
+ for _, dep in ipairs(rockspec.test_dependencies) do
+ util.printout(tostring(dep))
+ end
+ elseif args.rockspec then util.printout(rockspec_file)
+ elseif args.mversion then util.printout(version)
+ elseif args.porcelain then
+ show_rock(porcelain_template, namespace, name, version, rockspec, repo, minfo, args.tree)
+ else
+ show_rock(friendly_template, namespace, name, version, rockspec, repo, minfo, args.tree)
+ end
+ return true
+end
+
+return show
diff --git a/src/luarocks/cmd/test.lua b/src/luarocks/cmd/test.lua
new file mode 100644
index 0000000..b353bd8
--- /dev/null
+++ b/src/luarocks/cmd/test.lua
@@ -0,0 +1,48 @@
+
+--- Module implementing the LuaRocks "test" command.
+-- Tests a rock, compiling its C parts if any.
+local cmd_test = {}
+
+local util = require("luarocks.util")
+local test = require("luarocks.test")
+
+function cmd_test.add_to_parser(parser)
+ local cmd = parser:command("test", [[
+Run the test suite for the Lua project in the current directory.
+
+If the first argument is a rockspec, it will use it to determine the parameters
+for running tests; otherwise, it will attempt to detect the rockspec.
+
+Any additional arguments are forwarded to the test suite.
+To make sure that test suite flags are not interpreted as LuaRocks flags, use --
+to separate LuaRocks arguments from test suite arguments.]],
+ util.see_also())
+ :summary("Run the test suite in the current directory.")
+
+ cmd:argument("rockspec", "Project rockspec.")
+ :args("?")
+ cmd:argument("args", "Test suite arguments.")
+ :args("*")
+ cmd:flag("--prepare", "Only install dependencies needed for testing only, but do not run the test")
+
+ cmd:option("--test-type", "Specify the test suite type manually if it was "..
+ "not specified in the rockspec and it could not be auto-detected.")
+ :argname("<type>")
+end
+
+function cmd_test.command(args)
+ if args.rockspec and args.rockspec:match("rockspec$") then
+ return test.run_test_suite(args.rockspec, args.test_type, args.args, args.prepare)
+ end
+
+ table.insert(args.args, 1, args.rockspec)
+
+ local rockspec, err = util.get_default_rockspec()
+ if not rockspec then
+ return nil, err
+ end
+
+ return test.run_test_suite(rockspec, args.test_type, args.args, args.prepare)
+end
+
+return cmd_test
diff --git a/src/luarocks/cmd/unpack.lua b/src/luarocks/cmd/unpack.lua
new file mode 100644
index 0000000..a0ade4f
--- /dev/null
+++ b/src/luarocks/cmd/unpack.lua
@@ -0,0 +1,169 @@
+
+--- Module implementing the LuaRocks "unpack" command.
+-- Unpack the contents of a rock.
+local unpack = {}
+
+local fetch = require("luarocks.fetch")
+local fs = require("luarocks.fs")
+local util = require("luarocks.util")
+local build = require("luarocks.build")
+local dir = require("luarocks.dir")
+local search = require("luarocks.search")
+
+function unpack.add_to_parser(parser)
+ local cmd = parser:command("unpack", [[
+Unpacks the contents of a rock in a newly created directory.
+Argument may be a rock file, or the name of a rock in a rocks server.
+In the latter case, the rock version may be given as a second argument.]],
+ util.see_also())
+ :summary("Unpack the contents of a rock.")
+
+ cmd:argument("rock", "A rock file or the name of a rock.")
+ :action(util.namespaced_name_action)
+ cmd:argument("version", "Rock version.")
+ :args("?")
+
+ cmd:flag("--force", "Unpack files even if the output directory already exists.")
+ cmd:flag("--check-lua-versions", "If the rock can't be found, check repository "..
+ "and report if it is available for another Lua version.")
+end
+
+--- Load a rockspec file to the given directory, fetches the source
+-- files specified in the rockspec, and unpack them inside the directory.
+-- @param rockspec_file string: The URL for a rockspec file.
+-- @param dir_name string: The directory where to store and unpack files.
+-- @return table or (nil, string): the loaded rockspec table or
+-- nil and an error message.
+local function unpack_rockspec(rockspec_file, dir_name)
+ assert(type(rockspec_file) == "string")
+ assert(type(dir_name) == "string")
+
+ local rockspec, err = fetch.load_rockspec(rockspec_file)
+ if not rockspec then
+ return nil, "Failed loading rockspec "..rockspec_file..": "..err
+ end
+ local ok, err = fs.change_dir(dir_name)
+ if not ok then return nil, err end
+ local ok, sources_dir = fetch.fetch_sources(rockspec, true, ".")
+ if not ok then
+ return nil, sources_dir
+ end
+ ok, err = fs.change_dir(sources_dir)
+ if not ok then return nil, err end
+ ok, err = build.apply_patches(rockspec)
+ fs.pop_dir()
+ if not ok then return nil, err end
+ return rockspec
+end
+
+--- Load a .rock file to the given directory and unpack it inside it.
+-- @param rock_file string: The URL for a .rock file.
+-- @param dir_name string: The directory where to unpack.
+-- @param kind string: the kind of rock file, as in the second-level
+-- extension in the rock filename (eg. "src", "all", "linux-x86")
+-- @return table or (nil, string): the loaded rockspec table or
+-- nil and an error message.
+local function unpack_rock(rock_file, dir_name, kind)
+ assert(type(rock_file) == "string")
+ assert(type(dir_name) == "string")
+
+ local ok, err, errcode = fetch.fetch_and_unpack_rock(rock_file, dir_name)
+ if not ok then
+ return nil, err, errcode
+ end
+ ok, err = fs.change_dir(dir_name)
+ if not ok then return nil, err end
+ local rockspec_file = dir_name..".rockspec"
+ local rockspec, err = fetch.load_rockspec(rockspec_file)
+ if not rockspec then
+ return nil, "Failed loading rockspec "..rockspec_file..": "..err
+ end
+ if kind == "src" then
+ if rockspec.source.file then
+ local ok, err = fs.unpack_archive(rockspec.source.file)
+ if not ok then return nil, err end
+ ok, err = fetch.find_rockspec_source_dir(rockspec, ".")
+ if not ok then return nil, err end
+ ok, err = fs.change_dir(rockspec.source.dir)
+ if not ok then return nil, err end
+ ok, err = build.apply_patches(rockspec)
+ fs.pop_dir()
+ if not ok then return nil, err end
+ end
+ end
+ return rockspec
+end
+
+--- Create a directory and perform the necessary actions so that
+-- the sources for the rock and its rockspec are unpacked inside it,
+-- laid out properly so that the 'make' command is able to build the module.
+-- @param file string: A rockspec or .rock URL.
+-- @return boolean or (nil, string): true if successful or nil followed
+-- by an error message.
+local function run_unpacker(file, force)
+ assert(type(file) == "string")
+
+ local base_name = dir.base_name(file)
+ local dir_name, kind, extension = base_name:match("(.*)%.([^.]+)%.(rock)$")
+ if not extension then
+ dir_name, extension = base_name:match("(.*)%.(rockspec)$")
+ kind = "rockspec"
+ end
+ if not extension then
+ return nil, file.." does not seem to be a valid filename."
+ end
+
+ local exists = fs.exists(dir_name)
+ if exists and not force then
+ return nil, "Directory "..dir_name.." already exists."
+ end
+ if not exists then
+ local ok, err = fs.make_dir(dir_name)
+ if not ok then return nil, err end
+ end
+ local rollback = util.schedule_function(fs.delete, fs.absolute_name(dir_name))
+
+ local rockspec, err
+ if extension == "rock" then
+ rockspec, err = unpack_rock(file, dir_name, kind)
+ elseif extension == "rockspec" then
+ rockspec, err = unpack_rockspec(file, dir_name)
+ end
+ if not rockspec then
+ return nil, err
+ end
+ if kind == "src" or kind == "rockspec" then
+ fetch.find_rockspec_source_dir(rockspec, ".")
+ if rockspec.source.dir ~= "." then
+ local ok = fs.copy(rockspec.local_abs_filename, rockspec.source.dir, "read")
+ if not ok then
+ return nil, "Failed copying unpacked rockspec into unpacked source directory."
+ end
+ end
+ util.printout()
+ util.printout("Done. You may now enter directory ")
+ util.printout(dir.path(dir_name, rockspec.source.dir))
+ util.printout("and type 'luarocks make' to build.")
+ end
+ util.remove_scheduled_function(rollback)
+ return true
+end
+
+--- Driver function for the "unpack" command.
+-- @return boolean or (nil, string): true if successful or nil followed
+-- by an error message.
+function unpack.command(args)
+ local url, err
+ if args.rock:match(".*%.rock") or args.rock:match(".*%.rockspec") then
+ url = args.rock
+ else
+ url, err = search.find_src_or_rockspec(args.rock, args.namespace, args.version, args.check_lua_versions)
+ if not url then
+ return nil, err
+ end
+ end
+
+ return run_unpacker(url, args.force)
+end
+
+return unpack
diff --git a/src/luarocks/cmd/upload.lua b/src/luarocks/cmd/upload.lua
new file mode 100644
index 0000000..6b84e45
--- /dev/null
+++ b/src/luarocks/cmd/upload.lua
@@ -0,0 +1,128 @@
+
+local upload = {}
+
+local signing = require("luarocks.signing")
+local util = require("luarocks.util")
+local fetch = require("luarocks.fetch")
+local pack = require("luarocks.pack")
+local cfg = require("luarocks.core.cfg")
+local Api = require("luarocks.upload.api")
+
+function upload.add_to_parser(parser)
+ local cmd = parser:command("upload", "Pack a source rock file (.src.rock extension) "..
+ "and upload it and the rockspec to the public rocks repository.", util.see_also())
+ :summary("Upload a rockspec to the public rocks repository.")
+
+ cmd:argument("rockspec", "Rockspec for the rock to upload.")
+ cmd:argument("src-rock", "A corresponding .src.rock file; if not given it will be generated.")
+ :args("?")
+
+ cmd:flag("--skip-pack", "Do not pack and send source rock.")
+ cmd:option("--api-key", "Pass an API key. It will be stored for subsequent uses.")
+ :argname("<key>")
+ cmd:option("--temp-key", "Use the given a temporary API key in this "..
+ "invocation only. It will not be stored.")
+ :argname("<key>")
+ cmd:flag("--force", "Replace existing rockspec if the same revision of a "..
+ "module already exists. This should be used only in case of upload "..
+ "mistakes: when updating a rockspec, increment the revision number "..
+ "instead.")
+ cmd:flag("--sign", "Upload a signature file alongside each file as well.")
+ cmd:flag("--debug"):hidden(true)
+end
+
+local function is_dev_version(version)
+ return version:match("^dev") or version:match("^scm")
+end
+
+function upload.command(args)
+ local api, err = Api.new(args)
+ if not api then
+ return nil, err
+ end
+ if cfg.verbose then
+ api.debug = true
+ end
+
+ local rockspec, err, errcode = fetch.load_rockspec(args.rockspec)
+ if err then
+ return nil, err, errcode
+ end
+
+ util.printout("Sending " .. tostring(args.rockspec) .. " ...")
+ local res, err = api:method("check_rockspec", {
+ package = rockspec.package,
+ version = rockspec.version
+ })
+ if not res then return nil, err end
+
+ if not res.module then
+ util.printout("Will create new module (" .. tostring(rockspec.package) .. ")")
+ end
+ if res.version and not args.force then
+ return nil, "Revision "..rockspec.version.." already exists on the server. "..util.see_help("upload")
+ end
+
+ local sigfname
+ local rock_sigfname
+
+ if args.sign then
+ sigfname, err = signing.sign_file(args.rockspec)
+ if err then
+ return nil, "Failed signing rockspec: " .. err
+ end
+ util.printout("Signed rockspec: "..sigfname)
+ end
+
+ local rock_fname
+ if args.src_rock then
+ rock_fname = args.src_rock
+ elseif not args.skip_pack and not is_dev_version(rockspec.version) then
+ util.printout("Packing " .. tostring(rockspec.package))
+ rock_fname, err = pack.pack_source_rock(args.rockspec)
+ if not rock_fname then
+ return nil, err
+ end
+ end
+
+ if rock_fname and args.sign then
+ rock_sigfname, err = signing.sign_file(rock_fname)
+ if err then
+ return nil, "Failed signing rock: " .. err
+ end
+ util.printout("Signed packed rock: "..rock_sigfname)
+ end
+
+ local multipart = require("luarocks.upload.multipart")
+
+ res, err = api:method("upload", nil, {
+ rockspec_file = multipart.new_file(args.rockspec),
+ rockspec_sig = sigfname and multipart.new_file(sigfname),
+ })
+ if not res then return nil, err end
+
+ if res.is_new and #res.manifests == 0 then
+ util.printerr("Warning: module not added to root manifest due to name taken.")
+ end
+
+ local module_url = res.module_url
+
+ if rock_fname then
+ if (not res.version) or (not res.version.id) then
+ return nil, "Invalid response from server."
+ end
+ util.printout(("Sending " .. tostring(rock_fname) .. " ..."))
+ res, err = api:method("upload_rock/" .. ("%d"):format(res.version.id), nil, {
+ rock_file = multipart.new_file(rock_fname),
+ rock_sig = rock_sigfname and multipart.new_file(rock_sigfname),
+ })
+ if not res then return nil, err end
+ end
+
+ util.printout()
+ util.printout("Done: " .. tostring(module_url))
+ util.printout()
+ return true
+end
+
+return upload
diff --git a/src/luarocks/cmd/which.lua b/src/luarocks/cmd/which.lua
new file mode 100644
index 0000000..f50a43c
--- /dev/null
+++ b/src/luarocks/cmd/which.lua
@@ -0,0 +1,40 @@
+
+--- @module luarocks.which_cmd
+-- Driver for the `luarocks which` command.
+local which_cmd = {}
+
+local loader = require("luarocks.loader")
+local cfg = require("luarocks.core.cfg")
+local util = require("luarocks.util")
+
+function which_cmd.add_to_parser(parser)
+ local cmd = parser:command("which", 'Given a module name like "foo.bar", '..
+ "output which file would be loaded to resolve that module by "..
+ 'luarocks.loader, like "/usr/local/lua/'..cfg.lua_version..'/foo/bar.lua".',
+ util.see_also())
+ :summary("Tell which file corresponds to a given module name.")
+
+ cmd:argument("modname", "Module name.")
+end
+
+--- Driver function for "which" command.
+-- @return boolean This function terminates the interpreter.
+function which_cmd.command(args)
+ local pathname, rock_name, rock_version, where = loader.which(args.modname, "lp")
+
+ if pathname then
+ util.printout(pathname)
+ if where == "l" then
+ util.printout("(provided by " .. tostring(rock_name) .. " " .. tostring(rock_version) .. ")")
+ else
+ local key = rock_name
+ util.printout("(found directly via package." .. key.. " -- not installed as a rock?)")
+ end
+ return true
+ end
+
+ return nil, "Module '" .. args.modname .. "' not found."
+end
+
+return which_cmd
+
diff --git a/src/luarocks/cmd/write_rockspec.lua b/src/luarocks/cmd/write_rockspec.lua
new file mode 100644
index 0000000..871cdd4
--- /dev/null
+++ b/src/luarocks/cmd/write_rockspec.lua
@@ -0,0 +1,408 @@
+
+local write_rockspec = {}
+
+local builtin = require("luarocks.build.builtin")
+local cfg = require("luarocks.core.cfg")
+local dir = require("luarocks.dir")
+local fetch = require("luarocks.fetch")
+local fs = require("luarocks.fs")
+local persist = require("luarocks.persist")
+local rockspecs = require("luarocks.rockspecs")
+local type_rockspec = require("luarocks.type.rockspec")
+local util = require("luarocks.util")
+
+local lua_versions = {
+ "5.1",
+ "5.2",
+ "5.3",
+ "5.4",
+ "5.1,5.2",
+ "5.2,5.3",
+ "5.3,5.4",
+ "5.1,5.2,5.3",
+ "5.2,5.3,5.4",
+ "5.1,5.2,5.3,5.4"
+}
+
+function write_rockspec.cmd_options(parser)
+ return parser:option("--output", "Write the rockspec with the given filename.\n"..
+ "If not given, a file is written in the current directory with a "..
+ "filename based on given name and version.")
+ :argname("<file>"),
+ parser:option("--license", 'A license string, such as "MIT/X11" or "GNU GPL v3".')
+ :argname("<string>"),
+ parser:option("--summary", "A short one-line description summary.")
+ :argname("<txt>"),
+ parser:option("--detailed", "A longer description string.")
+ :argname("<txt>"),
+ parser:option("--homepage", "Project homepage.")
+ :argname("<txt>"),
+ parser:option("--lua-versions", 'Supported Lua versions. Accepted values are: "'..
+ table.concat(lua_versions, '", "')..'".')
+ :argname("<ver>")
+ :choices(lua_versions),
+ parser:option("--rockspec-format", 'Rockspec format version, such as "1.0" or "1.1".')
+ :argname("<ver>"),
+ parser:option("--tag", "Tag to use. Will attempt to extract version number from it."),
+ parser:option("--lib", "A comma-separated list of libraries that C files need to link to.")
+ :argname("<libs>")
+end
+
+function write_rockspec.add_to_parser(parser)
+ local cmd = parser:command("write_rockspec", [[
+This command writes an initial version of a rockspec file,
+based on a name, a version, and a location (an URL or a local path).
+If only two arguments are given, the first one is considered the name and the
+second one is the location.
+If only one argument is given, it must be the location.
+If no arguments are given, current directory is used as the location.
+LuaRocks will attempt to infer name and version if not given,
+using 'dev' as a fallback default version.
+
+Note that the generated file is a _starting point_ for writing a
+rockspec, and is not guaranteed to be complete or correct. ]], util.see_also())
+ :summary("Write a template for a rockspec file.")
+
+ cmd:argument("name", "Name of the rock.")
+ :args("?")
+ cmd:argument("version", "Rock version.")
+ :args("?")
+ cmd:argument("location", "URL or path to the rock sources.")
+ :args("?")
+
+ write_rockspec.cmd_options(cmd)
+end
+
+local function open_file(name)
+ return io.open(dir.path(fs.current_dir(), name), "r")
+end
+
+local function fetch_url(rockspec)
+ local file, temp_dir, err_code, err_file, err_temp_dir = fetch.fetch_sources(rockspec, false)
+ if err_code == "source.dir" then
+ file, temp_dir = err_file, err_temp_dir
+ elseif not file then
+ util.warning("Could not fetch sources - "..temp_dir)
+ return false
+ end
+ util.printout("File successfully downloaded. Making checksum and checking base dir...")
+ if dir.is_basic_protocol(rockspec.source.protocol) then
+ rockspec.source.md5 = fs.get_md5(file)
+ end
+ local inferred_dir, found_dir = fetch.find_base_dir(file, temp_dir, rockspec.source.url)
+ return true, found_dir or inferred_dir, temp_dir
+end
+
+local lua_version_dep = {
+ ["5.1"] = "lua ~> 5.1",
+ ["5.2"] = "lua ~> 5.2",
+ ["5.3"] = "lua ~> 5.3",
+ ["5.4"] = "lua ~> 5.4",
+ ["5.1,5.2"] = "lua >= 5.1, < 5.3",
+ ["5.2,5.3"] = "lua >= 5.2, < 5.4",
+ ["5.3,5.4"] = "lua >= 5.3, < 5.5",
+ ["5.1,5.2,5.3"] = "lua >= 5.1, < 5.4",
+ ["5.2,5.3,5.4"] = "lua >= 5.2, < 5.5",
+ ["5.1,5.2,5.3,5.4"] = "lua >= 5.1, < 5.5",
+}
+
+local simple_scm_protocols = {
+ git = true,
+ ["git+http"] = true,
+ ["git+https"] = true,
+ ["git+ssh"] = true,
+ hg = true,
+ ["hg+http"] = true,
+ ["hg+https"] = true,
+ ["hg+ssh"] = true,
+}
+
+local detect_url
+do
+ local function detect_url_from_command(program, args, directory)
+ local command = fs.Q(cfg.variables[program:upper()]).. " "..args
+ local pipe = io.popen(fs.command_at(directory, fs.quiet_stderr(command)))
+ if not pipe then return nil end
+ local url = pipe:read("*a"):match("^([^\r\n]+)")
+ pipe:close()
+ if not url then return nil end
+ if url:match("^[^@:/]+@[^@:/]+:.*$") then
+ local u, h, p = url:match("^([^@]+)@([^:]+):(.*)$")
+ url = program.."+ssh://"..u.."@"..h.."/"..p
+ elseif not util.starts_with(url, program.."://") then
+ url = program.."+"..url
+ end
+
+ if simple_scm_protocols[dir.split_url(url)] then
+ return url
+ end
+ end
+
+ local function detect_scm_url(directory)
+ return detect_url_from_command("git", "config --get remote.origin.url", directory) or
+ detect_url_from_command("hg", "paths default", directory)
+ end
+
+ detect_url = function(url_or_dir)
+ if url_or_dir:match("://") then
+ return url_or_dir
+ else
+ return detect_scm_url(url_or_dir) or "*** please add URL for source tarball, zip or repository here ***"
+ end
+ end
+end
+
+local function detect_homepage(url, homepage)
+ if homepage then
+ return homepage
+ end
+ local url_protocol, url_path = dir.split_url(url)
+
+ if simple_scm_protocols[url_protocol] then
+ for _, domain in ipairs({"github.com", "bitbucket.org", "gitlab.com"}) do
+ if util.starts_with(url_path, domain) then
+ return "https://"..url_path:gsub("%.git$", "")
+ end
+ end
+ end
+
+ return "*** please enter a project homepage ***"
+end
+
+local function detect_description()
+ local fd = open_file("README.md") or open_file("README")
+ if not fd then return end
+ local data = fd:read("*a")
+ fd:close()
+ local paragraph = data:match("\n\n([^%[].-)\n\n")
+ if not paragraph then paragraph = data:match("\n\n(.*)") end
+ local summary, detailed
+ if paragraph then
+ detailed = paragraph
+
+ if #paragraph < 80 then
+ summary = paragraph:gsub("\n", "")
+ else
+ summary = paragraph:gsub("\n", " "):match("([^.]*%.) ")
+ end
+ end
+ return summary, detailed
+end
+
+local licenses = {
+ [78656] = "MIT",
+ [49311] = "ISC",
+}
+
+local function detect_license(data)
+ local strip_copyright = (data:gsub("^Copyright [^\n]*\n", ""))
+ local sum = 0
+ for i = 1, #strip_copyright do
+ local num = string.byte(strip_copyright:sub(i,i))
+ if num > 32 and num <= 128 then
+ sum = sum + num
+ end
+ end
+ return licenses[sum]
+end
+
+local function check_license()
+ local fd = open_file("COPYING") or open_file("LICENSE") or open_file("MIT-LICENSE.txt")
+ if not fd then return nil end
+ local data = fd:read("*a")
+ fd:close()
+ local license = detect_license(data)
+ if license then
+ return license, data
+ end
+ return nil, data
+end
+
+local function fill_as_builtin(rockspec, libs)
+ rockspec.build.type = "builtin"
+
+ local incdirs, libdirs
+ if libs then
+ incdirs, libdirs = {}, {}
+ for _, lib in ipairs(libs) do
+ local upper = lib:upper()
+ incdirs[#incdirs+1] = "$("..upper.."_INCDIR)"
+ libdirs[#libdirs+1] = "$("..upper.."_LIBDIR)"
+ end
+ end
+
+ rockspec.build.modules, rockspec.build.install, rockspec.build.copy_directories = builtin.autodetect_modules(libs, incdirs, libdirs)
+end
+
+local function rockspec_cleanup(rockspec)
+ rockspec.source.file = nil
+ rockspec.source.protocol = nil
+ rockspec.source.identifier = nil
+ rockspec.source.dir = nil
+ rockspec.source.dir_set = nil
+ rockspec.source.pathname = nil
+ rockspec.variables = nil
+ rockspec.name = nil
+ rockspec.format_is_at_least = nil
+ rockspec.local_abs_filename = nil
+ rockspec.rocks_provided = nil
+ for _, list in ipairs({"dependencies", "build_dependencies", "test_dependencies"}) do
+ if rockspec[list] and not next(rockspec[list]) then
+ rockspec[list] = nil
+ end
+ end
+ for _, list in ipairs({"dependencies", "build_dependencies", "test_dependencies"}) do
+ if rockspec[list] then
+ for i, entry in ipairs(rockspec[list]) do
+ rockspec[list][i] = tostring(entry)
+ end
+ end
+ end
+end
+
+function write_rockspec.command(args)
+ local name, version = args.name, args.version
+ local location = args.location
+
+ if not name then
+ location = "."
+ elseif not version then
+ location = name
+ name = nil
+ elseif not location then
+ location = version
+ version = nil
+ end
+
+ if args.tag then
+ if not version then
+ version = args.tag:gsub("^v", "")
+ end
+ end
+
+ local protocol, pathname = dir.split_url(location)
+ if protocol == "file" then
+ if pathname == "." then
+ name = name or dir.base_name(fs.current_dir())
+ end
+ elseif dir.is_basic_protocol(protocol) then
+ local filename = dir.base_name(location)
+ local newname, newversion = filename:match("(.*)-([^-]+)")
+ if newname then
+ name = name or newname
+ version = version or newversion:gsub("%.[a-z]+$", ""):gsub("%.tar$", "")
+ end
+ else
+ name = name or dir.base_name(location):gsub("%.[^.]+$", "")
+ end
+
+ if not name then
+ return nil, "Could not infer rock name. "..util.see_help("write_rockspec")
+ end
+ version = version or "dev"
+
+ local filename = args.output or dir.path(fs.current_dir(), name:lower().."-"..version.."-1.rockspec")
+
+ local url = detect_url(location)
+ local homepage = detect_homepage(url, args.homepage)
+
+ local rockspec, err = rockspecs.from_persisted_table(filename, {
+ rockspec_format = args.rockspec_format,
+ package = name,
+ version = version.."-1",
+ source = {
+ url = url,
+ tag = args.tag,
+ },
+ description = {
+ summary = args.summary or "*** please specify description summary ***",
+ detailed = args.detailed or "*** please enter a detailed description ***",
+ homepage = homepage,
+ license = args.license or "*** please specify a license ***",
+ },
+ dependencies = {
+ lua_version_dep[args.lua_versions],
+ },
+ build = {},
+ })
+ assert(not err, err)
+ rockspec.source.protocol = protocol
+
+ if not next(rockspec.dependencies) then
+ util.warning("Please specify supported Lua versions with --lua-versions=<ver>. "..util.see_help("write_rockspec"))
+ end
+
+ local local_dir = location
+
+ if location:match("://") then
+ rockspec.source.file = dir.base_name(location)
+ if not dir.is_basic_protocol(rockspec.source.protocol) then
+ if version ~= "dev" then
+ rockspec.source.tag = args.tag or "v" .. version
+ end
+ end
+ rockspec.source.dir = nil
+ local ok, base_dir, temp_dir = fetch_url(rockspec)
+ if ok then
+ if base_dir ~= dir.base_name(location) then
+ rockspec.source.dir = base_dir
+ end
+ end
+ if base_dir then
+ local_dir = dir.path(temp_dir, base_dir)
+ else
+ local_dir = nil
+ end
+ end
+
+ if not local_dir then
+ local_dir = "."
+ end
+
+ local libs = nil
+ if args.lib then
+ libs = {}
+ rockspec.external_dependencies = {}
+ for lib in args.lib:gmatch("([^,]+)") do
+ table.insert(libs, lib)
+ rockspec.external_dependencies[lib:upper()] = {
+ library = lib
+ }
+ end
+ end
+
+ local ok, err = fs.change_dir(local_dir)
+ if not ok then return nil, "Failed reaching files from project - error entering directory "..local_dir end
+
+ if not (args.summary and args.detailed) then
+ local summary, detailed = detect_description()
+ rockspec.description.summary = args.summary or summary
+ rockspec.description.detailed = args.detailed or detailed
+ end
+
+ if not args.license then
+ local license, fulltext = check_license()
+ if license then
+ rockspec.description.license = license
+ elseif license then
+ util.title("Could not auto-detect type for project license:")
+ util.printout(fulltext)
+ util.printout()
+ util.title("Please fill in the source.license field manually or use --license.")
+ end
+ end
+
+ fill_as_builtin(rockspec, libs)
+
+ rockspec_cleanup(rockspec)
+
+ persist.save_from_table(filename, rockspec, type_rockspec.order)
+
+ util.printout()
+ util.printout("Wrote template at "..filename.." -- you should now edit and finish it.")
+ util.printout()
+
+ return true
+end
+
+return write_rockspec
diff --git a/src/luarocks/config.lua b/src/luarocks/config.lua
new file mode 100644
index 0000000..019b388
--- /dev/null
+++ b/src/luarocks/config.lua
@@ -0,0 +1,36 @@
+local config = {}
+
+local persist = require("luarocks.persist")
+
+local cfg_skip = {
+ errorcodes = true,
+ flags = true,
+ platforms = true,
+ root_dir = true,
+ upload_servers = true,
+}
+
+function config.should_skip(k, v)
+ return type(v) == "function" or cfg_skip[k]
+end
+
+local function cleanup(tbl)
+ local copy = {}
+ for k, v in pairs(tbl) do
+ if not config.should_skip(k, v) then
+ copy[k] = v
+ end
+ end
+ return copy
+end
+
+function config.get_config_for_display(cfg)
+ return cleanup(cfg)
+end
+
+function config.to_string(cfg)
+ local cleancfg = config.get_config_for_display(cfg)
+ return persist.save_from_table_to_string(cleancfg)
+end
+
+return config
diff --git a/src/luarocks/core/cfg.lua b/src/luarocks/core/cfg.lua
new file mode 100644
index 0000000..9cfd9dd
--- /dev/null
+++ b/src/luarocks/core/cfg.lua
@@ -0,0 +1,940 @@
+
+--- Configuration for LuaRocks.
+-- Tries to load the user's configuration file and
+-- defines defaults for unset values. See the
+-- <a href="http://luarocks.org/en/Config_file_format">config
+-- file format documentation</a> for details.
+--
+-- End-users shouldn't edit this file. They can override any defaults
+-- set in this file using their system-wide or user-specific configuration
+-- files. Run `luarocks` with no arguments to see the locations of
+-- these files in your platform.
+
+local table, pairs, require, os, pcall, ipairs, package, type, assert =
+ table, pairs, require, os, pcall, ipairs, package, type, assert
+
+local dir = require("luarocks.core.dir")
+local util = require("luarocks.core.util")
+local persist = require("luarocks.core.persist")
+local sysdetect = require("luarocks.core.sysdetect")
+local vers = require("luarocks.core.vers")
+
+--------------------------------------------------------------------------------
+
+local program_version = "3.11.1"
+
+local is_windows = package.config:sub(1,1) == "\\"
+
+-- Set order for platform overrides.
+-- More general platform identifiers should be listed first,
+-- more specific ones later.
+local platform_order = {
+ -- Unixes
+ "unix",
+ "bsd",
+ "solaris",
+ "netbsd",
+ "openbsd",
+ "freebsd",
+ "dragonfly",
+ "linux",
+ "macosx",
+ "cygwin",
+ "msys",
+ "haiku",
+ -- Windows
+ "windows",
+ "win32",
+ "mingw",
+ "mingw32",
+ "msys2_mingw_w64",
+}
+
+local function detect_sysconfdir()
+ if not debug then
+ return
+ end
+ local src = debug.getinfo(1, "S").source
+ if not src then
+ return
+ end
+ src = dir.normalize(src)
+ if src:sub(1, 1) == "@" then
+ src = src:sub(2)
+ end
+ local basedir = src:match("^(.*)[\\/]luarocks[\\/]core[\\/]cfg.lua$")
+ if not basedir then
+ return
+ end
+ -- If installed in a Unix-like tree, use a Unix-like sysconfdir
+ local installdir = basedir:match("^(.*)[\\/]share[\\/]lua[\\/][^/]*$")
+ if installdir then
+ if installdir == "/usr" then
+ return "/etc/luarocks"
+ end
+ return dir.path(installdir, "etc", "luarocks")
+ end
+ -- Otherwise, use base directory of sources
+ return basedir
+end
+
+local load_config_file
+do
+ -- Create global environment for the config files;
+ local function env_for_config_file(cfg, platforms)
+ local platforms_copy = {}
+ for k,v in pairs(platforms) do
+ platforms_copy[k] = v
+ end
+
+ local e
+ e = {
+ home = cfg.home,
+ lua_version = cfg.lua_version,
+ platforms = platforms_copy,
+ processor = cfg.target_cpu, -- remains for compat reasons
+ target_cpu = cfg.target_cpu, -- replaces `processor`
+ os_getenv = os.getenv,
+ variables = cfg.variables or {},
+ dump_env = function()
+ -- debug function, calling it from a config file will show all
+ -- available globals to that config file
+ print(util.show_table(e, "global environment"))
+ end,
+ }
+ return e
+ end
+
+ -- Merge values from config files read into the `cfg` table
+ local function merge_overrides(cfg, overrides)
+ -- remove some stuff we do not want to integrate
+ overrides.os_getenv = nil
+ overrides.dump_env = nil
+ -- remove tables to be copied verbatim instead of deeply merged
+ if overrides.rocks_trees then cfg.rocks_trees = nil end
+ if overrides.rocks_servers then cfg.rocks_servers = nil end
+ -- perform actual merge
+ util.deep_merge(cfg, overrides)
+ end
+
+ local function update_platforms(platforms, overrides)
+ if overrides[1] then
+ for k, _ in pairs(platforms) do
+ platforms[k] = nil
+ end
+ for _, v in ipairs(overrides) do
+ platforms[v] = true
+ end
+ -- set some fallback default in case the user provides an incomplete configuration.
+ -- LuaRocks expects a set of defaults to be available.
+ if not (platforms.unix or platforms.windows) then
+ platforms[is_windows and "windows" or "unix"] = true
+ end
+ end
+ end
+
+ -- Load config file and merge its contents into the `cfg` module table.
+ -- @return filepath of successfully loaded file or nil if it failed
+ load_config_file = function(cfg, platforms, filepath)
+ local result, err, errcode = persist.load_into_table(filepath, env_for_config_file(cfg, platforms))
+ if (not result) and errcode ~= "open" then
+ -- errcode is either "load" or "run"; bad config file, so error out
+ return nil, err, "config"
+ end
+ if result then
+ -- success in loading and running, merge contents and exit
+ update_platforms(platforms, result.platforms)
+ result.platforms = nil
+ merge_overrides(cfg, result)
+ return filepath
+ end
+ return nil -- nothing was loaded
+ end
+end
+
+local platform_sets = {
+ freebsd = { unix = true, bsd = true, freebsd = true },
+ openbsd = { unix = true, bsd = true, openbsd = true },
+ dragonfly = { unix = true, bsd = true, dragonfly = true },
+ solaris = { unix = true, solaris = true },
+ windows = { windows = true, win32 = true },
+ cygwin = { unix = true, cygwin = true },
+ macosx = { unix = true, bsd = true, macosx = true, macos = true },
+ netbsd = { unix = true, bsd = true, netbsd = true },
+ haiku = { unix = true, haiku = true },
+ linux = { unix = true, linux = true },
+ mingw = { windows = true, win32 = true, mingw32 = true, mingw = true },
+ msys = { unix = true, cygwin = true, msys = true },
+ msys2_mingw_w64 = { windows = true, win32 = true, mingw32 = true, mingw = true, msys = true, msys2_mingw_w64 = true },
+}
+
+local function make_platforms(system)
+ -- fallback to Unix in unknown systems
+ return platform_sets[system] or { unix = true }
+end
+
+--------------------------------------------------------------------------------
+
+local function make_defaults(lua_version, target_cpu, platforms, home)
+
+ -- Configure defaults:
+ local defaults = {
+
+ local_by_default = false,
+ accept_unknown_fields = false,
+ fs_use_modules = true,
+ hooks_enabled = true,
+ deps_mode = "one",
+ no_manifest = false,
+ check_certificates = false,
+
+ cache_timeout = 60,
+ cache_fail_timeout = 86400,
+
+ lua_modules_path = dir.path("share", "lua", lua_version),
+ lib_modules_path = dir.path("lib", "lua", lua_version),
+ rocks_subdir = dir.path("lib", "luarocks", "rocks-"..lua_version),
+
+ arch = "unknown",
+ lib_extension = "unknown",
+ obj_extension = "unknown",
+ link_lua_explicitly = false,
+
+ rocks_servers = {
+ {
+ "https://luarocks.org",
+ "https://raw.githubusercontent.com/rocks-moonscript-org/moonrocks-mirror/master/",
+ "https://loadk.com/luarocks/",
+ }
+ },
+ disabled_servers = {},
+
+ upload = {
+ server = "https://luarocks.org",
+ tool_version = "1.0.0",
+ api_version = "1",
+ },
+
+ lua_extension = "lua",
+ connection_timeout = 30, -- 0 = no timeout
+
+ variables = {
+ MAKE = os.getenv("MAKE") or "make",
+ CC = os.getenv("CC") or "cc",
+ LD = os.getenv("CC") or "ld",
+ AR = os.getenv("AR") or "ar",
+ RANLIB = os.getenv("RANLIB") or "ranlib",
+
+ CVS = "cvs",
+ GIT = "git",
+ SSCM = "sscm",
+ SVN = "svn",
+ HG = "hg",
+
+ GPG = "gpg",
+
+ RSYNC = "rsync",
+ WGET = "wget",
+ SCP = "scp",
+ CURL = "curl",
+
+ PWD = "pwd",
+ MKDIR = "mkdir",
+ RMDIR = "rmdir",
+ CP = "cp",
+ LN = "ln",
+ LS = "ls",
+ RM = "rm",
+ FIND = "find",
+ CHMOD = "chmod",
+ ICACLS = "icacls",
+ MKTEMP = "mktemp",
+
+ ZIP = "zip",
+ UNZIP = "unzip -n",
+ GUNZIP = "gunzip",
+ BUNZIP2 = "bunzip2",
+ TAR = "tar",
+
+ MD5SUM = "md5sum",
+ OPENSSL = "openssl",
+ MD5 = "md5",
+ TOUCH = "touch",
+
+ CMAKE = "cmake",
+ SEVENZ = "7z",
+
+ RSYNCFLAGS = "--exclude=.git -Oavz",
+ CURLNOCERTFLAG = "",
+ WGETNOCERTFLAG = "",
+ },
+
+ external_deps_subdirs = {
+ bin = "bin",
+ lib = "lib",
+ include = "include"
+ },
+ runtime_external_deps_subdirs = {
+ bin = "bin",
+ lib = "lib",
+ include = "include"
+ },
+ }
+
+ if platforms.windows then
+
+ defaults.arch = "win32-"..target_cpu
+ defaults.lib_extension = "dll"
+ defaults.external_lib_extension = "dll"
+ defaults.static_lib_extension = "lib"
+ defaults.obj_extension = "obj"
+ defaults.external_deps_dirs = {
+ dir.path("c:", "external"),
+ dir.path("c:", "windows", "system32"),
+ }
+
+ defaults.makefile = "Makefile.win"
+ defaults.variables.PWD = "echo %cd%"
+ defaults.variables.MKDIR = "md"
+ defaults.variables.MAKE = os.getenv("MAKE") or "nmake"
+ defaults.variables.CC = os.getenv("CC") or "cl"
+ defaults.variables.RC = os.getenv("WINDRES") or "rc"
+ defaults.variables.LD = os.getenv("LINK") or "link"
+ defaults.variables.MT = os.getenv("MT") or "mt"
+ defaults.variables.AR = os.getenv("AR") or "lib"
+ defaults.variables.CFLAGS = os.getenv("CFLAGS") or "/nologo /MD /O2"
+ defaults.variables.LDFLAGS = os.getenv("LDFLAGS")
+ defaults.variables.LIBFLAG = "/nologo /dll"
+
+ defaults.external_deps_patterns = {
+ bin = { "?.exe", "?.bat" },
+ lib = { "?.lib", "lib?.lib", "?.dll", "lib?.dll" },
+ include = { "?.h" }
+ }
+ defaults.runtime_external_deps_patterns = {
+ bin = { "?.exe", "?.bat" },
+ lib = { "?.dll", "lib?.dll" },
+ include = { "?.h" }
+ }
+ defaults.export_path_separator = ";"
+ defaults.wrapper_suffix = ".bat"
+
+ local localappdata = os.getenv("LOCALAPPDATA")
+ if not localappdata then
+ -- for Windows versions below Vista
+ localappdata = dir.path((os.getenv("USERPROFILE") or dir.path("c:", "Users", "All Users")), "Local Settings", "Application Data")
+ end
+ defaults.local_cache = dir.path(localappdata, "LuaRocks", "Cache")
+ defaults.web_browser = "start"
+
+ defaults.external_deps_subdirs.lib = { "lib", "", "bin" }
+ defaults.runtime_external_deps_subdirs.lib = { "lib", "", "bin" }
+ defaults.link_lua_explicitly = true
+ defaults.fs_use_modules = false
+ end
+
+ if platforms.mingw32 then
+ defaults.obj_extension = "o"
+ defaults.static_lib_extension = "a"
+ defaults.external_deps_dirs = {
+ dir.path("c:", "external"),
+ dir.path("c:", "mingw"),
+ dir.path("c:", "windows", "system32"),
+ }
+ defaults.cmake_generator = "MinGW Makefiles"
+ defaults.variables.MAKE = os.getenv("MAKE") or "mingw32-make"
+ if target_cpu == "x86_64" then
+ defaults.variables.CC = os.getenv("CC") or "x86_64-w64-mingw32-gcc"
+ defaults.variables.LD = os.getenv("CC") or "x86_64-w64-mingw32-gcc"
+ else
+ defaults.variables.CC = os.getenv("CC") or "mingw32-gcc"
+ defaults.variables.LD = os.getenv("CC") or "mingw32-gcc"
+ end
+ defaults.variables.AR = os.getenv("AR") or "ar"
+ defaults.variables.RC = os.getenv("WINDRES") or "windres"
+ defaults.variables.RANLIB = os.getenv("RANLIB") or "ranlib"
+ defaults.variables.CFLAGS = os.getenv("CFLAGS") or "-O2"
+ defaults.variables.LDFLAGS = os.getenv("LDFLAGS")
+ defaults.variables.LIBFLAG = "-shared"
+ defaults.makefile = "Makefile"
+ defaults.external_deps_patterns = {
+ bin = { "?.exe", "?.bat" },
+ -- mingw lookup list from http://stackoverflow.com/a/15853231/1793220
+ -- ...should we keep ?.lib at the end? It's not in the above list.
+ lib = { "lib?.dll.a", "?.dll.a", "lib?.a", "cyg?.dll", "lib?.dll", "?.dll", "?.lib" },
+ include = { "?.h" }
+ }
+ defaults.runtime_external_deps_patterns = {
+ bin = { "?.exe", "?.bat" },
+ lib = { "cyg?.dll", "?.dll", "lib?.dll" },
+ include = { "?.h" }
+ }
+ defaults.link_lua_explicitly = true
+ end
+
+ if platforms.unix then
+ defaults.lib_extension = "so"
+ defaults.static_lib_extension = "a"
+ defaults.external_lib_extension = "so"
+ defaults.obj_extension = "o"
+ defaults.external_deps_dirs = { "/usr/local", "/usr", "/" }
+
+ defaults.variables.CFLAGS = os.getenv("CFLAGS") or "-O2"
+ -- we pass -fPIC via CFLAGS because of old Makefile-based Lua projects
+ -- which didn't have -fPIC in their Makefiles but which honor CFLAGS
+ if not defaults.variables.CFLAGS:match("-fPIC") then
+ defaults.variables.CFLAGS = defaults.variables.CFLAGS.." -fPIC"
+ end
+
+ defaults.variables.LDFLAGS = os.getenv("LDFLAGS")
+
+ defaults.cmake_generator = "Unix Makefiles"
+ defaults.variables.CC = os.getenv("CC") or "gcc"
+ defaults.variables.LD = os.getenv("CC") or "gcc"
+ defaults.gcc_rpath = true
+ defaults.variables.LIBFLAG = "-shared"
+ defaults.variables.TEST = "test"
+
+ defaults.external_deps_patterns = {
+ bin = { "?" },
+ lib = { "lib?.a", "lib?.so", "lib?.so.*" },
+ include = { "?.h" }
+ }
+ defaults.runtime_external_deps_patterns = {
+ bin = { "?" },
+ lib = { "lib?.so", "lib?.so.*" },
+ include = { "?.h" }
+ }
+ defaults.export_path_separator = ":"
+ defaults.wrapper_suffix = ""
+ local xdg_cache_home = os.getenv("XDG_CACHE_HOME") or home.."/.cache"
+ defaults.local_cache = xdg_cache_home.."/luarocks"
+ defaults.web_browser = "xdg-open"
+ end
+
+ if platforms.cygwin then
+ defaults.lib_extension = "so" -- can be overridden in the config file for mingw builds
+ defaults.arch = "cygwin-"..target_cpu
+ defaults.cmake_generator = "Unix Makefiles"
+ defaults.variables.CC = "echo -llua | xargs " .. (os.getenv("CC") or "gcc")
+ defaults.variables.LD = "echo -llua | xargs " .. (os.getenv("CC") or "gcc")
+ defaults.variables.LIBFLAG = "-shared"
+ defaults.link_lua_explicitly = true
+ end
+
+ if platforms.msys then
+ -- msys is basically cygwin made out of mingw, meaning the subsytem is unixish
+ -- enough, yet we can freely mix with native win32
+ defaults.external_deps_patterns = {
+ bin = { "?.exe", "?.bat", "?" },
+ lib = { "lib?.so", "lib?.so.*", "lib?.dll.a", "?.dll.a",
+ "lib?.a", "lib?.dll", "?.dll", "?.lib" },
+ include = { "?.h" }
+ }
+ defaults.runtime_external_deps_patterns = {
+ bin = { "?.exe", "?.bat" },
+ lib = { "lib?.so", "?.dll", "lib?.dll" },
+ include = { "?.h" }
+ }
+ if platforms.mingw then
+ -- MSYS2 can build Windows programs that depend on
+ -- msys-2.0.dll (based on Cygwin) but MSYS2 is also designed
+ -- for building native Windows programs by MinGW. These
+ -- programs don't depend on msys-2.0.dll.
+ local pipe = io.popen("cygpath --windows %MINGW_PREFIX%")
+ local mingw_prefix = pipe:read("*l")
+ pipe:close()
+ defaults.external_deps_dirs = {
+ mingw_prefix,
+ dir.path("c:", "windows", "system32"),
+ }
+ defaults.makefile = "Makefile"
+ defaults.cmake_generator = "MSYS Makefiles"
+ defaults.local_cache = dir.path(home, ".cache", "luarocks")
+ defaults.variables.MAKE = os.getenv("MAKE") or "make"
+ defaults.variables.CC = os.getenv("CC") or "gcc"
+ defaults.variables.RC = os.getenv("WINDRES") or "windres"
+ defaults.variables.LD = os.getenv("CC") or "gcc"
+ defaults.variables.MT = os.getenv("MT") or nil
+ defaults.variables.AR = os.getenv("AR") or "ar"
+ defaults.variables.RANLIB = os.getenv("RANLIB") or "ranlib"
+
+ defaults.variables.CFLAGS = os.getenv("CFLAGS") or "-O2 -fPIC"
+ if not defaults.variables.CFLAGS:match("-fPIC") then
+ defaults.variables.CFLAGS = defaults.variables.CFLAGS.." -fPIC"
+ end
+
+ defaults.variables.LIBFLAG = "-shared"
+ end
+ end
+
+ if platforms.bsd then
+ defaults.variables.MAKE = "gmake"
+ defaults.gcc_rpath = false
+ defaults.variables.CC = os.getenv("CC") or "cc"
+ defaults.variables.LD = os.getenv("CC") or defaults.variables.CC
+ end
+
+ if platforms.macosx then
+ defaults.variables.MAKE = os.getenv("MAKE") or "make"
+ defaults.external_lib_extension = "dylib"
+ defaults.arch = "macosx-"..target_cpu
+ defaults.variables.LIBFLAG = "-bundle -undefined dynamic_lookup -all_load"
+ local version = util.popen_read("sw_vers -productVersion")
+ if not (version:match("^%d+%.%d+%.%d+$") or version:match("^%d+%.%d+$")) then
+ version = "10.3"
+ end
+ version = vers.parse_version(version)
+ if version >= vers.parse_version("11.0") then
+ version = vers.parse_version("11.0")
+ elseif version >= vers.parse_version("10.10") then
+ version = vers.parse_version("10.8")
+ elseif version >= vers.parse_version("10.5") then
+ version = vers.parse_version("10.5")
+ else
+ defaults.gcc_rpath = false
+ end
+ defaults.variables.CC = "env MACOSX_DEPLOYMENT_TARGET="..tostring(version).." gcc"
+ defaults.variables.LD = "env MACOSX_DEPLOYMENT_TARGET="..tostring(version).." gcc"
+ defaults.web_browser = "open"
+
+ -- XCode SDK
+ local sdk_path = util.popen_read("xcrun --show-sdk-path 2>/dev/null")
+ if sdk_path then
+ table.insert(defaults.external_deps_dirs, sdk_path .. "/usr")
+ table.insert(defaults.external_deps_patterns.lib, 1, "lib?.tbd")
+ table.insert(defaults.runtime_external_deps_patterns.lib, 1, "lib?.tbd")
+ end
+
+ -- Homebrew
+ table.insert(defaults.external_deps_dirs, "/usr/local/opt")
+ defaults.external_deps_subdirs.lib = { "lib", "" }
+ defaults.runtime_external_deps_subdirs.lib = { "lib", "" }
+ table.insert(defaults.external_deps_patterns.lib, 1, "/?/lib/lib?.dylib")
+ table.insert(defaults.runtime_external_deps_patterns.lib, 1, "/?/lib/lib?.dylib")
+ end
+
+ if platforms.linux then
+ defaults.arch = "linux-"..target_cpu
+
+ local gcc_arch = util.popen_read("gcc -print-multiarch 2>/dev/null")
+ if gcc_arch and gcc_arch ~= "" then
+ defaults.external_deps_subdirs.lib = { "lib/" .. gcc_arch, "lib64", "lib" }
+ defaults.runtime_external_deps_subdirs.lib = { "lib/" .. gcc_arch, "lib64", "lib" }
+ else
+ defaults.external_deps_subdirs.lib = { "lib64", "lib" }
+ defaults.runtime_external_deps_subdirs.lib = { "lib64", "lib" }
+ end
+ end
+
+ if platforms.freebsd then
+ defaults.arch = "freebsd-"..target_cpu
+ elseif platforms.dragonfly then
+ defaults.arch = "dragonfly-"..target_cpu
+ elseif platforms.openbsd then
+ defaults.arch = "openbsd-"..target_cpu
+ elseif platforms.netbsd then
+ defaults.arch = "netbsd-"..target_cpu
+ elseif platforms.solaris then
+ defaults.arch = "solaris-"..target_cpu
+ defaults.variables.MAKE = "gmake"
+ end
+
+ -- Expose some more values detected by LuaRocks for use by rockspec authors.
+ defaults.variables.LIB_EXTENSION = defaults.lib_extension
+ defaults.variables.OBJ_EXTENSION = defaults.obj_extension
+
+ return defaults
+end
+
+local function use_defaults(cfg, defaults)
+
+ -- Populate variables with values from their 'defaults' counterparts
+ -- if they were not already set by user.
+ if not cfg.variables then
+ cfg.variables = {}
+ end
+ for k,v in pairs(defaults.variables) do
+ if not cfg.variables[k] then
+ cfg.variables[k] = v
+ end
+ end
+
+ util.deep_merge_under(cfg, defaults)
+
+ -- FIXME get rid of this
+ if not cfg.check_certificates then
+ cfg.variables.CURLNOCERTFLAG = "-k"
+ cfg.variables.WGETNOCERTFLAG = "--no-check-certificate"
+ end
+end
+
+--------------------------------------------------------------------------------
+
+local cfg = {}
+
+--- Initializes the LuaRocks configuration for variables, paths
+-- and OS detection.
+-- @param detected table containing information detected about the
+-- environment. All fields below are optional:
+-- * lua_version (in x.y format, e.g. "5.3")
+-- * lua_bindir (e.g. "/usr/local/bin")
+-- * lua_dir (e.g. "/usr/local")
+-- * lua (e.g. "/usr/local/bin/lua-5.3")
+-- * project_dir (a string with the path of the project directory
+-- when using per-project environments, as created with `luarocks init`)
+-- @param warning a logging function for warnings that takes a string
+-- @return true on success; nil and an error message on failure.
+function cfg.init(detected, warning)
+ detected = detected or {}
+
+ local exit_ok = true
+ local exit_err = nil
+ local exit_what = nil
+
+ local hc_ok, hardcoded = pcall(require, "luarocks.core.hardcoded")
+ if not hc_ok then
+ hardcoded = {}
+ end
+
+ local init = cfg.init
+
+ ----------------------------------------
+ -- Reset the cfg table.
+ ----------------------------------------
+
+ for k, _ in pairs(cfg) do
+ cfg[k] = nil
+ end
+
+ cfg.program_version = program_version
+
+ if hardcoded.IS_BINARY then
+ cfg.is_binary = true
+ end
+
+ -- Use detected values as defaults, overridable via config files or CLI args
+
+ local hardcoded_lua = hardcoded.LUA
+ local hardcoded_lua_dir = hardcoded.LUA_DIR
+ local hardcoded_lua_bindir = hardcoded.LUA_BINDIR
+ local hardcoded_lua_incdir = hardcoded.LUA_INCDIR
+ local hardcoded_lua_libdir = hardcoded.LUA_LIBDIR
+ local hardcoded_lua_version = hardcoded.LUA_VERSION or _VERSION:sub(5)
+
+ -- if --lua-version or --lua-dir are passed from the CLI,
+ -- don't use the hardcoded paths at all
+ if detected.given_lua_version or detected.given_lua_dir then
+ hardcoded_lua = nil
+ hardcoded_lua_dir = nil
+ hardcoded_lua_bindir = nil
+ hardcoded_lua_incdir = nil
+ hardcoded_lua_libdir = nil
+ hardcoded_lua_version = nil
+ end
+
+ cfg.lua_version = detected.lua_version or hardcoded_lua_version
+ cfg.project_dir = (not hardcoded.FORCE_CONFIG) and detected.project_dir
+
+ do
+ local lua = detected.lua or hardcoded_lua
+ local lua_dir = detected.lua_dir or hardcoded_lua_dir
+ local lua_bindir = detected.lua_bindir or hardcoded_lua_bindir
+ cfg.variables = {
+ LUA = lua,
+ LUA_DIR = lua_dir,
+ LUA_BINDIR = lua_bindir,
+ LUA_LIBDIR = hardcoded_lua_libdir,
+ LUA_INCDIR = hardcoded_lua_incdir,
+ }
+ end
+
+ cfg.init = init
+
+ ----------------------------------------
+ -- System detection.
+ ----------------------------------------
+
+ -- A proper build of LuaRocks will hardcode the system
+ -- and proc values with hardcoded.SYSTEM and hardcoded.PROCESSOR.
+ -- If that is not available, we try to identify the system.
+ local system, processor = sysdetect.detect()
+ if hardcoded.SYSTEM then
+ system = hardcoded.SYSTEM
+ end
+ if hardcoded.PROCESSOR then
+ processor = hardcoded.PROCESSOR
+ end
+
+ if system == "windows" then
+ if os.getenv("VCINSTALLDIR") then
+ -- running from the Development Command prompt for VS 2017
+ system = "windows"
+ else
+ local msystem = os.getenv("MSYSTEM")
+ if msystem == nil then
+ system = "mingw"
+ elseif msystem == "MSYS" then
+ system = "msys"
+ else
+ -- MINGW32 or MINGW64
+ system = "msys2_mingw_w64"
+ end
+ end
+ end
+
+ cfg.target_cpu = processor
+
+ local platforms = make_platforms(system)
+
+ ----------------------------------------
+ -- Platform is determined.
+ -- Let's load the config files.
+ ----------------------------------------
+
+ local sys_config_file
+ local home_config_file
+ local project_config_file
+
+ local config_file_name = "config-"..cfg.lua_version..".lua"
+
+ do
+ local sysconfdir = os.getenv("LUAROCKS_SYSCONFDIR") or hardcoded.SYSCONFDIR
+ if platforms.windows and not platforms.msys2_mingw_w64 then
+ cfg.home = os.getenv("APPDATA") or "c:"
+ cfg.home_tree = dir.path(cfg.home, "luarocks")
+ cfg.sysconfdir = sysconfdir or dir.path((os.getenv("PROGRAMFILES") or "c:"), "luarocks")
+ else
+ cfg.home = os.getenv("HOME") or ""
+ cfg.home_tree = dir.path(cfg.home, ".luarocks")
+ cfg.sysconfdir = sysconfdir or detect_sysconfdir() or "/etc/luarocks"
+ end
+ end
+
+ -- Load system configuration file
+ sys_config_file = dir.path(cfg.sysconfdir, config_file_name)
+ local sys_config_ok, err = load_config_file(cfg, platforms, sys_config_file)
+ if err then
+ exit_ok, exit_err, exit_what = nil, err, "config"
+ end
+
+ -- Load user configuration file (if allowed)
+ local home_config_ok
+ local project_config_ok
+ if not hardcoded.FORCE_CONFIG then
+ local env_var = "LUAROCKS_CONFIG_" .. cfg.lua_version:gsub("%.", "_")
+ local env_value = os.getenv(env_var)
+ if not env_value then
+ env_var = "LUAROCKS_CONFIG"
+ env_value = os.getenv(env_var)
+ end
+ -- first try environment provided file, so we can explicitly warn when it is missing
+ if env_value then
+ local env_ok, err = load_config_file(cfg, platforms, env_value)
+ if err then
+ exit_ok, exit_err, exit_what = nil, err, "config"
+ elseif warning and not env_ok then
+ warning("Warning: could not load configuration file `"..env_value.."` given in environment variable "..env_var.."\n")
+ end
+ if env_ok then
+ home_config_ok = true
+ home_config_file = env_value
+ end
+ end
+
+ -- try XDG config home
+ if platforms.unix and not home_config_ok then
+ local xdg_config_home = os.getenv("XDG_CONFIG_HOME") or dir.path(cfg.home, ".config")
+ cfg.homeconfdir = dir.path(xdg_config_home, "luarocks")
+ home_config_file = dir.path(cfg.homeconfdir, config_file_name)
+ home_config_ok, err = load_config_file(cfg, platforms, home_config_file)
+ if err then
+ exit_ok, exit_err, exit_what = nil, err, "config"
+ end
+ end
+
+ -- try the alternative defaults if there was no environment specified file or it didn't work
+ if not home_config_ok then
+ cfg.homeconfdir = cfg.home_tree
+ home_config_file = dir.path(cfg.homeconfdir, config_file_name)
+ home_config_ok, err = load_config_file(cfg, platforms, home_config_file)
+ if err then
+ exit_ok, exit_err, exit_what = nil, err, "config"
+ end
+ end
+
+ -- finally, use the project-specific config file if any
+ if cfg.project_dir then
+ project_config_file = dir.path(cfg.project_dir, ".luarocks", config_file_name)
+ project_config_ok, err = load_config_file(cfg, platforms, project_config_file)
+ if err then
+ exit_ok, exit_err, exit_what = nil, err, "config"
+ end
+ end
+ end
+
+ -- backwards compatibility:
+ if cfg.lua_interpreter and cfg.variables.LUA_BINDIR and not cfg.variables.LUA then
+ cfg.variables.LUA = dir.path(cfg.variables.LUA_BINDIR, cfg.lua_interpreter)
+ end
+
+ ----------------------------------------
+ -- Config files are loaded.
+ -- Let's finish up the cfg table.
+ ----------------------------------------
+
+ -- Settings given via the CLI (i.e. --lua-dir) take precedence over config files.
+ cfg.project_dir = detected.given_project_dir or cfg.project_dir
+ cfg.lua_version = detected.given_lua_version or cfg.lua_version
+ if detected.given_lua_dir then
+ cfg.variables.LUA = detected.lua
+ cfg.variables.LUA_DIR = detected.given_lua_dir
+ cfg.variables.LUA_BINDIR = detected.lua_bindir
+ cfg.variables.LUA_LIBDIR = nil
+ cfg.variables.LUA_INCDIR = nil
+ end
+
+ -- Build a default list of rocks trees if not given
+ if cfg.rocks_trees == nil then
+ cfg.rocks_trees = {}
+ if cfg.home_tree then
+ table.insert(cfg.rocks_trees, { name = "user", root = cfg.home_tree } )
+ end
+ if hardcoded.PREFIX and hardcoded.PREFIX ~= cfg.home_tree then
+ table.insert(cfg.rocks_trees, { name = "system", root = hardcoded.PREFIX } )
+ end
+ end
+
+ local defaults = make_defaults(cfg.lua_version, processor, platforms, cfg.home)
+
+ if platforms.windows and hardcoded.WIN_TOOLS then
+ local tools = { "SEVENZ", "CP", "FIND", "LS", "MD5SUM", "WGET", }
+ for _, tool in ipairs(tools) do
+ defaults.variables[tool] = '"' .. dir.path(hardcoded.WIN_TOOLS, defaults.variables[tool] .. '.exe') .. '"'
+ end
+ else
+ defaults.fs_use_modules = true
+ end
+
+ -- if only cfg.variables.LUA is given in config files,
+ -- derive LUA_BINDIR and LUA_DIR from them.
+ if cfg.variables.LUA and not cfg.variables.LUA_BINDIR then
+ cfg.variables.LUA_BINDIR = cfg.variables.LUA:match("^(.*)[\\/][^\\/]*$")
+ if not cfg.variables.LUA_DIR then
+ cfg.variables.LUA_DIR = cfg.variables.LUA_BINDIR:gsub("[\\/]bin$", "") or cfg.variables.LUA_BINDIR
+ end
+ end
+
+ use_defaults(cfg, defaults)
+
+ cfg.user_agent = "LuaRocks/"..cfg.program_version.." "..cfg.arch
+
+ cfg.config_files = {
+ project = cfg.project_dir and {
+ file = project_config_file,
+ found = not not project_config_ok,
+ },
+ system = {
+ file = sys_config_file,
+ found = not not sys_config_ok,
+ },
+ user = {
+ file = home_config_file,
+ found = not not home_config_ok,
+ },
+ nearest = project_config_ok
+ and project_config_file
+ or (home_config_ok
+ and home_config_file
+ or sys_config_file),
+ }
+
+ cfg.cache = {}
+
+ ----------------------------------------
+ -- Attributes of cfg are set.
+ -- Let's add some methods.
+ ----------------------------------------
+
+ do
+ local function make_paths_from_tree(tree)
+ local lua_path, lib_path, bin_path
+ if type(tree) == "string" then
+ lua_path = dir.path(tree, cfg.lua_modules_path)
+ lib_path = dir.path(tree, cfg.lib_modules_path)
+ bin_path = dir.path(tree, "bin")
+ else
+ lua_path = tree.lua_dir or dir.path(tree.root, cfg.lua_modules_path)
+ lib_path = tree.lib_dir or dir.path(tree.root, cfg.lib_modules_path)
+ bin_path = tree.bin_dir or dir.path(tree.root, "bin")
+ end
+ return lua_path, lib_path, bin_path
+ end
+
+ function cfg.package_paths(current)
+ local new_path, new_cpath, new_bin = {}, {}, {}
+ local function add_tree_to_paths(tree)
+ local lua_path, lib_path, bin_path = make_paths_from_tree(tree)
+ table.insert(new_path, dir.path(lua_path, "?.lua"))
+ table.insert(new_path, dir.path(lua_path, "?", "init.lua"))
+ table.insert(new_cpath, dir.path(lib_path, "?."..cfg.lib_extension))
+ table.insert(new_bin, bin_path)
+ end
+ if current then
+ add_tree_to_paths(current)
+ end
+ for _,tree in ipairs(cfg.rocks_trees) do
+ add_tree_to_paths(tree)
+ end
+ return table.concat(new_path, ";"), table.concat(new_cpath, ";"), table.concat(new_bin, cfg.export_path_separator)
+ end
+ end
+
+ function cfg.init_package_paths()
+ local lr_path, lr_cpath, lr_bin = cfg.package_paths()
+ package.path = util.cleanup_path(package.path .. ";" .. lr_path, ";", cfg.lua_version, true)
+ package.cpath = util.cleanup_path(package.cpath .. ";" .. lr_cpath, ";", cfg.lua_version, true)
+ end
+
+ --- Check if platform was detected
+ -- @param name string: The platform name to check.
+ -- @return boolean: true if LuaRocks is currently running on queried platform.
+ function cfg.is_platform(name)
+ assert(type(name) == "string")
+ return platforms[name]
+ end
+
+ -- @param direction (optional) "least-specific-first" (default) or "most-specific-first"
+ function cfg.each_platform(direction)
+ direction = direction or "least-specific-first"
+ local i, delta
+ if direction == "least-specific-first" then
+ i = 0
+ delta = 1
+ else
+ i = #platform_order + 1
+ delta = -1
+ end
+ return function()
+ local p
+ repeat
+ i = i + delta
+ p = platform_order[i]
+ until (not p) or platforms[p]
+ return p
+ end
+ end
+
+ function cfg.print_platforms()
+ local platform_keys = {}
+ for k,_ in pairs(platforms) do
+ table.insert(platform_keys, k)
+ end
+ table.sort(platform_keys)
+ return table.concat(platform_keys, ", ")
+ end
+
+ return exit_ok, exit_err, exit_what
+end
+
+return cfg
diff --git a/src/luarocks/core/dir.lua b/src/luarocks/core/dir.lua
new file mode 100644
index 0000000..5d6f2c9
--- /dev/null
+++ b/src/luarocks/core/dir.lua
@@ -0,0 +1,98 @@
+
+local dir = {}
+
+local require = nil
+--------------------------------------------------------------------------------
+
+local dir_sep = package.config:sub(1, 1)
+
+local function unquote(c)
+ local first, last = c:sub(1,1), c:sub(-1)
+ if (first == '"' and last == '"') or
+ (first == "'" and last == "'") then
+ return c:sub(2,-2)
+ end
+ return c
+end
+
+--- Describe a path in a cross-platform way.
+-- Use this function to avoid platform-specific directory
+-- separators in other modules. Removes trailing slashes from
+-- each component given, to avoid repeated separators.
+-- Separators inside strings are kept, to handle URLs containing
+-- protocols.
+-- @param ... strings representing directories
+-- @return string: a string with a platform-specific representation
+-- of the path.
+function dir.path(...)
+ local t = {...}
+ while t[1] == "" do
+ table.remove(t, 1)
+ end
+ for i, c in ipairs(t) do
+ t[i] = unquote(c)
+ end
+ return dir.normalize(table.concat(t, "/"))
+end
+
+--- Split protocol and path from an URL or local pathname.
+-- URLs should be in the "protocol://path" format.
+-- For local pathnames, "file" is returned as the protocol.
+-- @param url string: an URL or a local pathname.
+-- @return string, string: the protocol, and the pathname without the protocol.
+function dir.split_url(url)
+ assert(type(url) == "string")
+
+ url = unquote(url)
+ local protocol, pathname = url:match("^([^:]*)://(.*)")
+ if not protocol then
+ protocol = "file"
+ pathname = url
+ end
+ return protocol, pathname
+end
+
+--- Normalize a url or local path.
+-- URLs should be in the "protocol://path" format.
+-- Removes trailing and double slashes, and '.' and '..' components.
+-- for 'file' URLs, the native system's slashes are used.
+-- @param url string: an URL or a local pathname.
+-- @return string: Normalized result.
+function dir.normalize(name)
+ local protocol, pathname = dir.split_url(name)
+ pathname = pathname:gsub("\\", "/"):gsub("(.)/*$", "%1"):gsub("//", "/")
+ local pieces = {}
+ local drive = ""
+ if pathname:match("^.:") then
+ drive, pathname = pathname:match("^(.:)(.*)$")
+ end
+ pathname = pathname .. "/"
+ for piece in pathname:gmatch("(.-)/") do
+ if piece == ".." then
+ local prev = pieces[#pieces]
+ if not prev or prev == ".." then
+ table.insert(pieces, "..")
+ elseif prev ~= "" then
+ table.remove(pieces)
+ end
+ elseif piece ~= "." then
+ table.insert(pieces, piece)
+ end
+ end
+ if #pieces == 0 then
+ pathname = drive .. "."
+ elseif #pieces == 1 and pieces[1] == "" then
+ pathname = drive .. "/"
+ else
+ pathname = drive .. table.concat(pieces, "/")
+ end
+ if protocol ~= "file" then
+ pathname = protocol .. "://" .. pathname
+ else
+ pathname = pathname:gsub("/", dir_sep)
+ end
+ return pathname
+end
+
+return dir
+
diff --git a/src/luarocks/core/manif.lua b/src/luarocks/core/manif.lua
new file mode 100644
index 0000000..3925f63
--- /dev/null
+++ b/src/luarocks/core/manif.lua
@@ -0,0 +1,114 @@
+
+--- Core functions for querying manifest files.
+local manif = {}
+
+local persist = require("luarocks.core.persist")
+local cfg = require("luarocks.core.cfg")
+local dir = require("luarocks.core.dir")
+local util = require("luarocks.core.util")
+local vers = require("luarocks.core.vers")
+local path = require("luarocks.core.path")
+local require = nil
+--------------------------------------------------------------------------------
+
+-- Table with repository identifiers as keys and tables mapping
+-- Lua versions to cached loaded manifests as values.
+local manifest_cache = {}
+
+--- Cache a loaded manifest.
+-- @param repo_url string: The repository identifier.
+-- @param lua_version string: Lua version in "5.x" format, defaults to installed version.
+-- @param manifest table: the manifest to be cached.
+function manif.cache_manifest(repo_url, lua_version, manifest)
+ lua_version = lua_version or cfg.lua_version
+ manifest_cache[repo_url] = manifest_cache[repo_url] or {}
+ manifest_cache[repo_url][lua_version] = manifest
+end
+
+--- Attempt to get cached loaded manifest.
+-- @param repo_url string: The repository identifier.
+-- @param lua_version string: Lua version in "5.x" format, defaults to installed version.
+-- @return table or nil: loaded manifest or nil if cache is empty.
+function manif.get_cached_manifest(repo_url, lua_version)
+ lua_version = lua_version or cfg.lua_version
+ return manifest_cache[repo_url] and manifest_cache[repo_url][lua_version]
+end
+
+--- Back-end function that actually loads the manifest
+-- and stores it in the manifest cache.
+-- @param file string: The local filename of the manifest file.
+-- @param repo_url string: The repository identifier.
+-- @param lua_version string: Lua version in "5.x" format, defaults to installed version.
+-- @return table or (nil, string, string): the manifest or nil,
+-- error message and error code ("open", "load", "run").
+function manif.manifest_loader(file, repo_url, lua_version)
+ local manifest, err, errcode = persist.load_into_table(file)
+ if not manifest then
+ return nil, "Failed loading manifest for "..repo_url..": "..err, errcode
+ end
+ manif.cache_manifest(repo_url, lua_version, manifest)
+ return manifest, err, errcode
+end
+
+--- Load a local manifest describing a repository.
+-- This is used by the luarocks.loader only.
+-- @param repo_url string: URL or pathname for the repository.
+-- @return table or (nil, string, string): A table representing the manifest,
+-- or nil followed by an error message and an error code, see manifest_loader.
+function manif.fast_load_local_manifest(repo_url)
+ assert(type(repo_url) == "string")
+
+ local cached_manifest = manif.get_cached_manifest(repo_url)
+ if cached_manifest then
+ return cached_manifest
+ end
+
+ local pathname = dir.path(repo_url, "manifest")
+ return manif.manifest_loader(pathname, repo_url, nil, true)
+end
+
+function manif.load_rocks_tree_manifests(deps_mode)
+ local trees = {}
+ path.map_trees(deps_mode, function(tree)
+ local manifest, err = manif.fast_load_local_manifest(path.rocks_dir(tree))
+ if manifest then
+ table.insert(trees, {tree=tree, manifest=manifest})
+ end
+ end)
+ return trees
+end
+
+function manif.scan_dependencies(name, version, tree_manifests, dest)
+ if dest[name] then
+ return
+ end
+ dest[name] = version
+
+ for _, tree in ipairs(tree_manifests) do
+ local manifest = tree.manifest
+
+ local pkgdeps
+ if manifest.dependencies and manifest.dependencies[name] then
+ pkgdeps = manifest.dependencies[name][version]
+ end
+ if pkgdeps then
+ for _, dep in ipairs(pkgdeps) do
+ local pkg, constraints = dep.name, dep.constraints
+
+ for _, t in ipairs(tree_manifests) do
+ local entries = t.manifest.repository[pkg]
+ if entries then
+ for ver, _ in util.sortedpairs(entries, vers.compare_versions) do
+ if (not constraints) or vers.match_constraints(vers.parse_version(ver), constraints) then
+ manif.scan_dependencies(pkg, ver, tree_manifests, dest)
+ end
+ end
+ end
+ end
+ end
+ return
+ end
+ end
+end
+
+return manif
diff --git a/src/luarocks/core/path.lua b/src/luarocks/core/path.lua
new file mode 100644
index 0000000..2f037b4
--- /dev/null
+++ b/src/luarocks/core/path.lua
@@ -0,0 +1,157 @@
+
+--- Core LuaRocks-specific path handling functions.
+local path = {}
+
+local cfg = require("luarocks.core.cfg")
+local dir = require("luarocks.core.dir")
+local require = nil
+
+local dir_sep = package.config:sub(1, 1)
+--------------------------------------------------------------------------------
+
+function path.rocks_dir(tree)
+ if tree == nil then
+ tree = cfg.root_dir
+ end
+ if type(tree) == "string" then
+ return dir.path(tree, cfg.rocks_subdir)
+ end
+ assert(type(tree) == "table")
+ return tree.rocks_dir or dir.path(tree.root, cfg.rocks_subdir)
+end
+
+--- Produce a versioned version of a filename.
+-- @param file string: filename (must start with prefix)
+-- @param prefix string: Path prefix for file
+-- @param name string: Rock name
+-- @param version string: Rock version
+-- @return string: a pathname with the same directory parts and a versioned basename.
+function path.versioned_name(file, prefix, name, version)
+ assert(type(file) == "string")
+ assert(type(name) == "string" and not name:match(dir_sep))
+ assert(type(version) == "string")
+
+ local rest = file:sub(#prefix+1):gsub("^" .. dir_sep .. "*", "")
+ local name_version = (name.."_"..version):gsub("%-", "_"):gsub("%.", "_")
+ return dir.path(prefix, name_version.."-"..rest)
+end
+
+--- Convert a pathname to a module identifier.
+-- In Unix, for example, a path "foo/bar/baz.lua" is converted to
+-- "foo.bar.baz"; "bla/init.lua" returns "bla.init"; "foo.so" returns "foo".
+-- @param file string: Pathname of module
+-- @return string: The module identifier, or nil if given path is
+-- not a conformant module path (the function does not check if the
+-- path actually exists).
+function path.path_to_module(file)
+ assert(type(file) == "string")
+
+ local exts = {}
+ local paths = package.path .. ";" .. package.cpath
+ for entry in paths:gmatch("[^;]+") do
+ local ext = entry:match("%.([a-z]+)$")
+ if ext then
+ exts[ext] = true
+ end
+ end
+
+ local name
+ for ext, _ in pairs(exts) do
+ name = file:match("(.*)%." .. ext .. "$")
+ if name then
+ name = name:gsub("[\\/]", ".")
+ break
+ end
+ end
+
+ if not name then name = file end
+
+ -- remove any beginning and trailing slashes-converted-to-dots
+ name = name:gsub("^%.+", ""):gsub("%.+$", "")
+
+ return name
+end
+
+function path.deploy_lua_dir(tree)
+ if type(tree) == "string" then
+ return dir.path(tree, cfg.lua_modules_path)
+ else
+ assert(type(tree) == "table")
+ return tree.lua_dir or dir.path(tree.root, cfg.lua_modules_path)
+ end
+end
+
+function path.deploy_lib_dir(tree)
+ if type(tree) == "string" then
+ return dir.path(tree, cfg.lib_modules_path)
+ else
+ assert(type(tree) == "table")
+ return tree.lib_dir or dir.path(tree.root, cfg.lib_modules_path)
+ end
+end
+
+local is_src_extension = { [".lua"] = true, [".tl"] = true, [".tld"] = true, [".moon"] = true }
+
+--- Return the pathname of the file that would be loaded for a module, indexed.
+-- @param file_name string: module file name as in manifest (eg. "socket/core.so")
+-- @param name string: name of the package (eg. "luasocket")
+-- @param version string: version number (eg. "2.0.2-1")
+-- @param tree string: repository path (eg. "/usr/local")
+-- @param i number: the index, 1 if version is the current default, > 1 otherwise.
+-- This is done this way for use by select_module in luarocks.loader.
+-- @return string: filename of the module (eg. "/usr/local/lib/lua/5.1/socket/core.so")
+function path.which_i(file_name, name, version, tree, i)
+ local deploy_dir
+ local extension = file_name:match("%.[a-z]+$")
+ if is_src_extension[extension] then
+ deploy_dir = path.deploy_lua_dir(tree)
+ file_name = dir.path(deploy_dir, file_name)
+ else
+ deploy_dir = path.deploy_lib_dir(tree)
+ file_name = dir.path(deploy_dir, file_name)
+ end
+ if i > 1 then
+ file_name = path.versioned_name(file_name, deploy_dir, name, version)
+ end
+ return file_name
+end
+
+function path.rocks_tree_to_string(tree)
+ if type(tree) == "string" then
+ return tree
+ else
+ assert(type(tree) == "table")
+ return tree.root
+ end
+end
+
+--- Apply a given function to the active rocks trees based on chosen dependency mode.
+-- @param deps_mode string: Dependency mode: "one" for the current default tree,
+-- "all" for all trees, "order" for all trees with priority >= the current default,
+-- "none" for no trees (this function becomes a nop).
+-- @param fn function: function to be applied, with the tree dir (string) as the first
+-- argument and the remaining varargs of map_trees as the following arguments.
+-- @return a table with all results of invocations of fn collected.
+function path.map_trees(deps_mode, fn, ...)
+ local result = {}
+ local current = cfg.root_dir or cfg.rocks_trees[1]
+ if deps_mode == "one" then
+ table.insert(result, (fn(current, ...)) or 0)
+ else
+ local use = false
+ if deps_mode == "all" then
+ use = true
+ end
+ for _, tree in ipairs(cfg.rocks_trees or {}) do
+ if dir.normalize(path.rocks_tree_to_string(tree)) == dir.normalize(path.rocks_tree_to_string(current)) then
+ use = true
+ end
+ if use then
+ table.insert(result, (fn(tree, ...)) or 0)
+ end
+ end
+ end
+ return result
+end
+
+return path
diff --git a/src/luarocks/core/persist.lua b/src/luarocks/core/persist.lua
new file mode 100644
index 0000000..57e7b5d
--- /dev/null
+++ b/src/luarocks/core/persist.lua
@@ -0,0 +1,81 @@
+
+local persist = {}
+
+local require = nil
+--------------------------------------------------------------------------------
+
+--- Load and run a Lua file in an environment.
+-- @param filename string: the name of the file.
+-- @param env table: the environment table.
+-- @return (true, any) or (nil, string, string): true and the return value
+-- of the file, or nil, an error message and an error code ("open", "load"
+-- or "run") in case of errors.
+function persist.run_file(filename, env)
+ local fd, err = io.open(filename)
+ if not fd then
+ return nil, err, "open"
+ end
+ local str, err = fd:read("*a")
+ fd:close()
+ if not str then
+ return nil, err, "open"
+ end
+ str = str:gsub("^#![^\n]*\n", "")
+ local chunk, ran
+ if _VERSION == "Lua 5.1" then -- Lua 5.1
+ chunk, err = loadstring(str, filename)
+ if chunk then
+ setfenv(chunk, env)
+ ran, err = pcall(chunk)
+ end
+ else -- Lua 5.2
+ chunk, err = load(str, filename, "t", env)
+ if chunk then
+ ran, err = pcall(chunk)
+ end
+ end
+ if not chunk then
+ return nil, "Error loading file: "..err, "load"
+ end
+ if not ran then
+ return nil, "Error running file: "..err, "run"
+ end
+ return true, err
+end
+
+--- Load a Lua file containing assignments, storing them in a table.
+-- The global environment is not propagated to the loaded file.
+-- @param filename string: the name of the file.
+-- @param tbl table or nil: if given, this table is used to store
+-- loaded values.
+-- @return (table, table) or (nil, string, string): a table with the file's assignments
+-- as fields and set of undefined globals accessed in file,
+-- or nil, an error message and an error code ("open"; couldn't open the file,
+-- "load"; compile-time error, or "run"; run-time error)
+-- in case of errors.
+function persist.load_into_table(filename, tbl)
+ assert(type(filename) == "string")
+ assert(type(tbl) == "table" or not tbl)
+
+ local result = tbl or {}
+ local globals = {}
+ local globals_mt = {
+ __index = function(t, k)
+ globals[k] = true
+ end
+ }
+ local save_mt = getmetatable(result)
+ setmetatable(result, globals_mt)
+
+ local ok, err, errcode = persist.run_file(filename, result)
+
+ setmetatable(result, save_mt)
+
+ if not ok then
+ return nil, err, errcode
+ end
+ return result, globals
+end
+
+return persist
+
diff --git a/src/luarocks/core/sysdetect.lua b/src/luarocks/core/sysdetect.lua
new file mode 100644
index 0000000..06454f2
--- /dev/null
+++ b/src/luarocks/core/sysdetect.lua
@@ -0,0 +1,419 @@
+-- Detect the operating system and architecture without forking a subprocess.
+--
+-- We are not going for exhaustive list of every historical system here,
+-- but aiming to cover every platform where LuaRocks is known to run.
+-- If your system is not detected, patches are welcome!
+
+local sysdetect = {}
+
+local function hex(s)
+ return s:gsub("$(..)", function(x)
+ return string.char(tonumber(x, 16))
+ end)
+end
+
+local function read_int8(fd)
+ if io.type(fd) == "closed file" then
+ return nil
+ end
+ local s = fd:read(1)
+ if not s then
+ fd:close()
+ return nil
+ end
+ return s:byte()
+end
+
+local LITTLE = 1
+-- local BIG = 2
+
+local function bytes2number(s, endian)
+ local r = 0
+ if endian == LITTLE then
+ for i = #s, 1, -1 do
+ r = r*256 + s:byte(i,i)
+ end
+ else
+ for i = 1, #s do
+ r = r*256 + s:byte(i,i)
+ end
+ end
+ return r
+end
+
+local function read(fd, bytes, endian)
+ if io.type(fd) == "closed file" then
+ return nil
+ end
+ local s = fd:read(bytes)
+ if not s
+ then fd:close()
+ return nil
+ end
+ return bytes2number(s, endian)
+end
+
+local function read_int32le(fd)
+ return read(fd, 4, LITTLE)
+end
+
+--------------------------------------------------------------------------------
+-- @section ELF
+--------------------------------------------------------------------------------
+
+local e_osabi = {
+ [0x00] = "sysv",
+ [0x01] = "hpux",
+ [0x02] = "netbsd",
+ [0x03] = "linux",
+ [0x04] = "hurd",
+ [0x06] = "solaris",
+ [0x07] = "aix",
+ [0x08] = "irix",
+ [0x09] = "freebsd",
+ [0x0c] = "openbsd",
+}
+
+local e_machines = {
+ [0x02] = "sparc",
+ [0x03] = "x86",
+ [0x08] = "mips",
+ [0x0f] = "hppa",
+ [0x12] = "sparcv8",
+ [0x14] = "ppc",
+ [0x15] = "ppc64",
+ [0x16] = "s390",
+ [0x28] = "arm",
+ [0x2a] = "superh",
+ [0x2b] = "sparcv9",
+ [0x32] = "ia_64",
+ [0x3E] = "x86_64",
+ [0xB6] = "alpha",
+ [0xB7] = "aarch64",
+ [0xF3] = "riscv64",
+ [0x9026] = "alpha",
+}
+
+local SHT_NOTE = 7
+
+local function read_elf_section_headers(fd, hdr)
+ local endian = hdr.endian
+ local word = hdr.word
+
+ local strtab_offset
+ local sections = {}
+ for i = 0, hdr.e_shnum - 1 do
+ fd:seek("set", hdr.e_shoff + (i * hdr.e_shentsize))
+ local section = {}
+ section.sh_name_off = read(fd, 4, endian)
+ section.sh_type = read(fd, 4, endian)
+ section.sh_flags = read(fd, word, endian)
+ section.sh_addr = read(fd, word, endian)
+ section.sh_offset = read(fd, word, endian)
+ section.sh_size = read(fd, word, endian)
+ section.sh_link = read(fd, 4, endian)
+ section.sh_info = read(fd, 4, endian)
+ if section.sh_type == SHT_NOTE then
+ fd:seek("set", section.sh_offset)
+ section.namesz = read(fd, 4, endian)
+ section.descsz = read(fd, 4, endian)
+ section.type = read(fd, 4, endian)
+ section.namedata = fd:read(section.namesz):gsub("%z.*", "")
+ section.descdata = fd:read(section.descsz)
+ elseif i == hdr.e_shstrndx then
+ strtab_offset = section.sh_offset
+ end
+ table.insert(sections, section)
+ end
+ if strtab_offset then
+ for _, section in ipairs(sections) do
+ fd:seek("set", strtab_offset + section.sh_name_off)
+ section.name = fd:read(32):gsub("%z.*", "")
+ sections[section.name] = section
+ end
+ end
+ return sections
+end
+
+local function detect_elf_system(fd, hdr, sections)
+ local system = e_osabi[hdr.osabi]
+ local endian = hdr.endian
+
+ if system == "sysv" then
+ local abitag = sections[".note.ABI-tag"]
+ if abitag then
+ if abitag.namedata == "GNU" and abitag.type == 1
+ and abitag.descdata:sub(0, 4) == "\0\0\0\0" then
+ return "linux"
+ end
+ elseif sections[".SUNW_version"]
+ or sections[".SUNW_signature"] then
+ return "solaris"
+ elseif sections[".note.netbsd.ident"] then
+ return "netbsd"
+ elseif sections[".note.openbsd.ident"] then
+ return "openbsd"
+ elseif sections[".note.tag"] and
+ sections[".note.tag"].namedata == "DragonFly" then
+ return "dragonfly"
+ end
+
+ local gnu_version_r = sections[".gnu.version_r"]
+ if gnu_version_r then
+
+ local dynstr = sections[".dynstr"].sh_offset
+
+ local idx = 0
+ for _ = 0, gnu_version_r.sh_info - 1 do
+ fd:seek("set", gnu_version_r.sh_offset + idx)
+ assert(read(fd, 2, endian)) -- vn_version
+ local vn_cnt = read(fd, 2, endian)
+ local vn_file = read(fd, 4, endian)
+ local vn_next = read(fd, 2, endian)
+
+ fd:seek("set", dynstr + vn_file)
+ local libname = fd:read(64):gsub("%z.*", "")
+
+ if hdr.e_type == 0x03 and libname == "libroot.so" then
+ return "haiku"
+ elseif libname:match("linux") then
+ return "linux"
+ end
+
+ idx = idx + (vn_next * (vn_cnt + 1))
+ end
+ end
+
+ local procfile = io.open("/proc/sys/kernel/ostype")
+ if procfile then
+ local version = procfile:read(6)
+ procfile:close()
+ if version == "Linux\n" then
+ return "linux"
+ end
+ end
+ end
+
+ return system
+end
+
+local function read_elf_header(fd)
+ local hdr = {}
+
+ hdr.bits = read_int8(fd)
+ hdr.endian = read_int8(fd)
+ hdr.elf_version = read_int8(fd)
+ if hdr.elf_version ~= 1 then
+ return nil
+ end
+ hdr.osabi = read_int8(fd)
+ if not hdr.osabi then
+ return nil
+ end
+
+ local endian = hdr.endian
+ fd:seek("set", 0x10)
+ hdr.e_type = read(fd, 2, endian)
+ local machine = read(fd, 2, endian)
+ local processor = e_machines[machine] or "unknown"
+ if endian == 1 and processor == "ppc64" then
+ processor = "ppc64le"
+ end
+
+ local elfversion = read(fd, 4, endian)
+ if elfversion ~= 1 then
+ return nil
+ end
+
+ local word = (hdr.bits == 1) and 4 or 8
+ hdr.word = word
+
+ hdr.e_entry = read(fd, word, endian)
+ hdr.e_phoff = read(fd, word, endian)
+ hdr.e_shoff = read(fd, word, endian)
+ hdr.e_flags = read(fd, 4, endian)
+ hdr.e_ehsize = read(fd, 2, endian)
+ hdr.e_phentsize = read(fd, 2, endian)
+ hdr.e_phnum = read(fd, 2, endian)
+ hdr.e_shentsize = read(fd, 2, endian)
+ hdr.e_shnum = read(fd, 2, endian)
+ hdr.e_shstrndx = read(fd, 2, endian)
+
+ return hdr, processor
+end
+
+local function detect_elf(fd)
+ local hdr, processor = read_elf_header(fd)
+ if not hdr then
+ return nil
+ end
+ local sections = read_elf_section_headers(fd, hdr)
+ local system = detect_elf_system(fd, hdr, sections)
+ return system, processor
+end
+
+--------------------------------------------------------------------------------
+-- @section Mach Objects (Apple)
+--------------------------------------------------------------------------------
+
+local mach_l64 = {
+ [7] = "x86_64",
+ [12] = "aarch64",
+}
+
+local mach_b64 = {
+ [0] = "ppc64",
+}
+
+local mach_l32 = {
+ [7] = "x86",
+ [12] = "arm",
+}
+
+local mach_b32 = {
+ [0] = "ppc",
+}
+
+local function detect_mach(magic, fd)
+ if not magic then
+ return nil
+ end
+
+ if magic == hex("$CA$FE$BA$BE") then
+ -- fat binary, go for the first one
+ fd:seek("set", 0x12)
+ local offs = read_int8(fd)
+ if not offs then
+ return nil
+ end
+ fd:seek("set", offs * 256)
+ magic = fd:read(4)
+ return detect_mach(magic, fd)
+ end
+
+ local cputype = read_int8(fd)
+
+ if magic == hex("$CF$FA$ED$FE") then
+ return "macosx", mach_l64[cputype] or "unknown"
+ elseif magic == hex("$FE$ED$CF$FA") then
+ return "macosx", mach_b64[cputype] or "unknown"
+ elseif magic == hex("$CE$FA$ED$FE") then
+ return "macosx", mach_l32[cputype] or "unknown"
+ elseif magic == hex("$FE$ED$FA$CE") then
+ return "macosx", mach_b32[cputype] or "unknown"
+ end
+end
+
+--------------------------------------------------------------------------------
+-- @section PE (Windows)
+--------------------------------------------------------------------------------
+
+local pe_machine = {
+ [0x8664] = "x86_64",
+ [0x01c0] = "arm",
+ [0x01c4] = "armv7l",
+ [0xaa64] = "arm64",
+ [0x014c] = "x86",
+}
+
+local function detect_pe(fd)
+ fd:seek("set", 60) -- position of PE header position
+ local peoffset = read_int32le(fd) -- read position of PE header
+ if not peoffset then
+ return nil
+ end
+ local system = "windows"
+ fd:seek("set", peoffset + 4) -- move to position of Machine section
+ local machine = read(fd, 2, LITTLE)
+ local processor = pe_machine[machine]
+
+ local rdata_pos = fd:read(736):match(".rdata%z%z............(....)")
+ if rdata_pos then
+ rdata_pos = bytes2number(rdata_pos, LITTLE)
+ fd:seek("set", rdata_pos)
+ local data = fd:read(512)
+ if data:match("cygwin") or data:match("cyggcc") then
+ system = "cygwin"
+ end
+ end
+
+ return system, processor or "unknown"
+end
+
+--------------------------------------------------------------------------------
+-- @section API
+--------------------------------------------------------------------------------
+
+function sysdetect.detect_file(file)
+ assert(type(file) == "string")
+ local fd = io.open(file, "rb")
+ if not fd then
+ return nil
+ end
+ local magic = fd:read(4)
+ if magic == hex("$7FELF") then
+ return detect_elf(fd)
+ end
+ if magic == hex("MZ$90$00") then
+ return detect_pe(fd)
+ end
+ return detect_mach(magic, fd)
+end
+
+local cache_system
+local cache_processor
+
+function sysdetect.detect(input_file)
+ local dirsep = package.config:sub(1,1)
+ local files
+
+ if input_file then
+ files = { input_file }
+ else
+ if cache_system then
+ return cache_system, cache_processor
+ end
+
+ local PATHsep
+ local interp = arg and arg[-1]
+ if dirsep == "/" then
+ -- Unix
+ files = {
+ "/bin/sh", -- Unix: well-known POSIX path
+ "/proc/self/exe", -- Linux: this should always have a working binary
+ }
+ PATHsep = ":"
+ else
+ -- Windows
+ local systemroot = os.getenv("SystemRoot")
+ files = {
+ systemroot .. "\\system32\\notepad.exe", -- well-known Windows path
+ systemroot .. "\\explorer.exe", -- well-known Windows path
+ }
+ if interp and not interp:lower():match("exe$") then
+ interp = interp .. ".exe"
+ end
+ PATHsep = ";"
+ end
+ if interp then
+ if interp:match(dirsep) then
+ -- interpreter path is absolute
+ table.insert(files, 1, interp)
+ else
+ for d in (os.getenv("PATH") or ""):gmatch("[^"..PATHsep.."]+") do
+ table.insert(files, d .. dirsep .. interp)
+ end
+ end
+ end
+ end
+ for _, f in ipairs(files) do
+ local system, processor = sysdetect.detect_file(f)
+ if system then
+ cache_system = system
+ cache_processor = processor
+ return system, processor
+ end
+ end
+end
+
+return sysdetect
diff --git a/src/luarocks/core/util.lua b/src/luarocks/core/util.lua
new file mode 100644
index 0000000..e9abdd3
--- /dev/null
+++ b/src/luarocks/core/util.lua
@@ -0,0 +1,322 @@
+
+local util = {}
+
+local require = nil
+--------------------------------------------------------------------------------
+
+local dir_sep = package.config:sub(1, 1)
+
+--- Run a process and read a its output.
+-- Equivalent to io.popen(cmd):read("*l"), except that it
+-- closes the fd right away.
+-- @param cmd string: The command to execute
+-- @param spec string: "*l" by default, to read a single line.
+-- May be used to read more, passing, for instance, "*a".
+-- @return string: the output of the program.
+function util.popen_read(cmd, spec)
+ local tmpfile = (dir_sep == "\\")
+ and (os.getenv("TMP") .. "/luarocks-" .. tostring(math.floor(math.random() * 10000)))
+ or os.tmpname()
+ os.execute(cmd .. " > " .. tmpfile)
+ local fd = io.open(tmpfile, "rb")
+ if not fd then
+ os.remove(tmpfile)
+ return ""
+ end
+ local out = fd:read(spec or "*l")
+ fd:close()
+ os.remove(tmpfile)
+ return out or ""
+end
+
+---
+-- Formats tables with cycles recursively to any depth.
+-- References to other tables are shown as values.
+-- Self references are indicated.
+-- The string returned is "Lua code", which can be processed
+-- (in the case in which indent is composed by spaces or "--").
+-- Userdata and function keys and values are shown as strings,
+-- which logically are exactly not equivalent to the original code.
+-- This routine can serve for pretty formating tables with
+-- proper indentations, apart from printing them:
+-- io.write(table.show(t, "t")) -- a typical use
+-- Written by Julio Manuel Fernandez-Diaz,
+-- Heavily based on "Saving tables with cycles", PIL2, p. 113.
+-- @param t table: is the table.
+-- @param tname string: is the name of the table (optional)
+-- @param top_indent string: is a first indentation (optional).
+-- @return string: the pretty-printed table
+function util.show_table(t, tname, top_indent)
+ local cart -- a container
+ local autoref -- for self references
+
+ local function is_empty_table(tbl) return next(tbl) == nil end
+
+ local function basic_serialize(o)
+ local so = tostring(o)
+ if type(o) == "function" then
+ local info = debug and debug.getinfo(o, "S")
+ if not info then
+ return ("%q"):format(so)
+ end
+ -- info.name is nil because o is not a calling level
+ if info.what == "C" then
+ return ("%q"):format(so .. ", C function")
+ else
+ -- the information is defined through lines
+ return ("%q"):format(so .. ", defined in (" .. info.linedefined .. "-" .. info.lastlinedefined .. ")" .. info.source)
+ end
+ elseif type(o) == "number" then
+ return so
+ else
+ return ("%q"):format(so)
+ end
+ end
+
+ local function add_to_cart(value, name, indent, saved, field)
+ indent = indent or ""
+ saved = saved or {}
+ field = field or name
+
+ cart = cart .. indent .. field
+
+ if type(value) ~= "table" then
+ cart = cart .. " = " .. basic_serialize(value) .. ";\n"
+ else
+ if saved[value] then
+ cart = cart .. " = {}; -- " .. saved[value] .. " (self reference)\n"
+ autoref = autoref .. name .. " = " .. saved[value] .. ";\n"
+ else
+ saved[value] = name
+ if is_empty_table(value) then
+ cart = cart .. " = {};\n"
+ else
+ cart = cart .. " = {\n"
+ for k, v in pairs(value) do
+ k = basic_serialize(k)
+ local fname = ("%s[%s]"):format(name, k)
+ field = ("[%s]"):format(k)
+ -- three spaces between levels
+ add_to_cart(v, fname, indent .. " ", saved, field)
+ end
+ cart = cart .. indent .. "};\n"
+ end
+ end
+ end
+ end
+
+ tname = tname or "__unnamed__"
+ if type(t) ~= "table" then
+ return tname .. " = " .. basic_serialize(t)
+ end
+ cart, autoref = "", ""
+ add_to_cart(t, tname, top_indent)
+ return cart .. autoref
+end
+
+--- Merges contents of src on top of dst's contents
+-- (i.e. if an key from src already exists in dst, replace it).
+-- @param dst Destination table, which will receive src's contents.
+-- @param src Table which provides new contents to dst.
+function util.deep_merge(dst, src)
+ for k, v in pairs(src) do
+ if type(v) == "table" then
+ if dst[k] == nil then
+ dst[k] = {}
+ end
+ if type(dst[k]) == "table" then
+ util.deep_merge(dst[k], v)
+ else
+ dst[k] = v
+ end
+ else
+ dst[k] = v
+ end
+ end
+end
+
+--- Merges contents of src below those of dst's contents
+-- (i.e. if an key from src already exists in dst, do not replace it).
+-- @param dst Destination table, which will receive src's contents.
+-- @param src Table which provides new contents to dst.
+function util.deep_merge_under(dst, src)
+ for k, v in pairs(src) do
+ if type(v) == "table" then
+ if dst[k] == nil then
+ dst[k] = {}
+ end
+ if type(dst[k]) == "table" then
+ util.deep_merge_under(dst[k], v)
+ end
+ elseif dst[k] == nil then
+ dst[k] = v
+ end
+ end
+end
+
+--- Clean up a path-style string ($PATH, $LUA_PATH/package.path, etc.),
+-- removing repeated entries and making sure only the relevant
+-- Lua version is used.
+-- Example: given ("a;b;c;a;b;d", ";"), returns "a;b;c;d".
+-- @param list string: A path string (from $PATH or package.path)
+-- @param sep string: The separator
+-- @param lua_version (optional) string: The Lua version to use.
+-- @param keep_first (optional) if true, keep first occurrence in case
+-- of duplicates; otherwise keep last occurrence. The default is false.
+function util.cleanup_path(list, sep, lua_version, keep_first)
+ assert(type(list) == "string")
+ assert(type(sep) == "string")
+
+ list = list:gsub(dir_sep, "/")
+
+ local parts = util.split_string(list, sep)
+ local final, entries = {}, {}
+ local start, stop, step
+
+ if keep_first then
+ start, stop, step = 1, #parts, 1
+ else
+ start, stop, step = #parts, 1, -1
+ end
+
+ for i = start, stop, step do
+ local part = parts[i]:gsub("//", "/")
+ if lua_version then
+ part = part:gsub("/lua/([%d.]+)/", function(part_version)
+ if part_version:sub(1, #lua_version) ~= lua_version then
+ return "/lua/"..lua_version.."/"
+ end
+ end)
+ end
+ if not entries[part] then
+ local at = keep_first and #final+1 or 1
+ table.insert(final, at, part)
+ entries[part] = true
+ end
+ end
+
+ return (table.concat(final, sep):gsub("/", dir_sep))
+end
+
+-- from http://lua-users.org/wiki/SplitJoin
+-- by Philippe Lhoste
+function util.split_string(str, delim, maxNb)
+ -- Eliminate bad cases...
+ if string.find(str, delim) == nil then
+ return { str }
+ end
+ if maxNb == nil or maxNb < 1 then
+ maxNb = 0 -- No limit
+ end
+ local result = {}
+ local pat = "(.-)" .. delim .. "()"
+ local nb = 0
+ local lastPos
+ for part, pos in string.gmatch(str, pat) do
+ nb = nb + 1
+ result[nb] = part
+ lastPos = pos
+ if nb == maxNb then break end
+ end
+ -- Handle the last field
+ if nb ~= maxNb then
+ result[nb + 1] = string.sub(str, lastPos)
+ end
+ return result
+end
+
+--- Return an array of keys of a table.
+-- @param tbl table: The input table.
+-- @return table: The array of keys.
+function util.keys(tbl)
+ local ks = {}
+ for k,_ in pairs(tbl) do
+ table.insert(ks, k)
+ end
+ return ks
+end
+
+--- Print a line to standard error
+function util.printerr(...)
+ io.stderr:write(table.concat({...},"\t"))
+ io.stderr:write("\n")
+end
+
+--- Display a warning message.
+-- @param msg string: the warning message
+function util.warning(msg)
+ util.printerr("Warning: "..msg)
+end
+
+--- Simple sort function used as a default for util.sortedpairs.
+local function default_sort(a, b)
+ local ta = type(a)
+ local tb = type(b)
+ if ta == "number" and tb == "number" then
+ return a < b
+ elseif ta == "number" then
+ return true
+ elseif tb == "number" then
+ return false
+ else
+ return tostring(a) < tostring(b)
+ end
+end
+
+--- A table iterator generator that returns elements sorted by key,
+-- to be used in "for" loops.
+-- @param tbl table: The table to be iterated.
+-- @param sort_function function or table or nil: An optional comparison function
+-- to be used by table.sort when sorting keys, or an array listing an explicit order
+-- for keys. If a value itself is an array, it is taken so that the first element
+-- is a string representing the field name, and the second element is a priority table
+-- for that key, which is returned by the iterator as the third value after the key
+-- and the value.
+-- @return function: the iterator function.
+function util.sortedpairs(tbl, sort_function)
+ sort_function = sort_function or default_sort
+ local keys = util.keys(tbl)
+ local sub_orders = {}
+
+ if type(sort_function) == "function" then
+ table.sort(keys, sort_function)
+ else
+ local order = sort_function
+ local ordered_keys = {}
+ local all_keys = keys
+ keys = {}
+
+ for _, order_entry in ipairs(order) do
+ local key, sub_order
+ if type(order_entry) == "table" then
+ key = order_entry[1]
+ sub_order = order_entry[2]
+ else
+ key = order_entry
+ end
+
+ if tbl[key] then
+ ordered_keys[key] = true
+ sub_orders[key] = sub_order
+ table.insert(keys, key)
+ end
+ end
+
+ table.sort(all_keys, default_sort)
+ for _, key in ipairs(all_keys) do
+ if not ordered_keys[key] then
+ table.insert(keys, key)
+ end
+ end
+ end
+
+ local i = 1
+ return function()
+ local key = keys[i]
+ i = i + 1
+ return key, tbl[key], sub_orders[key]
+ end
+end
+
+return util
+
diff --git a/src/luarocks/core/vers.lua b/src/luarocks/core/vers.lua
new file mode 100644
index 0000000..8e61798
--- /dev/null
+++ b/src/luarocks/core/vers.lua
@@ -0,0 +1,207 @@
+
+local vers = {}
+
+local util = require("luarocks.core.util")
+local require = nil
+--------------------------------------------------------------------------------
+
+local deltas = {
+ dev = 120000000,
+ scm = 110000000,
+ cvs = 100000000,
+ rc = -1000,
+ pre = -10000,
+ beta = -100000,
+ alpha = -1000000
+}
+
+local version_mt = {
+ --- Equality comparison for versions.
+ -- All version numbers must be equal.
+ -- If both versions have revision numbers, they must be equal;
+ -- otherwise the revision number is ignored.
+ -- @param v1 table: version table to compare.
+ -- @param v2 table: version table to compare.
+ -- @return boolean: true if they are considered equivalent.
+ __eq = function(v1, v2)
+ if #v1 ~= #v2 then
+ return false
+ end
+ for i = 1, #v1 do
+ if v1[i] ~= v2[i] then
+ return false
+ end
+ end
+ if v1.revision and v2.revision then
+ return (v1.revision == v2.revision)
+ end
+ return true
+ end,
+ --- Size comparison for versions.
+ -- All version numbers are compared.
+ -- If both versions have revision numbers, they are compared;
+ -- otherwise the revision number is ignored.
+ -- @param v1 table: version table to compare.
+ -- @param v2 table: version table to compare.
+ -- @return boolean: true if v1 is considered lower than v2.
+ __lt = function(v1, v2)
+ for i = 1, math.max(#v1, #v2) do
+ local v1i, v2i = v1[i] or 0, v2[i] or 0
+ if v1i ~= v2i then
+ return (v1i < v2i)
+ end
+ end
+ if v1.revision and v2.revision then
+ return (v1.revision < v2.revision)
+ end
+ return false
+ end,
+ -- @param v1 table: version table to compare.
+ -- @param v2 table: version table to compare.
+ -- @return boolean: true if v1 is considered lower than or equal to v2.
+ __le = function(v1, v2)
+ return not (v2 < v1) -- luacheck: ignore
+ end,
+ --- Return version as a string.
+ -- @param v The version table.
+ -- @return The string representation.
+ __tostring = function(v)
+ return v.string
+ end,
+}
+
+local version_cache = {}
+setmetatable(version_cache, {
+ __mode = "kv"
+})
+
+--- Parse a version string, converting to table format.
+-- A version table contains all components of the version string
+-- converted to numeric format, stored in the array part of the table.
+-- If the version contains a revision, it is stored numerically
+-- in the 'revision' field. The original string representation of
+-- the string is preserved in the 'string' field.
+-- Returned version tables use a metatable
+-- allowing later comparison through relational operators.
+-- @param vstring string: A version number in string format.
+-- @return table or nil: A version table or nil
+-- if the input string contains invalid characters.
+function vers.parse_version(vstring)
+ if not vstring then return nil end
+ assert(type(vstring) == "string")
+
+ local cached = version_cache[vstring]
+ if cached then
+ return cached
+ end
+
+ local version = {}
+ local i = 1
+
+ local function add_token(number)
+ version[i] = version[i] and version[i] + number/100000 or number
+ i = i + 1
+ end
+
+ -- trim leading and trailing spaces
+ local v = vstring:match("^%s*(.*)%s*$")
+ version.string = v
+ -- store revision separately if any
+ local main, revision = v:match("(.*)%-(%d+)$")
+ if revision then
+ v = main
+ version.revision = tonumber(revision)
+ end
+ while #v > 0 do
+ -- extract a number
+ local token, rest = v:match("^(%d+)[%.%-%_]*(.*)")
+ if token then
+ add_token(tonumber(token))
+ else
+ -- extract a word
+ token, rest = v:match("^(%a+)[%.%-%_]*(.*)")
+ if not token then
+ util.warning("version number '"..v.."' could not be parsed.")
+ version[i] = 0
+ break
+ end
+ version[i] = deltas[token] or (token:byte() / 1000)
+ end
+ v = rest
+ end
+ setmetatable(version, version_mt)
+ version_cache[vstring] = version
+ return version
+end
+
+--- Utility function to compare version numbers given as strings.
+-- @param a string: one version.
+-- @param b string: another version.
+-- @return boolean: True if a > b.
+function vers.compare_versions(a, b)
+ if a == b then
+ return false
+ end
+ return vers.parse_version(a) > vers.parse_version(b)
+end
+
+--- A more lenient check for equivalence between versions.
+-- This returns true if the requested components of a version
+-- match and ignore the ones that were not given. For example,
+-- when requesting "2", then "2", "2.1", "2.3.5-9"... all match.
+-- When requesting "2.1", then "2.1", "2.1.3" match, but "2.2"
+-- doesn't.
+-- @param version string or table: Version to be tested; may be
+-- in string format or already parsed into a table.
+-- @param requested string or table: Version requested; may be
+-- in string format or already parsed into a table.
+-- @return boolean: True if the tested version matches the requested
+-- version, false otherwise.
+local function partial_match(version, requested)
+ assert(type(version) == "string" or type(version) == "table")
+ assert(type(requested) == "string" or type(version) == "table")
+
+ if type(version) ~= "table" then version = vers.parse_version(version) end
+ if type(requested) ~= "table" then requested = vers.parse_version(requested) end
+ if not version or not requested then return false end
+
+ for i, ri in ipairs(requested) do
+ local vi = version[i] or 0
+ if ri ~= vi then return false end
+ end
+ if requested.revision then
+ return requested.revision == version.revision
+ end
+ return true
+end
+
+--- Check if a version satisfies a set of constraints.
+-- @param version table: A version in table format
+-- @param constraints table: An array of constraints in table format.
+-- @return boolean: True if version satisfies all constraints,
+-- false otherwise.
+function vers.match_constraints(version, constraints)
+ assert(type(version) == "table")
+ assert(type(constraints) == "table")
+ local ok = true
+ setmetatable(version, version_mt)
+ for _, constr in pairs(constraints) do
+ if type(constr.version) == "string" then
+ constr.version = vers.parse_version(constr.version)
+ end
+ local constr_version, constr_op = constr.version, constr.op
+ setmetatable(constr_version, version_mt)
+ if constr_op == "==" then ok = version == constr_version
+ elseif constr_op == "~=" then ok = version ~= constr_version
+ elseif constr_op == ">" then ok = version > constr_version
+ elseif constr_op == "<" then ok = version < constr_version
+ elseif constr_op == ">=" then ok = version >= constr_version
+ elseif constr_op == "<=" then ok = version <= constr_version
+ elseif constr_op == "~>" then ok = partial_match(version, constr_version)
+ end
+ if not ok then break end
+ end
+ return ok
+end
+
+return vers
diff --git a/src/luarocks/deplocks.lua b/src/luarocks/deplocks.lua
new file mode 100644
index 0000000..d62908f
--- /dev/null
+++ b/src/luarocks/deplocks.lua
@@ -0,0 +1,106 @@
+local deplocks = {}
+
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+local util = require("luarocks.util")
+local persist = require("luarocks.persist")
+
+local deptable = {}
+local deptable_mode = "start"
+local deplock_abs_filename
+local deplock_root_rock_name
+
+function deplocks.init(root_rock_name, dirname)
+ if deptable_mode ~= "start" then
+ return
+ end
+ deptable_mode = "create"
+
+ local filename = dir.path(dirname, "luarocks.lock")
+ deplock_abs_filename = fs.absolute_name(filename)
+ deplock_root_rock_name = root_rock_name
+
+ deptable = {}
+end
+
+function deplocks.get_abs_filename(root_rock_name)
+ if root_rock_name == deplock_root_rock_name then
+ return deplock_abs_filename
+ end
+end
+
+function deplocks.load(root_rock_name, dirname)
+ if deptable_mode ~= "start" then
+ return true, nil
+ end
+ deptable_mode = "locked"
+
+ local filename = dir.path(dirname, "luarocks.lock")
+ local ok, result, errcode = persist.run_file(filename, {})
+ if errcode == "load" or errcode == "run" then
+ -- bad config file or depends on env, so error out
+ return nil, nil, "Could not read existing lockfile " .. filename
+ end
+
+ if errcode == "open" then
+ -- could not open, maybe file does not exist
+ return true, nil
+ end
+
+ deplock_abs_filename = fs.absolute_name(filename)
+ deplock_root_rock_name = root_rock_name
+
+ deptable = result
+ return true, filename
+end
+
+function deplocks.add(depskey, name, version)
+ if deptable_mode == "locked" then
+ return
+ end
+
+ local dk = deptable[depskey]
+ if not dk then
+ dk = {}
+ deptable[depskey] = dk
+ end
+
+ if not dk[name] then
+ dk[name] = version
+ end
+end
+
+function deplocks.get(depskey, name)
+ local dk = deptable[depskey]
+ if not dk then
+ return nil
+ end
+
+ return deptable[name]
+end
+
+function deplocks.write_file()
+ if deptable_mode ~= "create" then
+ return true
+ end
+
+ return persist.save_as_module(deplock_abs_filename, deptable)
+end
+
+-- a table-like interface to deplocks
+function deplocks.proxy(depskey)
+ return setmetatable({}, {
+ __index = function(_, k)
+ return deplocks.get(depskey, k)
+ end,
+ __newindex = function(_, k, v)
+ return deplocks.add(depskey, k, v)
+ end,
+ })
+end
+
+function deplocks.each(depskey)
+ return util.sortedpairs(deptable[depskey] or {})
+end
+
+return deplocks
diff --git a/src/luarocks/deps.lua b/src/luarocks/deps.lua
new file mode 100644
index 0000000..1cd500c
--- /dev/null
+++ b/src/luarocks/deps.lua
@@ -0,0 +1,831 @@
+
+--- High-level dependency related functions.
+local deps = {}
+
+local cfg = require("luarocks.core.cfg")
+local manif = require("luarocks.manif")
+local path = require("luarocks.path")
+local dir = require("luarocks.dir")
+local fun = require("luarocks.fun")
+local util = require("luarocks.util")
+local vers = require("luarocks.core.vers")
+local queries = require("luarocks.queries")
+local deplocks = require("luarocks.deplocks")
+
+--- Generate a function that matches dep queries against the manifest,
+-- taking into account rocks_provided, the list of versions to skip,
+-- and the lockfile.
+-- @param deps_mode "one", "none", "all" or "order"
+-- @param rocks_provided a one-level table mapping names to versions,
+-- listing rocks to consider provided by the VM
+-- @param rocks_provided table: A table of auto-provided dependencies.
+-- by this Lua implementation for the given dependency.
+-- @param depskey key to use when matching the lockfile ("dependencies",
+-- "build_dependencies", etc.)
+-- @param skip_set a two-level table mapping names to versions to
+-- boolean, listing rocks that should not be matched
+-- @return function(dep): {string}, {string:string}, string, boolean
+-- * array of matching versions
+-- * map of versions to locations
+-- * version matched via lockfile if any
+-- * true if rock matched via rocks_provided
+local function prepare_get_versions(deps_mode, rocks_provided, depskey, skip_set)
+ assert(type(deps_mode) == "string")
+ assert(type(rocks_provided) == "table")
+ assert(type(depskey) == "string")
+ assert(type(skip_set) == "table" or skip_set == nil)
+
+ return function(dep)
+ local versions, locations
+ local provided = rocks_provided[dep.name]
+ if provided then
+ -- Provided rocks have higher priority than manifest's rocks.
+ versions, locations = { provided }, {}
+ else
+ if deps_mode == "none" then
+ deps_mode = "one"
+ end
+ versions, locations = manif.get_versions(dep, deps_mode)
+ end
+
+ if skip_set and skip_set[dep.name] then
+ for i = #versions, 1, -1 do
+ local v = versions[i]
+ if skip_set[dep.name][v] then
+ table.remove(versions, i)
+ end
+ end
+ end
+
+ local lockversion = deplocks.get(depskey, dep.name)
+
+ return versions, locations, lockversion, provided ~= nil
+ end
+end
+
+--- Attempt to match a dependency to an installed rock.
+-- @param get_versions a getter function obtained via prepare_get_versions
+-- @return (string, string, table) or (nil, nil, table):
+-- 1. latest installed version of the rock matching the dependency
+-- 2. location where the installed version is installed
+-- 3. the 'dep' query table
+-- 4. true if provided via VM
+-- or
+-- 1. nil
+-- 2. nil
+-- 3. either 'dep' or an alternative query to be used
+-- 4. false
+local function match_dep(dep, get_versions)
+ assert(type(dep) == "table")
+ assert(type(get_versions) == "function")
+
+ local versions, locations, lockversion, provided = get_versions(dep)
+
+ local latest_version
+ local latest_vstring
+ for _, vstring in ipairs(versions) do
+ local version = vers.parse_version(vstring)
+ if vers.match_constraints(version, dep.constraints) then
+ if not latest_version or version > latest_version then
+ latest_version = version
+ latest_vstring = vstring
+ end
+ end
+ end
+
+ if lockversion and not locations[lockversion] then
+ local latest_matching_msg = ""
+ if latest_vstring and latest_vstring ~= lockversion then
+ latest_matching_msg = " (latest matching is " .. latest_vstring .. ")"
+ end
+ util.printout("Forcing " .. dep.name .. " to pinned version " .. lockversion .. latest_matching_msg)
+ return nil, nil, queries.new(dep.name, dep.namespace, lockversion)
+ end
+
+ return latest_vstring, locations[latest_vstring], dep, provided
+end
+
+local function match_all_deps(dependencies, get_versions)
+ assert(type(dependencies) == "table")
+ assert(type(get_versions) == "function")
+
+ local matched, missing, no_upgrade = {}, {}, {}
+
+ for _, dep in ipairs(dependencies) do
+ local found, _, provided
+ found, _, dep, provided = match_dep(dep, get_versions)
+ if found then
+ if not provided then
+ matched[dep] = {name = dep.name, version = found}
+ end
+ else
+ if dep.constraints[1] and dep.constraints[1].no_upgrade then
+ no_upgrade[dep.name] = dep
+ else
+ missing[dep.name] = dep
+ end
+ end
+ end
+ return matched, missing, no_upgrade
+end
+
+--- Attempt to match dependencies of a rockspec to installed rocks.
+-- @param dependencies table: The table of dependencies.
+-- @param rocks_provided table: The table of auto-provided dependencies.
+-- @param skip_set table or nil: Program versions to not use as valid matches.
+-- Table where keys are program names and values are tables where keys
+-- are program versions and values are 'true'.
+-- @param deps_mode string: Which trees to check dependencies for
+-- @return table, table, table: A table where keys are dependencies parsed
+-- in table format and values are tables containing fields 'name' and
+-- version' representing matches; a table of missing dependencies
+-- parsed as tables; and a table of "no-upgrade" missing dependencies
+-- (to be used in plugin modules so that a plugin does not force upgrade of
+-- its parent application).
+function deps.match_deps(dependencies, rocks_provided, skip_set, deps_mode)
+ assert(type(dependencies) == "table")
+ assert(type(rocks_provided) == "table")
+ assert(type(skip_set) == "table" or skip_set == nil)
+ assert(type(deps_mode) == "string")
+
+ local get_versions = prepare_get_versions(deps_mode, rocks_provided, "dependencies", skip_set)
+ return match_all_deps(dependencies, get_versions)
+end
+
+local function rock_status(dep, get_versions)
+ assert(dep:type() == "query")
+ assert(type(get_versions) == "function")
+
+ local installed, _, _, provided = match_dep(dep, get_versions)
+ local installation_type = provided and "provided by VM" or "installed"
+ return installed and installed.." "..installation_type..": success" or "not installed"
+end
+
+--- Check depenendencies of a package and report any missing ones.
+-- @param name string: package name.
+-- @param version string: package version.
+-- @param dependencies table: array of dependencies.
+-- @param deps_mode string: Which trees to check dependencies for
+-- @param rocks_provided table: A table of auto-dependencies provided
+-- by this Lua implementation for the given dependency.
+-- "one" for the current default tree, "all" for all trees,
+-- "order" for all trees with priority >= the current default, "none" for no trees.
+function deps.report_missing_dependencies(name, version, dependencies, deps_mode, rocks_provided)
+ assert(type(name) == "string")
+ assert(type(version) == "string")
+ assert(type(dependencies) == "table")
+ assert(type(deps_mode) == "string")
+ assert(type(rocks_provided) == "table")
+
+ if deps_mode == "none" then
+ return
+ end
+
+ local get_versions = prepare_get_versions(deps_mode, rocks_provided, "dependencies")
+
+ local first_missing_dep = true
+
+ for _, dep in ipairs(dependencies) do
+ local found, _
+ found, _, dep = match_dep(dep, get_versions)
+ if not found then
+ if first_missing_dep then
+ util.printout(("Missing dependencies for %s %s:"):format(name, version))
+ first_missing_dep = false
+ end
+
+ util.printout((" %s (%s)"):format(tostring(dep), rock_status(dep, get_versions)))
+ end
+ end
+end
+
+function deps.fulfill_dependency(dep, deps_mode, rocks_provided, verify, depskey)
+ assert(dep:type() == "query")
+ assert(type(deps_mode) == "string" or deps_mode == nil)
+ assert(type(rocks_provided) == "table" or rocks_provided == nil)
+ assert(type(verify) == "boolean" or verify == nil)
+ assert(type(depskey) == "string")
+
+ deps_mode = deps_mode or "all"
+ rocks_provided = rocks_provided or {}
+
+ local get_versions = prepare_get_versions(deps_mode, rocks_provided, depskey)
+
+ local found, where
+ found, where, dep = match_dep(dep, get_versions)
+ if found then
+ local tree_manifests = manif.load_rocks_tree_manifests(deps_mode)
+ manif.scan_dependencies(dep.name, found, tree_manifests, deplocks.proxy(depskey))
+ return true, found, where
+ end
+
+ local search = require("luarocks.search")
+ local install = require("luarocks.cmd.install")
+
+ local url, search_err = search.find_suitable_rock(dep)
+ if not url then
+ return nil, "Could not satisfy dependency "..tostring(dep)..": "..search_err
+ end
+ util.printout("Installing "..url)
+ local install_args = {
+ rock = url,
+ deps_mode = deps_mode,
+ namespace = dep.namespace,
+ verify = verify,
+ }
+ local ok, install_err, errcode = install.command(install_args)
+ if not ok then
+ return nil, "Failed installing dependency: "..url.." - "..install_err, errcode
+ end
+
+ found, where = match_dep(dep, get_versions)
+ assert(found)
+ return true, found, where
+end
+
+local function check_supported_platforms(rockspec)
+ if rockspec.supported_platforms and next(rockspec.supported_platforms) then
+ local all_negative = true
+ local supported = false
+ for _, plat in pairs(rockspec.supported_platforms) do
+ local neg
+ neg, plat = plat:match("^(!?)(.*)")
+ if neg == "!" then
+ if cfg.is_platform(plat) then
+ return nil, "This rockspec for "..rockspec.package.." does not support "..plat.." platforms."
+ end
+ else
+ all_negative = false
+ if cfg.is_platform(plat) then
+ supported = true
+ break
+ end
+ end
+ end
+ if supported == false and not all_negative then
+ local plats = cfg.print_platforms()
+ return nil, "This rockspec for "..rockspec.package.." does not support "..plats.." platforms."
+ end
+ end
+
+ return true
+end
+
+--- Check dependencies of a rock and attempt to install any missing ones.
+-- Packages are installed using the LuaRocks "install" command.
+-- Aborts the program if a dependency could not be fulfilled.
+-- @param rockspec table: A rockspec in table format.
+-- @param depskey string: Rockspec key to fetch to get dependency table
+-- ("dependencies", "build_dependencies", etc.).
+-- @param deps_mode string
+-- @param verify boolean
+-- @param deplock_dir string: dirname of the deplock file
+-- @return boolean or (nil, string, [string]): True if no errors occurred, or
+-- nil and an error message if any test failed, followed by an optional
+-- error code.
+function deps.fulfill_dependencies(rockspec, depskey, deps_mode, verify, deplock_dir)
+ assert(type(rockspec) == "table")
+ assert(type(depskey) == "string")
+ assert(type(deps_mode) == "string")
+ assert(type(verify) == "boolean" or verify == nil)
+ assert(type(deplock_dir) == "string" or deplock_dir == nil)
+
+ local name = rockspec.name
+ local version = rockspec.version
+ local rocks_provided = rockspec.rocks_provided
+
+ local ok, filename, err = deplocks.load(name, deplock_dir or ".")
+ if filename then
+ util.printout("Using dependencies pinned in lockfile: " .. filename)
+
+ local get_versions = prepare_get_versions("none", rocks_provided, depskey)
+ for dnsname, dversion in deplocks.each(depskey) do
+ local dname, dnamespace = util.split_namespace(dnsname)
+ local dep = queries.new(dname, dnamespace, dversion)
+
+ util.printout(("%s %s is pinned to %s (%s)"):format(
+ name, version, tostring(dep), rock_status(dep, get_versions)))
+
+ local ok, err = deps.fulfill_dependency(dep, "none", rocks_provided, verify, depskey)
+ if not ok then
+ return nil, err
+ end
+ end
+ util.printout()
+ return true
+ elseif err then
+ util.warning(err)
+ end
+
+ ok, err = check_supported_platforms(rockspec)
+ if not ok then
+ return nil, err
+ end
+
+ deps.report_missing_dependencies(name, version, rockspec[depskey], deps_mode, rocks_provided)
+
+ util.printout()
+
+ local get_versions = prepare_get_versions(deps_mode, rocks_provided, depskey)
+ for _, dep in ipairs(rockspec[depskey]) do
+
+ util.printout(("%s %s depends on %s (%s)"):format(
+ name, version, tostring(dep), rock_status(dep, get_versions)))
+
+ local ok, found_or_err, _, no_upgrade = deps.fulfill_dependency(dep, deps_mode, rocks_provided, verify, depskey)
+ if ok then
+ deplocks.add(depskey, dep.name, found_or_err)
+ else
+ if no_upgrade then
+ util.printerr("This version of "..name.." is designed for use with")
+ util.printerr(tostring(dep)..", but is configured to avoid upgrading it")
+ util.printerr("automatically. Please upgrade "..dep.name.." with")
+ util.printerr(" luarocks install "..dep.name)
+ util.printerr("or look for a suitable version of "..name.." with")
+ util.printerr(" luarocks search "..name)
+ end
+ return nil, found_or_err
+ end
+ end
+
+ return true
+end
+
+--- If filename matches a pattern, return the capture.
+-- For example, given "libfoo.so" and "lib?.so" is a pattern,
+-- returns "foo" (which can then be used to build names
+-- based on other patterns.
+-- @param file string: a filename
+-- @param pattern string: a pattern, where ? is to be matched by the filename.
+-- @return string The pattern, if found, or nil.
+local function deconstruct_pattern(file, pattern)
+ local depattern = "^"..(pattern:gsub("%.", "%%."):gsub("%*", ".*"):gsub("?", "(.*)")).."$"
+ return (file:match(depattern))
+end
+
+--- Construct all possible patterns for a name and add to the files array.
+-- Run through the patterns array replacing all occurrences of "?"
+-- with the given file name and store them in the files array.
+-- @param file string A raw name (e.g. "foo")
+-- @param array of string An array of patterns with "?" as the wildcard
+-- (e.g. {"?.so", "lib?.so"})
+-- @param files The array of constructed names
+local function add_all_patterns(file, patterns, files)
+ for _, pattern in ipairs(patterns) do
+ table.insert(files, {#files + 1, (pattern:gsub("?", file))})
+ end
+end
+
+local function get_external_deps_dirs(mode)
+ local patterns = cfg.external_deps_patterns
+ local subdirs = cfg.external_deps_subdirs
+ if mode == "install" then
+ patterns = cfg.runtime_external_deps_patterns
+ subdirs = cfg.runtime_external_deps_subdirs
+ end
+ local dirs = {
+ BINDIR = { subdir = subdirs.bin, testfile = "program", pattern = patterns.bin },
+ INCDIR = { subdir = subdirs.include, testfile = "header", pattern = patterns.include },
+ LIBDIR = { subdir = subdirs.lib, testfile = "library", pattern = patterns.lib }
+ }
+ if mode == "install" then
+ dirs.INCDIR = nil
+ end
+ return dirs
+end
+
+local function resolve_prefix(prefix, dirs)
+ if type(prefix) == "string" then
+ return prefix
+ elseif type(prefix) == "table" then
+ if prefix.bin then
+ dirs.BINDIR.subdir = prefix.bin
+ end
+ if prefix.include then
+ if dirs.INCDIR then
+ dirs.INCDIR.subdir = prefix.include
+ end
+ end
+ if prefix.lib then
+ dirs.LIBDIR.subdir = prefix.lib
+ end
+ return prefix.prefix
+ end
+end
+
+local function add_patterns_for_file(files, file, patterns)
+ -- If it doesn't look like it contains a filename extension
+ if not (file:match("%.[a-z]+$") or file:match("%.[a-z]+%.")) then
+ add_all_patterns(file, patterns, files)
+ else
+ for _, pattern in ipairs(patterns) do
+ local matched = deconstruct_pattern(file, pattern)
+ if matched then
+ add_all_patterns(matched, patterns, files)
+ end
+ end
+ table.insert(files, {#files + 1, file})
+ end
+end
+
+local function check_external_dependency_at(prefix, name, ext_files, vars, dirs, err_files, cache)
+ local fs = require("luarocks.fs")
+ cache = cache or {}
+
+ for dirname, dirdata in util.sortedpairs(dirs) do
+ local paths
+ local path_var_value = vars[name.."_"..dirname]
+ if path_var_value then
+ paths = { path_var_value }
+ elseif type(dirdata.subdir) == "table" then
+ paths = {}
+ for i,v in ipairs(dirdata.subdir) do
+ paths[i] = dir.path(prefix, v)
+ end
+ else
+ paths = { dir.path(prefix, dirdata.subdir) }
+ end
+ local file_or_files = ext_files[dirdata.testfile]
+ if file_or_files then
+ local files = {}
+ if type(file_or_files) == "string" then
+ add_patterns_for_file(files, file_or_files, dirdata.pattern)
+ elseif type(file_or_files) == "table" then
+ for _, f in ipairs(file_or_files) do
+ add_patterns_for_file(files, f, dirdata.pattern)
+ end
+ end
+
+ local found = false
+ table.sort(files, function(a, b)
+ if (not a[2]:match("%*")) and b[2]:match("%*") then
+ return true
+ elseif a[2]:match("%*") and (not b[2]:match("%*")) then
+ return false
+ else
+ return a[1] < b[1]
+ end
+ end)
+ for _, fa in ipairs(files) do
+
+ local f = fa[2]
+ -- small convenience hack
+ if f:match("%.so$") or f:match("%.dylib$") or f:match("%.dll$") then
+ f = f:gsub("%.[^.]+$", "."..cfg.external_lib_extension)
+ end
+
+ local pattern
+ if f:match("%*") then
+ pattern = "^" .. f:gsub("([-.+])", "%%%1"):gsub("%*", ".*") .. "$"
+ f = "matching "..f
+ end
+
+ for _, d in ipairs(paths) do
+ if pattern then
+ if not cache[d] then
+ cache[d] = fs.list_dir(d)
+ end
+ local match = string.match
+ for _, entry in ipairs(cache[d]) do
+ if match(entry, pattern) then
+ found = true
+ break
+ end
+ end
+ else
+ found = fs.is_file(dir.path(d, f))
+ end
+ if found then
+ dirdata.dir = d
+ dirdata.file = f
+ break
+ else
+ table.insert(err_files[dirdata.testfile], f.." in "..d)
+ end
+ end
+ if found then
+ break
+ end
+ end
+ if not found then
+ return nil, dirname, dirdata.testfile
+ end
+ else
+ -- When we have a set of subdir suffixes, look for one that exists.
+ -- For these reason, we now put "lib" ahead of "" on Windows in our
+ -- default set.
+ dirdata.dir = paths[1]
+ for _, p in ipairs(paths) do
+ if fs.exists(p) then
+ dirdata.dir = p
+ break
+ end
+ end
+ end
+ end
+
+ for dirname, dirdata in pairs(dirs) do
+ vars[name.."_"..dirname] = dirdata.dir
+ vars[name.."_"..dirname.."_FILE"] = dirdata.file
+ end
+ vars[name.."_DIR"] = prefix
+ return true
+end
+
+local function check_external_dependency(name, ext_files, vars, mode, cache)
+ local ok
+ local err_dirname
+ local err_testfile
+ local err_files = {program = {}, header = {}, library = {}}
+
+ local dirs = get_external_deps_dirs(mode)
+
+ local prefixes
+ if vars[name .. "_DIR"] then
+ prefixes = { vars[name .. "_DIR"] }
+ elseif vars.DEPS_DIR then
+ prefixes = { vars.DEPS_DIR }
+ else
+ prefixes = cfg.external_deps_dirs
+ end
+
+ for _, prefix in ipairs(prefixes) do
+ prefix = resolve_prefix(prefix, dirs)
+ if cfg.is_platform("mingw32") and name == "LUA" then
+ dirs.LIBDIR.pattern = fun.filter(util.deep_copy(dirs.LIBDIR.pattern), function(s)
+ return not s:match("%.a$")
+ end)
+ elseif cfg.is_platform("windows") and name == "LUA" then
+ dirs.LIBDIR.pattern = fun.filter(util.deep_copy(dirs.LIBDIR.pattern), function(s)
+ return not s:match("%.dll$")
+ end)
+ end
+ ok, err_dirname, err_testfile = check_external_dependency_at(prefix, name, ext_files, vars, dirs, err_files, cache)
+ if ok then
+ return true
+ end
+ end
+
+ return nil, err_dirname, err_testfile, err_files
+end
+
+function deps.autodetect_external_dependencies(build)
+ -- only applies to the 'builtin' build type
+ if not build or not build.modules then
+ return nil
+ end
+
+ local extdeps = {}
+ local any = false
+ for _, data in pairs(build.modules) do
+ if type(data) == "table" and data.libraries then
+ local libraries = data.libraries
+ if type(libraries) == "string" then
+ libraries = { libraries }
+ end
+ local incdirs = {}
+ local libdirs = {}
+ for _, lib in ipairs(libraries) do
+ local upper = lib:upper():gsub("%+", "P"):gsub("[^%w]", "_")
+ any = true
+ extdeps[upper] = { library = lib }
+ table.insert(incdirs, "$(" .. upper .. "_INCDIR)")
+ table.insert(libdirs, "$(" .. upper .. "_LIBDIR)")
+ end
+ if not data.incdirs then
+ data.incdirs = incdirs
+ end
+ if not data.libdirs then
+ data.libdirs = libdirs
+ end
+ end
+ end
+ return any and extdeps or nil
+end
+
+--- Set up path-related variables for external dependencies.
+-- For each key in the external_dependencies table in the
+-- rockspec file, four variables are created: <key>_DIR, <key>_BINDIR,
+-- <key>_INCDIR and <key>_LIBDIR. These are not overwritten
+-- if already set (e.g. by the LuaRocks config file or through the
+-- command-line). Values in the external_dependencies table
+-- are tables that may contain a "header" or a "library" field,
+-- with filenames to be tested for existence.
+-- @param rockspec table: The rockspec table.
+-- @param mode string: if "build" is given, checks all files;
+-- if "install" is given, do not scan for headers.
+-- @return boolean or (nil, string): True if no errors occurred, or
+-- nil and an error message if any test failed.
+function deps.check_external_deps(rockspec, mode)
+ assert(rockspec:type() == "rockspec")
+
+ if not rockspec.external_dependencies then
+ rockspec.external_dependencies = deps.autodetect_external_dependencies(rockspec.build)
+ end
+ if not rockspec.external_dependencies then
+ return true
+ end
+
+ for name, ext_files in util.sortedpairs(rockspec.external_dependencies) do
+ local ok, err_dirname, err_testfile, err_files = check_external_dependency(name, ext_files, rockspec.variables, mode)
+ if not ok then
+ local lines = {"Could not find "..err_testfile.." file for "..name}
+
+ local err_paths = {}
+ for _, err_file in ipairs(err_files[err_testfile]) do
+ if not err_paths[err_file] then
+ err_paths[err_file] = true
+ table.insert(lines, " No file "..err_file)
+ end
+ end
+
+ table.insert(lines, "You may have to install "..name.." in your system and/or pass "..name.."_DIR or "..name.."_"..err_dirname.." to the luarocks command.")
+ table.insert(lines, "Example: luarocks install "..rockspec.name.." "..name.."_DIR=/usr/local")
+
+ return nil, table.concat(lines, "\n"), "dependency"
+ end
+ end
+ return true
+end
+
+--- Recursively add satisfied dependencies of a package to a table,
+-- to build a transitive closure of all dependent packages.
+-- Additionally ensures that `dependencies` table of the manifest is up-to-date.
+-- @param results table: The results table being built, maps package names to versions.
+-- @param mdeps table: The manifest dependencies table.
+-- @param name string: Package name.
+-- @param version string: Package version.
+function deps.scan_deps(results, mdeps, name, version, deps_mode)
+ assert(type(results) == "table")
+ assert(type(mdeps) == "table")
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+
+ local fetch = require("luarocks.fetch")
+
+ if results[name] then
+ return
+ end
+ if not mdeps[name] then mdeps[name] = {} end
+ local mdn = mdeps[name]
+ local dependencies = mdn[version]
+ local rocks_provided
+ if not dependencies then
+ local rockspec, err = fetch.load_local_rockspec(path.rockspec_file(name, version), false)
+ if not rockspec then
+ return
+ end
+ dependencies = rockspec.dependencies
+ rocks_provided = rockspec.rocks_provided
+ mdn[version] = dependencies
+ else
+ rocks_provided = util.get_rocks_provided()
+ end
+
+ local get_versions = prepare_get_versions(deps_mode, rocks_provided, "dependencies")
+
+ local matched = match_all_deps(dependencies, get_versions)
+ results[name] = version
+ for _, match in pairs(matched) do
+ deps.scan_deps(results, mdeps, match.name, match.version, deps_mode)
+ end
+end
+
+local function lua_h_exists(d, luaver)
+ local major, minor = luaver:match("(%d+)%.(%d+)")
+ local luanum = ("%s%02d"):format(major, tonumber(minor))
+
+ local lua_h = dir.path(d, "lua.h")
+ local fd = io.open(lua_h)
+ if fd then
+ local data = fd:read("*a")
+ fd:close()
+ if data:match("LUA_VERSION_NUM%s*" .. tostring(luanum)) then
+ return d
+ end
+ return nil, "Lua header lua.h found at " .. d .. " does not match Lua version " .. luaver .. ". You can use `luarocks config variables.LUA_INCDIR <path>` to set the correct location.", "dependency", 2
+ end
+
+ return nil, "Failed finding Lua header lua.h (searched at " .. d .. "). You may need to install Lua development headers. You can use `luarocks config variables.LUA_INCDIR <path>` to set the correct location.", "dependency", 1
+end
+
+local function find_lua_incdir(prefix, luaver, luajitver)
+ luajitver = luajitver and luajitver:gsub("%-.*", "")
+ local shortv = luaver:gsub("%.", "")
+ local incdirs = {
+ prefix .. "/include/lua/" .. luaver,
+ prefix .. "/include/lua" .. luaver,
+ prefix .. "/include/lua-" .. luaver,
+ prefix .. "/include/lua" .. shortv,
+ prefix .. "/include",
+ prefix,
+ luajitver and (prefix .. "/include/luajit-" .. (luajitver:match("^(%d+%.%d+)") or "")),
+ }
+ local errprio = 0
+ local mainerr
+ for _, d in ipairs(incdirs) do
+ local ok, err, _, prio = lua_h_exists(d, luaver)
+ if ok then
+ return d
+ end
+ if prio > errprio then
+ mainerr = err
+ errprio = prio
+ end
+ end
+
+ -- not found, will fallback to a default
+ return nil, mainerr
+end
+
+function deps.check_lua_incdir(vars)
+ if vars.LUA_INCDIR_OK == true
+ then return true
+ end
+
+ local ljv = util.get_luajit_version()
+
+ if vars.LUA_INCDIR then
+ local ok, err = lua_h_exists(vars.LUA_INCDIR, cfg.lua_version)
+ if ok then
+ vars.LUA_INCDIR_OK = true
+ end
+ return ok, err
+ end
+
+ if vars.LUA_DIR then
+ local d, err = find_lua_incdir(vars.LUA_DIR, cfg.lua_version, ljv)
+ if d then
+ vars.LUA_INCDIR = d
+ vars.LUA_INCDIR_OK = true
+ return true
+ end
+ return nil, err
+ end
+
+ return nil, "Failed finding Lua headers; neither LUA_DIR or LUA_INCDIR are set. You may need to install them or configure LUA_INCDIR.", "dependency"
+end
+
+function deps.check_lua_libdir(vars)
+ if vars.LUA_LIBDIR_OK == true
+ then return true
+ end
+
+ local fs = require("luarocks.fs")
+ local ljv = util.get_luajit_version()
+
+ if vars.LUA_LIBDIR and vars.LUALIB and fs.exists(dir.path(vars.LUA_LIBDIR, vars.LUALIB)) then
+ vars.LUA_LIBDIR_OK = true
+ return true
+ end
+
+ local shortv = cfg.lua_version:gsub("%.", "")
+ local libnames = {
+ "lua" .. cfg.lua_version,
+ "lua" .. shortv,
+ "lua-" .. cfg.lua_version,
+ "lua-" .. shortv,
+ "lua",
+ }
+ if ljv then
+ table.insert(libnames, 1, "luajit-" .. cfg.lua_version)
+ table.insert(libnames, 2, "luajit")
+ end
+ local cache = {}
+ local save_LUA_INCDIR = vars.LUA_INCDIR
+ local ok, _, _, errfiles = check_external_dependency("LUA", { library = libnames }, vars, "build", cache)
+ vars.LUA_INCDIR = save_LUA_INCDIR
+ local err
+ if ok then
+ local filename = dir.path(vars.LUA_LIBDIR, vars.LUA_LIBDIR_FILE)
+ local fd = io.open(filename, "r")
+ if fd then
+ if not vars.LUA_LIBDIR_FILE:match((cfg.lua_version:gsub("%.", "%%.?"))) then
+ -- if filename isn't versioned, check file contents
+ local txt = fd:read("*a")
+ ok = txt:match("Lua " .. cfg.lua_version, 1, true)
+ or txt:match("lua" .. (cfg.lua_version:gsub("%.", "")), 1, true)
+ if not ok then
+ err = "Lua library at " .. filename .. " does not match Lua version " .. cfg.lua_version .. ". You can use `luarocks config variables.LUA_LIBDIR <path>` to set the correct location."
+ end
+ end
+
+ fd:close()
+ end
+ end
+
+ if ok then
+ vars.LUALIB = vars.LUA_LIBDIR_FILE
+ vars.LUA_LIBDIR_OK = true
+ return true
+ else
+ err = err or "Failed finding the Lua library. You can use `luarocks config variables.LUA_LIBDIR <path>` to set the correct location."
+ return nil, err, "dependency", errfiles
+ end
+end
+
+function deps.get_deps_mode(args)
+ return args.deps_mode or cfg.deps_mode
+end
+
+return deps
diff --git a/src/luarocks/dir.lua b/src/luarocks/dir.lua
new file mode 100644
index 0000000..be89e37
--- /dev/null
+++ b/src/luarocks/dir.lua
@@ -0,0 +1,63 @@
+
+--- Generic utilities for handling pathnames.
+local dir = {}
+
+local core = require("luarocks.core.dir")
+
+dir.path = core.path
+dir.split_url = core.split_url
+dir.normalize = core.normalize
+
+local dir_sep = package.config:sub(1, 1)
+
+--- Strip the path off a path+filename.
+-- @param pathname string: A path+name, such as "/a/b/c"
+-- or "\a\b\c".
+-- @return string: The filename without its path, such as "c".
+function dir.base_name(pathname)
+ assert(type(pathname) == "string")
+
+ local b
+ b = pathname:gsub("[/\\]", "/") -- canonicalize to forward slashes
+ b = b:gsub("/*$", "") -- drop trailing slashes
+ b = b:match(".*[/\\]([^/\\]*)") -- match last component
+ b = b or pathname -- fallback to original if no slashes
+
+ return b
+end
+
+--- Strip the name off a path+filename.
+-- @param pathname string: A path+name, such as "/a/b/c".
+-- @return string: The filename without its path, such as "/a/b".
+-- For entries such as "/a/b/", "/a" is returned. If there are
+-- no directory separators in input, "" is returned.
+function dir.dir_name(pathname)
+ assert(type(pathname) == "string")
+
+ local d
+ d = pathname:gsub("[/\\]", "/") -- canonicalize to forward slashes
+ d = d:gsub("/*$", "") -- drop trailing slashes
+ d = d:match("(.*)[/]+[^/]*") -- match all components but the last
+ d = d or "" -- switch to "" if there's no match
+ d = d:gsub("/", dir_sep) -- decanonicalize to native slashes
+
+ return d
+end
+
+--- Returns true if protocol does not require additional tools.
+-- @param protocol The protocol name
+function dir.is_basic_protocol(protocol)
+ return protocol == "http" or protocol == "https" or protocol == "ftp" or protocol == "file"
+end
+
+function dir.deduce_base_dir(url)
+ -- for extensions like foo.tar.gz, "gz" is stripped first
+ local known_exts = {}
+ for _, ext in ipairs{"zip", "git", "tgz", "tar", "gz", "bz2"} do
+ known_exts[ext] = ""
+ end
+ local base = dir.base_name(url)
+ return (base:gsub("%.([^.]*)$", known_exts):gsub("%.tar", ""))
+end
+
+return dir
diff --git a/src/luarocks/download.lua b/src/luarocks/download.lua
new file mode 100644
index 0000000..07a2a65
--- /dev/null
+++ b/src/luarocks/download.lua
@@ -0,0 +1,68 @@
+local download = {}
+
+local path = require("luarocks.path")
+local fetch = require("luarocks.fetch")
+local search = require("luarocks.search")
+local queries = require("luarocks.queries")
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+local util = require("luarocks.util")
+
+local function get_file(filename)
+ local protocol, pathname = dir.split_url(filename)
+ if protocol == "file" then
+ local ok, err = fs.copy(pathname, fs.current_dir(), "read")
+ if ok then
+ return pathname
+ else
+ return nil, err
+ end
+ else
+ -- discard third result
+ local ok, err = fetch.fetch_url(filename)
+ return ok, err
+ end
+end
+
+function download.download(arch, name, namespace, version, all, check_lua_versions)
+ local substring = (all and name == "")
+ local query = queries.new(name, namespace, version, substring, arch)
+ local search_err
+
+ if all then
+ local results = search.search_repos(query)
+ local has_result = false
+ local all_ok = true
+ local any_err = ""
+ for name, result in pairs(results) do -- luacheck: ignore 422
+ for version, items in pairs(result) do -- luacheck: ignore 422
+ for _, item in ipairs(items) do
+ -- Ignore provided rocks.
+ if item.arch ~= "installed" then
+ has_result = true
+ local filename = path.make_url(item.repo, name, version, item.arch)
+ local ok, err = get_file(filename)
+ if not ok then
+ all_ok = false
+ any_err = any_err .. "\n" .. err
+ end
+ end
+ end
+ end
+ end
+
+ if has_result then
+ return all_ok, any_err
+ end
+ else
+ local url
+ url, search_err = search.find_rock_checking_lua_versions(query, check_lua_versions)
+ if url then
+ return get_file(url)
+ end
+ end
+ local rock = util.format_rock_name(name, namespace, version)
+ return nil, "Could not find a result named "..rock..(search_err and ": "..search_err or ".")
+end
+
+return download
diff --git a/src/luarocks/fetch.lua b/src/luarocks/fetch.lua
new file mode 100644
index 0000000..193e5e3
--- /dev/null
+++ b/src/luarocks/fetch.lua
@@ -0,0 +1,610 @@
+
+--- Functions related to fetching and loading local and remote files.
+local fetch = {}
+
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+local rockspecs = require("luarocks.rockspecs")
+local signing = require("luarocks.signing")
+local persist = require("luarocks.persist")
+local util = require("luarocks.util")
+local cfg = require("luarocks.core.cfg")
+
+
+--- Fetch a local or remote file, using a local cache directory.
+-- Make a remote or local URL/pathname local, fetching the file if necessary.
+-- Other "fetch" and "load" functions use this function to obtain files.
+-- If a local pathname is given, it is returned as a result.
+-- @param url string: a local pathname or a remote URL.
+-- @param mirroring string: mirroring mode.
+-- If set to "no_mirror", then rocks_servers mirror configuration is not used.
+-- @return (string, nil, nil, boolean) or (nil, string, [string]):
+-- in case of success:
+-- * the absolute local pathname for the fetched file
+-- * nil
+-- * nil
+-- * `true` if the file was fetched from cache
+-- in case of failure:
+-- * nil
+-- * an error message
+-- * an optional error code.
+function fetch.fetch_caching(url, mirroring)
+ local repo_url, filename = url:match("^(.*)/([^/]+)$")
+ local name = repo_url:gsub("[/:]","_")
+ local cache_dir = dir.path(cfg.local_cache, name)
+ local ok = fs.make_dir(cache_dir)
+
+ local cachefile = dir.path(cache_dir, filename)
+ local checkfile = cachefile .. ".check"
+
+ if (fs.file_age(checkfile) < 10 or
+ cfg.aggressive_cache and (not name:match("^manifest"))) and fs.exists(cachefile)
+ then
+ return cachefile, nil, nil, true
+ end
+
+ local lock, errlock
+ if ok then
+ lock, errlock = fs.lock_access(cache_dir)
+ end
+
+ if not (ok and lock) then
+ cfg.local_cache = fs.make_temp_dir("local_cache")
+ if not cfg.local_cache then
+ return nil, "Failed creating temporary local_cache directory"
+ end
+ cache_dir = dir.path(cfg.local_cache, name)
+ ok = fs.make_dir(cache_dir)
+ if not ok then
+ return nil, "Failed creating temporary cache directory "..cache_dir
+ end
+ lock = fs.lock_access(cache_dir)
+ end
+
+ local file, err, errcode, from_cache = fetch.fetch_url(url, cachefile, true, mirroring)
+ if not file then
+ fs.unlock_access(lock)
+ return nil, err or "Failed downloading "..url, errcode
+ end
+
+ local fd, err = io.open(checkfile, "wb")
+ if err then
+ fs.unlock_access(lock)
+ return nil, err
+ end
+ fd:write("!")
+ fd:close()
+
+ fs.unlock_access(lock)
+ return file, nil, nil, from_cache
+end
+
+local function ensure_trailing_slash(url)
+ return (url:gsub("/*$", "/"))
+end
+
+local function is_url_relative_to_rocks_servers(url, servers)
+ for _, item in ipairs(servers) do
+ if type(item) == "table" then
+ for i, s in ipairs(item) do
+ local base = ensure_trailing_slash(s)
+ if string.find(url, base, 1, true) == 1 then
+ return i, url:sub(#base + 1), item
+ end
+ end
+ end
+ end
+end
+
+local function download_with_mirrors(url, filename, cache, servers)
+ local idx, rest, mirrors = is_url_relative_to_rocks_servers(url, servers)
+
+ if not idx then
+ -- URL is not from a rock server
+ return fs.download(url, filename, cache)
+ end
+
+ -- URL is from a rock server: try to download it falling back to mirrors.
+ local err = "\n"
+ for i = idx, #mirrors do
+ local try_url = ensure_trailing_slash(mirrors[i]) .. rest
+ if i > idx then
+ util.warning("Failed downloading. Attempting mirror at " .. try_url)
+ end
+ local ok, name, from_cache = fs.download(try_url, filename, cache)
+ if ok then
+ return ok, name, from_cache
+ else
+ err = err .. name .. "\n"
+ end
+ end
+
+ return nil, err, "network"
+end
+
+--- Fetch a local or remote file.
+-- Make a remote or local URL/pathname local, fetching the file if necessary.
+-- Other "fetch" and "load" functions use this function to obtain files.
+-- If a local pathname is given, it is returned as a result.
+-- @param url string: a local pathname or a remote URL.
+-- @param filename string or nil: this function attempts to detect the
+-- resulting local filename of the remote file as the basename of the URL;
+-- if that is not correct (due to a redirection, for example), the local
+-- filename can be given explicitly as this second argument.
+-- @param cache boolean: compare remote timestamps via HTTP HEAD prior to
+-- re-downloading the file.
+-- @param mirroring string: mirroring mode.
+-- If set to "no_mirror", then rocks_servers mirror configuration is not used.
+-- @return (string, nil, nil, boolean) or (nil, string, [string]):
+-- in case of success:
+-- * the absolute local pathname for the fetched file
+-- * nil
+-- * nil
+-- * `true` if the file was fetched from cache
+-- in case of failure:
+-- * nil
+-- * an error message
+-- * an optional error code.
+function fetch.fetch_url(url, filename, cache, mirroring)
+ assert(type(url) == "string")
+ assert(type(filename) == "string" or not filename)
+
+ local protocol, pathname = dir.split_url(url)
+ if protocol == "file" then
+ local fullname = fs.absolute_name(pathname)
+ if not fs.exists(fullname) then
+ local hint = (not pathname:match("^/"))
+ and (" - note that given path in rockspec is not absolute: " .. url)
+ or ""
+ return nil, "Local file not found: " .. fullname .. hint
+ end
+ filename = filename or dir.base_name(pathname)
+ local dstname = fs.absolute_name(dir.path(".", filename))
+ local ok, err
+ if fullname == dstname then
+ ok = true
+ else
+ ok, err = fs.copy(fullname, dstname)
+ end
+ if ok then
+ return dstname
+ else
+ return nil, "Failed copying local file " .. fullname .. " to " .. dstname .. ": " .. err
+ end
+ elseif dir.is_basic_protocol(protocol) then
+ local ok, name, from_cache
+ if mirroring ~= "no_mirror" then
+ ok, name, from_cache = download_with_mirrors(url, filename, cache, cfg.rocks_servers)
+ else
+ ok, name, from_cache = fs.download(url, filename, cache)
+ end
+ if not ok then
+ return nil, "Failed downloading "..url..(name and " - "..name or ""), from_cache
+ end
+ return name, nil, nil, from_cache
+ else
+ return nil, "Unsupported protocol "..protocol
+ end
+end
+
+--- For remote URLs, create a temporary directory and download URL inside it.
+-- This temporary directory will be deleted on program termination.
+-- For local URLs, just return the local pathname and its directory.
+-- @param url string: URL to be downloaded
+-- @param tmpname string: name pattern to use for avoiding conflicts
+-- when creating temporary directory.
+-- @param filename string or nil: local filename of URL to be downloaded,
+-- in case it can't be inferred from the URL.
+-- @return (string, string) or (nil, string, [string]): absolute local pathname of
+-- the fetched file and temporary directory name; or nil and an error message
+-- followed by an optional error code
+function fetch.fetch_url_at_temp_dir(url, tmpname, filename, cache)
+ assert(type(url) == "string")
+ assert(type(tmpname) == "string")
+ assert(type(filename) == "string" or not filename)
+ filename = filename or dir.base_name(url)
+
+ local protocol, pathname = dir.split_url(url)
+ if protocol == "file" then
+ if fs.exists(pathname) then
+ return pathname, dir.dir_name(fs.absolute_name(pathname))
+ else
+ return nil, "File not found: "..pathname
+ end
+ else
+ local temp_dir, err = fs.make_temp_dir(tmpname)
+ if not temp_dir then
+ return nil, "Failed creating temporary directory "..tmpname..": "..err
+ end
+ util.schedule_function(fs.delete, temp_dir)
+ local ok, err = fs.change_dir(temp_dir)
+ if not ok then return nil, err end
+
+ local file, err, errcode
+
+ if cache then
+ local cachefile
+ cachefile, err, errcode = fetch.fetch_caching(url)
+
+ if cachefile then
+ file = dir.path(temp_dir, filename)
+ fs.copy(cachefile, file)
+ end
+ end
+
+ if not file then
+ file, err, errcode = fetch.fetch_url(url, filename, cache)
+ end
+
+ fs.pop_dir()
+ if not file then
+ return nil, "Error fetching file: "..err, errcode
+ end
+
+ return file, temp_dir
+ end
+end
+
+-- Determine base directory of a fetched URL by extracting its
+-- archive and looking for a directory in the root.
+-- @param file string: absolute local pathname of the fetched file
+-- @param temp_dir string: temporary directory in which URL was fetched.
+-- @param src_url string: URL to use when inferring base directory.
+-- @param src_dir string or nil: expected base directory (inferred
+-- from src_url if not given).
+-- @return (string, string) or (string, nil) or (nil, string):
+-- The inferred base directory and the one actually found (which may
+-- be nil if not found), or nil followed by an error message.
+-- The inferred dir is returned first to avoid confusion with errors,
+-- because it is never nil.
+function fetch.find_base_dir(file, temp_dir, src_url, src_dir)
+ local ok, err = fs.change_dir(temp_dir)
+ if not ok then return nil, err end
+ fs.unpack_archive(file)
+
+ if not src_dir then
+ local rockspec = {
+ source = {
+ file = file,
+ dir = src_dir,
+ url = src_url,
+ }
+ }
+ ok, err = fetch.find_rockspec_source_dir(rockspec, ".")
+ if ok then
+ src_dir = rockspec.source.dir
+ end
+ end
+
+ local inferred_dir = src_dir or dir.deduce_base_dir(src_url)
+ local found_dir = nil
+ if fs.exists(inferred_dir) then
+ found_dir = inferred_dir
+ else
+ util.printerr("Directory "..inferred_dir.." not found")
+ local files = fs.list_dir()
+ if files then
+ table.sort(files)
+ for i,filename in ipairs(files) do
+ if fs.is_dir(filename) then
+ util.printerr("Found "..filename)
+ found_dir = filename
+ break
+ end
+ end
+ end
+ end
+ fs.pop_dir()
+ return inferred_dir, found_dir
+end
+
+local function fetch_and_verify_signature_for(url, filename, tmpdir)
+ local sig_url = signing.signature_url(url)
+ local sig_file, err, errcode = fetch.fetch_url_at_temp_dir(sig_url, tmpdir)
+ if not sig_file then
+ return nil, "Could not fetch signature file for verification: " .. err, errcode
+ end
+
+ local ok, err = signing.verify_signature(filename, sig_file)
+ if not ok then
+ return nil, "Failed signature verification: " .. err
+ end
+
+ return fs.absolute_name(sig_file)
+end
+
+--- Obtain a rock and unpack it.
+-- If a directory is not given, a temporary directory will be created,
+-- which will be deleted on program termination.
+-- @param rock_file string: URL or filename of the rock.
+-- @param dest string or nil: if given, directory will be used as
+-- a permanent destination.
+-- @param verify boolean: if true, download and verify signature for rockspec
+-- @return string or (nil, string, [string]): the directory containing the contents
+-- of the unpacked rock.
+function fetch.fetch_and_unpack_rock(url, dest, verify)
+ assert(type(url) == "string")
+ assert(type(dest) == "string" or not dest)
+
+ local name = dir.base_name(url):match("(.*)%.[^.]*%.rock")
+ local tmpname = "luarocks-rock-" .. name
+
+ local rock_file, err, errcode = fetch.fetch_url_at_temp_dir(url, tmpname, nil, true)
+ if not rock_file then
+ return nil, "Could not fetch rock file: " .. err, errcode
+ end
+
+ local sig_file
+ if verify then
+ sig_file, err = fetch_and_verify_signature_for(url, rock_file, tmpname)
+ if err then
+ return nil, err
+ end
+ end
+
+ rock_file = fs.absolute_name(rock_file)
+
+ local unpack_dir
+ if dest then
+ unpack_dir = dest
+ local ok, err = fs.make_dir(unpack_dir)
+ if not ok then
+ return nil, "Failed unpacking rock file: " .. err
+ end
+ else
+ unpack_dir, err = fs.make_temp_dir(name)
+ if not unpack_dir then
+ return nil, "Failed creating temporary dir: " .. err
+ end
+ end
+ if not dest then
+ util.schedule_function(fs.delete, unpack_dir)
+ end
+ local ok, err = fs.change_dir(unpack_dir)
+ if not ok then return nil, err end
+ ok, err = fs.unzip(rock_file)
+ if not ok then
+ return nil, "Failed unpacking rock file: " .. rock_file .. ": " .. err
+ end
+ if sig_file then
+ ok, err = fs.copy(sig_file, ".")
+ if not ok then
+ return nil, "Failed copying signature file"
+ end
+ end
+ fs.pop_dir()
+ return unpack_dir
+end
+
+--- Back-end function that actually loads the local rockspec.
+-- Performs some validation and postprocessing of the rockspec contents.
+-- @param rel_filename string: The local filename of the rockspec file.
+-- @param quick boolean: if true, skips some steps when loading
+-- rockspec.
+-- @return table or (nil, string): A table representing the rockspec
+-- or nil followed by an error message.
+function fetch.load_local_rockspec(rel_filename, quick)
+ assert(type(rel_filename) == "string")
+ local abs_filename = fs.absolute_name(rel_filename)
+
+ local basename = dir.base_name(abs_filename)
+ if basename ~= "rockspec" then
+ if not basename:match("(.*)%-[^-]*%-[0-9]*") then
+ return nil, "Expected filename in format 'name-version-revision.rockspec'."
+ end
+ end
+
+ local tbl, err = persist.load_into_table(abs_filename)
+ if not tbl then
+ return nil, "Could not load rockspec file "..abs_filename.." ("..err..")"
+ end
+
+ local rockspec, err = rockspecs.from_persisted_table(abs_filename, tbl, err, quick)
+ if not rockspec then
+ return nil, abs_filename .. ": " .. err
+ end
+
+ local name_version = rockspec.package:lower() .. "-" .. rockspec.version
+ if basename ~= "rockspec" and basename ~= name_version .. ".rockspec" then
+ return nil, "Inconsistency between rockspec filename ("..basename..") and its contents ("..name_version..".rockspec)."
+ end
+
+ return rockspec
+end
+
+--- Load a local or remote rockspec into a table.
+-- This is the entry point for the LuaRocks tools.
+-- Only the LuaRocks runtime loader should use
+-- load_local_rockspec directly.
+-- @param filename string: Local or remote filename of a rockspec.
+-- @param location string or nil: Where to download. If not given,
+-- a temporary dir is created.
+-- @param verify boolean: if true, download and verify signature for rockspec
+-- @return table or (nil, string, [string]): A table representing the rockspec
+-- or nil followed by an error message and optional error code.
+function fetch.load_rockspec(url, location, verify)
+ assert(type(url) == "string")
+
+ local name
+ local basename = dir.base_name(url)
+ if basename == "rockspec" then
+ name = "rockspec"
+ else
+ name = basename:match("(.*)%.rockspec")
+ if not name then
+ return nil, "Filename '"..url.."' does not look like a rockspec."
+ end
+ end
+
+ local tmpname = "luarocks-rockspec-"..name
+ local filename, err, errcode
+ if location then
+ local ok, err = fs.change_dir(location)
+ if not ok then return nil, err end
+ filename, err = fetch.fetch_url(url)
+ fs.pop_dir()
+ else
+ filename, err, errcode = fetch.fetch_url_at_temp_dir(url, tmpname, nil, true)
+ end
+ if not filename then
+ return nil, err, errcode
+ end
+
+ if verify then
+ local _, err = fetch_and_verify_signature_for(url, filename, tmpname)
+ if err then
+ return nil, err
+ end
+ end
+
+ return fetch.load_local_rockspec(filename)
+end
+
+--- Download sources for building a rock using the basic URL downloader.
+-- @param rockspec table: The rockspec table
+-- @param extract boolean: Whether to extract the sources from
+-- the fetched source tarball or not.
+-- @param dest_dir string or nil: If set, will extract to the given directory;
+-- if not given, will extract to a temporary directory.
+-- @return (string, string) or (nil, string, [string]): The absolute pathname of
+-- the fetched source tarball and the temporary directory created to
+-- store it; or nil and an error message and optional error code.
+function fetch.get_sources(rockspec, extract, dest_dir)
+ assert(rockspec:type() == "rockspec")
+ assert(type(extract) == "boolean")
+ assert(type(dest_dir) == "string" or not dest_dir)
+
+ local url = rockspec.source.url
+ local name = rockspec.name.."-"..rockspec.version
+ local filename = rockspec.source.file
+ local source_file, store_dir
+ local ok, err, errcode
+ if dest_dir then
+ ok, err = fs.change_dir(dest_dir)
+ if not ok then return nil, err, "dest_dir" end
+ source_file, err, errcode = fetch.fetch_url(url, filename)
+ fs.pop_dir()
+ store_dir = dest_dir
+ else
+ source_file, store_dir, errcode = fetch.fetch_url_at_temp_dir(url, "luarocks-source-"..name, filename)
+ end
+ if not source_file then
+ return nil, err or store_dir, errcode
+ end
+ if rockspec.source.md5 then
+ if not fs.check_md5(source_file, rockspec.source.md5) then
+ return nil, "MD5 check for "..filename.." has failed.", "md5"
+ end
+ end
+ if extract then
+ local ok, err = fs.change_dir(store_dir)
+ if not ok then return nil, err end
+ ok, err = fs.unpack_archive(rockspec.source.file)
+ if not ok then return nil, err end
+ ok, err = fetch.find_rockspec_source_dir(rockspec, ".")
+ if not ok then return nil, err end
+ fs.pop_dir()
+ end
+ return source_file, store_dir
+end
+
+function fetch.find_rockspec_source_dir(rockspec, store_dir)
+ local ok, err = fs.change_dir(store_dir)
+ if not ok then return nil, err end
+
+ local file_count, dir_count, found_dir = 0, 0, 0
+
+ if rockspec.source.dir and fs.exists(rockspec.source.dir) then
+ ok, err = true, nil
+ elseif rockspec.source.file and rockspec.source.dir then
+ ok, err = nil, "Directory "..rockspec.source.dir.." not found inside archive "..rockspec.source.file
+ elseif not rockspec.source.dir_set then -- and rockspec:format_is_at_least("3.0") then
+
+ local name = dir.base_name(rockspec.source.file or rockspec.source.url or "")
+
+ if name:match("%.lua$") or name:match("%.c$") then
+ if fs.is_file(name) then
+ rockspec.source.dir = "."
+ ok, err = true, nil
+ end
+ end
+
+ if not rockspec.source.dir then
+ for file in fs.dir() do
+ file_count = file_count + 1
+ if fs.is_dir(file) then
+ dir_count = dir_count + 1
+ found_dir = file
+ end
+ end
+
+ if dir_count == 1 then
+ rockspec.source.dir = found_dir
+ ok, err = true, nil
+ else
+ ok, err = nil, "Could not determine source directory from rock contents (" .. tostring(file_count).." file(s), "..tostring(dir_count).." dir(s))"
+ end
+ end
+ else
+ ok, err = nil, "Could not determine source directory, please set source.dir in rockspec."
+ end
+
+ fs.pop_dir()
+
+ assert(rockspec.source.dir or not ok)
+ return ok, err
+end
+
+--- Download sources for building a rock, calling the appropriate protocol method.
+-- @param rockspec table: The rockspec table
+-- @param extract boolean: When downloading compressed formats, whether to extract
+-- the sources from the fetched archive or not.
+-- @param dest_dir string or nil: If set, will extract to the given directory.
+-- if not given, will extract to a temporary directory.
+-- @return (string, string) or (nil, string): The absolute pathname of
+-- the fetched source tarball and the temporary directory created to
+-- store it; or nil and an error message.
+function fetch.fetch_sources(rockspec, extract, dest_dir)
+ assert(rockspec:type() == "rockspec")
+ assert(type(extract) == "boolean")
+ assert(type(dest_dir) == "string" or not dest_dir)
+
+ -- auto-convert git://github.com URLs to use git+https
+ -- see https://github.blog/2021-09-01-improving-git-protocol-security-github/
+ if rockspec.source.url:match("^git://github%.com/")
+ or rockspec.source.url:match("^git://www%.github%.com/") then
+ rockspec.source.url = rockspec.source.url:gsub("^git://", "git+https://")
+ rockspec.source.protocol = "git+https"
+ end
+
+ local protocol = rockspec.source.protocol
+ local ok, err, proto
+ if dir.is_basic_protocol(protocol) then
+ proto = fetch
+ else
+ ok, proto = pcall(require, "luarocks.fetch."..protocol:gsub("[+-]", "_"))
+ if not ok then
+ return nil, "Unknown protocol "..protocol
+ end
+ end
+
+ if cfg.only_sources_from
+ and rockspec.source.pathname
+ and #rockspec.source.pathname > 0 then
+ if #cfg.only_sources_from == 0 then
+ return nil, "Can't download "..rockspec.source.url.." -- download from remote servers disabled"
+ elseif rockspec.source.pathname:find(cfg.only_sources_from, 1, true) ~= 1 then
+ return nil, "Can't download "..rockspec.source.url.." -- only downloading from "..cfg.only_sources_from
+ end
+ end
+
+ local source_file, store_dir = proto.get_sources(rockspec, extract, dest_dir)
+ if not source_file then return nil, store_dir end
+
+ ok, err = fetch.find_rockspec_source_dir(rockspec, store_dir)
+ if not ok then return nil, err, "source.dir", source_file, store_dir end
+
+ return source_file, store_dir
+end
+
+return fetch
diff --git a/src/luarocks/fetch/cvs.lua b/src/luarocks/fetch/cvs.lua
new file mode 100644
index 0000000..d78e6e6
--- /dev/null
+++ b/src/luarocks/fetch/cvs.lua
@@ -0,0 +1,55 @@
+
+--- Fetch back-end for retrieving sources from CVS.
+local cvs = {}
+
+local unpack = unpack or table.unpack
+
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+local util = require("luarocks.util")
+
+--- Download sources for building a rock, using CVS.
+-- @param rockspec table: The rockspec table
+-- @param extract boolean: Unused in this module (required for API purposes.)
+-- @param dest_dir string or nil: If set, will extract to the given directory.
+-- @return (string, string) or (nil, string): The absolute pathname of
+-- the fetched source tarball and the temporary directory created to
+-- store it; or nil and an error message.
+function cvs.get_sources(rockspec, extract, dest_dir)
+ assert(rockspec:type() == "rockspec")
+ assert(type(dest_dir) == "string" or not dest_dir)
+
+ local cvs_cmd = rockspec.variables.CVS
+ local ok, err_msg = fs.is_tool_available(cvs_cmd, "CVS")
+ if not ok then
+ return nil, err_msg
+ end
+
+ local name_version = rockspec.name .. "-" .. rockspec.version
+ local module = rockspec.source.module or dir.base_name(rockspec.source.url)
+ local command = {cvs_cmd, "-d"..rockspec.source.pathname, "export", module}
+ if rockspec.source.tag then
+ table.insert(command, 4, "-r")
+ table.insert(command, 5, rockspec.source.tag)
+ end
+ local store_dir
+ if not dest_dir then
+ store_dir = fs.make_temp_dir(name_version)
+ if not store_dir then
+ return nil, "Failed creating temporary directory."
+ end
+ util.schedule_function(fs.delete, store_dir)
+ else
+ store_dir = dest_dir
+ end
+ local ok, err = fs.change_dir(store_dir)
+ if not ok then return nil, err end
+ if not fs.execute(unpack(command)) then
+ return nil, "Failed fetching files from CVS."
+ end
+ fs.pop_dir()
+ return module, store_dir
+end
+
+
+return cvs
diff --git a/src/luarocks/fetch/git.lua b/src/luarocks/fetch/git.lua
new file mode 100644
index 0000000..29892e9
--- /dev/null
+++ b/src/luarocks/fetch/git.lua
@@ -0,0 +1,165 @@
+
+--- Fetch back-end for retrieving sources from GIT.
+local git = {}
+
+local unpack = unpack or table.unpack
+
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+local vers = require("luarocks.core.vers")
+local util = require("luarocks.util")
+
+local cached_git_version
+
+--- Get git version.
+-- @param git_cmd string: name of git command.
+-- @return table: git version as returned by luarocks.core.vers.parse_version.
+local function git_version(git_cmd)
+ if not cached_git_version then
+ local version_line = io.popen(fs.Q(git_cmd)..' --version'):read()
+ local version_string = version_line:match('%d-%.%d+%.?%d*')
+ cached_git_version = vers.parse_version(version_string)
+ end
+
+ return cached_git_version
+end
+
+--- Check if git satisfies version requirement.
+-- @param git_cmd string: name of git command.
+-- @param version string: required version.
+-- @return boolean: true if git matches version or is newer, false otherwise.
+local function git_is_at_least(git_cmd, version)
+ return git_version(git_cmd) >= vers.parse_version(version)
+end
+
+--- Git >= 1.7.10 can clone a branch **or tag**, < 1.7.10 by branch only. We
+-- need to know this in order to build the appropriate command; if we can't
+-- clone by tag then we'll have to issue a subsequent command to check out the
+-- given tag.
+-- @param git_cmd string: name of git command.
+-- @return boolean: Whether Git can clone by tag.
+local function git_can_clone_by_tag(git_cmd)
+ return git_is_at_least(git_cmd, "1.7.10")
+end
+
+--- Git >= 1.8.4 can fetch submodules shallowly, saving bandwidth and time for
+-- submodules with large history.
+-- @param git_cmd string: name of git command.
+-- @return boolean: Whether Git can fetch submodules shallowly.
+local function git_supports_shallow_submodules(git_cmd)
+ return git_is_at_least(git_cmd, "1.8.4")
+end
+
+--- Git >= 2.10 can fetch submodules shallowly according to .gitmodules configuration, allowing more granularity.
+-- @param git_cmd string: name of git command.
+-- @return boolean: Whether Git can fetch submodules shallowly according to .gitmodules.
+local function git_supports_shallow_recommendations(git_cmd)
+ return git_is_at_least(git_cmd, "2.10.0")
+end
+
+local function git_identifier(git_cmd, ver)
+ if not (ver:match("^dev%-%d+$") or ver:match("^scm%-%d+$")) then
+ return nil
+ end
+ local pd = io.popen(fs.command_at(fs.current_dir(), fs.Q(git_cmd).." log --pretty=format:%ai_%h -n 1"))
+ if not pd then
+ return nil
+ end
+ local date_hash = pd:read("*l")
+ pd:close()
+ if not date_hash then
+ return nil
+ end
+ local date, time, tz, hash = date_hash:match("([^%s]+) ([^%s]+) ([^%s]+)_([^%s]+)")
+ date = date:gsub("%-", "")
+ time = time:gsub(":", "")
+ return date .. "." .. time .. "." .. hash
+end
+
+--- Download sources for building a rock, using git.
+-- @param rockspec table: The rockspec table
+-- @param extract boolean: Unused in this module (required for API purposes.)
+-- @param dest_dir string or nil: If set, will extract to the given directory.
+-- @return (string, string) or (nil, string): The absolute pathname of
+-- the fetched source tarball and the temporary directory created to
+-- store it; or nil and an error message.
+function git.get_sources(rockspec, extract, dest_dir, depth)
+ assert(rockspec:type() == "rockspec")
+ assert(type(dest_dir) == "string" or not dest_dir)
+
+ local git_cmd = rockspec.variables.GIT
+ local name_version = rockspec.name .. "-" .. rockspec.version
+ local module = dir.base_name(rockspec.source.url)
+ -- Strip off .git from base name if present
+ module = module:gsub("%.git$", "")
+
+ local ok, err_msg = fs.is_tool_available(git_cmd, "Git")
+ if not ok then
+ return nil, err_msg
+ end
+
+ local store_dir
+ if not dest_dir then
+ store_dir = fs.make_temp_dir(name_version)
+ if not store_dir then
+ return nil, "Failed creating temporary directory."
+ end
+ util.schedule_function(fs.delete, store_dir)
+ else
+ store_dir = dest_dir
+ end
+ store_dir = fs.absolute_name(store_dir)
+ local ok, err = fs.change_dir(store_dir)
+ if not ok then return nil, err end
+
+ local command = {fs.Q(git_cmd), "clone", depth or "--depth=1", rockspec.source.url, module}
+ local tag_or_branch = rockspec.source.tag or rockspec.source.branch
+ -- If the tag or branch is explicitly set to "master" in the rockspec, then
+ -- we can avoid passing it to Git since it's the default.
+ if tag_or_branch == "master" then tag_or_branch = nil end
+ if tag_or_branch then
+ if git_can_clone_by_tag(git_cmd) then
+ -- The argument to `--branch` can actually be a branch or a tag as of
+ -- Git 1.7.10.
+ table.insert(command, 3, "--branch=" .. tag_or_branch)
+ end
+ end
+ if not fs.execute(unpack(command)) then
+ return nil, "Failed cloning git repository."
+ end
+ ok, err = fs.change_dir(module)
+ if not ok then return nil, err end
+ if tag_or_branch and not git_can_clone_by_tag() then
+ if not fs.execute(fs.Q(git_cmd), "checkout", tag_or_branch) then
+ return nil, 'Failed to check out the "' .. tag_or_branch ..'" tag or branch.'
+ end
+ end
+
+ -- Fetching git submodules is supported only when rockspec format is >= 3.0.
+ if rockspec:format_is_at_least("3.0") then
+ command = {fs.Q(git_cmd), "submodule", "update", "--init", "--recursive"}
+
+ if git_supports_shallow_recommendations(git_cmd) then
+ table.insert(command, 5, "--recommend-shallow")
+ elseif git_supports_shallow_submodules(git_cmd) then
+ -- Fetch only the last commit of each submodule.
+ table.insert(command, 5, "--depth=1")
+ end
+
+ if not fs.execute(unpack(command)) then
+ return nil, 'Failed to fetch submodules.'
+ end
+ end
+
+ if not rockspec.source.tag then
+ rockspec.source.identifier = git_identifier(git_cmd, rockspec.version)
+ end
+
+ fs.delete(dir.path(store_dir, module, ".git"))
+ fs.delete(dir.path(store_dir, module, ".gitignore"))
+ fs.pop_dir()
+ fs.pop_dir()
+ return module, store_dir
+end
+
+return git
diff --git a/src/luarocks/fetch/git_file.lua b/src/luarocks/fetch/git_file.lua
new file mode 100644
index 0000000..8d46bbc
--- /dev/null
+++ b/src/luarocks/fetch/git_file.lua
@@ -0,0 +1,19 @@
+
+--- Fetch back-end for retrieving sources from local Git repositories.
+local git_file = {}
+
+local git = require("luarocks.fetch.git")
+
+--- Fetch sources for building a rock from a local Git repository.
+-- @param rockspec table: The rockspec table
+-- @param extract boolean: Unused in this module (required for API purposes.)
+-- @param dest_dir string or nil: If set, will extract to the given directory.
+-- @return (string, string) or (nil, string): The absolute pathname of
+-- the fetched source tarball and the temporary directory created to
+-- store it; or nil and an error message.
+function git_file.get_sources(rockspec, extract, dest_dir)
+ rockspec.source.url = rockspec.source.url:gsub("^git.file://", "")
+ return git.get_sources(rockspec, extract, dest_dir)
+end
+
+return git_file
diff --git a/src/luarocks/fetch/git_http.lua b/src/luarocks/fetch/git_http.lua
new file mode 100644
index 0000000..d85e257
--- /dev/null
+++ b/src/luarocks/fetch/git_http.lua
@@ -0,0 +1,26 @@
+
+--- Fetch back-end for retrieving sources from Git repositories
+-- that use http:// transport. For example, for fetching a repository
+-- that requires the following command line:
+-- `git clone http://example.com/foo.git`
+-- you can use this in the rockspec:
+-- source = { url = "git+http://example.com/foo.git" }
+-- Prefer using the normal git:// fetch mode as it is more widely
+-- available in older versions of LuaRocks.
+local git_http = {}
+
+local git = require("luarocks.fetch.git")
+
+--- Fetch sources for building a rock from a local Git repository.
+-- @param rockspec table: The rockspec table
+-- @param extract boolean: Unused in this module (required for API purposes.)
+-- @param dest_dir string or nil: If set, will extract to the given directory.
+-- @return (string, string) or (nil, string): The absolute pathname of
+-- the fetched source tarball and the temporary directory created to
+-- store it; or nil and an error message.
+function git_http.get_sources(rockspec, extract, dest_dir)
+ rockspec.source.url = rockspec.source.url:gsub("^git.", "")
+ return git.get_sources(rockspec, extract, dest_dir, "--")
+end
+
+return git_http
diff --git a/src/luarocks/fetch/git_https.lua b/src/luarocks/fetch/git_https.lua
new file mode 100644
index 0000000..67f8ad6
--- /dev/null
+++ b/src/luarocks/fetch/git_https.lua
@@ -0,0 +1,7 @@
+--- Fetch back-end for retrieving sources from Git repositories
+-- that use https:// transport. For example, for fetching a repository
+-- that requires the following command line:
+-- `git clone https://example.com/foo.git`
+-- you can use this in the rockspec:
+-- source = { url = "git+https://example.com/foo.git" }
+return require "luarocks.fetch.git_http"
diff --git a/src/luarocks/fetch/git_ssh.lua b/src/luarocks/fetch/git_ssh.lua
new file mode 100644
index 0000000..0c2c075
--- /dev/null
+++ b/src/luarocks/fetch/git_ssh.lua
@@ -0,0 +1,32 @@
+--- Fetch back-end for retrieving sources from Git repositories
+-- that use ssh:// transport. For example, for fetching a repository
+-- that requires the following command line:
+-- `git clone ssh://git@example.com/path/foo.git
+-- you can use this in the rockspec:
+-- source = { url = "git+ssh://git@example.com/path/foo.git" }
+-- It also handles scp-style ssh urls: git@example.com:path/foo.git,
+-- but you have to prepend the "git+ssh://" and why not use the "newer"
+-- style anyway?
+local git_ssh = {}
+
+local git = require("luarocks.fetch.git")
+
+--- Fetch sources for building a rock from a local Git repository.
+-- @param rockspec table: The rockspec table
+-- @param extract boolean: Unused in this module (required for API purposes.)
+-- @param dest_dir string or nil: If set, will extract to the given directory.
+-- @return (string, string) or (nil, string): The absolute pathname of
+-- the fetched source tarball and the temporary directory created to
+-- store it; or nil and an error message.
+function git_ssh.get_sources(rockspec, extract, dest_dir)
+ rockspec.source.url = rockspec.source.url:gsub("^git.", "")
+
+ -- Handle old-style scp-like git ssh urls
+ if rockspec.source.url:match("^ssh://[^/]+:[^%d]") then
+ rockspec.source.url = rockspec.source.url:gsub("^ssh://", "")
+ end
+
+ return git.get_sources(rockspec, extract, dest_dir, "--")
+end
+
+return git_ssh
diff --git a/src/luarocks/fetch/hg.lua b/src/luarocks/fetch/hg.lua
new file mode 100644
index 0000000..0ef0f5e
--- /dev/null
+++ b/src/luarocks/fetch/hg.lua
@@ -0,0 +1,65 @@
+
+--- Fetch back-end for retrieving sources from HG.
+local hg = {}
+
+local unpack = unpack or table.unpack
+
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+local util = require("luarocks.util")
+
+--- Download sources for building a rock, using hg.
+-- @param rockspec table: The rockspec table
+-- @param extract boolean: Unused in this module (required for API purposes.)
+-- @param dest_dir string or nil: If set, will extract to the given directory.
+-- @return (string, string) or (nil, string): The absolute pathname of
+-- the fetched source tarball and the temporary directory created to
+-- store it; or nil and an error message.
+function hg.get_sources(rockspec, extract, dest_dir)
+ assert(rockspec:type() == "rockspec")
+ assert(type(dest_dir) == "string" or not dest_dir)
+
+ local hg_cmd = rockspec.variables.HG
+ local ok, err_msg = fs.is_tool_available(hg_cmd, "Mercurial")
+ if not ok then
+ return nil, err_msg
+ end
+
+ local name_version = rockspec.name .. "-" .. rockspec.version
+ -- Strip off special hg:// protocol type
+ local url = rockspec.source.url:gsub("^hg://", "")
+
+ local module = dir.base_name(url)
+
+ local command = {hg_cmd, "clone", url, module}
+ local tag_or_branch = rockspec.source.tag or rockspec.source.branch
+ if tag_or_branch then
+ command = {hg_cmd, "clone", "--rev", tag_or_branch, url, module}
+ end
+ local store_dir
+ if not dest_dir then
+ store_dir = fs.make_temp_dir(name_version)
+ if not store_dir then
+ return nil, "Failed creating temporary directory."
+ end
+ util.schedule_function(fs.delete, store_dir)
+ else
+ store_dir = dest_dir
+ end
+ local ok, err = fs.change_dir(store_dir)
+ if not ok then return nil, err end
+ if not fs.execute(unpack(command)) then
+ return nil, "Failed cloning hg repository."
+ end
+ ok, err = fs.change_dir(module)
+ if not ok then return nil, err end
+
+ fs.delete(dir.path(store_dir, module, ".hg"))
+ fs.delete(dir.path(store_dir, module, ".hgignore"))
+ fs.pop_dir()
+ fs.pop_dir()
+ return module, store_dir
+end
+
+
+return hg
diff --git a/src/luarocks/fetch/hg_http.lua b/src/luarocks/fetch/hg_http.lua
new file mode 100644
index 0000000..8f506da
--- /dev/null
+++ b/src/luarocks/fetch/hg_http.lua
@@ -0,0 +1,24 @@
+
+--- Fetch back-end for retrieving sources from hg repositories
+-- that use http:// transport. For example, for fetching a repository
+-- that requires the following command line:
+-- `hg clone http://example.com/foo`
+-- you can use this in the rockspec:
+-- source = { url = "hg+http://example.com/foo" }
+local hg_http = {}
+
+local hg = require("luarocks.fetch.hg")
+
+--- Download sources for building a rock, using hg over http.
+-- @param rockspec table: The rockspec table
+-- @param extract boolean: Unused in this module (required for API purposes.)
+-- @param dest_dir string or nil: If set, will extract to the given directory.
+-- @return (string, string) or (nil, string): The absolute pathname of
+-- the fetched source tarball and the temporary directory created to
+-- store it; or nil and an error message.
+function hg_http.get_sources(rockspec, extract, dest_dir)
+ rockspec.source.url = rockspec.source.url:gsub("^hg.", "")
+ return hg.get_sources(rockspec, extract, dest_dir)
+end
+
+return hg_http
diff --git a/src/luarocks/fetch/hg_https.lua b/src/luarocks/fetch/hg_https.lua
new file mode 100644
index 0000000..e67417f
--- /dev/null
+++ b/src/luarocks/fetch/hg_https.lua
@@ -0,0 +1,8 @@
+
+--- Fetch back-end for retrieving sources from hg repositories
+-- that use https:// transport. For example, for fetching a repository
+-- that requires the following command line:
+-- `hg clone https://example.com/foo`
+-- you can use this in the rockspec:
+-- source = { url = "hg+https://example.com/foo" }
+return require "luarocks.fetch.hg_http"
diff --git a/src/luarocks/fetch/hg_ssh.lua b/src/luarocks/fetch/hg_ssh.lua
new file mode 100644
index 0000000..0c365fa
--- /dev/null
+++ b/src/luarocks/fetch/hg_ssh.lua
@@ -0,0 +1,8 @@
+
+--- Fetch back-end for retrieving sources from hg repositories
+-- that use ssh:// transport. For example, for fetching a repository
+-- that requires the following command line:
+-- `hg clone ssh://example.com/foo`
+-- you can use this in the rockspec:
+-- source = { url = "hg+ssh://example.com/foo" }
+return require "luarocks.fetch.hg_http"
diff --git a/src/luarocks/fetch/sscm.lua b/src/luarocks/fetch/sscm.lua
new file mode 100644
index 0000000..32bb2ec
--- /dev/null
+++ b/src/luarocks/fetch/sscm.lua
@@ -0,0 +1,44 @@
+
+--- Fetch back-end for retrieving sources from Surround SCM Server
+local sscm = {}
+
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+
+--- Download sources via Surround SCM Server for building a rock.
+-- @param rockspec table: The rockspec table
+-- @param extract boolean: Unused in this module (required for API purposes.)
+-- @param dest_dir string or nil: If set, will extract to the given directory.
+-- @return (string, string) or (nil, string): The absolute pathname of
+-- the fetched source tarball and the temporary directory created to
+-- store it; or nil and an error message.
+function sscm.get_sources(rockspec, extract, dest_dir)
+ assert(rockspec:type() == "rockspec")
+ assert(type(dest_dir) == "string" or not dest_dir)
+
+ local sscm_cmd = rockspec.variables.SSCM
+ local module = rockspec.source.module or dir.base_name(rockspec.source.url)
+ local branch, repository = string.match(rockspec.source.pathname, "^([^/]*)/(.*)")
+ if not branch or not repository then
+ return nil, "Error retrieving branch and repository from rockspec."
+ end
+ -- Search for working directory.
+ local working_dir
+ local tmp = io.popen(string.format(sscm_cmd..[[ property "/" -d -b%s -p%s]], branch, repository))
+ for line in tmp:lines() do
+ --%c because a chr(13) comes in the end.
+ working_dir = string.match(line, "Working directory:[%s]*(.*)%c$")
+ if working_dir then break end
+ end
+ tmp:close()
+ if not working_dir then
+ return nil, "Error retrieving working directory from SSCM."
+ end
+ if not fs.execute(sscm_cmd, "get", "*", "-e" , "-r", "-b"..branch, "-p"..repository, "-tmodify", "-wreplace") then
+ return nil, "Failed fetching files from SSCM."
+ end
+ -- FIXME: This function does not honor the dest_dir parameter.
+ return module, working_dir
+end
+
+return sscm
diff --git a/src/luarocks/fetch/svn.lua b/src/luarocks/fetch/svn.lua
new file mode 100644
index 0000000..b6618af
--- /dev/null
+++ b/src/luarocks/fetch/svn.lua
@@ -0,0 +1,64 @@
+
+--- Fetch back-end for retrieving sources from Subversion.
+local svn = {}
+
+local unpack = unpack or table.unpack
+
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+local util = require("luarocks.util")
+
+--- Download sources for building a rock, using Subversion.
+-- @param rockspec table: The rockspec table
+-- @param extract boolean: Unused in this module (required for API purposes.)
+-- @param dest_dir string or nil: If set, will extract to the given directory.
+-- @return (string, string) or (nil, string): The absolute pathname of
+-- the fetched source tarball and the temporary directory created to
+-- store it; or nil and an error message.
+function svn.get_sources(rockspec, extract, dest_dir)
+ assert(rockspec:type() == "rockspec")
+ assert(type(dest_dir) == "string" or not dest_dir)
+
+ local svn_cmd = rockspec.variables.SVN
+ local ok, err_msg = fs.is_tool_available(svn_cmd, "Subversion")
+ if not ok then
+ return nil, err_msg
+ end
+
+ local name_version = rockspec.name .. "-" .. rockspec.version
+ local module = rockspec.source.module or dir.base_name(rockspec.source.url)
+ local url = rockspec.source.url:gsub("^svn://", "")
+ local command = {svn_cmd, "checkout", url, module}
+ if rockspec.source.tag then
+ table.insert(command, 5, "-r")
+ table.insert(command, 6, rockspec.source.tag)
+ end
+ local store_dir
+ if not dest_dir then
+ store_dir = fs.make_temp_dir(name_version)
+ if not store_dir then
+ return nil, "Failed creating temporary directory."
+ end
+ util.schedule_function(fs.delete, store_dir)
+ else
+ store_dir = dest_dir
+ end
+ local ok, err = fs.change_dir(store_dir)
+ if not ok then return nil, err end
+ if not fs.execute(unpack(command)) then
+ return nil, "Failed fetching files from Subversion."
+ end
+ ok, err = fs.change_dir(module)
+ if not ok then return nil, err end
+ for _, d in ipairs(fs.find(".")) do
+ if dir.base_name(d) == ".svn" then
+ fs.delete(dir.path(store_dir, module, d))
+ end
+ end
+ fs.pop_dir()
+ fs.pop_dir()
+ return module, store_dir
+end
+
+
+return svn
diff --git a/src/luarocks/fs.lua b/src/luarocks/fs.lua
new file mode 100644
index 0000000..a8156a2
--- /dev/null
+++ b/src/luarocks/fs.lua
@@ -0,0 +1,148 @@
+
+--- Proxy module for filesystem and platform abstractions.
+-- All code using "fs" code should require "luarocks.fs",
+-- and not the various platform-specific implementations.
+-- However, see the documentation of the implementation
+-- for the API reference.
+
+local pairs = pairs
+
+local fs = {}
+-- To avoid a loop when loading the other fs modules.
+package.loaded["luarocks.fs"] = fs
+
+local cfg = require("luarocks.core.cfg")
+
+local pack = table.pack or function(...) return { n = select("#", ...), ... } end
+
+math.randomseed(os.time())
+
+local fs_is_verbose = false
+
+do
+ local old_popen, old_execute
+
+ -- patch io.popen and os.execute to display commands in verbose mode
+ function fs.verbose()
+ fs_is_verbose = true
+
+ if old_popen or old_execute then return end
+ old_popen = io.popen
+ -- luacheck: push globals io os
+ io.popen = function(one, two)
+ if two == nil then
+ print("\nio.popen: ", one)
+ else
+ print("\nio.popen: ", one, "Mode:", two)
+ end
+ return old_popen(one, two)
+ end
+
+ old_execute = os.execute
+ os.execute = function(cmd)
+ -- redact api keys if present
+ print("\nos.execute: ", (cmd:gsub("(/api/[^/]+/)([^/]+)/", function(cap, key) return cap.."<redacted>/" end)) )
+ local a, b, c = old_execute(cmd)
+ if type(a) == "boolean" then
+ print((a and ".........." or "##########") .. ": " .. tostring(c) .. (b == "exit" and "" or " (" .. tostring(b) .. ")"))
+ elseif type(a) == "number" then
+ print(((a == 0) and ".........." or "##########") .. ": " .. tostring(a))
+ end
+ return a, b, c
+ end
+ -- luacheck: pop
+ end
+end
+
+do
+ local skip_verbose_wrap = {
+ ["current_dir"] = true,
+ }
+
+ local function load_fns(fs_table, inits)
+ for name, fn in pairs(fs_table) do
+ if name ~= "init" and not fs[name] then
+ if skip_verbose_wrap[name] then
+ fs[name] = fn
+ else
+ fs[name] = function(...)
+ if fs_is_verbose then
+ local args = pack(...)
+ for i=1, args.n do
+ local arg = args[i]
+ local pok, v = pcall(string.format, "%q", arg)
+ args[i] = pok and v or tostring(arg)
+ end
+ print("fs." .. name .. "(" .. table.concat(args, ", ") .. ")")
+ end
+ return fn(...)
+ end
+ end
+ end
+ end
+ if fs_table.init then
+ table.insert(inits, fs_table.init)
+ end
+ end
+
+ local function load_platform_fns(patt, inits)
+ local each_platform = cfg.each_platform
+
+ -- FIXME A quick hack for the experimental Windows build
+ if os.getenv("LUAROCKS_CROSS_COMPILING") then
+ each_platform = function()
+ local i = 0
+ local plats = { "linux", "unix" }
+ return function()
+ i = i + 1
+ return plats[i]
+ end
+ end
+ end
+
+ for platform in each_platform("most-specific-first") do
+ local ok, fs_plat = pcall(require, patt:format(platform))
+ if ok and fs_plat then
+ load_fns(fs_plat, inits)
+ end
+ end
+ end
+
+ function fs.init()
+ local inits = {}
+
+ if fs.current_dir then
+ -- unload luarocks fs so it can be reloaded using all modules
+ -- providing extra functionality in the current package paths
+ for k, _ in pairs(fs) do
+ if k ~= "init" and k ~= "verbose" then
+ fs[k] = nil
+ end
+ end
+ for m, _ in pairs(package.loaded) do
+ if m:match("luarocks%.fs%.") then
+ package.loaded[m] = nil
+ end
+ end
+ end
+
+ -- Load platform-specific functions
+ load_platform_fns("luarocks.fs.%s", inits)
+
+ -- Load platform-independent pure-Lua functionality
+ load_fns(require("luarocks.fs.lua"), inits)
+
+ -- Load platform-specific fallbacks for missing Lua modules
+ load_platform_fns("luarocks.fs.%s.tools", inits)
+
+ -- Load platform-independent external tool functionality
+ load_fns(require("luarocks.fs.tools"), inits)
+
+ -- Run platform-specific initializations after everything is loaded
+ for _, init in ipairs(inits) do
+ init()
+ end
+ end
+end
+
+return fs
diff --git a/src/luarocks/fs/linux.lua b/src/luarocks/fs/linux.lua
new file mode 100644
index 0000000..c1b057c
--- /dev/null
+++ b/src/luarocks/fs/linux.lua
@@ -0,0 +1,50 @@
+--- Linux-specific implementation of filesystem and platform abstractions.
+local linux = {}
+
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+
+function linux.is_dir(file)
+ file = fs.absolute_name(file)
+ file = dir.normalize(file) .. "/."
+ local fd, _, code = io.open(file, "r")
+ if code == 2 then -- "No such file or directory"
+ return false
+ end
+ if code == 20 then -- "Not a directory", regardless of permissions
+ return false
+ end
+ if code == 13 then -- "Permission denied", but is a directory
+ return true
+ end
+ if fd then
+ local _, _, ecode = fd:read(1)
+ fd:close()
+ if ecode == 21 then -- "Is a directory"
+ return true
+ end
+ end
+ return false
+end
+
+function linux.is_file(file)
+ file = fs.absolute_name(file)
+ if fs.is_dir(file) then
+ return false
+ end
+ file = dir.normalize(file)
+ local fd, _, code = io.open(file, "r")
+ if code == 2 then -- "No such file or directory"
+ return false
+ end
+ if code == 13 then -- "Permission denied", but it exists
+ return true
+ end
+ if fd then
+ fd:close()
+ return true
+ end
+ return false
+end
+
+return linux
diff --git a/src/luarocks/fs/lua.lua b/src/luarocks/fs/lua.lua
new file mode 100644
index 0000000..4016ddc
--- /dev/null
+++ b/src/luarocks/fs/lua.lua
@@ -0,0 +1,1307 @@
+
+--- Native Lua implementation of filesystem and platform abstractions,
+-- using LuaFileSystem, LuaSocket, LuaSec, lua-zlib, LuaPosix, MD5.
+-- module("luarocks.fs.lua")
+local fs_lua = {}
+
+local fs = require("luarocks.fs")
+
+local cfg = require("luarocks.core.cfg")
+local dir = require("luarocks.dir")
+local util = require("luarocks.util")
+local vers = require("luarocks.core.vers")
+
+local pack = table.pack or function(...) return { n = select("#", ...), ... } end
+
+local socket_ok, zip_ok, lfs_ok, md5_ok, posix_ok, bz2_ok, _
+local http, ftp, zip, lfs, md5, posix, bz2
+
+if cfg.fs_use_modules then
+ socket_ok, http = pcall(require, "socket.http")
+ _, ftp = pcall(require, "socket.ftp")
+ zip_ok, zip = pcall(require, "luarocks.tools.zip")
+ bz2_ok, bz2 = pcall(require, "bz2")
+ lfs_ok, lfs = pcall(require, "lfs")
+ md5_ok, md5 = pcall(require, "md5")
+ posix_ok, posix = pcall(require, "posix")
+end
+
+local patch = require("luarocks.tools.patch")
+local tar = require("luarocks.tools.tar")
+
+local dir_sep = package.config:sub(1, 1)
+
+local dir_stack = {}
+
+--- Test is file/dir is writable.
+-- Warning: testing if a file/dir is writable does not guarantee
+-- that it will remain writable and therefore it is no replacement
+-- for checking the result of subsequent operations.
+-- @param file string: filename to test
+-- @return boolean: true if file exists, false otherwise.
+function fs_lua.is_writable(file)
+ assert(file)
+ file = dir.normalize(file)
+ local result
+ if fs.is_dir(file) then
+ local file2 = dir.path(file, '.tmpluarockstestwritable')
+ local fh = io.open(file2, 'wb')
+ result = fh ~= nil
+ if fh then fh:close() end
+ os.remove(file2)
+ else
+ local fh = io.open(file, 'r+b')
+ result = fh ~= nil
+ if fh then fh:close() end
+ end
+ return result
+end
+
+function fs_lua.quote_args(command, ...)
+ local out = { command }
+ local args = pack(...)
+ for i=1, args.n do
+ local arg = args[i]
+ assert(type(arg) == "string")
+ out[#out+1] = fs.Q(arg)
+ end
+ return table.concat(out, " ")
+end
+
+--- Run the given command, quoting its arguments.
+-- The command is executed in the current directory in the dir stack.
+-- @param command string: The command to be executed. No quoting/escaping
+-- is applied.
+-- @param ... Strings containing additional arguments, which are quoted.
+-- @return boolean: true if command succeeds (status code 0), false
+-- otherwise.
+function fs_lua.execute(command, ...)
+ assert(type(command) == "string")
+ return fs.execute_string(fs.quote_args(command, ...))
+end
+
+--- Run the given command, quoting its arguments, silencing its output.
+-- The command is executed in the current directory in the dir stack.
+-- Silencing is omitted if 'verbose' mode is enabled.
+-- @param command string: The command to be executed. No quoting/escaping
+-- is applied.
+-- @param ... Strings containing additional arguments, which will be quoted.
+-- @return boolean: true if command succeeds (status code 0), false
+-- otherwise.
+function fs_lua.execute_quiet(command, ...)
+ assert(type(command) == "string")
+ if cfg.verbose then -- omit silencing output
+ return fs.execute_string(fs.quote_args(command, ...))
+ else
+ return fs.execute_string(fs.quiet(fs.quote_args(command, ...)))
+ end
+end
+
+function fs_lua.execute_env(env, command, ...)
+ assert(type(command) == "string")
+ local envstr = {}
+ for var, val in pairs(env) do
+ table.insert(envstr, fs.export_cmd(var, val))
+ end
+ return fs.execute_string(table.concat(envstr, "\n") .. "\n" .. fs.quote_args(command, ...))
+end
+
+local tool_available_cache = {}
+
+function fs_lua.set_tool_available(tool_name, value)
+ assert(type(value) == "boolean")
+ tool_available_cache[tool_name] = value
+end
+
+--- Checks if the given tool is available.
+-- The tool is executed using a flag, usually just to ask its version.
+-- @param tool_cmd string: The command to be used to check the tool's presence (e.g. hg in case of Mercurial)
+-- @param tool_name string: The actual name of the tool (e.g. Mercurial)
+function fs_lua.is_tool_available(tool_cmd, tool_name)
+ assert(type(tool_cmd) == "string")
+ assert(type(tool_name) == "string")
+
+ local ok
+ if tool_available_cache[tool_name] ~= nil then
+ ok = tool_available_cache[tool_name]
+ else
+ local tool_cmd_no_args = tool_cmd:gsub(" [^\"]*$", "")
+
+ -- if it looks like the tool has a pathname, try that first
+ if tool_cmd_no_args:match("[/\\]") then
+ local tool_cmd_no_args_normalized = dir.path(tool_cmd_no_args)
+ local fd = io.open(tool_cmd_no_args_normalized, "r")
+ if fd then
+ fd:close()
+ ok = true
+ end
+ end
+
+ if not ok then
+ ok = fs.search_in_path(tool_cmd_no_args)
+ end
+
+ tool_available_cache[tool_name] = (ok == true)
+ end
+
+ if ok then
+ return true
+ else
+ local msg = "'%s' program not found. Make sure %s is installed and is available in your PATH " ..
+ "(or you may want to edit the 'variables.%s' value in file '%s')"
+ return nil, msg:format(tool_cmd, tool_name, tool_name:upper(), cfg.config_files.nearest)
+ end
+end
+
+--- Check the MD5 checksum for a file.
+-- @param file string: The file to be checked.
+-- @param md5sum string: The string with the expected MD5 checksum.
+-- @return boolean: true if the MD5 checksum for 'file' equals 'md5sum', false + msg if not
+-- or if it could not perform the check for any reason.
+function fs_lua.check_md5(file, md5sum)
+ file = dir.normalize(file)
+ local computed, msg = fs.get_md5(file)
+ if not computed then
+ return false, msg
+ end
+ if computed:match("^"..md5sum) then
+ return true
+ else
+ return false, "Mismatch MD5 hash for file "..file
+ end
+end
+
+--- List the contents of a directory.
+-- @param at string or nil: directory to list (will be the current
+-- directory if none is given).
+-- @return table: an array of strings with the filenames representing
+-- the contents of a directory.
+function fs_lua.list_dir(at)
+ local result = {}
+ for file in fs.dir(at) do
+ result[#result+1] = file
+ end
+ return result
+end
+
+--- Iterate over the contents of a directory.
+-- @param at string or nil: directory to list (will be the current
+-- directory if none is given).
+-- @return function: an iterator function suitable for use with
+-- the for statement.
+function fs_lua.dir(at)
+ if not at then
+ at = fs.current_dir()
+ end
+ at = dir.normalize(at)
+ if not fs.is_dir(at) then
+ return function() end
+ end
+ return coroutine.wrap(function() fs.dir_iterator(at) end)
+end
+
+--- List the Lua modules at a specific require path.
+-- eg. `modules("luarocks.cmd")` would return a list of all LuaRocks command
+-- modules, in the current Lua path.
+function fs_lua.modules(at)
+ at = at or ""
+ if #at > 0 then
+ -- turn require path into file path
+ at = at:gsub("%.", package.config:sub(1,1)) .. package.config:sub(1,1)
+ end
+
+ local path = package.path:sub(-1, -1) == ";" and package.path or package.path .. ";"
+ local paths = {}
+ for location in path:gmatch("(.-);") do
+ if location:lower() == "?.lua" then
+ location = "./?.lua"
+ end
+ local _, q_count = location:gsub("%?", "") -- only use the ones with a single '?'
+ if location:match("%?%.[lL][uU][aA]$") and q_count == 1 then -- only use when ending with "?.lua"
+ location = location:gsub("%?%.[lL][uU][aA]$", at)
+ table.insert(paths, location)
+ end
+ end
+
+ if #paths == 0 then
+ return {}
+ end
+
+ local modules = {}
+ local is_duplicate = {}
+ for _, path in ipairs(paths) do -- luacheck: ignore 421
+ local files = fs.list_dir(path)
+ for _, filename in ipairs(files or {}) do
+ if filename:match("%.[lL][uU][aA]$") then
+ filename = filename:sub(1,-5) -- drop the extension
+ if not is_duplicate[filename] then
+ is_duplicate[filename] = true
+ table.insert(modules, filename)
+ end
+ end
+ end
+ end
+
+ return modules
+end
+
+function fs_lua.filter_file(fn, input_filename, output_filename)
+ local fd, err = io.open(input_filename, "rb")
+ if not fd then
+ return nil, err
+ end
+
+ local input, err = fd:read("*a")
+ fd:close()
+ if not input then
+ return nil, err
+ end
+
+ local output, err = fn(input)
+ if not output then
+ return nil, err
+ end
+
+ fd, err = io.open(output_filename, "wb")
+ if not fd then
+ return nil, err
+ end
+
+ local ok, err = fd:write(output)
+ fd:close()
+ if not ok then
+ return nil, err
+ end
+
+ return true
+end
+
+function fs_lua.system_temp_dir()
+ return os.getenv("TMPDIR") or os.getenv("TEMP") or "/tmp"
+end
+
+local function temp_dir_pattern(name_pattern)
+ return dir.path(fs.system_temp_dir(),
+ "luarocks_" .. dir.normalize(name_pattern):gsub("[/\\]", "_") .. "-")
+end
+
+---------------------------------------------------------------------
+-- LuaFileSystem functions
+---------------------------------------------------------------------
+
+if lfs_ok then
+
+function fs_lua.file_age(filename)
+ local attr = lfs.attributes(filename)
+ if attr and attr.change then
+ return os.difftime(os.time(), attr.change)
+ end
+ return math.huge
+end
+
+function fs_lua.lock_access(dirname, force)
+ fs.make_dir(dirname)
+ local lockfile = dir.path(dirname, "lockfile.lfs")
+
+ -- drop stale lock, older than 1 hour
+ local age = fs.file_age(lockfile)
+ if age > 3600 and age < math.huge then
+ force = true
+ end
+
+ if force then
+ os.remove(lockfile)
+ end
+ return lfs.lock_dir(dirname)
+end
+
+function fs_lua.unlock_access(lock)
+ return lock:free()
+end
+
+--- Run the given command.
+-- The command is executed in the current directory in the dir stack.
+-- @param cmd string: No quoting/escaping is applied to the command.
+-- @return boolean: true if command succeeds (status code 0), false
+-- otherwise.
+function fs_lua.execute_string(cmd)
+ local code = os.execute(cmd)
+ return (code == 0 or code == true)
+end
+
+--- Obtain current directory.
+-- Uses the module's internal dir stack.
+-- @return string: the absolute pathname of the current directory.
+function fs_lua.current_dir()
+ return lfs.currentdir()
+end
+
+--- Change the current directory.
+-- Uses the module's internal dir stack. This does not have exact
+-- semantics of chdir, as it does not handle errors the same way,
+-- but works well for our purposes for now.
+-- @param d string: The directory to switch to.
+function fs_lua.change_dir(d)
+ table.insert(dir_stack, lfs.currentdir())
+ d = dir.normalize(d)
+ return lfs.chdir(d)
+end
+
+--- Change directory to root.
+-- Allows leaving a directory (e.g. for deleting it) in
+-- a crossplatform way.
+function fs_lua.change_dir_to_root()
+ local current = lfs.currentdir()
+ if not current or current == "" then
+ return false
+ end
+ table.insert(dir_stack, current)
+ lfs.chdir("/") -- works on Windows too
+ return true
+end
+
+--- Change working directory to the previous in the dir stack.
+-- @return true if a pop occurred, false if the stack was empty.
+function fs_lua.pop_dir()
+ local d = table.remove(dir_stack)
+ if d then
+ lfs.chdir(d)
+ return true
+ else
+ return false
+ end
+end
+
+--- Create a directory if it does not already exist.
+-- If any of the higher levels in the path name do not exist
+-- too, they are created as well.
+-- @param directory string: pathname of directory to create.
+-- @return boolean or (boolean, string): true on success or (false, error message) on failure.
+function fs_lua.make_dir(directory)
+ assert(type(directory) == "string")
+ directory = dir.normalize(directory)
+ local path = nil
+ if directory:sub(2, 2) == ":" then
+ path = directory:sub(1, 2)
+ directory = directory:sub(4)
+ else
+ if directory:match("^" .. dir_sep) then
+ path = ""
+ end
+ end
+ for d in directory:gmatch("([^" .. dir_sep .. "]+)" .. dir_sep .. "*") do
+ path = path and path .. dir_sep .. d or d
+ local mode = lfs.attributes(path, "mode")
+ if not mode then
+ local ok, err = lfs.mkdir(path)
+ if not ok then
+ return false, err
+ end
+ if cfg.is_platform("unix") then
+ ok, err = fs.set_permissions(path, "exec", "all")
+ if not ok then
+ return false, err
+ end
+ end
+ elseif mode ~= "directory" then
+ return false, path.." is not a directory"
+ end
+ end
+ return true
+end
+
+--- Remove a directory if it is empty.
+-- Does not return errors (for example, if directory is not empty or
+-- if already does not exist)
+-- @param d string: pathname of directory to remove.
+function fs_lua.remove_dir_if_empty(d)
+ assert(d)
+ d = dir.normalize(d)
+ lfs.rmdir(d)
+end
+
+--- Remove a directory if it is empty.
+-- Does not return errors (for example, if directory is not empty or
+-- if already does not exist)
+-- @param d string: pathname of directory to remove.
+function fs_lua.remove_dir_tree_if_empty(d)
+ assert(d)
+ d = dir.normalize(d)
+ for i=1,10 do
+ lfs.rmdir(d)
+ d = dir.dir_name(d)
+ end
+end
+
+local function are_the_same_file(f1, f2)
+ if f1 == f2 then
+ return true
+ end
+ if cfg.is_platform("unix") then
+ local i1 = lfs.attributes(f1, "ino")
+ local i2 = lfs.attributes(f2, "ino")
+ if i1 ~= nil and i1 == i2 then
+ return true
+ end
+ end
+ return false
+end
+
+--- Copy a file.
+-- @param src string: Pathname of source
+-- @param dest string: Pathname of destination
+-- @param perms string ("read" or "exec") or nil: Permissions for destination
+-- file or nil to use the source file permissions
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message.
+function fs_lua.copy(src, dest, perms)
+ assert(src and dest)
+ src = dir.normalize(src)
+ dest = dir.normalize(dest)
+ local destmode = lfs.attributes(dest, "mode")
+ if destmode == "directory" then
+ dest = dir.path(dest, dir.base_name(src))
+ end
+ if are_the_same_file(src, dest) then
+ return nil, "The source and destination are the same files"
+ end
+ local src_h, err = io.open(src, "rb")
+ if not src_h then return nil, err end
+ local dest_h, err = io.open(dest, "w+b")
+ if not dest_h then src_h:close() return nil, err end
+ while true do
+ local block = src_h:read(8192)
+ if not block then break end
+ local ok, err = dest_h:write(block)
+ if not ok then return nil, err end
+ end
+ src_h:close()
+ dest_h:close()
+
+ local fullattrs
+ if not perms then
+ fullattrs = lfs.attributes(src, "permissions")
+ end
+ if fullattrs and posix_ok then
+ return posix.chmod(dest, fullattrs)
+ else
+ if cfg.is_platform("unix") then
+ if not perms then
+ perms = fullattrs:match("x") and "exec" or "read"
+ end
+ return fs.set_permissions(dest, perms, "all")
+ else
+ return true
+ end
+ end
+end
+
+--- Implementation function for recursive copy of directory contents.
+-- Assumes paths are normalized.
+-- @param src string: Pathname of source
+-- @param dest string: Pathname of destination
+-- @param perms string ("read" or "exec") or nil: Optional permissions.
+-- If not given, permissions of the source are copied over to the destination.
+-- @return boolean or (boolean, string): true on success, false on failure
+local function recursive_copy(src, dest, perms)
+ local srcmode = lfs.attributes(src, "mode")
+
+ if srcmode == "file" then
+ local ok = fs.copy(src, dest, perms)
+ if not ok then return false end
+ elseif srcmode == "directory" then
+ local subdir = dir.path(dest, dir.base_name(src))
+ local ok, err = fs.make_dir(subdir)
+ if not ok then return nil, err end
+ if pcall(lfs.dir, src) == false then
+ return false
+ end
+ for file in lfs.dir(src) do
+ if file ~= "." and file ~= ".." then
+ local ok = recursive_copy(dir.path(src, file), subdir, perms)
+ if not ok then return false end
+ end
+ end
+ end
+ return true
+end
+
+--- Recursively copy the contents of a directory.
+-- @param src string: Pathname of source
+-- @param dest string: Pathname of destination
+-- @param perms string ("read" or "exec") or nil: Optional permissions.
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message.
+function fs_lua.copy_contents(src, dest, perms)
+ assert(src)
+ assert(dest)
+ src = dir.normalize(src)
+ dest = dir.normalize(dest)
+ if not fs.is_dir(src) then
+ return false, src .. " is not a directory"
+ end
+ if pcall(lfs.dir, src) == false then
+ return false, "Permission denied"
+ end
+ for file in lfs.dir(src) do
+ if file ~= "." and file ~= ".." then
+ local ok = recursive_copy(dir.path(src, file), dest, perms)
+ if not ok then
+ return false, "Failed copying "..src.." to "..dest
+ end
+ end
+ end
+ return true
+end
+
+--- Implementation function for recursive removal of directories.
+-- Assumes paths are normalized.
+-- @param name string: Pathname of file
+-- @return boolean or (boolean, string): true on success,
+-- or nil and an error message on failure.
+local function recursive_delete(name)
+ local ok = os.remove(name)
+ if ok then return true end
+ local pok, ok, err = pcall(function()
+ for file in lfs.dir(name) do
+ if file ~= "." and file ~= ".." then
+ local ok, err = recursive_delete(dir.path(name, file))
+ if not ok then return nil, err end
+ end
+ end
+ local ok, err = lfs.rmdir(name)
+ return ok, (not ok) and err
+ end)
+ if pok then
+ return ok, err
+ else
+ return pok, ok
+ end
+end
+
+--- Delete a file or a directory and all its contents.
+-- @param name string: Pathname of source
+-- @return nil
+function fs_lua.delete(name)
+ name = dir.normalize(name)
+ recursive_delete(name)
+end
+
+--- Internal implementation function for fs.dir.
+-- Yields a filename on each iteration.
+-- @param at string: directory to list
+-- @return nil or (nil and string): an error message on failure
+function fs_lua.dir_iterator(at)
+ local pok, iter, arg = pcall(lfs.dir, at)
+ if not pok then
+ return nil, iter
+ end
+ for file in iter, arg do
+ if file ~= "." and file ~= ".." then
+ coroutine.yield(file)
+ end
+ end
+end
+
+--- Implementation function for recursive find.
+-- Assumes paths are normalized.
+-- @param cwd string: Current working directory in recursion.
+-- @param prefix string: Auxiliary prefix string to form pathname.
+-- @param result table: Array of strings where results are collected.
+local function recursive_find(cwd, prefix, result)
+ local pok, iter, arg = pcall(lfs.dir, cwd)
+ if not pok then
+ return nil
+ end
+ for file in iter, arg do
+ if file ~= "." and file ~= ".." then
+ local item = prefix .. file
+ table.insert(result, item)
+ local pathname = dir.path(cwd, file)
+ if lfs.attributes(pathname, "mode") == "directory" then
+ recursive_find(pathname, item .. dir_sep, result)
+ end
+ end
+ end
+end
+
+--- Recursively scan the contents of a directory.
+-- @param at string or nil: directory to scan (will be the current
+-- directory if none is given).
+-- @return table: an array of strings with the filenames representing
+-- the contents of a directory.
+function fs_lua.find(at)
+ assert(type(at) == "string" or not at)
+ if not at then
+ at = fs.current_dir()
+ end
+ at = dir.normalize(at)
+ local result = {}
+ recursive_find(at, "", result)
+ return result
+end
+
+--- Test for existence of a file.
+-- @param file string: filename to test
+-- @return boolean: true if file exists, false otherwise.
+function fs_lua.exists(file)
+ assert(file)
+ file = dir.normalize(file)
+ return type(lfs.attributes(file)) == "table"
+end
+
+--- Test is pathname is a directory.
+-- @param file string: pathname to test
+-- @return boolean: true if it is a directory, false otherwise.
+function fs_lua.is_dir(file)
+ assert(file)
+ file = dir.normalize(file)
+ return lfs.attributes(file, "mode") == "directory"
+end
+
+--- Test is pathname is a regular file.
+-- @param file string: pathname to test
+-- @return boolean: true if it is a file, false otherwise.
+function fs_lua.is_file(file)
+ assert(file)
+ file = dir.normalize(file)
+ return lfs.attributes(file, "mode") == "file"
+end
+
+-- Set access and modification times for a file.
+-- @param filename File to set access and modification times for.
+-- @param time may be a number containing the format returned
+-- by os.time, or a table ready to be processed via os.time; if
+-- nil, current time is assumed.
+function fs_lua.set_time(file, time)
+ assert(time == nil or type(time) == "table" or type(time) == "number")
+ file = dir.normalize(file)
+ if type(time) == "table" then
+ time = os.time(time)
+ end
+ return lfs.touch(file, time)
+end
+
+else -- if not lfs_ok
+
+function fs_lua.exists(file)
+ assert(file)
+ -- check if file exists by attempting to open it
+ return util.exists(fs.absolute_name(file))
+end
+
+function fs_lua.file_age(_)
+ return math.huge
+end
+
+end
+
+---------------------------------------------------------------------
+-- lua-bz2 functions
+---------------------------------------------------------------------
+
+if bz2_ok then
+
+local function bunzip2_string(data)
+ local decompressor = bz2.initDecompress()
+ local output, err = decompressor:update(data)
+ if not output then
+ return nil, err
+ end
+ decompressor:close()
+ return output
+end
+
+--- Uncompresses a .bz2 file.
+-- @param infile string: pathname of .bz2 file to be extracted.
+-- @param outfile string or nil: pathname of output file to be produced.
+-- If not given, name is derived from input file.
+-- @return boolean: true on success; nil and error message on failure.
+function fs_lua.bunzip2(infile, outfile)
+ assert(type(infile) == "string")
+ assert(outfile == nil or type(outfile) == "string")
+ if not outfile then
+ outfile = infile:gsub("%.bz2$", "")
+ end
+
+ return fs.filter_file(bunzip2_string, infile, outfile)
+end
+
+end
+
+---------------------------------------------------------------------
+-- luarocks.tools.zip functions
+---------------------------------------------------------------------
+
+if zip_ok then
+
+function fs_lua.zip(zipfile, ...)
+ return zip.zip(zipfile, ...)
+end
+
+function fs_lua.unzip(zipfile)
+ return zip.unzip(zipfile)
+end
+
+function fs_lua.gunzip(infile, outfile)
+ return zip.gunzip(infile, outfile)
+end
+
+end
+
+---------------------------------------------------------------------
+-- LuaSocket functions
+---------------------------------------------------------------------
+
+if socket_ok then
+
+local ltn12 = require("ltn12")
+local luasec_ok, https = pcall(require, "ssl.https")
+
+if luasec_ok and not vers.compare_versions(https._VERSION, "1.0.3") then
+ luasec_ok = false
+ https = nil
+end
+
+local redirect_protocols = {
+ http = http,
+ https = luasec_ok and https,
+}
+
+local function request(url, method, http, loop_control) -- luacheck: ignore 431
+ local result = {}
+
+ if cfg.verbose then
+ print(method, url)
+ end
+
+ local proxy = os.getenv("http_proxy")
+ if type(proxy) ~= "string" then proxy = nil end
+ -- LuaSocket's http.request crashes when given URLs missing the scheme part.
+ if proxy and not proxy:find("://") then
+ proxy = "http://" .. proxy
+ end
+
+ if cfg.show_downloads then
+ io.write(method.." "..url.." ...\n")
+ end
+ local dots = 0
+ if cfg.connection_timeout and cfg.connection_timeout > 0 then
+ http.TIMEOUT = cfg.connection_timeout
+ end
+ local res, status, headers, err = http.request {
+ url = url,
+ proxy = proxy,
+ method = method,
+ redirect = false,
+ sink = ltn12.sink.table(result),
+ step = cfg.show_downloads and function(...)
+ io.write(".")
+ io.flush()
+ dots = dots + 1
+ if dots == 70 then
+ io.write("\n")
+ dots = 0
+ end
+ return ltn12.pump.step(...)
+ end,
+ headers = {
+ ["user-agent"] = cfg.user_agent.." via LuaSocket"
+ },
+ }
+ if cfg.show_downloads then
+ io.write("\n")
+ end
+ if not res then
+ return nil, status
+ elseif status == 301 or status == 302 then
+ local location = headers.location
+ if location then
+ local protocol, rest = dir.split_url(location)
+ if redirect_protocols[protocol] then
+ if not loop_control then
+ loop_control = {}
+ elseif loop_control[location] then
+ return nil, "Redirection loop -- broken URL?"
+ end
+ loop_control[url] = true
+ return request(location, method, redirect_protocols[protocol], loop_control)
+ else
+ return nil, "URL redirected to unsupported protocol - install luasec >= 1.1 to get HTTPS support.", "https"
+ end
+ end
+ return nil, err
+ elseif status ~= 200 then
+ return nil, err
+ else
+ return result, status, headers, err
+ end
+end
+
+local function write_timestamp(filename, data)
+ local fd = io.open(filename, "w")
+ if fd then
+ fd:write(data)
+ fd:close()
+ end
+end
+
+local function read_timestamp(filename)
+ local fd = io.open(filename, "r")
+ if fd then
+ local data = fd:read("*a")
+ fd:close()
+ return data
+ end
+end
+
+local function fail_with_status(filename, status, headers)
+ write_timestamp(filename .. ".unixtime", os.time())
+ write_timestamp(filename .. ".status", status)
+ return nil, status, headers
+end
+
+-- @param url string: URL to fetch.
+-- @param filename string: local filename of the file to fetch.
+-- @param http table: The library to use (http from LuaSocket or LuaSec)
+-- @param cache boolean: Whether to use a `.timestamp` file to check
+-- via the HTTP Last-Modified header if the full download is needed.
+-- @return (boolean | (nil, string, string?)): True if successful, or
+-- nil, error message and optionally HTTPS error in case of errors.
+local function http_request(url, filename, http, cache) -- luacheck: ignore 431
+ if cache then
+ local status = read_timestamp(filename..".status")
+ local timestamp = read_timestamp(filename..".timestamp")
+ if status or timestamp then
+ local unixtime = read_timestamp(filename..".unixtime")
+ if tonumber(unixtime) then
+ local diff = os.time() - tonumber(unixtime)
+ if status then
+ if diff < cfg.cache_fail_timeout then
+ return nil, status, {}
+ end
+ else
+ if diff < cfg.cache_timeout then
+ return true, nil, nil, true
+ end
+ end
+ end
+
+ local result, status, headers, err = request(url, "HEAD", http) -- luacheck: ignore 421
+ if not result then
+ return fail_with_status(filename, status, headers)
+ end
+ if status == 200 and headers["last-modified"] == timestamp then
+ write_timestamp(filename .. ".unixtime", os.time())
+ return true, nil, nil, true
+ end
+ end
+ end
+ local result, status, headers, err = request(url, "GET", http)
+ if not result then
+ if status then
+ return fail_with_status(filename, status, headers)
+ end
+ end
+ if cache and headers["last-modified"] then
+ write_timestamp(filename .. ".timestamp", headers["last-modified"])
+ write_timestamp(filename .. ".unixtime", os.time())
+ end
+ local file = io.open(filename, "wb")
+ if not file then return nil, 0, {} end
+ for _, data in ipairs(result) do
+ file:write(data)
+ end
+ file:close()
+ return true
+end
+
+local function ftp_request(url, filename)
+ local content, err = ftp.get(url)
+ if not content then
+ return false, err
+ end
+ local file = io.open(filename, "wb")
+ if not file then return false, err end
+ file:write(content)
+ file:close()
+ return true
+end
+
+local downloader_warning = false
+
+--- Download a remote file.
+-- @param url string: URL to be fetched.
+-- @param filename string or nil: this function attempts to detect the
+-- resulting local filename of the remote file as the basename of the URL;
+-- if that is not correct (due to a redirection, for example), the local
+-- filename can be given explicitly as this second argument.
+-- @return (boolean, string, boolean):
+-- In case of success:
+-- * true
+-- * a string with the filename
+-- * true if the file was retrieved from local cache
+-- In case of failure:
+-- * false
+-- * error message
+function fs_lua.download(url, filename, cache)
+ assert(type(url) == "string")
+ assert(type(filename) == "string" or not filename)
+
+ filename = fs.absolute_name(filename or dir.base_name(url))
+
+ -- delegate to the configured downloader so we don't have to deal with whitelists
+ if os.getenv("no_proxy") then
+ return fs.use_downloader(url, filename, cache)
+ end
+
+ local ok, err, https_err, from_cache
+ if util.starts_with(url, "http:") then
+ ok, err, https_err, from_cache = http_request(url, filename, http, cache)
+ elseif util.starts_with(url, "ftp:") then
+ ok, err = ftp_request(url, filename)
+ elseif util.starts_with(url, "https:") then
+ -- skip LuaSec when proxy is enabled since it is not supported
+ if luasec_ok and not os.getenv("https_proxy") then
+ local _
+ ok, err, _, from_cache = http_request(url, filename, https, cache)
+ else
+ https_err = true
+ end
+ else
+ err = "Unsupported protocol"
+ end
+ if https_err then
+ local downloader, err = fs.which_tool("downloader")
+ if not downloader then
+ return nil, err
+ end
+ if not downloader_warning then
+ util.warning("falling back to "..downloader.." - install luasec >= 1.1 to get native HTTPS support")
+ downloader_warning = true
+ end
+ return fs.use_downloader(url, filename, cache)
+ elseif not ok then
+ return nil, err, "network"
+ end
+ return true, filename, from_cache
+end
+
+else --...if socket_ok == false then
+
+function fs_lua.download(url, filename, cache)
+ return fs.use_downloader(url, filename, cache)
+end
+
+end
+---------------------------------------------------------------------
+-- MD5 functions
+---------------------------------------------------------------------
+
+if md5_ok then
+
+-- Support the interface of lmd5 by lhf in addition to md5 by Roberto
+-- and the keplerproject.
+if not md5.sumhexa and md5.digest then
+ md5.sumhexa = function(msg)
+ return md5.digest(msg)
+ end
+end
+
+if md5.sumhexa then
+
+--- Get the MD5 checksum for a file.
+-- @param file string: The file to be computed.
+-- @return string: The MD5 checksum or nil + error
+function fs_lua.get_md5(file)
+ file = fs.absolute_name(file)
+ local file_handler = io.open(file, "rb")
+ if not file_handler then return nil, "Failed to open file for reading: "..file end
+ local computed = md5.sumhexa(file_handler:read("*a"))
+ file_handler:close()
+ if computed then return computed end
+ return nil, "Failed to compute MD5 hash for file "..file
+end
+
+end
+end
+
+---------------------------------------------------------------------
+-- POSIX functions
+---------------------------------------------------------------------
+
+function fs_lua._unix_rwx_to_number(rwx, neg)
+ local num = 0
+ neg = neg or false
+ for i = 1, 9 do
+ local c = rwx:sub(10 - i, 10 - i) == "-"
+ if neg == c then
+ num = num + 2^(i-1)
+ end
+ end
+ return math.floor(num)
+end
+
+if posix_ok then
+
+local octal_to_rwx = {
+ ["0"] = "---",
+ ["1"] = "--x",
+ ["2"] = "-w-",
+ ["3"] = "-wx",
+ ["4"] = "r--",
+ ["5"] = "r-x",
+ ["6"] = "rw-",
+ ["7"] = "rwx",
+}
+
+do
+ local umask_cache
+ function fs_lua._unix_umask()
+ if umask_cache then
+ return umask_cache
+ end
+ -- LuaPosix (as of 34.0.4) only returns the umask as rwx
+ local rwx = posix.umask()
+ local num = fs_lua._unix_rwx_to_number(rwx, true)
+ umask_cache = ("%03o"):format(num)
+ return umask_cache
+ end
+end
+
+function fs_lua.set_permissions(filename, mode, scope)
+ local perms, err = fs._unix_mode_scope_to_perms(mode, scope)
+ if err then
+ return false, err
+ end
+
+ -- LuaPosix (as of 5.1.15) does not support octal notation...
+ local new_perms = {}
+ for c in perms:sub(-3):gmatch(".") do
+ table.insert(new_perms, octal_to_rwx[c])
+ end
+ perms = table.concat(new_perms)
+ local err = posix.chmod(filename, perms)
+ return err == 0
+end
+
+function fs_lua.current_user()
+ return posix.getpwuid(posix.geteuid()).pw_name
+end
+
+function fs_lua.is_superuser()
+ return posix.geteuid() == 0
+end
+
+-- This call is not available on all systems, see #677
+if posix.mkdtemp then
+
+--- Create a temporary directory.
+-- @param name_pattern string: name pattern to use for avoiding conflicts
+-- when creating temporary directory.
+-- @return string or (nil, string): name of temporary directory or (nil, error message) on failure.
+function fs_lua.make_temp_dir(name_pattern)
+ assert(type(name_pattern) == "string")
+
+ return posix.mkdtemp(temp_dir_pattern(name_pattern) .. "-XXXXXX")
+end
+
+end -- if posix.mkdtemp
+
+end
+
+---------------------------------------------------------------------
+-- Other functions
+---------------------------------------------------------------------
+
+if not fs_lua.make_temp_dir then
+
+function fs_lua.make_temp_dir(name_pattern)
+ assert(type(name_pattern) == "string")
+
+ local ok, err
+ for _ = 1, 3 do
+ local name = temp_dir_pattern(name_pattern) .. tostring(math.random(10000000))
+ ok, err = fs.make_dir(name)
+ if ok then
+ return name
+ end
+ end
+
+ return nil, err
+end
+
+end
+
+--- Apply a patch.
+-- @param patchname string: The filename of the patch.
+-- @param patchdata string or nil: The actual patch as a string.
+-- @param create_delete boolean: Support creating and deleting files in a patch.
+function fs_lua.apply_patch(patchname, patchdata, create_delete)
+ local p, all_ok = patch.read_patch(patchname, patchdata)
+ if not all_ok then
+ return nil, "Failed reading patch "..patchname
+ end
+ if p then
+ return patch.apply_patch(p, 1, create_delete)
+ end
+end
+
+--- Move a file.
+-- @param src string: Pathname of source
+-- @param dest string: Pathname of destination
+-- @param perms string ("read" or "exec") or nil: Permissions for destination
+-- file or nil to use the source file permissions.
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message.
+function fs_lua.move(src, dest, perms)
+ assert(src and dest)
+ if fs.exists(dest) and not fs.is_dir(dest) then
+ return false, "File already exists: "..dest
+ end
+ local ok, err = fs.copy(src, dest, perms)
+ if not ok then
+ return false, err
+ end
+ fs.delete(src)
+ if fs.exists(src) then
+ return false, "Failed move: could not delete "..src.." after copy."
+ end
+ return true
+end
+
+local function get_local_tree()
+ for _, tree in ipairs(cfg.rocks_trees) do
+ if type(tree) == "table" and tree.name == "user" then
+ return fs.absolute_name(tree.root)
+ end
+ end
+end
+
+local function is_local_tree_in_env(local_tree)
+ local lua_path
+ if _VERSION == "Lua 5.1" then
+ lua_path = os.getenv("LUA_PATH")
+ else
+ lua_path = os.getenv("LUA_PATH_" .. _VERSION:sub(5):gsub("%.", "_"))
+ or os.getenv("LUA_PATH")
+ end
+ if lua_path and lua_path:match(local_tree, 1, true) then
+ return true
+ end
+end
+
+--- Check if user has write permissions for the command.
+-- Assumes the configuration variables under cfg have been previously set up.
+-- @param args table: the args table passed to run() drivers.
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message.
+function fs_lua.check_command_permissions(args)
+ local ok = true
+ local err = ""
+ if args._command_permissions_checked then
+ return true
+ end
+ for _, directory in ipairs { cfg.rocks_dir, cfg.deploy_lua_dir, cfg.deploy_bin_dir, cfg.deploy_lua_dir } do
+ if fs.exists(directory) then
+ if not fs.is_writable(directory) then
+ ok = false
+ err = "Your user does not have write permissions in " .. directory
+ break
+ end
+ else
+ local root = fs.root_of(directory)
+ local parent = directory
+ repeat
+ parent = dir.dir_name(parent)
+ if parent == "" then
+ parent = root
+ end
+ until parent == root or fs.exists(parent)
+ if not fs.is_writable(parent) then
+ ok = false
+ err = directory.." does not exist\nand your user does not have write permissions in " .. parent
+ break
+ end
+ end
+ end
+ if ok then
+ args._command_permissions_checked = true
+ return true
+ else
+ if args["local"] or cfg.local_by_default then
+ err = err .. "\n\nPlease check your permissions.\n"
+ else
+ local local_tree = get_local_tree()
+ if local_tree then
+ err = err .. "\n\nYou may want to run as a privileged user,"
+ .. "\nor use --local to install into your local tree at " .. local_tree
+ .. "\nor run 'luarocks config local_by_default true' to make --local the default.\n"
+
+ if not is_local_tree_in_env(local_tree) then
+ err = err .. "\n(You may need to configure your Lua package paths\nto use the local tree, see 'luarocks path --help')\n"
+ end
+ else
+ err = err .. "\n\nYou may want to run as a privileged user.\n"
+ end
+ end
+ return nil, err
+ end
+end
+
+--- Check whether a file is a Lua script
+-- When the file can be successfully compiled by the configured
+-- Lua interpreter, it's considered to be a valid Lua file.
+-- @param filename filename of file to check
+-- @return boolean true, if it is a Lua script, false otherwise
+function fs_lua.is_lua(filename)
+ filename = filename:gsub([[%\]],"/") -- normalize on fw slash to prevent escaping issues
+ local lua = fs.Q(cfg.variables.LUA) -- get lua interpreter configured
+ -- execute on configured interpreter, might not be the same as the interpreter LR is run on
+ local result = fs.execute_string(lua..[[ -e "if loadfile(']]..filename..[[') then os.exit(0) else os.exit(1) end"]])
+ return (result == true)
+end
+
+--- Unpack an archive.
+-- Extract the contents of an archive, detecting its format by
+-- filename extension.
+-- @param archive string: Filename of archive.
+-- @return boolean or (boolean, string): true on success, false and an error message on failure.
+function fs_lua.unpack_archive(archive)
+ assert(type(archive) == "string")
+
+ local ok, err
+ archive = fs.absolute_name(archive)
+ if archive:match("%.tar%.gz$") then
+ local tar_filename = archive:gsub("%.gz$", "")
+ ok, err = fs.gunzip(archive, tar_filename)
+ if ok then
+ ok, err = tar.untar(tar_filename, ".")
+ end
+ elseif archive:match("%.tgz$") then
+ local tar_filename = archive:gsub("%.tgz$", ".tar")
+ ok, err = fs.gunzip(archive, tar_filename)
+ if ok then
+ ok, err = tar.untar(tar_filename, ".")
+ end
+ elseif archive:match("%.tar%.bz2$") then
+ local tar_filename = archive:gsub("%.bz2$", "")
+ ok, err = fs.bunzip2(archive, tar_filename)
+ if ok then
+ ok, err = tar.untar(tar_filename, ".")
+ end
+ elseif archive:match("%.zip$") then
+ ok, err = fs.unzip(archive)
+ elseif archive:match("%.lua$") or archive:match("%.c$") then
+ -- Ignore .lua and .c files; they don't need to be extracted.
+ return true
+ else
+ return false, "Couldn't extract archive "..archive..": unrecognized filename extension"
+ end
+ if not ok then
+ return false, "Failed extracting "..archive..": "..err
+ end
+ return true
+end
+
+return fs_lua
diff --git a/src/luarocks/fs/macosx.lua b/src/luarocks/fs/macosx.lua
new file mode 100644
index 0000000..b71e7f1
--- /dev/null
+++ b/src/luarocks/fs/macosx.lua
@@ -0,0 +1,50 @@
+--- macOS-specific implementation of filesystem and platform abstractions.
+local macosx = {}
+
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+
+function macosx.is_dir(file)
+ file = fs.absolute_name(file)
+ file = dir.normalize(file) .. "/."
+ local fd, _, code = io.open(file, "r")
+ if code == 2 then -- "No such file or directory"
+ return false
+ end
+ if code == 20 then -- "Not a directory", regardless of permissions
+ return false
+ end
+ if code == 13 then -- "Permission denied", but is a directory
+ return true
+ end
+ if fd then
+ local _, _, ecode = fd:read(1)
+ fd:close()
+ if ecode == 21 then -- "Is a directory"
+ return true
+ end
+ end
+ return false
+end
+
+function macosx.is_file(file)
+ file = fs.absolute_name(file)
+ if fs.is_dir(file) then
+ return false
+ end
+ file = dir.normalize(file)
+ local fd, _, code = io.open(file, "r")
+ if code == 2 then -- "No such file or directory"
+ return false
+ end
+ if code == 13 then -- "Permission denied", but it exists
+ return true
+ end
+ if fd then
+ fd:close()
+ return true
+ end
+ return false
+end
+
+return macosx
diff --git a/src/luarocks/fs/tools.lua b/src/luarocks/fs/tools.lua
new file mode 100644
index 0000000..23f2561
--- /dev/null
+++ b/src/luarocks/fs/tools.lua
@@ -0,0 +1,222 @@
+
+--- Common fs operations implemented with third-party tools.
+local tools = {}
+
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+local cfg = require("luarocks.core.cfg")
+
+local vars = setmetatable({}, { __index = function(_,k) return cfg.variables[k] end })
+
+local dir_stack = {}
+
+do
+ local tool_cache = {}
+
+ local tool_options = {
+ downloader = {
+ desc = "downloader",
+ { var = "WGET", name = "wget" },
+ { var = "CURL", name = "curl" },
+ },
+ md5checker = {
+ desc = "MD5 checker",
+ { var = "MD5SUM", name = "md5sum" },
+ { var = "OPENSSL", name = "openssl", cmdarg = "md5" },
+ { var = "MD5", name = "md5" },
+ },
+ }
+
+ function tools.which_tool(tooltype)
+ local tool = tool_cache[tooltype]
+ local names = {}
+ if not tool then
+ for _, opt in ipairs(tool_options[tooltype]) do
+ table.insert(names, opt.name)
+ if fs.is_tool_available(vars[opt.var], opt.name) then
+ tool = opt
+ tool_cache[tooltype] = opt
+ break
+ end
+ end
+ end
+ if not tool then
+ local tool_names = table.concat(names, ", ", 1, #names - 1) .. " or " .. names[#names]
+ return nil, "no " .. tool_options[tooltype].desc .. " tool available," .. " please install " .. tool_names .. " in your system"
+ end
+ return tool.name, vars[tool.var] .. (tool.cmdarg and " "..tool.cmdarg or "")
+ end
+end
+
+local current_dir_with_cache
+do
+ local cache_pwd
+
+ current_dir_with_cache = function()
+ local current = cache_pwd
+ if not current then
+ local pipe = io.popen(fs.quiet_stderr(vars.PWD))
+ current = pipe:read("*a"):gsub("^%s*", ""):gsub("%s*$", "")
+ pipe:close()
+ cache_pwd = current
+ end
+ for _, directory in ipairs(dir_stack) do
+ current = fs.absolute_name(directory, current)
+ end
+ return current, cache_pwd
+ end
+
+ --- Obtain current directory.
+ -- Uses the module's internal directory stack.
+ -- @return string: the absolute pathname of the current directory.
+ function tools.current_dir()
+ return (current_dir_with_cache()) -- drop second return
+ end
+end
+
+--- Change the current directory.
+-- Uses the module's internal directory stack. This does not have exact
+-- semantics of chdir, as it does not handle errors the same way,
+-- but works well for our purposes for now.
+-- @param directory string: The directory to switch to.
+-- @return boolean or (nil, string): true if successful, (nil, error message) if failed.
+function tools.change_dir(directory)
+ assert(type(directory) == "string")
+ if fs.is_dir(directory) then
+ table.insert(dir_stack, directory)
+ return true
+ end
+ return nil, "directory not found: "..directory
+end
+
+--- Change directory to root.
+-- Allows leaving a directory (e.g. for deleting it) in
+-- a crossplatform way.
+function tools.change_dir_to_root()
+ local curr_dir = fs.current_dir()
+ if not curr_dir or not fs.is_dir(curr_dir) then
+ return false
+ end
+ table.insert(dir_stack, "/")
+ return true
+end
+
+--- Change working directory to the previous in the directory stack.
+function tools.pop_dir()
+ local directory = table.remove(dir_stack)
+ return directory ~= nil
+end
+
+--- Run the given command.
+-- The command is executed in the current directory in the directory stack.
+-- @param cmd string: No quoting/escaping is applied to the command.
+-- @return boolean: true if command succeeds (status code 0), false
+-- otherwise.
+function tools.execute_string(cmd)
+ local current, cache_pwd = current_dir_with_cache()
+ if not current then return false end
+ if current ~= cache_pwd then
+ cmd = fs.command_at(current, cmd)
+ end
+ local code = os.execute(cmd)
+ if code == 0 or code == true then
+ return true
+ else
+ return false
+ end
+end
+
+--- Internal implementation function for fs.dir.
+-- Yields a filename on each iteration.
+-- @param at string: directory to list
+-- @return nil
+function tools.dir_iterator(at)
+ local pipe = io.popen(fs.command_at(at, vars.LS, true))
+ for file in pipe:lines() do
+ if file ~= "." and file ~= ".." then
+ coroutine.yield(file)
+ end
+ end
+ pipe:close()
+end
+
+--- Download a remote file.
+-- @param url string: URL to be fetched.
+-- @param filename string or nil: this function attempts to detect the
+-- resulting local filename of the remote file as the basename of the URL;
+-- if that is not correct (due to a redirection, for example), the local
+-- filename can be given explicitly as this second argument.
+-- @param cache boolean: compare remote timestamps via HTTP HEAD prior to
+-- re-downloading the file.
+-- @return (boolean, string, string): true and the filename on success,
+-- false and the error message and code on failure.
+function tools.use_downloader(url, filename, cache)
+ assert(type(url) == "string")
+ assert(type(filename) == "string" or not filename)
+
+ filename = fs.absolute_name(filename or dir.base_name(url))
+
+ local downloader, err = fs.which_tool("downloader")
+ if not downloader then
+ return nil, err, "downloader"
+ end
+
+ local ok = false
+ if downloader == "wget" then
+ local wget_cmd = vars.WGET.." "..vars.WGETNOCERTFLAG.." --no-cache --user-agent=\""..cfg.user_agent.." via wget\" --quiet "
+ if cfg.connection_timeout and cfg.connection_timeout > 0 then
+ wget_cmd = wget_cmd .. "--timeout="..tostring(cfg.connection_timeout).." --tries=1 "
+ end
+ if cache then
+ -- --timestamping is incompatible with --output-document,
+ -- but that's not a problem for our use cases.
+ fs.delete(filename .. ".unixtime")
+ fs.change_dir(dir.dir_name(filename))
+ ok = fs.execute_quiet(wget_cmd.." --timestamping ", url)
+ fs.pop_dir()
+ elseif filename then
+ ok = fs.execute_quiet(wget_cmd.." --output-document ", filename, url)
+ else
+ ok = fs.execute_quiet(wget_cmd, url)
+ end
+ elseif downloader == "curl" then
+ local curl_cmd = vars.CURL.." "..vars.CURLNOCERTFLAG.." -f -L --user-agent \""..cfg.user_agent.." via curl\" "
+ if cfg.connection_timeout and cfg.connection_timeout > 0 then
+ curl_cmd = curl_cmd .. "--connect-timeout "..tostring(cfg.connection_timeout).." "
+ end
+ if cache then
+ curl_cmd = curl_cmd .. " -R -z \"" .. filename .. "\" "
+ end
+ ok = fs.execute_string(fs.quiet_stderr(curl_cmd..fs.Q(url).." --output "..fs.Q(filename)))
+ end
+ if ok then
+ return true, filename
+ else
+ os.remove(filename)
+ return false, "failed downloading " .. url, "network"
+ end
+end
+
+--- Get the MD5 checksum for a file.
+-- @param file string: The file to be computed.
+-- @return string: The MD5 checksum or nil + message
+function tools.get_md5(file)
+ local ok, md5checker = fs.which_tool("md5checker")
+ if not ok then
+ return false, md5checker
+ end
+
+ local pipe = io.popen(md5checker.." "..fs.Q(fs.absolute_name(file)))
+ local computed = pipe:read("*l")
+ pipe:close()
+ if computed then
+ computed = computed:match("("..("%x"):rep(32)..")")
+ end
+ if computed then
+ return computed
+ else
+ return nil, "Failed to compute MD5 hash for file "..tostring(fs.absolute_name(file))
+ end
+end
+
+return tools
diff --git a/src/luarocks/fs/unix.lua b/src/luarocks/fs/unix.lua
new file mode 100644
index 0000000..41a9ba8
--- /dev/null
+++ b/src/luarocks/fs/unix.lua
@@ -0,0 +1,266 @@
+
+--- Unix implementation of filesystem and platform abstractions.
+local unix = {}
+
+local fs = require("luarocks.fs")
+
+local cfg = require("luarocks.core.cfg")
+local dir = require("luarocks.dir")
+local path = require("luarocks.path")
+local util = require("luarocks.util")
+
+--- Annotate command string for quiet execution.
+-- @param cmd string: A command-line string.
+-- @return string: The command-line, with silencing annotation.
+function unix.quiet(cmd)
+ return cmd.." 1> /dev/null 2> /dev/null"
+end
+
+--- Annotate command string for execution with quiet stderr.
+-- @param cmd string: A command-line string.
+-- @return string: The command-line, with stderr silencing annotation.
+function unix.quiet_stderr(cmd)
+ return cmd.." 2> /dev/null"
+end
+
+--- Quote argument for shell processing.
+-- Adds single quotes and escapes.
+-- @param arg string: Unquoted argument.
+-- @return string: Quoted argument.
+function unix.Q(arg)
+ assert(type(arg) == "string")
+ return "'" .. arg:gsub("'", "'\\''") .. "'"
+end
+
+--- Return an absolute pathname from a potentially relative one.
+-- @param pathname string: pathname to convert.
+-- @param relative_to string or nil: path to prepend when making
+-- pathname absolute, or the current dir in the dir stack if
+-- not given.
+-- @return string: The pathname converted to absolute.
+function unix.absolute_name(pathname, relative_to)
+ assert(type(pathname) == "string")
+ assert(type(relative_to) == "string" or not relative_to)
+
+ local unquoted = pathname:match("^['\"](.*)['\"]$")
+ if unquoted then
+ pathname = unquoted
+ end
+
+ relative_to = relative_to or fs.current_dir()
+ if pathname:sub(1,1) == "/" then
+ return dir.normalize(pathname)
+ else
+ return dir.path(relative_to, pathname)
+ end
+end
+
+--- Return the root directory for the given path.
+-- In Unix, root is always "/".
+-- @param pathname string: pathname to use.
+-- @return string: The root of the given pathname.
+function unix.root_of(_)
+ return "/"
+end
+
+--- Create a wrapper to make a script executable from the command-line.
+-- @param script string: Pathname of script to be made executable.
+-- @param target string: wrapper target pathname (without wrapper suffix).
+-- @param name string: rock name to be used in loader context.
+-- @param version string: rock version to be used in loader context.
+-- @return boolean or (nil, string): True if succeeded, or nil and
+-- an error message.
+function unix.wrap_script(script, target, deps_mode, name, version, ...)
+ assert(type(script) == "string" or not script)
+ assert(type(target) == "string")
+ assert(type(deps_mode) == "string")
+ assert(type(name) == "string" or not name)
+ assert(type(version) == "string" or not version)
+
+ local wrapper = io.open(target, "w")
+ if not wrapper then
+ return nil, "Could not open "..target.." for writing."
+ end
+
+ local lpath, lcpath = path.package_paths(deps_mode)
+
+ local luainit = {
+ "package.path="..util.LQ(lpath..";").."..package.path",
+ "package.cpath="..util.LQ(lcpath..";").."..package.cpath",
+ }
+
+ local remove_interpreter = false
+ local base = dir.base_name(target):gsub("%..*$", "")
+ if base == "luarocks" or base == "luarocks-admin" then
+ if cfg.is_binary then
+ remove_interpreter = true
+ end
+ luainit = {
+ "package.path="..util.LQ(package.path),
+ "package.cpath="..util.LQ(package.cpath),
+ }
+ end
+
+ if name and version then
+ local addctx = "local k,l,_=pcall(require,"..util.LQ("luarocks.loader")..") _=k " ..
+ "and l.add_context("..util.LQ(name)..","..util.LQ(version)..")"
+ table.insert(luainit, addctx)
+ end
+
+ local argv = {
+ fs.Q(cfg.variables["LUA"]),
+ "-e",
+ fs.Q(table.concat(luainit, ";")),
+ script and fs.Q(script) or [[$([ "$*" ] || echo -i)]],
+ ...
+ }
+ if remove_interpreter then
+ table.remove(argv, 1)
+ table.remove(argv, 1)
+ table.remove(argv, 1)
+ end
+
+ wrapper:write("#!/bin/sh\n\n")
+ wrapper:write("LUAROCKS_SYSCONFDIR="..fs.Q(cfg.sysconfdir) .. " ")
+ wrapper:write("exec "..table.concat(argv, " ")..' "$@"\n')
+ wrapper:close()
+
+ if fs.set_permissions(target, "exec", "all") then
+ return true
+ else
+ return nil, "Could not make "..target.." executable."
+ end
+end
+
+--- Check if a file (typically inside path.bin_dir) is an actual binary
+-- or a Lua wrapper.
+-- @param filename string: the file name with full path.
+-- @return boolean: returns true if file is an actual binary
+-- (or if it couldn't check) or false if it is a Lua wrapper.
+function unix.is_actual_binary(filename)
+ if filename:match("%.lua$") then
+ return false
+ end
+ local file = io.open(filename)
+ if not file then
+ return true
+ end
+ local first = file:read(2)
+ file:close()
+ if not first then
+ util.warning("could not read "..filename)
+ return true
+ end
+ return first ~= "#!"
+end
+
+function unix.copy_binary(filename, dest)
+ return fs.copy(filename, dest, "exec")
+end
+
+--- Move a file on top of the other.
+-- The new file ceases to exist under its original name,
+-- and takes over the name of the old file.
+-- On Unix this is done through a single rename operation.
+-- @param old_file The name of the original file,
+-- which will be the new name of new_file.
+-- @param new_file The name of the new file,
+-- which will replace old_file.
+-- @return boolean or (nil, string): True if succeeded, or nil and
+-- an error message.
+function unix.replace_file(old_file, new_file)
+ return os.rename(new_file, old_file)
+end
+
+function unix.tmpname()
+ return os.tmpname()
+end
+
+function unix.export_cmd(var, val)
+ return ("export %s='%s'"):format(var, val)
+end
+
+local octal_to_rwx = {
+ ["0"] = "---",
+ ["1"] = "--x",
+ ["2"] = "-w-",
+ ["3"] = "-wx",
+ ["4"] = "r--",
+ ["5"] = "r-x",
+ ["6"] = "rw-",
+ ["7"] = "rwx",
+}
+local rwx_to_octal = {}
+for octal, rwx in pairs(octal_to_rwx) do
+ rwx_to_octal[rwx] = octal
+end
+--- Moderate the given permissions based on the local umask
+-- @param perms string: permissions to moderate
+-- @return string: the moderated permissions
+local function apply_umask(perms)
+ local umask = fs._unix_umask()
+
+ local moderated_perms = ""
+ for i = 1, 3 do
+ local p_rwx = octal_to_rwx[perms:sub(i, i)]
+ local u_rwx = octal_to_rwx[umask:sub(i, i)]
+ local new_perm = ""
+ for j = 1, 3 do
+ local p_val = p_rwx:sub(j, j)
+ local u_val = u_rwx:sub(j, j)
+ if p_val == u_val then
+ new_perm = new_perm .. "-"
+ else
+ new_perm = new_perm .. p_val
+ end
+ end
+ moderated_perms = moderated_perms .. rwx_to_octal[new_perm]
+ end
+ return moderated_perms
+end
+
+function unix._unix_mode_scope_to_perms(mode, scope)
+ local perms
+ if mode == "read" and scope == "user" then
+ perms = apply_umask("600")
+ elseif mode == "exec" and scope == "user" then
+ perms = apply_umask("700")
+ elseif mode == "read" and scope == "all" then
+ perms = apply_umask("666")
+ elseif mode == "exec" and scope == "all" then
+ perms = apply_umask("777")
+ else
+ return false, "Invalid permission " .. mode .. " for " .. scope
+ end
+ return perms
+end
+
+function unix.system_cache_dir()
+ if fs.is_dir("/var/cache") then
+ return "/var/cache"
+ end
+ return dir.path(fs.system_temp_dir(), "cache")
+end
+
+function unix.search_in_path(program)
+ if program:match("/") then
+ local fd = io.open(dir.path(program), "r")
+ if fd then
+ fd:close()
+ return true, program
+ end
+
+ return false
+ end
+
+ for d in (os.getenv("PATH") or ""):gmatch("([^:]+)") do
+ local fd = io.open(dir.path(d, program), "r")
+ if fd then
+ fd:close()
+ return true, d
+ end
+ end
+ return false
+end
+
+return unix
diff --git a/src/luarocks/fs/unix/tools.lua b/src/luarocks/fs/unix/tools.lua
new file mode 100644
index 0000000..d733473
--- /dev/null
+++ b/src/luarocks/fs/unix/tools.lua
@@ -0,0 +1,353 @@
+
+--- fs operations implemented with third-party tools for Unix platform abstractions.
+local tools = {}
+
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+local cfg = require("luarocks.core.cfg")
+
+local vars = setmetatable({}, { __index = function(_,k) return cfg.variables[k] end })
+
+--- Adds prefix to command to make it run from a directory.
+-- @param directory string: Path to a directory.
+-- @param cmd string: A command-line string.
+-- @return string: The command-line with prefix.
+function tools.command_at(directory, cmd)
+ return "cd " .. fs.Q(fs.absolute_name(directory)) .. " && " .. cmd
+end
+
+--- Create a directory if it does not already exist.
+-- If any of the higher levels in the path name does not exist
+-- too, they are created as well.
+-- @param directory string: pathname of directory to create.
+-- @return boolean: true on success, false on failure.
+function tools.make_dir(directory)
+ assert(directory)
+ local ok, err = fs.execute(vars.MKDIR.." -p", directory)
+ if not ok then
+ err = "failed making directory "..directory
+ end
+ return ok, err
+end
+
+--- Remove a directory if it is empty.
+-- Does not return errors (for example, if directory is not empty or
+-- if already does not exist)
+-- @param directory string: pathname of directory to remove.
+function tools.remove_dir_if_empty(directory)
+ assert(directory)
+ fs.execute_quiet(vars.RMDIR, directory)
+end
+
+--- Remove a directory if it is empty.
+-- Does not return errors (for example, if directory is not empty or
+-- if already does not exist)
+-- @param directory string: pathname of directory to remove.
+function tools.remove_dir_tree_if_empty(directory)
+ assert(directory)
+ fs.execute_quiet(vars.RMDIR, "-p", directory)
+end
+
+--- Copy a file.
+-- @param src string: Pathname of source
+-- @param dest string: Pathname of destination
+-- @param perm string ("read" or "exec") or nil: Permissions for destination
+-- file or nil to use the source permissions
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message.
+function tools.copy(src, dest, perm)
+ assert(src and dest)
+ if fs.execute(vars.CP, src, dest) then
+ if perm then
+ if fs.is_dir(dest) then
+ dest = dir.path(dest, dir.base_name(src))
+ end
+ if fs.set_permissions(dest, perm, "all") then
+ return true
+ else
+ return false, "Failed setting permissions of "..dest
+ end
+ end
+ return true
+ else
+ return false, "Failed copying "..src.." to "..dest
+ end
+end
+
+--- Recursively copy the contents of a directory.
+-- @param src string: Pathname of source
+-- @param dest string: Pathname of destination
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message.
+function tools.copy_contents(src, dest)
+ assert(src and dest)
+ if not fs.is_dir(src) then
+ return false, src .. " is not a directory"
+ end
+ if fs.make_dir(dest) and fs.execute_quiet(vars.CP.." -pPR "..fs.Q(src).."/* "..fs.Q(dest)) then
+ return true
+ else
+ return false, "Failed copying "..src.." to "..dest
+ end
+end
+--- Delete a file or a directory and all its contents.
+-- For safety, this only accepts absolute paths.
+-- @param arg string: Pathname of source
+-- @return nil
+function tools.delete(arg)
+ assert(arg)
+ assert(arg:sub(1,1) == "/")
+ fs.execute_quiet(vars.RM, "-rf", arg)
+end
+
+--- Recursively scan the contents of a directory.
+-- @param at string or nil: directory to scan (will be the current
+-- directory if none is given).
+-- @return table: an array of strings with the filenames representing
+-- the contents of a directory.
+function tools.find(at)
+ assert(type(at) == "string" or not at)
+ if not at then
+ at = fs.current_dir()
+ end
+ if not fs.is_dir(at) then
+ return {}
+ end
+ local result = {}
+ local pipe = io.popen(fs.command_at(at, fs.quiet_stderr(vars.FIND.." *")))
+ for file in pipe:lines() do
+ table.insert(result, file)
+ end
+ pipe:close()
+ return result
+end
+
+--- Compress files in a .zip archive.
+-- @param zipfile string: pathname of .zip archive to be created.
+-- @param ... Filenames to be stored in the archive are given as
+-- additional arguments.
+-- @return boolean: true on success, nil and error message on failure.
+function tools.zip(zipfile, ...)
+ local ok, err = fs.is_tool_available(vars.ZIP, "zip")
+ if not ok then
+ return nil, err
+ end
+ if fs.execute_quiet(vars.ZIP.." -r", zipfile, ...) then
+ return true
+ else
+ return nil, "failed compressing " .. zipfile
+ end
+end
+
+--- Uncompress files from a .zip archive.
+-- @param zipfile string: pathname of .zip archive to be extracted.
+-- @return boolean: true on success, nil and error message on failure.
+function tools.unzip(zipfile)
+ assert(zipfile)
+ local ok, err = fs.is_tool_available(vars.UNZIP, "unzip")
+ if not ok then
+ return nil, err
+ end
+ if fs.execute_quiet(vars.UNZIP, zipfile) then
+ return true
+ else
+ return nil, "failed extracting " .. zipfile
+ end
+end
+
+local function uncompress(default_ext, program, infile, outfile)
+ assert(type(infile) == "string")
+ assert(outfile == nil or type(outfile) == "string")
+ if not outfile then
+ outfile = infile:gsub("%."..default_ext.."$", "")
+ end
+ if fs.execute(fs.Q(program).." -c "..fs.Q(infile).." > "..fs.Q(outfile)) then
+ return true
+ else
+ return nil, "failed extracting " .. infile
+ end
+end
+
+--- Uncompresses a .gz file.
+-- @param infile string: pathname of .gz file to be extracted.
+-- @param outfile string or nil: pathname of output file to be produced.
+-- If not given, name is derived from input file.
+-- @return boolean: true on success; nil and error message on failure.
+function tools.gunzip(infile, outfile)
+ return uncompress("gz", "gunzip", infile, outfile)
+end
+
+--- Uncompresses a .bz2 file.
+-- @param infile string: pathname of .bz2 file to be extracted.
+-- @param outfile string or nil: pathname of output file to be produced.
+-- If not given, name is derived from input file.
+-- @return boolean: true on success; nil and error message on failure.
+function tools.bunzip2(infile, outfile)
+ return uncompress("bz2", "bunzip2", infile, outfile)
+end
+
+do
+ local function rwx_to_octal(rwx)
+ return (rwx:match "r" and 4 or 0)
+ + (rwx:match "w" and 2 or 0)
+ + (rwx:match "x" and 1 or 0)
+ end
+ local umask_cache
+ function tools._unix_umask()
+ if umask_cache then
+ return umask_cache
+ end
+ local fd = assert(io.popen("umask -S"))
+ local umask = assert(fd:read("*a"))
+ fd:close()
+ local u, g, o = umask:match("u=([rwx]*),g=([rwx]*),o=([rwx]*)")
+ if not u then
+ error("invalid umask result")
+ end
+ umask_cache = string.format("%d%d%d",
+ 7 - rwx_to_octal(u),
+ 7 - rwx_to_octal(g),
+ 7 - rwx_to_octal(o))
+ return umask_cache
+ end
+end
+
+--- Set permissions for file or directory
+-- @param filename string: filename whose permissions are to be modified
+-- @param mode string ("read" or "exec"): permissions to set
+-- @param scope string ("user" or "all"): the user(s) to whom the permission applies
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message
+function tools.set_permissions(filename, mode, scope)
+ assert(filename and mode and scope)
+
+ local perms, err = fs._unix_mode_scope_to_perms(mode, scope)
+ if err then
+ return false, err
+ end
+
+ return fs.execute(vars.CHMOD, perms, filename)
+end
+
+function tools.browser(url)
+ return fs.execute(cfg.web_browser, url)
+end
+
+-- Set access and modification times for a file.
+-- @param filename File to set access and modification times for.
+-- @param time may be a string or number containing the format returned
+-- by os.time, or a table ready to be processed via os.time; if
+-- nil, current time is assumed.
+function tools.set_time(file, time)
+ assert(time == nil or type(time) == "table" or type(time) == "number")
+ file = dir.normalize(file)
+ local flag = ""
+ if type(time) == "number" then
+ time = os.date("*t", time)
+ end
+ if type(time) == "table" then
+ flag = ("-t %04d%02d%02d%02d%02d.%02d"):format(time.year, time.month, time.day, time.hour, time.min, time.sec)
+ end
+ return fs.execute(vars.TOUCH .. " " .. flag, file)
+end
+
+--- Create a temporary directory.
+-- @param name_pattern string: name pattern to use for avoiding conflicts
+-- when creating temporary directory.
+-- @return string or (nil, string): name of temporary directory or (nil, error message) on failure.
+function tools.make_temp_dir(name_pattern)
+ assert(type(name_pattern) == "string")
+ name_pattern = dir.normalize(name_pattern)
+
+ local template = (os.getenv("TMPDIR") or "/tmp") .. "/luarocks_" .. name_pattern:gsub("/", "_") .. "-XXXXXX"
+ local pipe = io.popen(vars.MKTEMP.." -d "..fs.Q(template))
+ local dirname = pipe:read("*l")
+ pipe:close()
+ if dirname and dirname:match("^/") then
+ return dirname
+ end
+ return nil, "Failed to create temporary directory "..tostring(dirname)
+end
+
+--- Test is file/directory exists
+-- @param file string: filename to test
+-- @return boolean: true if file exists, false otherwise.
+function tools.exists(file)
+ assert(file)
+ return fs.execute(vars.TEST, "-e", file)
+end
+
+--- Test is pathname is a directory.
+-- @param file string: pathname to test
+-- @return boolean: true if it is a directory, false otherwise.
+function tools.is_dir(file)
+ assert(file)
+ return fs.execute(vars.TEST, "-d", file)
+end
+
+--- Test is pathname is a regular file.
+-- @param file string: pathname to test
+-- @return boolean: true if it is a regular file, false otherwise.
+function tools.is_file(file)
+ assert(file)
+ return fs.execute(vars.TEST, "-f", file)
+end
+
+function tools.current_user()
+ local user = os.getenv("USER")
+ if user then
+ return user
+ end
+ local pd = io.popen("whoami", "r")
+ if not pd then
+ return ""
+ end
+ user = pd:read("*l")
+ pd:close()
+ return user
+end
+
+function tools.is_superuser()
+ return fs.current_user() == "root"
+end
+
+function tools.lock_access(dirname, force)
+ local ok, err = fs.make_dir(dirname)
+ if not ok then
+ return nil, err
+ end
+
+ local tempfile = dir.path(dirname, ".lock.tmp." .. tostring(math.random(100000000)))
+
+ local fd, fderr = io.open(tempfile, "w")
+ if not fd then
+ return nil, "failed opening temp file " .. tempfile .. " for locking: " .. fderr
+ end
+
+ local ok, werr = fd:write("lock file for " .. dirname)
+ if not ok then
+ return nil, "failed writing temp file " .. tempfile .. " for locking: " .. werr
+ end
+
+ fd:close()
+
+ local lockfile = dir.path(dirname, "lockfile.lfs")
+
+ local force_flag = force and " -f" or ""
+
+ if fs.execute(vars.LN .. force_flag, tempfile, lockfile) then
+ return {
+ tempfile = tempfile,
+ lockfile = lockfile,
+ }
+ else
+ return nil, "File exists" -- same message as luafilesystem
+ end
+end
+
+function tools.unlock_access(lock)
+ os.remove(lock.lockfile)
+ os.remove(lock.tempfile)
+end
+
+return tools
diff --git a/src/luarocks/fs/win32.lua b/src/luarocks/fs/win32.lua
new file mode 100644
index 0000000..bba6873
--- /dev/null
+++ b/src/luarocks/fs/win32.lua
@@ -0,0 +1,384 @@
+--- Windows implementation of filesystem and platform abstractions.
+-- Download http://unxutils.sourceforge.net/ for Windows GNU utilities
+-- used by this module.
+local win32 = {}
+
+local fs = require("luarocks.fs")
+
+local cfg = require("luarocks.core.cfg")
+local dir = require("luarocks.dir")
+local path = require("luarocks.path")
+local util = require("luarocks.util")
+
+-- Monkey patch io.popen and os.execute to make sure quoting
+-- works as expected.
+-- See http://lua-users.org/lists/lua-l/2013-11/msg00367.html
+local _prefix = "type NUL && "
+local _popen, _execute = io.popen, os.execute
+
+-- luacheck: push globals io os
+io.popen = function(cmd, ...) return _popen(_prefix..cmd, ...) end
+os.execute = function(cmd, ...) return _execute(_prefix..cmd, ...) end
+-- luacheck: pop
+
+--- Annotate command string for quiet execution.
+-- @param cmd string: A command-line string.
+-- @return string: The command-line, with silencing annotation.
+function win32.quiet(cmd)
+ return cmd.." 2> NUL 1> NUL"
+end
+
+--- Annotate command string for execution with quiet stderr.
+-- @param cmd string: A command-line string.
+-- @return string: The command-line, with stderr silencing annotation.
+function win32.quiet_stderr(cmd)
+ return cmd.." 2> NUL"
+end
+
+function win32.execute_env(env, command, ...)
+ assert(type(command) == "string")
+ local cmdstr = {}
+ for var, val in pairs(env) do
+ table.insert(cmdstr, fs.export_cmd(var, val))
+ end
+ table.insert(cmdstr, fs.quote_args(command, ...))
+ return fs.execute_string(table.concat(cmdstr, " & "))
+end
+
+-- Split path into drive, root and the rest.
+-- Example: "c:\\hello\\world" becomes "c:" "\\" "hello\\world"
+-- if any part is missing from input, it becomes an empty string.
+local function split_root(pathname)
+ local drive = ""
+ local root = ""
+ local rest
+
+ local unquoted = pathname:match("^['\"](.*)['\"]$")
+ if unquoted then
+ pathname = unquoted
+ end
+
+ if pathname:match("^.:") then
+ drive = pathname:sub(1, 2)
+ pathname = pathname:sub(3)
+ end
+
+ if pathname:match("^[\\/]") then
+ root = pathname:sub(1, 1)
+ rest = pathname:sub(2)
+ else
+ rest = pathname
+ end
+
+ return drive, root, rest
+end
+
+--- Quote argument for shell processing. Fixes paths on Windows.
+-- Adds double quotes and escapes.
+-- @param arg string: Unquoted argument.
+-- @return string: Quoted argument.
+function win32.Q(arg)
+ assert(type(arg) == "string")
+ -- Use Windows-specific directory separator for paths.
+ -- Paths should be converted to absolute by now.
+ local drive, root, rest = split_root(arg)
+ if root ~= "" then
+ arg = arg:gsub("/", "\\")
+ end
+ if arg == "\\" then
+ return '\\' -- CHDIR needs special handling for root dir
+ end
+ -- URLs and anything else
+ arg = arg:gsub('\\(\\*)"', '\\%1%1"')
+ arg = arg:gsub('\\+$', '%0%0')
+ arg = arg:gsub('"', '\\"')
+ arg = arg:gsub('(\\*)%%', '%1%1"%%"')
+ return '"' .. arg .. '"'
+end
+
+--- Quote argument for shell processing in batch files.
+-- Adds double quotes and escapes.
+-- @param arg string: Unquoted argument.
+-- @return string: Quoted argument.
+function win32.Qb(arg)
+ assert(type(arg) == "string")
+ -- Use Windows-specific directory separator for paths.
+ -- Paths should be converted to absolute by now.
+ local drive, root, rest = split_root(arg)
+ if root ~= "" then
+ arg = arg:gsub("/", "\\")
+ end
+ if arg == "\\" then
+ return '\\' -- CHDIR needs special handling for root dir
+ end
+ -- URLs and anything else
+ arg = arg:gsub('\\(\\*)"', '\\%1%1"')
+ arg = arg:gsub('\\+$', '%0%0')
+ arg = arg:gsub('"', '\\"')
+ arg = arg:gsub('%%', '%%%%')
+ return '"' .. arg .. '"'
+end
+
+--- Return an absolute pathname from a potentially relative one.
+-- @param pathname string: pathname to convert.
+-- @param relative_to string or nil: path to prepend when making
+-- pathname absolute, or the current dir in the dir stack if
+-- not given.
+-- @return string: The pathname converted to absolute.
+function win32.absolute_name(pathname, relative_to)
+ assert(type(pathname) == "string")
+ assert(type(relative_to) == "string" or not relative_to)
+
+ relative_to = (relative_to or fs.current_dir()):gsub("[\\/]*$", "")
+ local drive, root, rest = split_root(pathname)
+ if root:match("[\\/]$") then
+ -- It's an absolute path already. Ensure is not quoted.
+ return dir.normalize(drive .. root .. rest)
+ else
+ -- It's a relative path, join it with base path.
+ -- This drops drive letter from paths like "C:foo".
+ return dir.path(relative_to, rest)
+ end
+end
+
+--- Return the root directory for the given path.
+-- For example, for "c:\hello", returns "c:\"
+-- @param pathname string: pathname to use.
+-- @return string: The root of the given pathname.
+function win32.root_of(pathname)
+ local drive, root, rest = split_root(fs.absolute_name(pathname))
+ return drive .. root
+end
+
+--- Create a wrapper to make a script executable from the command-line.
+-- @param script string: Pathname of script to be made executable.
+-- @param target string: wrapper target pathname (without wrapper suffix).
+-- @param name string: rock name to be used in loader context.
+-- @param version string: rock version to be used in loader context.
+-- @return boolean or (nil, string): True if succeeded, or nil and
+-- an error message.
+function win32.wrap_script(script, target, deps_mode, name, version, ...)
+ assert(type(script) == "string" or not script)
+ assert(type(target) == "string")
+ assert(type(deps_mode) == "string")
+ assert(type(name) == "string" or not name)
+ assert(type(version) == "string" or not version)
+
+ local wrapper = io.open(target, "wb")
+ if not wrapper then
+ return nil, "Could not open "..target.." for writing."
+ end
+
+ local lpath, lcpath = path.package_paths(deps_mode)
+
+ local luainit = {
+ "package.path="..util.LQ(lpath..";").."..package.path",
+ "package.cpath="..util.LQ(lcpath..";").."..package.cpath",
+ }
+
+ local remove_interpreter = false
+ local base = dir.base_name(target):gsub("%..*$", "")
+ if base == "luarocks" or base == "luarocks-admin" then
+ if cfg.is_binary then
+ remove_interpreter = true
+ end
+ luainit = {
+ "package.path="..util.LQ(package.path),
+ "package.cpath="..util.LQ(package.cpath),
+ }
+ end
+
+ if name and version then
+ local addctx = "local k,l,_=pcall(require,'luarocks.loader') _=k " ..
+ "and l.add_context('"..name.."','"..version.."')"
+ table.insert(luainit, addctx)
+ end
+
+ local argv = {
+ fs.Qb(cfg.variables["LUA"]),
+ "-e",
+ fs.Qb(table.concat(luainit, ";")),
+ script and fs.Qb(script) or "%I%",
+ ...
+ }
+ if remove_interpreter then
+ table.remove(argv, 1)
+ table.remove(argv, 1)
+ table.remove(argv, 1)
+ end
+
+ wrapper:write("@echo off\r\n")
+ wrapper:write("setlocal\r\n")
+ if not script then
+ wrapper:write([[IF "%*"=="" (set I=-i) ELSE (set I=)]] .. "\r\n")
+ end
+ wrapper:write("set "..fs.Qb("LUAROCKS_SYSCONFDIR="..cfg.sysconfdir) .. "\r\n")
+ wrapper:write(table.concat(argv, " ") .. " %*\r\n")
+ wrapper:write("exit /b %ERRORLEVEL%\r\n")
+ wrapper:close()
+ return true
+end
+
+function win32.is_actual_binary(name)
+ name = name:lower()
+ if name:match("%.bat$") or name:match("%.exe$") then
+ return true
+ end
+ return false
+end
+
+function win32.copy_binary(filename, dest)
+ local ok, err = fs.copy(filename, dest)
+ if not ok then
+ return nil, err
+ end
+ local exe_pattern = "%.[Ee][Xx][Ee]$"
+ local base = dir.base_name(filename)
+ dest = dir.dir_name(dest)
+ if base:match(exe_pattern) then
+ base = base:gsub(exe_pattern, ".lua")
+ local helpname = dest.."\\"..base
+ local helper = io.open(helpname, "w")
+ if not helper then
+ return nil, "Could not open "..helpname.." for writing."
+ end
+ helper:write('package.path=\"'..package.path:gsub("\\","\\\\")..';\"..package.path\n')
+ helper:write('package.cpath=\"'..package.path:gsub("\\","\\\\")..';\"..package.cpath\n')
+ helper:close()
+ end
+ return true
+end
+
+--- Move a file on top of the other.
+-- The new file ceases to exist under its original name,
+-- and takes over the name of the old file.
+-- On Windows this is done by removing the original file and
+-- renaming the new file to its original name.
+-- @param old_file The name of the original file,
+-- which will be the new name of new_file.
+-- @param new_file The name of the new file,
+-- which will replace old_file.
+-- @return boolean or (nil, string): True if succeeded, or nil and
+-- an error message.
+function win32.replace_file(old_file, new_file)
+ os.remove(old_file)
+ return os.rename(new_file, old_file)
+end
+
+function win32.is_dir(file)
+ file = fs.absolute_name(file)
+ file = dir.normalize(file)
+ local fd, _, code = io.open(file, "r")
+ if code == 13 then -- directories return "Permission denied"
+ fd, _, code = io.open(file .. "\\", "r")
+ if code == 2 then -- directories return 2, files return 22
+ return true
+ end
+ end
+ if fd then
+ fd:close()
+ end
+ return false
+end
+
+function win32.is_file(file)
+ file = fs.absolute_name(file)
+ file = dir.normalize(file)
+ local fd, _, code = io.open(file, "r")
+ if code == 13 then -- if "Permission denied"
+ fd, _, code = io.open(file .. "\\", "r")
+ if code == 2 then -- directories return 2, files return 22
+ return false
+ elseif code == 22 then
+ return true
+ end
+ end
+ if fd then
+ fd:close()
+ return true
+ end
+ return false
+end
+
+--- Test is file/dir is writable.
+-- Warning: testing if a file/dir is writable does not guarantee
+-- that it will remain writable and therefore it is no replacement
+-- for checking the result of subsequent operations.
+-- @param file string: filename to test
+-- @return boolean: true if file exists, false otherwise.
+function win32.is_writable(file)
+ assert(file)
+ file = dir.normalize(file)
+ local result
+ local tmpname = 'tmpluarockstestwritable.deleteme'
+ if fs.is_dir(file) then
+ local file2 = dir.path(file, tmpname)
+ local fh = io.open(file2, 'wb')
+ result = fh ~= nil
+ if fh then fh:close() end
+ if result then
+ -- the above test might give a false positive when writing to
+ -- c:\program files\ because of VirtualStore redirection on Vista and up
+ -- So check whether it's really there
+ result = fs.exists(file2)
+ end
+ os.remove(file2)
+ else
+ local fh = io.open(file, 'r+b')
+ result = fh ~= nil
+ if fh then fh:close() end
+ end
+ return result
+end
+
+function win32.tmpname()
+ local name = os.tmpname()
+ local tmp = os.getenv("TMP")
+ if tmp and name:sub(1, #tmp) ~= tmp then
+ name = (tmp .. "\\" .. name):gsub("\\+", "\\")
+ end
+ return name
+end
+
+function win32.current_user()
+ return os.getenv("USERNAME")
+end
+
+function win32.is_superuser()
+ return false
+end
+
+function win32.export_cmd(var, val)
+ return ("SET %s"):format(fs.Q(var.."="..val))
+end
+
+function win32.system_cache_dir()
+ return dir.path(fs.system_temp_dir(), "cache")
+end
+
+function win32.search_in_path(program)
+ if program:match("\\") then
+ local fd = io.open(dir.path(program), "r")
+ if fd then
+ fd:close()
+ return true, program
+ end
+
+ return false
+ end
+
+ if not program:lower():match("exe$") then
+ program = program .. ".exe"
+ end
+
+ for d in (os.getenv("PATH") or ""):gmatch("([^;]+)") do
+ local fd = io.open(dir.path(d, program), "r")
+ if fd then
+ fd:close()
+ return true, d
+ end
+ end
+ return false
+end
+
+return win32
diff --git a/src/luarocks/fs/win32/tools.lua b/src/luarocks/fs/win32/tools.lua
new file mode 100644
index 0000000..86cbb45
--- /dev/null
+++ b/src/luarocks/fs/win32/tools.lua
@@ -0,0 +1,330 @@
+
+--- fs operations implemented with third-party tools for Windows platform abstractions.
+-- Download http://unxutils.sourceforge.net/ for Windows GNU utilities
+-- used by this module.
+local tools = {}
+
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+local cfg = require("luarocks.core.cfg")
+
+local vars = setmetatable({}, { __index = function(_,k) return cfg.variables[k] end })
+
+local dir_sep = package.config:sub(1, 1)
+
+--- Adds prefix to command to make it run from a directory.
+-- @param directory string: Path to a directory.
+-- @param cmd string: A command-line string.
+-- @param exit_on_error bool: Exits immediately if entering the directory failed.
+-- @return string: The command-line with prefix.
+function tools.command_at(directory, cmd, exit_on_error)
+ local drive = directory:match("^([A-Za-z]:)")
+ local op = " & "
+ if exit_on_error then
+ op = " && "
+ end
+ local cmd_prefixed = "cd " .. fs.Q(directory) .. op .. cmd
+ if drive then
+ cmd_prefixed = drive .. " & " .. cmd_prefixed
+ end
+ return cmd_prefixed
+end
+
+--- Create a directory if it does not already exist.
+-- If any of the higher levels in the path name does not exist
+-- too, they are created as well.
+-- @param directory string: pathname of directory to create.
+-- @return boolean: true on success, false on failure.
+function tools.make_dir(directory)
+ assert(directory)
+ directory = dir.normalize(directory)
+ fs.execute_quiet(vars.MKDIR, directory)
+ if not fs.is_dir(directory) then
+ return false, "failed making directory "..directory
+ end
+ return true
+end
+
+--- Remove a directory if it is empty.
+-- Does not return errors (for example, if directory is not empty or
+-- if already does not exist)
+-- @param directory string: pathname of directory to remove.
+function tools.remove_dir_if_empty(directory)
+ assert(directory)
+ fs.execute_quiet(vars.RMDIR, directory)
+end
+
+--- Remove a directory if it is empty.
+-- Does not return errors (for example, if directory is not empty or
+-- if already does not exist)
+-- @param directory string: pathname of directory to remove.
+function tools.remove_dir_tree_if_empty(directory)
+ assert(directory)
+ while true do
+ fs.execute_quiet(vars.RMDIR, directory)
+ local parent = dir.dir_name(directory)
+ if parent ~= directory then
+ directory = parent
+ else
+ break
+ end
+ end
+end
+
+--- Copy a file.
+-- @param src string: Pathname of source
+-- @param dest string: Pathname of destination
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message.
+function tools.copy(src, dest)
+ assert(src and dest)
+ if dest:match("[/\\]$") then dest = dest:sub(1, -2) end
+ local ok = fs.execute(vars.CP, src, dest)
+ if ok then
+ return true
+ else
+ return false, "Failed copying "..src.." to "..dest
+ end
+end
+
+--- Recursively copy the contents of a directory.
+-- @param src string: Pathname of source
+-- @param dest string: Pathname of destination
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message.
+function tools.copy_contents(src, dest)
+ assert(src and dest)
+ if not fs.is_dir(src) then
+ return false, src .. " is not a directory"
+ end
+ if fs.make_dir(dest) and fs.execute_quiet(vars.CP, "-dR", src.."\\*.*", dest) then
+ return true
+ else
+ return false, "Failed copying "..src.." to "..dest
+ end
+end
+
+--- Delete a file or a directory and all its contents.
+-- For safety, this only accepts absolute paths.
+-- @param arg string: Pathname of source
+-- @return nil
+function tools.delete(arg)
+ assert(arg)
+ assert(arg:match("^[a-zA-Z]?:?[\\/]"))
+ fs.execute_quiet("if exist "..fs.Q(arg.."\\*").." ( RMDIR /S /Q "..fs.Q(arg).." ) else ( DEL /Q /F "..fs.Q(arg).." )")
+end
+
+--- Recursively scan the contents of a directory.
+-- @param at string or nil: directory to scan (will be the current
+-- directory if none is given).
+-- @return table: an array of strings with the filenames representing
+-- the contents of a directory. Paths are returned with forward slashes.
+function tools.find(at)
+ assert(type(at) == "string" or not at)
+ if not at then
+ at = fs.current_dir()
+ end
+ if not fs.is_dir(at) then
+ return {}
+ end
+ local result = {}
+ local pipe = io.popen(fs.command_at(at, fs.quiet_stderr(vars.FIND), true))
+ for file in pipe:lines() do
+ -- Windows find is a bit different
+ local first_two = file:sub(1,2)
+ if first_two == ".\\" or first_two == "./" then file=file:sub(3) end
+ if file ~= "." then
+ table.insert(result, (file:gsub("[\\/]", dir_sep)))
+ end
+ end
+ pipe:close()
+ return result
+end
+
+--- Compress files in a .zip archive.
+-- @param zipfile string: pathname of .zip archive to be created.
+-- @param ... Filenames to be stored in the archive are given as
+-- additional arguments.
+-- @return boolean: true on success, nil and error message on failure.
+function tools.zip(zipfile, ...)
+ if fs.execute_quiet(vars.SEVENZ.." -aoa a -tzip", zipfile, ...) then
+ return true
+ else
+ return nil, "failed compressing " .. zipfile
+ end
+end
+
+--- Uncompress files from a .zip archive.
+-- @param zipfile string: pathname of .zip archive to be extracted.
+-- @return boolean: true on success, nil and error message on failure.
+function tools.unzip(zipfile)
+ assert(zipfile)
+ if fs.execute_quiet(vars.SEVENZ.." -aoa x", zipfile) then
+ return true
+ else
+ return nil, "failed extracting " .. zipfile
+ end
+end
+
+local function sevenz(default_ext, infile, outfile)
+ assert(type(infile) == "string")
+ assert(outfile == nil or type(outfile) == "string")
+
+ local dropext = infile:gsub("%."..default_ext.."$", "")
+ local outdir = dir.dir_name(dropext)
+
+ infile = fs.absolute_name(infile)
+
+ local cmdline = vars.SEVENZ.." -aoa -t* -o"..fs.Q(outdir).." x "..fs.Q(infile)
+ local ok, err = fs.execute_quiet(cmdline)
+ if not ok then
+ return nil, "failed extracting " .. infile
+ end
+
+ if outfile then
+ outfile = fs.absolute_name(outfile)
+ dropext = fs.absolute_name(dropext)
+ ok, err = os.rename(dropext, outfile)
+ if not ok then
+ return nil, "failed creating new file " .. outfile
+ end
+ end
+
+ return true
+end
+
+--- Uncompresses a .gz file.
+-- @param infile string: pathname of .gz file to be extracted.
+-- @param outfile string or nil: pathname of output file to be produced.
+-- If not given, name is derived from input file.
+-- @return boolean: true on success; nil and error message on failure.
+function tools.gunzip(infile, outfile)
+ return sevenz("gz", infile, outfile)
+end
+
+--- Uncompresses a .bz2 file.
+-- @param infile string: pathname of .bz2 file to be extracted.
+-- @param outfile string or nil: pathname of output file to be produced.
+-- If not given, name is derived from input file.
+-- @return boolean: true on success; nil and error message on failure.
+function tools.bunzip2(infile, outfile)
+ return sevenz("bz2", infile, outfile)
+end
+
+--- Helper function for fs.set_permissions
+-- @return table: an array of all system users
+local function get_system_users()
+ local exclude = {
+ [""] = true,
+ ["Name"] = true,
+ ["\128\164\172\168\173\168\225\226\224\160\226\174\224"] = true, -- Administrator in cp866
+ ["Administrator"] = true,
+ }
+ local result = {}
+ local fd = assert(io.popen("wmic UserAccount get name"))
+ for user in fd:lines() do
+ user = user:gsub("%s+$", "")
+ if not exclude[user] then
+ table.insert(result, user)
+ end
+ end
+ return result
+end
+
+--- Set permissions for file or directory
+-- @param filename string: filename whose permissions are to be modified
+-- @param mode string ("read" or "exec"): permission to set
+-- @param scope string ("user" or "all"): the user(s) to whom the permission applies
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message
+function tools.set_permissions(filename, mode, scope)
+ assert(filename and mode and scope)
+
+ if scope == "user" then
+ local perms
+ if mode == "read" then
+ perms = "(R,W,M)"
+ elseif mode == "exec" then
+ perms = "(F)"
+ end
+
+ local ok
+ -- Take ownership of the given file
+ ok = fs.execute_quiet("takeown /f " .. fs.Q(filename))
+ if not ok then
+ return false, "Could not take ownership of the given file"
+ end
+ local username = os.getenv('USERNAME')
+ -- Grant the current user the proper rights
+ ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /inheritance:d /grant:r " .. fs.Q(username) .. ":" .. perms)
+ if not ok then
+ return false, "Failed setting permission " .. mode .. " for " .. scope
+ end
+ -- Finally, remove all the other users from the ACL in order to deny them access to the file
+ for _, user in pairs(get_system_users()) do
+ if username ~= user then
+ local ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /remove " .. fs.Q(user))
+ if not ok then
+ return false, "Failed setting permission " .. mode .. " for " .. scope
+ end
+ end
+ end
+ elseif scope == "all" then
+ local my_perms, others_perms
+ if mode == "read" then
+ my_perms = "(R,W,M)"
+ others_perms = "(R)"
+ elseif mode == "exec" then
+ my_perms = "(F)"
+ others_perms = "(RX)"
+ end
+
+ local ok
+ -- Grant permissions available to all users
+ ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /inheritance:d /grant:r *S-1-1-0:" .. others_perms)
+ if not ok then
+ return false, "Failed setting permission " .. mode .. " for " .. scope
+ end
+
+ -- Grant permissions available only to the current user
+ ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /inheritance:d /grant \"%USERNAME%\":" .. my_perms)
+
+ -- This may not be necessary if the above syntax is correct,
+ -- but I couldn't really test the extra quotes above, so if that
+ -- fails we try again with the syntax used in previous releases
+ -- just to be on the safe side
+ if not ok then
+ ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /inheritance:d /grant %USERNAME%:" .. my_perms)
+ end
+
+ if not ok then
+ return false, "Failed setting permission " .. mode .. " for " .. scope
+ end
+ end
+
+ return true
+end
+
+function tools.browser(url)
+ return fs.execute(cfg.web_browser..' "Starting docs..." '..fs.Q(url))
+end
+
+-- Set access and modification times for a file.
+-- @param filename File to set access and modification times for.
+-- @param time may be a string or number containing the format returned
+-- by os.time, or a table ready to be processed via os.time; if
+-- nil, current time is assumed.
+function tools.set_time(filename, time)
+ return true -- FIXME
+end
+
+function tools.lock_access(dirname)
+ -- NYI
+ return {}
+end
+
+function tools.unlock_access(lock)
+ -- NYI
+end
+
+return tools
diff --git a/src/luarocks/fun.lua b/src/luarocks/fun.lua
new file mode 100644
index 0000000..80bf7c2
--- /dev/null
+++ b/src/luarocks/fun.lua
@@ -0,0 +1,143 @@
+
+--- A set of basic functional utilities
+local fun = {}
+
+local unpack = table.unpack or unpack
+
+function fun.concat(xs, ys)
+ local rs = {}
+ local n = #xs
+ for i = 1, n do
+ rs[i] = xs[i]
+ end
+ for i = 1, #ys do
+ rs[i + n] = ys[i]
+ end
+ return rs
+end
+
+function fun.contains(xs, v)
+ for _, x in ipairs(xs) do
+ if v == x then
+ return true
+ end
+ end
+ return false
+end
+
+function fun.map(xs, f)
+ local rs = {}
+ for i = 1, #xs do
+ rs[i] = f(xs[i])
+ end
+ return rs
+end
+
+function fun.filter(xs, f)
+ local rs = {}
+ for i = 1, #xs do
+ local v = xs[i]
+ if f(v) then
+ rs[#rs+1] = v
+ end
+ end
+ return rs
+end
+
+function fun.traverse(t, f)
+ return fun.map(t, function(x)
+ return type(x) == "table" and fun.traverse(x, f) or f(x)
+ end)
+end
+
+function fun.reverse_in(t)
+ for i = 1, math.floor(#t/2) do
+ local m, n = i, #t - i + 1
+ local a, b = t[m], t[n]
+ t[m] = b
+ t[n] = a
+ end
+ return t
+end
+
+function fun.sort_in(t, f)
+ table.sort(t, f)
+ return t
+end
+
+function fun.flip(f)
+ return function(a, b)
+ return f(b, a)
+ end
+end
+
+function fun.find(xs, f)
+ if type(xs) == "function" then
+ for v in xs do
+ local x = f(v)
+ if x then
+ return x
+ end
+ end
+ elseif type(xs) == "table" then
+ for _, v in ipairs(xs) do
+ local x = f(v)
+ if x then
+ return x
+ end
+ end
+ end
+end
+
+function fun.partial(f, ...)
+ local n = select("#", ...)
+ if n == 1 then
+ local a = ...
+ return function(...)
+ return f(a, ...)
+ end
+ elseif n == 2 then
+ local a, b = ...
+ return function(...)
+ return f(a, b, ...)
+ end
+ else
+ local pargs = { n = n, ... }
+ return function(...)
+ local m = select("#", ...)
+ local fargs = { ... }
+ local args = {}
+ for i = 1, n do
+ args[i] = pargs[i]
+ end
+ for i = 1, m do
+ args[i+n] = fargs[i]
+ end
+ return f(unpack(args, 1, n+m))
+ end
+ end
+end
+
+function fun.memoize(fn)
+ local memory = setmetatable({}, { __mode = "k" })
+ local errors = setmetatable({}, { __mode = "k" })
+ local NIL = {}
+ return function(arg)
+ if memory[arg] then
+ if memory[arg] == NIL then
+ return nil, errors[arg]
+ end
+ return memory[arg]
+ end
+ local ret1, ret2 = fn(arg)
+ if ret1 ~= nil then
+ memory[arg] = ret1
+ else
+ memory[arg] = NIL
+ errors[arg] = ret2
+ end
+ return ret1, ret2
+ end
+end
+
+return fun
diff --git a/src/luarocks/loader.lua b/src/luarocks/loader.lua
new file mode 100644
index 0000000..772fdfc
--- /dev/null
+++ b/src/luarocks/loader.lua
@@ -0,0 +1,269 @@
+--- A module which installs a Lua package loader that is LuaRocks-aware.
+-- This loader uses dependency information from the LuaRocks tree to load
+-- correct versions of modules. It does this by constructing a "context"
+-- table in the environment, which records which versions of packages were
+-- used to load previous modules, so that the loader chooses versions
+-- that are declared to be compatible with the ones loaded earlier.
+
+-- luacheck: globals luarocks
+
+local loaders = package.loaders or package.searchers
+local require, ipairs, table, type, next, tostring, error =
+ require, ipairs, table, type, next, tostring, error
+local unpack = unpack or table.unpack
+
+local loader = {}
+
+local is_clean = not package.loaded["luarocks.core.cfg"]
+
+-- This loader module depends only on core modules.
+local cfg = require("luarocks.core.cfg")
+local cfg_ok, err = cfg.init()
+if cfg_ok then
+ cfg.init_package_paths()
+end
+
+local path = require("luarocks.core.path")
+local manif = require("luarocks.core.manif")
+local vers = require("luarocks.core.vers")
+local require = nil -- luacheck: ignore 411
+--------------------------------------------------------------------------------
+
+-- Workaround for wrappers produced by older versions of LuaRocks
+local temporary_global = false
+local status, luarocks_value = pcall(function() return luarocks end)
+if status and luarocks_value then
+ -- The site_config.lua file generated by old versions uses module(),
+ -- so it produces a global `luarocks` table. Since we have the table,
+ -- add the `loader` field to make the old wrappers happy.
+ luarocks.loader = loader
+else
+ -- When a new version is installed on top of an old version,
+ -- site_config.lua may be replaced, and then it no longer creates
+ -- a global.
+ -- Detect when being called via -lluarocks.loader; this is
+ -- most likely a wrapper.
+ local info = debug and debug.getinfo(2, "nS")
+ if info and info.what == "C" and not info.name then
+ luarocks = { loader = loader }
+ temporary_global = true
+ -- For the other half of this hack,
+ -- see the next use of `temporary_global` below.
+ end
+end
+
+loader.context = {}
+
+--- Process the dependencies of a package to determine its dependency
+-- chain for loading modules.
+-- @param name string: The name of an installed rock.
+-- @param version string: The version of the rock, in string format
+function loader.add_context(name, version)
+ -- assert(type(name) == "string")
+ -- assert(type(version) == "string")
+
+ if temporary_global then
+ -- The first thing a wrapper does is to call add_context.
+ -- From here on, it's safe to clean the global environment.
+ luarocks = nil
+ temporary_global = false
+ end
+
+ local tree_manifests = manif.load_rocks_tree_manifests()
+ if not tree_manifests then
+ return nil
+ end
+
+ return manif.scan_dependencies(name, version, tree_manifests, loader.context)
+end
+
+--- Internal sorting function.
+-- @param a table: A provider table.
+-- @param b table: Another provider table.
+-- @return boolean: True if the version of a is greater than that of b.
+local function sort_versions(a,b)
+ return a.version > b.version
+end
+
+--- Request module to be loaded through other loaders,
+-- once the proper name of the module has been determined.
+-- For example, in case the module "socket.core" has been requested
+-- to the LuaRocks loader and it determined based on context that
+-- the version 2.0.2 needs to be loaded and it is not the current
+-- version, the module requested for the other loaders will be
+-- "socket.core_2_0_2".
+-- @param module The module name requested by the user, such as "socket.core"
+-- @param name The rock name, such as "luasocket"
+-- @param version The rock version, such as "2.0.2-1"
+-- @param module_name The actual module name, such as "socket.core" or "socket.core_2_0_2".
+-- @return table or (nil, string): The module table as returned by some other loader,
+-- or nil followed by an error message if no other loader managed to load the module.
+local function call_other_loaders(module, name, version, module_name)
+ for _, a_loader in ipairs(loaders) do
+ if a_loader ~= loader.luarocks_loader then
+ local results = { a_loader(module_name) }
+ if type(results[1]) == "function" then
+ return unpack(results)
+ end
+ end
+ end
+ return "Failed loading module "..module.." in LuaRocks rock "..name.." "..version
+end
+
+local function add_providers(providers, entries, tree, module, filter_file_name)
+ for i, entry in ipairs(entries) do
+ local name, version = entry:match("^([^/]*)/(.*)$")
+ local file_name = tree.manifest.repository[name][version][1].modules[module]
+ if type(file_name) ~= "string" then
+ error("Invalid data in manifest file for module "..tostring(module).." (invalid data for "..tostring(name).." "..tostring(version)..")")
+ end
+ file_name = filter_file_name(file_name, name, version, tree.tree, i)
+ if loader.context[name] == version then
+ return name, version, file_name
+ end
+ version = vers.parse_version(version)
+ table.insert(providers, {name = name, version = version, module_name = file_name, tree = tree})
+ end
+end
+
+--- Search for a module in the rocks trees
+-- @param module string: module name (eg. "socket.core")
+-- @param filter_file_name function(string, string, string, string, number):
+-- a function that takes the module file name (eg "socket/core.so"), the rock name
+-- (eg "luasocket"), the version (eg "2.0.2-1"), the path of the rocks tree
+-- (eg "/usr/local"), and the numeric index of the matching entry, so the
+-- filter function can know if the matching module was the first entry or not.
+-- @return string, string, string, (string or table):
+-- * name of the rock containing the module (eg. "luasocket")
+-- * version of the rock (eg. "2.0.2-1")
+-- * return value of filter_file_name
+-- * tree of the module (string or table in `tree_manifests` format)
+local function select_module(module, filter_file_name)
+ --assert(type(module) == "string")
+ --assert(type(filter_module_name) == "function")
+
+ local tree_manifests = manif.load_rocks_tree_manifests()
+ if not tree_manifests then
+ return nil
+ end
+
+ local providers = {}
+ local initmodule
+ for _, tree in ipairs(tree_manifests) do
+ local entries = tree.manifest.modules[module]
+ if entries then
+ local n, v, f = add_providers(providers, entries, tree, module, filter_file_name)
+ if n then
+ return n, v, f
+ end
+ else
+ initmodule = initmodule or module .. ".init"
+ entries = tree.manifest.modules[initmodule]
+ if entries then
+ local n, v, f = add_providers(providers, entries, tree, initmodule, filter_file_name)
+ if n then
+ return n, v, f
+ end
+ end
+ end
+ end
+
+ if next(providers) then
+ table.sort(providers, sort_versions)
+ local first = providers[1]
+ return first.name, first.version.string, first.module_name, first.tree
+ end
+end
+
+--- Search for a module
+-- @param module string: module name (eg. "socket.core")
+-- @return string, string, string, (string or table):
+-- * name of the rock containing the module (eg. "luasocket")
+-- * version of the rock (eg. "2.0.2-1")
+-- * name of the module (eg. "socket.core", or "socket.core_2_0_2" if file is stored versioned).
+-- * tree of the module (string or table in `tree_manifests` format)
+local function pick_module(module)
+ return
+ select_module(module, function(file_name, name, version, tree, i)
+ if i > 1 then
+ file_name = path.versioned_name(file_name, "", name, version)
+ end
+ return path.path_to_module(file_name)
+ end)
+end
+
+--- Return the pathname of the file that would be loaded for a module.
+-- @param module string: module name (eg. "socket.core")
+-- @param where string: places to look for the module. If `where` contains
+-- "l", it will search using the LuaRocks loader; if it contains "p",
+-- it will look in the filesystem using package.path and package.cpath.
+-- You can use both at the same time.
+-- @return If successful, it will return four values.
+-- * If found using the LuaRocks loader, it will return:
+-- * filename of the module (eg. "/usr/local/lib/lua/5.1/socket/core.so"),
+-- * rock name
+-- * rock version
+-- * "l" to indicate the match comes from the loader.
+-- * If found scanning package.path and package.cpath, it will return:
+-- * filename of the module (eg. "/usr/local/lib/lua/5.1/socket/core.so"),
+-- * "path" or "cpath"
+-- * nil
+-- * "p" to indicate the match comes from scanning package.path and cpath.
+-- If unsuccessful, nothing is returned.
+function loader.which(module, where)
+ where = where or "l"
+ if where:match("l") then
+ local rock_name, rock_version, file_name = select_module(module, path.which_i)
+ if rock_name then
+ local fd = io.open(file_name)
+ if fd then
+ fd:close()
+ return file_name, rock_name, rock_version, "l"
+ end
+ end
+ end
+ if where:match("p") then
+ local modpath = module:gsub("%.", "/")
+ for _, v in ipairs({"path", "cpath"}) do
+ for p in package[v]:gmatch("([^;]+)") do
+ local file_name = p:gsub("%?", modpath) -- luacheck: ignore 421
+ local fd = io.open(file_name)
+ if fd then
+ fd:close()
+ return file_name, v, nil, "p"
+ end
+ end
+ end
+ end
+end
+
+--- Package loader for LuaRocks support.
+-- A module is searched in installed rocks that match the
+-- current LuaRocks context. If module is not part of the
+-- context, or if a context has not yet been set, the module
+-- in the package with the highest version is used.
+-- @param module string: The module name, like in plain require().
+-- @return table: The module table (typically), like in plain
+-- require(). See <a href="http://www.lua.org/manual/5.1/manual.html#pdf-require">require()</a>
+-- in the Lua reference manual for details.
+function loader.luarocks_loader(module)
+ local name, version, module_name = pick_module(module)
+ if not name then
+ return "No LuaRocks module found for "..module
+ else
+ loader.add_context(name, version)
+ return call_other_loaders(module, name, version, module_name)
+ end
+end
+
+table.insert(loaders, 1, loader.luarocks_loader)
+
+if is_clean then
+ for modname, _ in pairs(package.loaded) do
+ if modname:match("^luarocks%.") then
+ package.loaded[modname] = nil
+ end
+ end
+end
+
+return loader
diff --git a/src/luarocks/manif.lua b/src/luarocks/manif.lua
new file mode 100644
index 0000000..a4ddda1
--- /dev/null
+++ b/src/luarocks/manif.lua
@@ -0,0 +1,225 @@
+--- Module for handling manifest files and tables.
+-- Manifest files describe the contents of a LuaRocks tree or server.
+-- They are loaded into manifest tables, which are then used for
+-- performing searches, matching dependencies, etc.
+local manif = {}
+
+local core = require("luarocks.core.manif")
+local persist = require("luarocks.persist")
+local fetch = require("luarocks.fetch")
+local dir = require("luarocks.dir")
+local fs = require("luarocks.fs")
+local cfg = require("luarocks.core.cfg")
+local path = require("luarocks.path")
+local util = require("luarocks.util")
+local queries = require("luarocks.queries")
+local type_manifest = require("luarocks.type.manifest")
+
+manif.cache_manifest = core.cache_manifest
+manif.load_rocks_tree_manifests = core.load_rocks_tree_manifests
+manif.scan_dependencies = core.scan_dependencies
+
+manif.rock_manifest_cache = {}
+
+local function check_manifest(repo_url, manifest, globals)
+ local ok, err = type_manifest.check(manifest, globals)
+ if not ok then
+ core.cache_manifest(repo_url, cfg.lua_version, nil)
+ return nil, "Error checking manifest: "..err, "type"
+ end
+ return manifest
+end
+
+local postprocess_dependencies
+do
+ local postprocess_check = setmetatable({}, { __mode = "k" })
+ postprocess_dependencies = function(manifest)
+ if postprocess_check[manifest] then
+ return
+ end
+ if manifest.dependencies then
+ for name, versions in pairs(manifest.dependencies) do
+ for version, entries in pairs(versions) do
+ for k, v in pairs(entries) do
+ entries[k] = queries.from_persisted_table(v)
+ end
+ end
+ end
+ end
+ postprocess_check[manifest] = true
+ end
+end
+
+function manif.load_rock_manifest(name, version, root)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+
+ local name_version = name.."/"..version
+ if manif.rock_manifest_cache[name_version] then
+ return manif.rock_manifest_cache[name_version].rock_manifest
+ end
+ local pathname = path.rock_manifest_file(name, version, root)
+ local rock_manifest = persist.load_into_table(pathname)
+ if not rock_manifest then
+ return nil, "rock_manifest file not found for "..name.." "..version.." - not a LuaRocks tree?"
+ end
+ manif.rock_manifest_cache[name_version] = rock_manifest
+ return rock_manifest.rock_manifest
+end
+
+--- Load a local or remote manifest describing a repository.
+-- All functions that use manifest tables assume they were obtained
+-- through this function.
+-- @param repo_url string: URL or pathname for the repository.
+-- @param lua_version string: Lua version in "5.x" format, defaults to installed version.
+-- @param versioned_only boolean: If true, do not fall back to the main manifest
+-- if a versioned manifest was not found.
+-- @return table or (nil, string, [string]): A table representing the manifest,
+-- or nil followed by an error message and an optional error code.
+function manif.load_manifest(repo_url, lua_version, versioned_only)
+ assert(type(repo_url) == "string")
+ assert(type(lua_version) == "string" or not lua_version)
+ lua_version = lua_version or cfg.lua_version
+
+ local cached_manifest = core.get_cached_manifest(repo_url, lua_version)
+ if cached_manifest then
+ postprocess_dependencies(cached_manifest)
+ return cached_manifest
+ end
+
+ local filenames = {
+ "manifest-"..lua_version..".zip",
+ "manifest-"..lua_version,
+ not versioned_only and "manifest" or nil,
+ }
+
+ local protocol, repodir = dir.split_url(repo_url)
+ local pathname, from_cache
+ if protocol == "file" then
+ for _, filename in ipairs(filenames) do
+ pathname = dir.path(repodir, filename)
+ if fs.exists(pathname) then
+ break
+ end
+ end
+ else
+ local err, errcode
+ for _, filename in ipairs(filenames) do
+ pathname, err, errcode, from_cache = fetch.fetch_caching(dir.path(repo_url, filename), "no_mirror")
+ if pathname then
+ break
+ end
+ end
+ if not pathname then
+ return nil, err, errcode
+ end
+ end
+ if pathname:match(".*%.zip$") then
+ pathname = fs.absolute_name(pathname)
+ local nozip = pathname:match("(.*)%.zip$")
+ if not from_cache then
+ local dirname = dir.dir_name(pathname)
+ fs.change_dir(dirname)
+ fs.delete(nozip)
+ local ok, err = fs.unzip(pathname)
+ fs.pop_dir()
+ if not ok then
+ fs.delete(pathname)
+ fs.delete(pathname..".timestamp")
+ return nil, "Failed extracting manifest file: " .. err
+ end
+ end
+ pathname = nozip
+ end
+ local manifest, err, errcode = core.manifest_loader(pathname, repo_url, lua_version)
+ if not manifest then
+ return nil, err, errcode
+ end
+
+ postprocess_dependencies(manifest)
+ return check_manifest(repo_url, manifest, err)
+end
+
+--- Get type and name of an item (a module or a command) provided by a file.
+-- @param deploy_type string: rock manifest subtree the file comes from ("bin", "lua", or "lib").
+-- @param file_path string: path to the file relatively to deploy_type subdirectory.
+-- @return (string, string): item type ("module" or "command") and name.
+function manif.get_provided_item(deploy_type, file_path)
+ assert(type(deploy_type) == "string")
+ assert(type(file_path) == "string")
+ local item_type = deploy_type == "bin" and "command" or "module"
+ local item_name = item_type == "command" and file_path or path.path_to_module(file_path)
+ return item_type, item_name
+end
+
+local function get_providers(item_type, item_name, repo)
+ assert(type(item_type) == "string")
+ assert(type(item_name) == "string")
+ local rocks_dir = path.rocks_dir(repo or cfg.root_dir)
+ local manifest = manif.load_manifest(rocks_dir)
+ return manifest and manifest[item_type .. "s"][item_name]
+end
+
+--- Given a name of a module or a command, figure out which rock name and version
+-- correspond to it in the rock tree manifest.
+-- @param item_type string: "module" or "command".
+-- @param item_name string: module or command name.
+-- @param root string or nil: A local root dir for a rocks tree. If not given, the default is used.
+-- @return (string, string) or nil: name and version of the provider rock or nil if there
+-- is no provider.
+function manif.get_current_provider(item_type, item_name, repo)
+ local providers = get_providers(item_type, item_name, repo)
+ if providers then
+ return providers[1]:match("([^/]*)/([^/]*)")
+ end
+end
+
+function manif.get_next_provider(item_type, item_name, repo)
+ local providers = get_providers(item_type, item_name, repo)
+ if providers and providers[2] then
+ return providers[2]:match("([^/]*)/([^/]*)")
+ end
+end
+
+--- Get all versions of a package listed in a manifest file.
+-- @param name string: a package name.
+-- @param deps_mode string: "one", to use only the currently
+-- configured tree; "order" to select trees based on order
+-- (use the current tree and all trees below it on the list)
+-- or "all", to use all trees.
+-- @return table: An array of strings listing installed
+-- versions of a package, and a table indicating where they are found.
+function manif.get_versions(dep, deps_mode)
+ assert(type(dep) == "table")
+ assert(type(deps_mode) == "string")
+
+ local name = dep.name
+ local namespace = dep.namespace
+
+ local version_set = {}
+ path.map_trees(deps_mode, function(tree)
+ local manifest = manif.load_manifest(path.rocks_dir(tree))
+
+ if manifest and manifest.repository[name] then
+ for version in pairs(manifest.repository[name]) do
+ if dep.namespace then
+ local ns_file = path.rock_namespace_file(name, version, tree)
+ local fd = io.open(ns_file, "r")
+ if fd then
+ local ns = fd:read("*a")
+ fd:close()
+ if ns == namespace then
+ version_set[version] = tree
+ end
+ end
+ else
+ version_set[version] = tree
+ end
+ end
+ end
+ end)
+
+ return util.keys(version_set), version_set
+end
+
+return manif
diff --git a/src/luarocks/manif/writer.lua b/src/luarocks/manif/writer.lua
new file mode 100644
index 0000000..36f5f57
--- /dev/null
+++ b/src/luarocks/manif/writer.lua
@@ -0,0 +1,498 @@
+
+local writer = {}
+
+local cfg = require("luarocks.core.cfg")
+local search = require("luarocks.search")
+local repos = require("luarocks.repos")
+local deps = require("luarocks.deps")
+local vers = require("luarocks.core.vers")
+local fs = require("luarocks.fs")
+local util = require("luarocks.util")
+local dir = require("luarocks.dir")
+local fetch = require("luarocks.fetch")
+local path = require("luarocks.path")
+local persist = require("luarocks.persist")
+local manif = require("luarocks.manif")
+local queries = require("luarocks.queries")
+
+--- Update storage table to account for items provided by a package.
+-- @param storage table: a table storing items in the following format:
+-- keys are item names and values are arrays of packages providing each item,
+-- where a package is specified as string `name/version`.
+-- @param items table: a table mapping item names to paths.
+-- @param name string: package name.
+-- @param version string: package version.
+local function store_package_items(storage, name, version, items)
+ assert(type(storage) == "table")
+ assert(type(items) == "table")
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+
+ local package_identifier = name.."/"..version
+
+ for item_name, path in pairs(items) do -- luacheck: ignore 431
+ if not storage[item_name] then
+ storage[item_name] = {}
+ end
+
+ table.insert(storage[item_name], package_identifier)
+ end
+end
+
+--- Update storage table removing items provided by a package.
+-- @param storage table: a table storing items in the following format:
+-- keys are item names and values are arrays of packages providing each item,
+-- where a package is specified as string `name/version`.
+-- @param items table: a table mapping item names to paths.
+-- @param name string: package name.
+-- @param version string: package version.
+local function remove_package_items(storage, name, version, items)
+ assert(type(storage) == "table")
+ assert(type(items) == "table")
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+
+ local package_identifier = name.."/"..version
+
+ for item_name, path in pairs(items) do -- luacheck: ignore 431
+ local key = item_name
+ local all_identifiers = storage[key]
+ if not all_identifiers then
+ key = key .. ".init"
+ all_identifiers = storage[key]
+ end
+
+ if all_identifiers then
+ for i, identifier in ipairs(all_identifiers) do
+ if identifier == package_identifier then
+ table.remove(all_identifiers, i)
+ break
+ end
+ end
+
+ if #all_identifiers == 0 then
+ storage[key] = nil
+ end
+ else
+ util.warning("Cannot find entry for " .. item_name .. " in manifest -- corrupted manifest?")
+ end
+ end
+end
+
+--- Process the dependencies of a manifest table to determine its dependency
+-- chains for loading modules. The manifest dependencies information is filled
+-- and any dependency inconsistencies or missing dependencies are reported to
+-- standard error.
+-- @param manifest table: a manifest table.
+-- @param deps_mode string: Dependency mode: "one" for the current default tree,
+-- "all" for all trees, "order" for all trees with priority >= the current default,
+-- "none" for no trees.
+local function update_dependencies(manifest, deps_mode)
+ assert(type(manifest) == "table")
+ assert(type(deps_mode) == "string")
+
+ if not manifest.dependencies then manifest.dependencies = {} end
+ local mdeps = manifest.dependencies
+
+ for pkg, versions in pairs(manifest.repository) do
+ for version, repositories in pairs(versions) do
+ for _, repo in ipairs(repositories) do
+ if repo.arch == "installed" then
+ local rd = {}
+ repo.dependencies = rd
+ deps.scan_deps(rd, mdeps, pkg, version, deps_mode)
+ rd[pkg] = nil
+ end
+ end
+ end
+ end
+end
+
+
+
+--- Sort function for ordering rock identifiers in a manifest's
+-- modules table. Rocks are ordered alphabetically by name, and then
+-- by version which greater first.
+-- @param a string: Version to compare.
+-- @param b string: Version to compare.
+-- @return boolean: The comparison result, according to the
+-- rule outlined above.
+local function sort_pkgs(a, b)
+ assert(type(a) == "string")
+ assert(type(b) == "string")
+
+ local na, va = a:match("(.*)/(.*)$")
+ local nb, vb = b:match("(.*)/(.*)$")
+
+ return (na == nb) and vers.compare_versions(va, vb) or na < nb
+end
+
+--- Sort items of a package matching table by version number (higher versions first).
+-- @param tbl table: the package matching table: keys should be strings
+-- and values arrays of strings with packages names in "name/version" format.
+local function sort_package_matching_table(tbl)
+ assert(type(tbl) == "table")
+
+ if next(tbl) then
+ for item, pkgs in pairs(tbl) do
+ if #pkgs > 1 then
+ table.sort(pkgs, sort_pkgs)
+ -- Remove duplicates from the sorted array.
+ local prev = nil
+ local i = 1
+ while pkgs[i] do
+ local curr = pkgs[i]
+ if curr == prev then
+ table.remove(pkgs, i)
+ else
+ prev = curr
+ i = i + 1
+ end
+ end
+ end
+ end
+ end
+end
+
+--- Filter manifest table by Lua version, removing rockspecs whose Lua version
+-- does not match.
+-- @param manifest table: a manifest table.
+-- @param lua_version string or nil: filter by Lua version
+-- @param repodir string: directory of repository being scanned
+-- @param cache table: temporary rockspec cache table
+local function filter_by_lua_version(manifest, lua_version, repodir, cache)
+ assert(type(manifest) == "table")
+ assert(type(repodir) == "string")
+ assert((not cache) or type(cache) == "table")
+
+ cache = cache or {}
+ lua_version = vers.parse_version(lua_version)
+ for pkg, versions in pairs(manifest.repository) do
+ local to_remove = {}
+ for version, repositories in pairs(versions) do
+ for _, repo in ipairs(repositories) do
+ if repo.arch == "rockspec" then
+ local pathname = dir.path(repodir, pkg.."-"..version..".rockspec")
+ local rockspec, err = cache[pathname]
+ if not rockspec then
+ rockspec, err = fetch.load_local_rockspec(pathname, true)
+ end
+ if rockspec then
+ cache[pathname] = rockspec
+ for _, dep in ipairs(rockspec.dependencies) do
+ if dep.name == "lua" then
+ if not vers.match_constraints(lua_version, dep.constraints) then
+ table.insert(to_remove, version)
+ end
+ break
+ end
+ end
+ else
+ util.printerr("Error loading rockspec for "..pkg.." "..version..": "..err)
+ end
+ end
+ end
+ end
+ if next(to_remove) then
+ for _, incompat in ipairs(to_remove) do
+ versions[incompat] = nil
+ end
+ if not next(versions) then
+ manifest.repository[pkg] = nil
+ end
+ end
+ end
+end
+
+--- Store search results in a manifest table.
+-- @param results table: The search results as returned by search.disk_search.
+-- @param manifest table: A manifest table (must contain repository, modules, commands tables).
+-- It will be altered to include the search results.
+-- @return boolean or (nil, string): true in case of success, or nil followed by an error message.
+local function store_results(results, manifest)
+ assert(type(results) == "table")
+ assert(type(manifest) == "table")
+
+ for name, versions in pairs(results) do
+ local pkgtable = manifest.repository[name] or {}
+ for version, entries in pairs(versions) do
+ local versiontable = {}
+ for _, entry in ipairs(entries) do
+ local entrytable = {}
+ entrytable.arch = entry.arch
+ if entry.arch == "installed" then
+ local rock_manifest, err = manif.load_rock_manifest(name, version)
+ if not rock_manifest then return nil, err end
+
+ entrytable.modules = repos.package_modules(name, version)
+ store_package_items(manifest.modules, name, version, entrytable.modules)
+ entrytable.commands = repos.package_commands(name, version)
+ store_package_items(manifest.commands, name, version, entrytable.commands)
+ end
+ table.insert(versiontable, entrytable)
+ end
+ pkgtable[version] = versiontable
+ end
+ manifest.repository[name] = pkgtable
+ end
+ sort_package_matching_table(manifest.modules)
+ sort_package_matching_table(manifest.commands)
+ return true
+end
+
+--- Commit a table to disk in given local path.
+-- @param where string: The directory where the table should be saved.
+-- @param name string: The filename.
+-- @param tbl table: The table to be saved.
+-- @return boolean or (nil, string): true if successful, or nil and a
+-- message in case of errors.
+local function save_table(where, name, tbl)
+ assert(type(where) == "string")
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(tbl) == "table")
+
+ local filename = dir.path(where, name)
+ local ok, err = persist.save_from_table(filename..".tmp", tbl)
+ if ok then
+ ok, err = fs.replace_file(filename, filename..".tmp")
+ end
+ return ok, err
+end
+
+function writer.make_rock_manifest(name, version)
+ local install_dir = path.install_dir(name, version)
+ local tree = {}
+ for _, file in ipairs(fs.find(install_dir)) do
+ local full_path = dir.path(install_dir, file)
+ local walk = tree
+ local last
+ local last_name
+ for filename in file:gmatch("[^\\/]+") do
+ local next = walk[filename]
+ if not next then
+ next = {}
+ walk[filename] = next
+ end
+ last = walk
+ last_name = filename
+ walk = next
+ end
+ if fs.is_file(full_path) then
+ local sum, err = fs.get_md5(full_path)
+ if not sum then
+ return nil, "Failed producing checksum: "..tostring(err)
+ end
+ last[last_name] = sum
+ end
+ end
+ local rock_manifest = { rock_manifest=tree }
+ manif.rock_manifest_cache[name.."/"..version] = rock_manifest
+ save_table(install_dir, "rock_manifest", rock_manifest )
+ return true
+end
+
+-- Writes a 'rock_namespace' file in a locally installed rock directory.
+-- @param name string: the rock name, without a namespace
+-- @param version string: the rock version
+-- @param namespace string?: the namespace
+-- @return true if successful (or unnecessary, if there is no namespace),
+-- or nil and an error message.
+function writer.make_namespace_file(name, version, namespace)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+ assert(type(namespace) == "string" or not namespace)
+ if not namespace then
+ return true
+ end
+ local fd, err = io.open(path.rock_namespace_file(name, version), "w")
+ if not fd then
+ return nil, err
+ end
+ local ok, err = fd:write(namespace)
+ if not ok then
+ return nil, err
+ end
+ fd:close()
+ return true
+end
+
+--- Scan a LuaRocks repository and output a manifest file.
+-- A file called 'manifest' will be written in the root of the given
+-- repository directory.
+-- @param repo A local repository directory.
+-- @param deps_mode string: Dependency mode: "one" for the current default tree,
+-- "all" for all trees, "order" for all trees with priority >= the current default,
+-- "none" for the default dependency mode from the configuration.
+-- @param remote boolean: 'true' if making a manifest for a rocks server.
+-- @return boolean or (nil, string): True if manifest was generated,
+-- or nil and an error message.
+function writer.make_manifest(repo, deps_mode, remote)
+ assert(type(repo) == "string")
+ assert(type(deps_mode) == "string")
+
+ if deps_mode == "none" then deps_mode = cfg.deps_mode end
+
+ if not fs.is_dir(repo) then
+ return nil, "Cannot access repository at "..repo
+ end
+
+ local query = queries.all("any")
+ local results = search.disk_search(repo, query)
+ local manifest = { repository = {}, modules = {}, commands = {} }
+
+ manif.cache_manifest(repo, nil, manifest)
+
+ local ok, err = store_results(results, manifest)
+ if not ok then return nil, err end
+
+ if remote then
+ local cache = {}
+ for luaver in util.lua_versions() do
+ local vmanifest = { repository = {}, modules = {}, commands = {} }
+ local ok, err = store_results(results, vmanifest)
+ filter_by_lua_version(vmanifest, luaver, repo, cache)
+ if not cfg.no_manifest then
+ save_table(repo, "manifest-"..luaver, vmanifest)
+ end
+ end
+ else
+ update_dependencies(manifest, deps_mode)
+ end
+
+ if cfg.no_manifest then
+ -- We want to have cache updated; but exit before save_table is called
+ return true
+ end
+ return save_table(repo, "manifest", manifest)
+end
+
+--- Update manifest file for a local repository
+-- adding information about a version of a package installed in that repository.
+-- @param name string: Name of a package from the repository.
+-- @param version string: Version of a package from the repository.
+-- @param repo string or nil: Pathname of a local repository. If not given,
+-- the default local repository is used.
+-- @param deps_mode string: Dependency mode: "one" for the current default tree,
+-- "all" for all trees, "order" for all trees with priority >= the current default,
+-- "none" for using the default dependency mode from the configuration.
+-- @return boolean or (nil, string): True if manifest was updated successfully,
+-- or nil and an error message.
+function writer.add_to_manifest(name, version, repo, deps_mode)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+ local rocks_dir = path.rocks_dir(repo or cfg.root_dir)
+ assert(type(deps_mode) == "string")
+
+ if deps_mode == "none" then deps_mode = cfg.deps_mode end
+
+ local manifest, err = manif.load_manifest(rocks_dir)
+ if not manifest then
+ util.printerr("No existing manifest. Attempting to rebuild...")
+ -- Manifest built by `writer.make_manifest` should already
+ -- include information about given name and version,
+ -- no need to update it.
+ return writer.make_manifest(rocks_dir, deps_mode)
+ end
+
+ local results = {[name] = {[version] = {{arch = "installed", repo = rocks_dir}}}}
+
+ local ok, err = store_results(results, manifest)
+ if not ok then return nil, err end
+
+ update_dependencies(manifest, deps_mode)
+
+ if cfg.no_manifest then
+ return true
+ end
+ return save_table(rocks_dir, "manifest", manifest)
+end
+
+--- Update manifest file for a local repository
+-- removing information about a version of a package.
+-- @param name string: Name of a package removed from the repository.
+-- @param version string: Version of a package removed from the repository.
+-- @param repo string or nil: Pathname of a local repository. If not given,
+-- the default local repository is used.
+-- @param deps_mode string: Dependency mode: "one" for the current default tree,
+-- "all" for all trees, "order" for all trees with priority >= the current default,
+-- "none" for using the default dependency mode from the configuration.
+-- @return boolean or (nil, string): True if manifest was updated successfully,
+-- or nil and an error message.
+function writer.remove_from_manifest(name, version, repo, deps_mode)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+ local rocks_dir = path.rocks_dir(repo or cfg.root_dir)
+ assert(type(deps_mode) == "string")
+
+ if deps_mode == "none" then deps_mode = cfg.deps_mode end
+
+ local manifest, err = manif.load_manifest(rocks_dir)
+ if not manifest then
+ util.printerr("No existing manifest. Attempting to rebuild...")
+ -- Manifest built by `writer.make_manifest` should already
+ -- include up-to-date information, no need to update it.
+ return writer.make_manifest(rocks_dir, deps_mode)
+ end
+
+ local package_entry = manifest.repository[name]
+ if package_entry == nil or package_entry[version] == nil then
+ -- entry is already missing from repository, no need to do anything
+ return true
+ end
+
+ local version_entry = package_entry[version][1]
+ if not version_entry then
+ -- manifest looks corrupted, rebuild
+ return writer.make_manifest(rocks_dir, deps_mode)
+ end
+
+ remove_package_items(manifest.modules, name, version, version_entry.modules)
+ remove_package_items(manifest.commands, name, version, version_entry.commands)
+
+ package_entry[version] = nil
+ manifest.dependencies[name][version] = nil
+
+ if not next(package_entry) then
+ -- No more versions of this package.
+ manifest.repository[name] = nil
+ manifest.dependencies[name] = nil
+ end
+
+ update_dependencies(manifest, deps_mode)
+
+ if cfg.no_manifest then
+ return true
+ end
+ return save_table(rocks_dir, "manifest", manifest)
+end
+
+--- Report missing dependencies for all rocks installed in a repository.
+-- @param repo string or nil: Pathname of a local repository. If not given,
+-- the default local repository is used.
+-- @param deps_mode string: Dependency mode: "one" for the current default tree,
+-- "all" for all trees, "order" for all trees with priority >= the current default,
+-- "none" for using the default dependency mode from the configuration.
+function writer.check_dependencies(repo, deps_mode)
+ local rocks_dir = path.rocks_dir(repo or cfg.root_dir)
+ assert(type(deps_mode) == "string")
+ if deps_mode == "none" then deps_mode = cfg.deps_mode end
+
+ local manifest = manif.load_manifest(rocks_dir)
+ if not manifest then
+ return
+ end
+
+ for name, versions in util.sortedpairs(manifest.repository) do
+ for version, version_entries in util.sortedpairs(versions, vers.compare_versions) do
+ for _, entry in ipairs(version_entries) do
+ if entry.arch == "installed" then
+ if manifest.dependencies[name] and manifest.dependencies[name][version] then
+ deps.report_missing_dependencies(name, version, manifest.dependencies[name][version], deps_mode, util.get_rocks_provided())
+ end
+ end
+ end
+ end
+ end
+end
+
+return writer
diff --git a/src/luarocks/pack.lua b/src/luarocks/pack.lua
new file mode 100644
index 0000000..731f49d
--- /dev/null
+++ b/src/luarocks/pack.lua
@@ -0,0 +1,184 @@
+
+-- Create rock files, packing sources or binaries.
+local pack = {}
+
+local unpack = unpack or table.unpack
+
+local queries = require("luarocks.queries")
+local path = require("luarocks.path")
+local repos = require("luarocks.repos")
+local fetch = require("luarocks.fetch")
+local fs = require("luarocks.fs")
+local cfg = require("luarocks.core.cfg")
+local util = require("luarocks.util")
+local dir = require("luarocks.dir")
+local manif = require("luarocks.manif")
+local search = require("luarocks.search")
+local signing = require("luarocks.signing")
+
+--- Create a source rock.
+-- Packages a rockspec and its required source files in a rock
+-- file with the .src.rock extension, which can later be built and
+-- installed with the "build" command.
+-- @param rockspec_file string: An URL or pathname for a rockspec file.
+-- @return string or (nil, string): The filename of the resulting
+-- .src.rock file; or nil and an error message.
+function pack.pack_source_rock(rockspec_file)
+ assert(type(rockspec_file) == "string")
+
+ local rockspec, err = fetch.load_rockspec(rockspec_file)
+ if err then
+ return nil, "Error loading rockspec: "..err
+ end
+ rockspec_file = rockspec.local_abs_filename
+
+ local name_version = rockspec.name .. "-" .. rockspec.version
+ local rock_file = fs.absolute_name(name_version .. ".src.rock")
+
+ local temp_dir, err = fs.make_temp_dir("pack-"..name_version)
+ if not temp_dir then
+ return nil, "Failed creating temporary directory: "..err
+ end
+ util.schedule_function(fs.delete, temp_dir)
+
+ local source_file, source_dir = fetch.fetch_sources(rockspec, true, temp_dir)
+ if not source_file then
+ return nil, source_dir
+ end
+ local ok, err = fs.change_dir(source_dir)
+ if not ok then return nil, err end
+
+ fs.delete(rock_file)
+ fs.copy(rockspec_file, source_dir, "read")
+ ok, err = fs.zip(rock_file, dir.base_name(rockspec_file), dir.base_name(source_file))
+ if not ok then
+ return nil, "Failed packing "..rock_file.." - "..err
+ end
+ fs.pop_dir()
+
+ return rock_file
+end
+
+local function copy_back_files(name, version, file_tree, deploy_dir, pack_dir, perms)
+ local ok, err = fs.make_dir(pack_dir)
+ if not ok then return nil, err end
+ for file, sub in pairs(file_tree) do
+ local source = dir.path(deploy_dir, file)
+ local target = dir.path(pack_dir, file)
+ if type(sub) == "table" then
+ local ok, err = copy_back_files(name, version, sub, source, target)
+ if not ok then return nil, err end
+ else
+ local versioned = path.versioned_name(source, deploy_dir, name, version)
+ if fs.exists(versioned) then
+ fs.copy(versioned, target, perms)
+ else
+ fs.copy(source, target, perms)
+ end
+ end
+ end
+ return true
+end
+
+-- @param name string: Name of package to pack.
+-- @param version string or nil: A version number may also be passed.
+-- @param tree string or nil: An optional tree to pick the package from.
+-- @return string or (nil, string): The filename of the resulting
+-- .src.rock file; or nil and an error message.
+function pack.pack_installed_rock(query, tree)
+
+ local name, version, repo, repo_url = search.pick_installed_rock(query, tree)
+ if not name then
+ return nil, version
+ end
+
+ local root = path.root_from_rocks_dir(repo_url)
+ local prefix = path.install_dir(name, version, root)
+ if not fs.exists(prefix) then
+ return nil, "'"..name.." "..version.."' does not seem to be an installed rock."
+ end
+
+ local rock_manifest, err = manif.load_rock_manifest(name, version, root)
+ if not rock_manifest then return nil, err end
+
+ local name_version = name .. "-" .. version
+ local rock_file = fs.absolute_name(name_version .. "."..cfg.arch..".rock")
+
+ local temp_dir = fs.make_temp_dir("pack")
+ fs.copy_contents(prefix, temp_dir)
+
+ local is_binary = false
+ if rock_manifest.lib then
+ local ok, err = copy_back_files(name, version, rock_manifest.lib, path.deploy_lib_dir(repo), dir.path(temp_dir, "lib"), "exec")
+ if not ok then return nil, "Failed copying back files: " .. err end
+ is_binary = true
+ end
+ if rock_manifest.lua then
+ local ok, err = copy_back_files(name, version, rock_manifest.lua, path.deploy_lua_dir(repo), dir.path(temp_dir, "lua"), "read")
+ if not ok then return nil, "Failed copying back files: " .. err end
+ end
+
+ local ok, err = fs.change_dir(temp_dir)
+ if not ok then return nil, err end
+ if not is_binary and not repos.has_binaries(name, version) then
+ rock_file = rock_file:gsub("%."..cfg.arch:gsub("%-","%%-").."%.", ".all.")
+ end
+ fs.delete(rock_file)
+ ok, err = fs.zip(rock_file, unpack(fs.list_dir()))
+ if not ok then
+ return nil, "Failed packing " .. rock_file .. " - " .. err
+ end
+ fs.pop_dir()
+ fs.delete(temp_dir)
+ return rock_file
+end
+
+function pack.report_and_sign_local_file(file, err, sign)
+ if err then
+ return nil, err
+ end
+ local sigfile
+ if sign then
+ sigfile, err = signing.sign_file(file)
+ util.printout()
+ end
+ util.printout("Packed: "..file)
+ if sigfile then
+ util.printout("Signature stored in: "..sigfile)
+ end
+ if err then
+ return nil, err
+ end
+ return true
+end
+
+function pack.pack_binary_rock(name, namespace, version, sign, cmd)
+
+ -- The --pack-binary-rock option for "luarocks build" basically performs
+ -- "luarocks build" on a temporary tree and then "luarocks pack". The
+ -- alternative would require refactoring parts of luarocks.build and
+ -- luarocks.pack, which would save a few file operations: the idea would be
+ -- to shave off the final deploy steps from the build phase and the initial
+ -- collect steps from the pack phase.
+
+ local temp_dir, err = fs.make_temp_dir("luarocks-build-pack-"..dir.base_name(name))
+ if not temp_dir then
+ return nil, "Failed creating temporary directory: "..err
+ end
+ util.schedule_function(fs.delete, temp_dir)
+
+ path.use_tree(temp_dir)
+ local ok, err = cmd()
+ if not ok then
+ return nil, err
+ end
+ local rname, rversion = path.parse_name(name)
+ if not rname then
+ rname, rversion = name, version
+ end
+ local query = queries.new(rname, namespace, rversion)
+ local file, err = pack.pack_installed_rock(query, temp_dir)
+ return pack.report_and_sign_local_file(file, err, sign)
+end
+
+return pack
diff --git a/src/luarocks/path.lua b/src/luarocks/path.lua
new file mode 100644
index 0000000..19657c8
--- /dev/null
+++ b/src/luarocks/path.lua
@@ -0,0 +1,263 @@
+
+--- LuaRocks-specific path handling functions.
+-- All paths are configured in this module, making it a single
+-- point where the layout of the local installation is defined in LuaRocks.
+local path = {}
+
+local core = require("luarocks.core.path")
+local dir = require("luarocks.dir")
+local cfg = require("luarocks.core.cfg")
+local util = require("luarocks.util")
+
+path.rocks_dir = core.rocks_dir
+path.versioned_name = core.versioned_name
+path.path_to_module = core.path_to_module
+path.deploy_lua_dir = core.deploy_lua_dir
+path.deploy_lib_dir = core.deploy_lib_dir
+path.map_trees = core.map_trees
+path.rocks_tree_to_string = core.rocks_tree_to_string
+
+--- Infer rockspec filename from a rock filename.
+-- @param rock_name string: Pathname of a rock file.
+-- @return string: Filename of the rockspec, without path.
+function path.rockspec_name_from_rock(rock_name)
+ assert(type(rock_name) == "string")
+ local base_name = dir.base_name(rock_name)
+ return base_name:match("(.*)%.[^.]*.rock") .. ".rockspec"
+end
+
+function path.root_from_rocks_dir(rocks_dir)
+ assert(type(rocks_dir) == "string")
+ return rocks_dir:match("(.*)" .. util.matchquote(cfg.rocks_subdir) .. ".*$")
+end
+
+function path.root_dir(tree)
+ if type(tree) == "string" then
+ return tree
+ else
+ assert(type(tree) == "table")
+ return tree.root
+ end
+end
+
+function path.deploy_bin_dir(tree)
+ return dir.path(path.root_dir(tree), "bin")
+end
+
+function path.manifest_file(tree)
+ return dir.path(path.rocks_dir(tree), "manifest")
+end
+
+--- Get the directory for all versions of a package in a tree.
+-- @param name string: The package name.
+-- @return string: The resulting path -- does not guarantee that
+-- @param tree string or nil: If given, specifies the local tree to use.
+-- the package (and by extension, the path) exists.
+function path.versions_dir(name, tree)
+ assert(type(name) == "string" and not name:match("/"))
+ return dir.path(path.rocks_dir(tree), name)
+end
+
+--- Get the local installation directory (prefix) for a package.
+-- @param name string: The package name.
+-- @param version string: The package version.
+-- @param tree string or nil: If given, specifies the local tree to use.
+-- @return string: The resulting path -- does not guarantee that
+-- the package (and by extension, the path) exists.
+function path.install_dir(name, version, tree)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+ return dir.path(path.rocks_dir(tree), name, version)
+end
+
+--- Get the local filename of the rockspec of an installed rock.
+-- @param name string: The package name.
+-- @param version string: The package version.
+-- @param tree string or nil: If given, specifies the local tree to use.
+-- @return string: The resulting path -- does not guarantee that
+-- the package (and by extension, the file) exists.
+function path.rockspec_file(name, version, tree)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+ return dir.path(path.rocks_dir(tree), name, version, name.."-"..version..".rockspec")
+end
+
+--- Get the local filename of the rock_manifest file of an installed rock.
+-- @param name string: The package name.
+-- @param version string: The package version.
+-- @param tree string or nil: If given, specifies the local tree to use.
+-- @return string: The resulting path -- does not guarantee that
+-- the package (and by extension, the file) exists.
+function path.rock_manifest_file(name, version, tree)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+ return dir.path(path.rocks_dir(tree), name, version, "rock_manifest")
+end
+
+--- Get the local filename of the rock_namespace file of an installed rock.
+-- @param name string: The package name (without a namespace).
+-- @param version string: The package version.
+-- @param tree string or nil: If given, specifies the local tree to use.
+-- @return string: The resulting path -- does not guarantee that
+-- the package (and by extension, the file) exists.
+function path.rock_namespace_file(name, version, tree)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+ return dir.path(path.rocks_dir(tree), name, version, "rock_namespace")
+end
+
+--- Get the local installation directory for C libraries of a package.
+-- @param name string: The package name.
+-- @param version string: The package version.
+-- @param tree string or nil: If given, specifies the local tree to use.
+-- @return string: The resulting path -- does not guarantee that
+-- the package (and by extension, the path) exists.
+function path.lib_dir(name, version, tree)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+ return dir.path(path.rocks_dir(tree), name, version, "lib")
+end
+
+--- Get the local installation directory for Lua modules of a package.
+-- @param name string: The package name.
+-- @param version string: The package version.
+-- @param tree string or nil: If given, specifies the local tree to use.
+-- @return string: The resulting path -- does not guarantee that
+-- the package (and by extension, the path) exists.
+function path.lua_dir(name, version, tree)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+ return dir.path(path.rocks_dir(tree), name, version, "lua")
+end
+
+--- Get the local installation directory for documentation of a package.
+-- @param name string: The package name.
+-- @param version string: The package version.
+-- @param tree string or nil: If given, specifies the local tree to use.
+-- @return string: The resulting path -- does not guarantee that
+-- the package (and by extension, the path) exists.
+function path.doc_dir(name, version, tree)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+ return dir.path(path.rocks_dir(tree), name, version, "doc")
+end
+
+--- Get the local installation directory for configuration files of a package.
+-- @param name string: The package name.
+-- @param version string: The package version.
+-- @param tree string or nil: If given, specifies the local tree to use.
+-- @return string: The resulting path -- does not guarantee that
+-- the package (and by extension, the path) exists.
+function path.conf_dir(name, version, tree)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+ return dir.path(path.rocks_dir(tree), name, version, "conf")
+end
+
+--- Get the local installation directory for command-line scripts
+-- of a package.
+-- @param name string: The package name.
+-- @param version string: The package version.
+-- @param tree string or nil: If given, specifies the local tree to use.
+-- @return string: The resulting path -- does not guarantee that
+-- the package (and by extension, the path) exists.
+function path.bin_dir(name, version, tree)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+ return dir.path(path.rocks_dir(tree), name, version, "bin")
+end
+
+--- Extract name, version and arch of a rock filename,
+-- or name, version and "rockspec" from a rockspec name.
+-- @param file_name string: pathname of a rock or rockspec
+-- @return (string, string, string) or nil: name, version and arch
+-- or nil if name could not be parsed
+function path.parse_name(file_name)
+ assert(type(file_name) == "string")
+ if file_name:match("%.rock$") then
+ return dir.base_name(file_name):match("(.*)-([^-]+-%d+)%.([^.]+)%.rock$")
+ else
+ return dir.base_name(file_name):match("(.*)-([^-]+-%d+)%.(rockspec)")
+ end
+end
+
+--- Make a rockspec or rock URL.
+-- @param pathname string: Base URL or pathname.
+-- @param name string: Package name.
+-- @param version string: Package version.
+-- @param arch string: Architecture identifier, or "rockspec" or "installed".
+-- @return string: A URL or pathname following LuaRocks naming conventions.
+function path.make_url(pathname, name, version, arch)
+ assert(type(pathname) == "string")
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+ assert(type(arch) == "string")
+
+ local filename = name.."-"..version
+ if arch == "installed" then
+ filename = dir.path(name, version, filename..".rockspec")
+ elseif arch == "rockspec" then
+ filename = filename..".rockspec"
+ else
+ filename = filename.."."..arch..".rock"
+ end
+ return dir.path(pathname, filename)
+end
+
+--- Obtain the directory name where a module should be stored.
+-- For example, on Unix, "foo.bar.baz" will return "foo/bar".
+-- @param mod string: A module name in Lua dot-separated format.
+-- @return string: A directory name using the platform's separator.
+function path.module_to_path(mod)
+ assert(type(mod) == "string")
+ return (mod:gsub("[^.]*$", ""):gsub("%.", "/"))
+end
+
+function path.use_tree(tree)
+ cfg.root_dir = tree
+ cfg.rocks_dir = path.rocks_dir(tree)
+ cfg.deploy_bin_dir = path.deploy_bin_dir(tree)
+ cfg.deploy_lua_dir = path.deploy_lua_dir(tree)
+ cfg.deploy_lib_dir = path.deploy_lib_dir(tree)
+end
+
+function path.add_to_package_paths(tree)
+ package.path = dir.path(path.deploy_lua_dir(tree), "?.lua") .. ";"
+ .. dir.path(path.deploy_lua_dir(tree), "?/init.lua") .. ";"
+ .. package.path
+ package.cpath = dir.path(path.deploy_lib_dir(tree), "?." .. cfg.lib_extension) .. ";"
+ .. package.cpath
+end
+
+--- Get the namespace of a locally-installed rock, if any.
+-- @param name string: The rock name, without a namespace.
+-- @param version string: The rock version.
+-- @param tree string: The local tree to use.
+-- @return string?: The namespace if it exists, or nil.
+function path.read_namespace(name, version, tree)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+ assert(type(tree) == "string")
+
+ local namespace
+ local fd = io.open(path.rock_namespace_file(name, version, tree), "r")
+ if fd then
+ namespace = fd:read("*a")
+ fd:close()
+ end
+ return namespace
+end
+
+function path.package_paths(deps_mode)
+ local lpaths = {}
+ local lcpaths = {}
+ path.map_trees(deps_mode, function(tree)
+ local root = path.root_dir(tree)
+ table.insert(lpaths, dir.path(root, cfg.lua_modules_path, "?.lua"))
+ table.insert(lpaths, dir.path(root, cfg.lua_modules_path, "?/init.lua"))
+ table.insert(lcpaths, dir.path(root, cfg.lib_modules_path, "?." .. cfg.lib_extension))
+ end)
+ return table.concat(lpaths, ";"), table.concat(lcpaths, ";")
+end
+
+return path
diff --git a/src/luarocks/persist.lua b/src/luarocks/persist.lua
new file mode 100644
index 0000000..4dcd930
--- /dev/null
+++ b/src/luarocks/persist.lua
@@ -0,0 +1,259 @@
+
+--- Utility module for loading files into tables and
+-- saving tables into files.
+local persist = {}
+
+local core = require("luarocks.core.persist")
+local util = require("luarocks.util")
+local dir = require("luarocks.dir")
+local fs = require("luarocks.fs")
+
+persist.run_file = core.run_file
+persist.load_into_table = core.load_into_table
+
+local write_table
+
+--- Write a value as Lua code.
+-- This function handles only numbers and strings, invoking write_table
+-- to write tables.
+-- @param out table or userdata: a writer object supporting :write() method.
+-- @param v: the value to be written.
+-- @param level number: the indentation level
+-- @param sub_order table: optional prioritization table
+-- @see write_table
+function persist.write_value(out, v, level, sub_order)
+ if type(v) == "table" then
+ level = level or 0
+ write_table(out, v, level + 1, sub_order)
+ elseif type(v) == "string" then
+ if v:match("[\r\n]") then
+ local open, close = "[[", "]]"
+ local equals = 0
+ local v_with_bracket = v.."]"
+ while v_with_bracket:find(close, 1, true) do
+ equals = equals + 1
+ local eqs = ("="):rep(equals)
+ open, close = "["..eqs.."[", "]"..eqs.."]"
+ end
+ out:write(open.."\n"..v..close)
+ else
+ out:write(("%q"):format(v))
+ end
+ else
+ out:write(tostring(v))
+ end
+end
+
+local is_valid_plain_key
+do
+ local keywords = {
+ ["and"] = true,
+ ["break"] = true,
+ ["do"] = true,
+ ["else"] = true,
+ ["elseif"] = true,
+ ["end"] = true,
+ ["false"] = true,
+ ["for"] = true,
+ ["function"] = true,
+ ["goto"] = true,
+ ["if"] = true,
+ ["in"] = true,
+ ["local"] = true,
+ ["nil"] = true,
+ ["not"] = true,
+ ["or"] = true,
+ ["repeat"] = true,
+ ["return"] = true,
+ ["then"] = true,
+ ["true"] = true,
+ ["until"] = true,
+ ["while"] = true,
+ }
+ function is_valid_plain_key(k)
+ return type(k) == "string"
+ and k:match("^[a-zA-Z_][a-zA-Z0-9_]*$")
+ and not keywords[k]
+ end
+end
+
+local function write_table_key_assignment(out, k, level)
+ if is_valid_plain_key(k) then
+ out:write(k)
+ else
+ out:write("[")
+ persist.write_value(out, k, level)
+ out:write("]")
+ end
+
+ out:write(" = ")
+end
+
+--- Write a table as Lua code in curly brackets notation to a writer object.
+-- Only numbers, strings and tables (containing numbers, strings
+-- or other recursively processed tables) are supported.
+-- @param out table or userdata: a writer object supporting :write() method.
+-- @param tbl table: the table to be written.
+-- @param level number: the indentation level
+-- @param field_order table: optional prioritization table
+write_table = function(out, tbl, level, field_order)
+ out:write("{")
+ local sep = "\n"
+ local indentation = " "
+ local indent = true
+ local i = 1
+ for k, v, sub_order in util.sortedpairs(tbl, field_order) do
+ out:write(sep)
+ if indent then
+ for _ = 1, level do out:write(indentation) end
+ end
+
+ if k == i then
+ i = i + 1
+ else
+ write_table_key_assignment(out, k, level)
+ end
+
+ persist.write_value(out, v, level, sub_order)
+ if type(v) == "number" then
+ sep = ", "
+ indent = false
+ else
+ sep = ",\n"
+ indent = true
+ end
+ end
+ if sep ~= "\n" then
+ out:write("\n")
+ for _ = 1, level - 1 do out:write(indentation) end
+ end
+ out:write("}")
+end
+
+--- Write a table as series of assignments to a writer object.
+-- @param out table or userdata: a writer object supporting :write() method.
+-- @param tbl table: the table to be written.
+-- @param field_order table: optional prioritization table
+-- @return true if successful; nil and error message if failed.
+local function write_table_as_assignments(out, tbl, field_order)
+ for k, v, sub_order in util.sortedpairs(tbl, field_order) do
+ if not is_valid_plain_key(k) then
+ return nil, "cannot store '"..tostring(k).."' as a plain key."
+ end
+ out:write(k.." = ")
+ persist.write_value(out, v, 0, sub_order)
+ out:write("\n")
+ end
+ return true
+end
+
+--- Write a table using Lua table syntax to a writer object.
+-- @param out table or userdata: a writer object supporting :write() method.
+-- @param tbl table: the table to be written.
+local function write_table_as_table(out, tbl)
+ out:write("return {\n")
+ for k, v, sub_order in util.sortedpairs(tbl) do
+ out:write(" ")
+ write_table_key_assignment(out, k, 1)
+ persist.write_value(out, v, 1, sub_order)
+ out:write(",\n")
+ end
+ out:write("}\n")
+end
+
+--- Save the contents of a table to a string.
+-- Each element of the table is saved as a global assignment.
+-- Only numbers, strings and tables (containing numbers, strings
+-- or other recursively processed tables) are supported.
+-- @param tbl table: the table containing the data to be written
+-- @param field_order table: an optional array indicating the order of top-level fields.
+-- @return persisted data as string; or nil and an error message
+function persist.save_from_table_to_string(tbl, field_order)
+ local out = {buffer = {}}
+ function out:write(data) table.insert(self.buffer, data) end
+ local ok, err = write_table_as_assignments(out, tbl, field_order)
+ if not ok then
+ return nil, err
+ end
+ return table.concat(out.buffer)
+end
+
+--- Save the contents of a table in a file.
+-- Each element of the table is saved as a global assignment.
+-- Only numbers, strings and tables (containing numbers, strings
+-- or other recursively processed tables) are supported.
+-- @param filename string: the output filename
+-- @param tbl table: the table containing the data to be written
+-- @param field_order table: an optional array indicating the order of top-level fields.
+-- @return boolean or (nil, string): true if successful, or nil and a
+-- message in case of errors.
+function persist.save_from_table(filename, tbl, field_order)
+ local prefix = dir.dir_name(filename)
+ fs.make_dir(prefix)
+ local out = io.open(filename, "w")
+ if not out then
+ return nil, "Cannot create file at "..filename
+ end
+ local ok, err = write_table_as_assignments(out, tbl, field_order)
+ out:close()
+ if not ok then
+ return nil, err
+ end
+ return true
+end
+
+--- Save the contents of a table as a module.
+-- The module contains a 'return' statement that returns the table.
+-- Only numbers, strings and tables (containing numbers, strings
+-- or other recursively processed tables) are supported.
+-- @param filename string: the output filename
+-- @param tbl table: the table containing the data to be written
+-- @return boolean or (nil, string): true if successful, or nil and a
+-- message in case of errors.
+function persist.save_as_module(filename, tbl)
+ local out = io.open(filename, "w")
+ if not out then
+ return nil, "Cannot create file at "..filename
+ end
+ write_table_as_table(out, tbl)
+ out:close()
+ return true
+end
+
+function persist.load_config_file_if_basic(filename, cfg)
+ local env = {
+ home = cfg.home
+ }
+ local result, err, errcode = persist.load_into_table(filename, env)
+ if errcode == "load" or errcode == "run" then
+ -- bad config file or depends on env, so error out
+ return nil, "Could not read existing config file " .. filename
+ end
+
+ local tbl
+ if errcode == "open" then
+ -- could not open, maybe file does not exist
+ tbl = {}
+ else
+ tbl = result
+ tbl.home = nil
+ end
+
+ return tbl
+end
+
+function persist.save_default_lua_version(prefix, lua_version)
+ local ok, err = fs.make_dir(prefix)
+ if not ok then
+ return nil, err
+ end
+ local fd, err = io.open(dir.path(prefix, "default-lua-version.lua"), "w")
+ if not fd then
+ return nil, err
+ end
+ fd:write('return "' .. lua_version .. '"\n')
+ fd:close()
+ return true
+end
+
+return persist
diff --git a/src/luarocks/queries.lua b/src/luarocks/queries.lua
new file mode 100644
index 0000000..0c8790f
--- /dev/null
+++ b/src/luarocks/queries.lua
@@ -0,0 +1,217 @@
+
+local queries = {}
+
+local vers = require("luarocks.core.vers")
+local util = require("luarocks.util")
+local cfg = require("luarocks.core.cfg")
+
+local query_mt = {}
+
+query_mt.__index = query_mt
+
+function query_mt.type()
+ return "query"
+end
+
+-- Fallback default value for the `arch` field, if not explicitly set.
+query_mt.arch = {
+ src = true,
+ all = true,
+ rockspec = true,
+ installed = true,
+ -- [cfg.arch] = true, -- this is set later
+}
+
+-- Fallback default value for the `substring` field, if not explicitly set.
+query_mt.substring = false
+
+--- Convert the arch field of a query table to table format.
+-- @param input string, table or nil
+local function arch_to_table(input)
+ if type(input) == "table" then
+ return input
+ elseif type(input) == "string" then
+ local arch = {}
+ for a in input:gmatch("[%w_-]+") do
+ arch[a] = true
+ end
+ return arch
+ end
+end
+
+--- Prepare a query in dependency table format.
+-- @param name string: the package name.
+-- @param namespace string?: the package namespace.
+-- @param version string?: the package version.
+-- @param substring boolean?: match substrings of the name
+-- (default is false, match full name)
+-- @param arch string?: a string with pipe-separated accepted arch values
+-- @param operator string?: operator for version matching (default is "==")
+-- @return table: A query in table format
+function queries.new(name, namespace, version, substring, arch, operator)
+ assert(type(name) == "string")
+ assert(type(namespace) == "string" or not namespace)
+ assert(type(version) == "string" or not version)
+ assert(type(substring) == "boolean" or not substring)
+ assert(type(arch) == "string" or not arch)
+ assert(type(operator) == "string" or not operator)
+
+ operator = operator or "=="
+
+ local self = {
+ name = name,
+ namespace = namespace,
+ constraints = {},
+ substring = substring,
+ arch = arch_to_table(arch),
+ }
+ if version then
+ table.insert(self.constraints, { op = operator, version = vers.parse_version(version)})
+ end
+
+ query_mt.arch[cfg.arch] = true
+ return setmetatable(self, query_mt)
+end
+
+-- Query for all packages
+-- @param arch string (optional)
+function queries.all(arch)
+ assert(type(arch) == "string" or not arch)
+
+ return queries.new("", nil, nil, true, arch)
+end
+
+do
+ local parse_constraints
+ do
+ local parse_constraint
+ do
+ local operators = {
+ ["=="] = "==",
+ ["~="] = "~=",
+ [">"] = ">",
+ ["<"] = "<",
+ [">="] = ">=",
+ ["<="] = "<=",
+ ["~>"] = "~>",
+ -- plus some convenience translations
+ [""] = "==",
+ ["="] = "==",
+ ["!="] = "~="
+ }
+
+ --- Consumes a constraint from a string, converting it to table format.
+ -- For example, a string ">= 1.0, > 2.0" is converted to a table in the
+ -- format {op = ">=", version={1,0}} and the rest, "> 2.0", is returned
+ -- back to the caller.
+ -- @param input string: A list of constraints in string format.
+ -- @return (table, string) or nil: A table representing the same
+ -- constraints and the string with the unused input, or nil if the
+ -- input string is invalid.
+ parse_constraint = function(input)
+ assert(type(input) == "string")
+
+ local no_upgrade, op, version, rest = input:match("^(@?)([<>=~!]*)%s*([%w%.%_%-]+)[%s,]*(.*)")
+ local _op = operators[op]
+ version = vers.parse_version(version)
+ if not _op then
+ return nil, "Encountered bad constraint operator: '"..tostring(op).."' in '"..input.."'"
+ end
+ if not version then
+ return nil, "Could not parse version from constraint: '"..input.."'"
+ end
+ return { op = _op, version = version, no_upgrade = no_upgrade=="@" and true or nil }, rest
+ end
+ end
+
+ --- Convert a list of constraints from string to table format.
+ -- For example, a string ">= 1.0, < 2.0" is converted to a table in the format
+ -- {{op = ">=", version={1,0}}, {op = "<", version={2,0}}}.
+ -- Version tables use a metatable allowing later comparison through
+ -- relational operators.
+ -- @param input string: A list of constraints in string format.
+ -- @return table or nil: A table representing the same constraints,
+ -- or nil if the input string is invalid.
+ parse_constraints = function(input)
+ assert(type(input) == "string")
+
+ local constraints, oinput, constraint = {}, input
+ while #input > 0 do
+ constraint, input = parse_constraint(input)
+ if constraint then
+ table.insert(constraints, constraint)
+ else
+ return nil, "Failed to parse constraint '"..tostring(oinput).."' with error: ".. input
+ end
+ end
+ return constraints
+ end
+ end
+
+ --- Prepare a query in dependency table format.
+ -- @param depstr string: A dependency in string format
+ -- as entered in rockspec files.
+ -- @return table: A query in table format, or nil and an error message in case of errors.
+ function queries.from_dep_string(depstr)
+ assert(type(depstr) == "string")
+
+ local ns_name, rest = depstr:match("^%s*([a-zA-Z0-9%.%-%_]*/?[a-zA-Z0-9][a-zA-Z0-9%.%-%_]*)%s*([^/]*)")
+ if not ns_name then
+ return nil, "failed to extract dependency name from '"..depstr.."'"
+ end
+
+ ns_name = ns_name:lower()
+
+ local constraints, err = parse_constraints(rest)
+ if not constraints then
+ return nil, err
+ end
+
+ local name, namespace = util.split_namespace(ns_name)
+
+ local self = {
+ name = name,
+ namespace = namespace,
+ constraints = constraints,
+ }
+
+ query_mt.arch[cfg.arch] = true
+ return setmetatable(self, query_mt)
+ end
+end
+
+function queries.from_persisted_table(tbl)
+ query_mt.arch[cfg.arch] = true
+ return setmetatable(tbl, query_mt)
+end
+
+--- Build a string representation of a query package name.
+-- Includes namespace, name and version, but not arch or constraints.
+-- @param query table: a query table
+-- @return string: a result such as `my_user/my_rock 1.0` or `my_rock`.
+function query_mt:__tostring()
+ local out = {}
+ if self.namespace then
+ table.insert(out, self.namespace)
+ table.insert(out, "/")
+ end
+ table.insert(out, self.name)
+
+ if #self.constraints > 0 then
+ local pretty = {}
+ for _, c in ipairs(self.constraints) do
+ local v = c.version.string
+ if c.op == "==" then
+ table.insert(pretty, v)
+ else
+ table.insert(pretty, c.op .. " " .. v)
+ end
+ end
+ table.insert(out, " ")
+ table.insert(out, table.concat(pretty, ", "))
+ end
+
+ return table.concat(out)
+end
+
+return queries
diff --git a/src/luarocks/remove.lua b/src/luarocks/remove.lua
new file mode 100644
index 0000000..a24b54b
--- /dev/null
+++ b/src/luarocks/remove.lua
@@ -0,0 +1,135 @@
+local remove = {}
+
+local search = require("luarocks.search")
+local deps = require("luarocks.deps")
+local fetch = require("luarocks.fetch")
+local repos = require("luarocks.repos")
+local path = require("luarocks.path")
+local util = require("luarocks.util")
+local cfg = require("luarocks.core.cfg")
+local manif = require("luarocks.manif")
+local queries = require("luarocks.queries")
+
+--- Obtain a list of packages that depend on the given set of packages
+-- (where all packages of the set are versions of one program).
+-- @param name string: the name of a program
+-- @param versions array of string: the versions to be deleted.
+-- @return array of string: an empty table if no packages depend on any
+-- of the given list, or an array of strings in "name/version" format.
+local function check_dependents(name, versions, deps_mode)
+ local dependents = {}
+
+ local skip_set = {}
+ skip_set[name] = {}
+ for version, _ in pairs(versions) do
+ skip_set[name][version] = true
+ end
+
+ local local_rocks = {}
+ local query_all = queries.all()
+ search.local_manifest_search(local_rocks, cfg.rocks_dir, query_all)
+ local_rocks[name] = nil
+ for rock_name, rock_versions in pairs(local_rocks) do
+ for rock_version, _ in pairs(rock_versions) do
+ local rockspec, err = fetch.load_rockspec(path.rockspec_file(rock_name, rock_version))
+ if rockspec then
+ local _, missing = deps.match_deps(rockspec.dependencies, rockspec.rocks_provided, skip_set, deps_mode)
+ if missing[name] then
+ table.insert(dependents, { name = rock_name, version = rock_version })
+ end
+ end
+ end
+ end
+
+ return dependents
+end
+
+--- Delete given versions of a program.
+-- @param name string: the name of a program
+-- @param versions array of string: the versions to be deleted.
+-- @param deps_mode: string: Which trees to check dependencies for:
+-- "one" for the current default tree, "all" for all trees,
+-- "order" for all trees with priority >= the current default, "none" for no trees.
+-- @return boolean or (nil, string): true on success or nil and an error message.
+local function delete_versions(name, versions, deps_mode)
+
+ for version, _ in pairs(versions) do
+ util.printout("Removing "..name.." "..version.."...")
+ local ok, err = repos.delete_version(name, version, deps_mode)
+ if not ok then return nil, err end
+ end
+
+ return true
+end
+
+function remove.remove_search_results(results, name, deps_mode, force, fast)
+ local versions = results[name]
+
+ local version = next(versions)
+ local second = next(versions, version)
+
+ local dependents = {}
+ if not fast then
+ util.printout("Checking stability of dependencies in the absence of")
+ util.printout(name.." "..table.concat(util.keys(versions), ", ").."...")
+ util.printout()
+ dependents = check_dependents(name, versions, deps_mode)
+ end
+
+ if #dependents > 0 then
+ if force or fast then
+ util.printerr("The following packages may be broken by this forced removal:")
+ for _, dependent in ipairs(dependents) do
+ util.printerr(dependent.name.." "..dependent.version)
+ end
+ util.printerr()
+ else
+ if not second then
+ util.printerr("Will not remove "..name.." "..version..".")
+ util.printerr("Removing it would break dependencies for: ")
+ else
+ util.printerr("Will not remove installed versions of "..name..".")
+ util.printerr("Removing them would break dependencies for: ")
+ end
+ for _, dependent in ipairs(dependents) do
+ util.printerr(dependent.name.." "..dependent.version)
+ end
+ util.printerr()
+ util.printerr("Use --force to force removal (warning: this may break modules).")
+ return nil, "Failed removing."
+ end
+ end
+
+ local ok, err = delete_versions(name, versions, deps_mode)
+ if not ok then return nil, err end
+
+ util.printout("Removal successful.")
+ return true
+end
+
+function remove.remove_other_versions(name, version, force, fast)
+ local results = {}
+ local query = queries.new(name, nil, version, false, nil, "~=")
+ search.local_manifest_search(results, cfg.rocks_dir, query)
+ local warn
+ if results[name] then
+ local ok, err = remove.remove_search_results(results, name, cfg.deps_mode, force, fast)
+ if not ok then -- downgrade failure to a warning
+ warn = err
+ end
+ end
+
+ if not fast then
+ -- since we're not using --keep, this means that all files of the rock being installed
+ -- should be available as non-versioned variants. Double-check that:
+ local rock_manifest, load_err = manif.load_rock_manifest(name, version)
+ local ok, err = repos.check_everything_is_installed(name, version, rock_manifest, cfg.root_dir, false)
+ if not ok then
+ return nil, err
+ end
+ end
+
+ return true, nil, warn
+end
+
+return remove
diff --git a/src/luarocks/repos.lua b/src/luarocks/repos.lua
new file mode 100644
index 0000000..764fe3a
--- /dev/null
+++ b/src/luarocks/repos.lua
@@ -0,0 +1,697 @@
+
+--- Functions for managing the repository on disk.
+local repos = {}
+
+local fs = require("luarocks.fs")
+local path = require("luarocks.path")
+local cfg = require("luarocks.core.cfg")
+local util = require("luarocks.util")
+local dir = require("luarocks.dir")
+local manif = require("luarocks.manif")
+local vers = require("luarocks.core.vers")
+
+local unpack = unpack or table.unpack -- luacheck: ignore 211
+
+--- Get type and name of an item (a module or a command) provided by a file.
+-- @param deploy_type string: rock manifest subtree the file comes from ("bin", "lua", or "lib").
+-- @param file_path string: path to the file relatively to deploy_type subdirectory.
+-- @return (string, string): item type ("module" or "command") and name.
+local function get_provided_item(deploy_type, file_path)
+ assert(type(deploy_type) == "string")
+ assert(type(file_path) == "string")
+ local item_type = deploy_type == "bin" and "command" or "module"
+ local item_name = item_type == "command" and file_path or path.path_to_module(file_path)
+ return item_type, item_name
+end
+
+-- Tree of files installed by a package are stored
+-- in its rock manifest. Some of these files have to
+-- be deployed to locations where Lua can load them as
+-- modules or where they can be used as commands.
+-- These files are characterised by pair
+-- (deploy_type, file_path), where deploy_type is the first
+-- component of the file path and file_path is the rest of the
+-- path. Only files with deploy_type in {"lua", "lib", "bin"}
+-- are deployed somewhere.
+-- Each deployed file provides an "item". An item is
+-- characterised by pair (item_type, item_name).
+-- item_type is "command" for files with deploy_type
+-- "bin" and "module" for deploy_type in {"lua", "lib"}.
+-- item_name is same as file_path for commands
+-- and is produced using path.path_to_module(file_path)
+-- for modules.
+
+--- Get all installed versions of a package.
+-- @param name string: a package name.
+-- @return table or nil: An array of strings listing installed
+-- versions of a package, or nil if none is available.
+local function get_installed_versions(name)
+ assert(type(name) == "string" and not name:match("/"))
+
+ local dirs = fs.list_dir(path.versions_dir(name))
+ return (dirs and #dirs > 0) and dirs or nil
+end
+
+--- Check if a package exists in a local repository.
+-- Version numbers are compared as exact string comparison.
+-- @param name string: name of package
+-- @param version string: package version in string format
+-- @return boolean: true if a package is installed,
+-- false otherwise.
+function repos.is_installed(name, version)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+
+ return fs.is_dir(path.install_dir(name, version))
+end
+
+function repos.recurse_rock_manifest_entry(entry, action)
+ assert(type(action) == "function")
+
+ if entry == nil then
+ return true
+ end
+
+ local function do_recurse_rock_manifest_entry(tree, parent_path)
+
+ for file, sub in pairs(tree) do
+ local sub_path = (parent_path and (parent_path .. "/") or "") .. file
+ local ok, err -- luacheck: ignore 231
+
+ if type(sub) == "table" then
+ ok, err = do_recurse_rock_manifest_entry(sub, sub_path)
+ else
+ ok, err = action(sub_path)
+ end
+
+ if err then return nil, err end
+ end
+ return true
+ end
+ return do_recurse_rock_manifest_entry(entry)
+end
+
+local function store_package_data(result, rock_manifest, deploy_type)
+ if rock_manifest[deploy_type] then
+ repos.recurse_rock_manifest_entry(rock_manifest[deploy_type], function(file_path)
+ local _, item_name = get_provided_item(deploy_type, file_path)
+ result[item_name] = file_path
+ return true
+ end)
+ end
+end
+
+--- Obtain a table of modules within an installed package.
+-- @param name string: The package name; for example "luasocket"
+-- @param version string: The exact version number including revision;
+-- for example "2.0.1-1".
+-- @return table: A table of modules where keys are module names
+-- and values are file paths of files providing modules
+-- relative to "lib" or "lua" rock manifest subtree.
+-- If no modules are found or if package name or version
+-- are invalid, an empty table is returned.
+function repos.package_modules(name, version)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+
+ local result = {}
+ local rock_manifest = manif.load_rock_manifest(name, version)
+ if not rock_manifest then return result end
+ store_package_data(result, rock_manifest, "lib")
+ store_package_data(result, rock_manifest, "lua")
+ return result
+end
+
+--- Obtain a table of command-line scripts within an installed package.
+-- @param name string: The package name; for example "luasocket"
+-- @param version string: The exact version number including revision;
+-- for example "2.0.1-1".
+-- @return table: A table of commands where keys and values are command names
+-- as strings - file paths of files providing commands
+-- relative to "bin" rock manifest subtree.
+-- If no commands are found or if package name or version
+-- are invalid, an empty table is returned.
+function repos.package_commands(name, version)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+
+ local result = {}
+ local rock_manifest = manif.load_rock_manifest(name, version)
+ if not rock_manifest then return result end
+ store_package_data(result, rock_manifest, "bin")
+ return result
+end
+
+
+--- Check if a rock contains binary executables.
+-- @param name string: name of an installed rock
+-- @param version string: version of an installed rock
+-- @return boolean: returns true if rock contains platform-specific
+-- binary executables, or false if it is a pure-Lua rock.
+function repos.has_binaries(name, version)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+
+ local rock_manifest = manif.load_rock_manifest(name, version)
+ if rock_manifest and rock_manifest.bin then
+ for bin_name, md5 in pairs(rock_manifest.bin) do
+ -- TODO verify that it is the same file. If it isn't, find the actual command.
+ if fs.is_actual_binary(dir.path(cfg.deploy_bin_dir, bin_name)) then
+ return true
+ end
+ end
+ end
+ return false
+end
+
+function repos.run_hook(rockspec, hook_name)
+ assert(rockspec:type() == "rockspec")
+ assert(type(hook_name) == "string")
+
+ local hooks = rockspec.hooks
+ if not hooks then
+ return true
+ end
+
+ if cfg.hooks_enabled == false then
+ return nil, "This rockspec contains hooks, which are blocked by the 'hooks_enabled' setting in your LuaRocks configuration."
+ end
+
+ if not hooks.substituted_variables then
+ util.variable_substitutions(hooks, rockspec.variables)
+ hooks.substituted_variables = true
+ end
+ local hook = hooks[hook_name]
+ if hook then
+ util.printout(hook)
+ if not fs.execute(hook) then
+ return nil, "Failed running "..hook_name.." hook."
+ end
+ end
+ return true
+end
+
+function repos.should_wrap_bin_scripts(rockspec)
+ assert(rockspec:type() == "rockspec")
+
+ if cfg.wrap_bin_scripts ~= nil then
+ return cfg.wrap_bin_scripts
+ end
+ if rockspec.deploy and rockspec.deploy.wrap_bin_scripts == false then
+ return false
+ end
+ return true
+end
+
+local function find_suffixed(file, suffix)
+ local filenames = {file}
+ if suffix and suffix ~= "" then
+ table.insert(filenames, 1, file .. suffix)
+ end
+
+ for _, filename in ipairs(filenames) do
+ if fs.exists(filename) then
+ return filename
+ end
+ end
+
+ return nil, table.concat(filenames, ", ") .. " not found"
+end
+
+local function check_suffix(filename, suffix)
+ local suffixed_filename, err = find_suffixed(filename, suffix)
+ if not suffixed_filename then
+ return ""
+ end
+ return suffixed_filename:sub(#filename + 1)
+end
+
+-- Files can be deployed using versioned and non-versioned names.
+-- Several items with same type and name can exist if they are
+-- provided by different packages or versions. In any case
+-- item from the newest version of lexicographically smallest package
+-- is deployed using non-versioned name and others use versioned names.
+
+local function get_deploy_paths(name, version, deploy_type, file_path, repo)
+ assert(type(name) == "string")
+ assert(type(version) == "string")
+ assert(type(deploy_type) == "string")
+ assert(type(file_path) == "string")
+
+ repo = repo or cfg.root_dir
+ local deploy_dir = path["deploy_" .. deploy_type .. "_dir"](repo)
+ local non_versioned = dir.path(deploy_dir, file_path)
+ local versioned = path.versioned_name(non_versioned, deploy_dir, name, version)
+ return { nv = non_versioned, v = versioned }
+end
+
+local function check_spot_if_available(name, version, deploy_type, file_path)
+ local item_type, item_name = get_provided_item(deploy_type, file_path)
+ local cur_name, cur_version = manif.get_current_provider(item_type, item_name)
+
+ -- older versions of LuaRocks (< 3) registered "foo.init" files as "foo"
+ -- (which caused problems, so that behavior was changed). But look for that
+ -- in the manifest anyway for backward compatibility.
+ if not cur_name and deploy_type == "lua" and item_name:match("%.init$") then
+ cur_name, cur_version = manif.get_current_provider(item_type, (item_name:gsub("%.init$", "")))
+ end
+
+ if (not cur_name)
+ or (name < cur_name)
+ or (name == cur_name and (version == cur_version
+ or vers.compare_versions(version, cur_version))) then
+ return "nv", cur_name, cur_version, item_name
+ else
+ -- Existing version has priority, deploy new version using versioned name.
+ return "v", cur_name, cur_version, item_name
+ end
+end
+
+local function backup_existing(should_backup, target)
+ if not should_backup then
+ fs.delete(target)
+ return
+ end
+ if fs.exists(target) then
+ local backup = target
+ repeat
+ backup = backup.."~"
+ until not fs.exists(backup) -- Slight race condition here, but shouldn't be a problem.
+
+ util.warning(target.." is not tracked by this installation of LuaRocks. Moving it to "..backup)
+ local move_ok, move_err = os.rename(target, backup)
+ if not move_ok then
+ return nil, move_err
+ end
+ return backup
+ end
+end
+
+local function prepare_op_install()
+ local mkdirs = {}
+ local rmdirs = {}
+
+ local function memoize_mkdir(d)
+ if mkdirs[d] then
+ return true
+ end
+ local ok, err = fs.make_dir(d)
+ if not ok then
+ return nil, err
+ end
+ mkdirs[d] = true
+ return true
+ end
+
+ local function op_install(op)
+ local ok, err = memoize_mkdir(dir.dir_name(op.dst))
+ if not ok then
+ return nil, err
+ end
+
+ local backup, err = backup_existing(op.backup, op.dst)
+ if err then
+ return nil, err
+ end
+ if backup then
+ op.backup_file = backup
+ end
+
+ ok, err = op.fn(op.src, op.dst, op.backup)
+ if not ok then
+ return nil, err
+ end
+
+ rmdirs[dir.dir_name(op.src)] = true
+ return true
+ end
+
+ local function done_op_install()
+ for d, _ in pairs(rmdirs) do
+ fs.remove_dir_tree_if_empty(d)
+ end
+ end
+
+ return op_install, done_op_install
+end
+
+local function rollback_install(op)
+ fs.delete(op.dst)
+ if op.backup_file then
+ os.rename(op.backup_file, op.dst)
+ end
+ fs.remove_dir_tree_if_empty(dir.dir_name(op.dst))
+ return true
+end
+
+local function op_rename(op)
+ if op.suffix then
+ local suffix = check_suffix(op.src, op.suffix)
+ op.src = op.src .. suffix
+ op.dst = op.dst .. suffix
+ end
+
+ if fs.exists(op.src) then
+ fs.make_dir(dir.dir_name(op.dst))
+ fs.delete(op.dst)
+ local ok, err = os.rename(op.src, op.dst)
+ fs.remove_dir_tree_if_empty(dir.dir_name(op.src))
+ return ok, err
+ else
+ return true
+ end
+end
+
+local function rollback_rename(op)
+ return op_rename({ src = op.dst, dst = op.src })
+end
+
+local function prepare_op_delete()
+ local deletes = {}
+ local rmdirs = {}
+
+ local function done_op_delete()
+ for _, f in ipairs(deletes) do
+ os.remove(f)
+ end
+
+ for d, _ in pairs(rmdirs) do
+ fs.remove_dir_tree_if_empty(d)
+ end
+ end
+
+ local function op_delete(op)
+ if op.suffix then
+ local suffix = check_suffix(op.name, op.suffix)
+ op.name = op.name .. suffix
+ end
+
+ table.insert(deletes, op.name)
+
+ rmdirs[dir.dir_name(op.name)] = true
+ end
+
+ return op_delete, done_op_delete
+end
+
+local function rollback_ops(ops, op_fn, n)
+ for i = 1, n do
+ op_fn(ops[i])
+ end
+end
+
+--- Double check that all files referenced in `rock_manifest` are installed in `repo`.
+function repos.check_everything_is_installed(name, version, rock_manifest, repo, accept_versioned)
+ local missing = {}
+ local suffix = cfg.wrapper_suffix or ""
+ for _, category in ipairs({"bin", "lua", "lib"}) do
+ if rock_manifest[category] then
+ repos.recurse_rock_manifest_entry(rock_manifest[category], function(file_path)
+ local paths = get_deploy_paths(name, version, category, file_path, repo)
+ if category == "bin" then
+ if (fs.exists(paths.nv) or fs.exists(paths.nv .. suffix))
+ or (accept_versioned and (fs.exists(paths.v) or fs.exists(paths.v .. suffix))) then
+ return
+ end
+ else
+ if fs.exists(paths.nv) or (accept_versioned and fs.exists(paths.v)) then
+ return
+ end
+ end
+ table.insert(missing, paths.nv)
+ end)
+ end
+ end
+ if #missing > 0 then
+ return nil, "failed deploying files. " ..
+ "The following files were not installed:\n" ..
+ table.concat(missing, "\n")
+ end
+ return true
+end
+
+--- Deploy a package from the rocks subdirectory.
+-- @param name string: name of package
+-- @param version string: exact package version in string format
+-- @param wrap_bin_scripts bool: whether commands written in Lua should be wrapped.
+-- @param deps_mode: string: Which trees to check dependencies for:
+-- "one" for the current default tree, "all" for all trees,
+-- "order" for all trees with priority >= the current default, "none" for no trees.
+function repos.deploy_files(name, version, wrap_bin_scripts, deps_mode)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+ assert(type(wrap_bin_scripts) == "boolean")
+
+ local rock_manifest, load_err = manif.load_rock_manifest(name, version)
+ if not rock_manifest then return nil, load_err end
+
+ local repo = cfg.root_dir
+ local renames = {}
+ local installs = {}
+
+ local function install_binary(source, target)
+ if wrap_bin_scripts and fs.is_lua(source) then
+ return fs.wrap_script(source, target, deps_mode, name, version)
+ else
+ return fs.copy_binary(source, target)
+ end
+ end
+
+ local function move_lua(source, target)
+ return fs.move(source, target, "read")
+ end
+
+ local function move_lib(source, target)
+ return fs.move(source, target, "exec")
+ end
+
+ if rock_manifest.bin then
+ local source_dir = path.bin_dir(name, version)
+ repos.recurse_rock_manifest_entry(rock_manifest.bin, function(file_path)
+ local source = dir.path(source_dir, file_path)
+ local paths = get_deploy_paths(name, version, "bin", file_path, repo)
+ local mode, cur_name, cur_version = check_spot_if_available(name, version, "bin", file_path)
+
+ if mode == "nv" and cur_name then
+ local cur_paths = get_deploy_paths(cur_name, cur_version, "bin", file_path, repo)
+ table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v, suffix = cfg.wrapper_suffix })
+ end
+ local target = mode == "nv" and paths.nv or paths.v
+ local backup = name ~= cur_name or version ~= cur_version
+ if wrap_bin_scripts and fs.is_lua(source) then
+ target = target .. (cfg.wrapper_suffix or "")
+ end
+ table.insert(installs, { fn = install_binary, src = source, dst = target, backup = backup })
+ end)
+ end
+
+ if rock_manifest.lua then
+ local source_dir = path.lua_dir(name, version)
+ repos.recurse_rock_manifest_entry(rock_manifest.lua, function(file_path)
+ local source = dir.path(source_dir, file_path)
+ local paths = get_deploy_paths(name, version, "lua", file_path, repo)
+ local mode, cur_name, cur_version = check_spot_if_available(name, version, "lua", file_path)
+
+ if mode == "nv" and cur_name then
+ local cur_paths = get_deploy_paths(cur_name, cur_version, "lua", file_path, repo)
+ table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v })
+ cur_paths = get_deploy_paths(cur_name, cur_version, "lib", file_path:gsub("%.lua$", "." .. cfg.lib_extension), repo)
+ table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v })
+ end
+ local target = mode == "nv" and paths.nv or paths.v
+ local backup = name ~= cur_name or version ~= cur_version
+ table.insert(installs, { fn = move_lua, src = source, dst = target, backup = backup })
+ end)
+ end
+
+ if rock_manifest.lib then
+ local source_dir = path.lib_dir(name, version)
+ repos.recurse_rock_manifest_entry(rock_manifest.lib, function(file_path)
+ local source = dir.path(source_dir, file_path)
+ local paths = get_deploy_paths(name, version, "lib", file_path, repo)
+ local mode, cur_name, cur_version = check_spot_if_available(name, version, "lib", file_path)
+
+ if mode == "nv" and cur_name then
+ local cur_paths = get_deploy_paths(cur_name, cur_version, "lua", file_path:gsub("%.[^.]+$", ".lua"), repo)
+ table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v })
+ cur_paths = get_deploy_paths(cur_name, cur_version, "lib", file_path, repo)
+ table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v })
+ end
+ local target = mode == "nv" and paths.nv or paths.v
+ local backup = name ~= cur_name or version ~= cur_version
+ table.insert(installs, { fn = move_lib, src = source, dst = target, backup = backup })
+ end)
+ end
+
+ for i, op in ipairs(renames) do
+ local ok, err = op_rename(op)
+ if not ok then
+ rollback_ops(renames, rollback_rename, i - 1)
+ return nil, err
+ end
+ end
+ local op_install, done_op_install = prepare_op_install()
+ for i, op in ipairs(installs) do
+ local ok, err = op_install(op)
+ if not ok then
+ rollback_ops(installs, rollback_install, i - 1)
+ rollback_ops(renames, rollback_rename, #renames)
+ return nil, err
+ end
+ end
+ done_op_install()
+
+ local ok, err = repos.check_everything_is_installed(name, version, rock_manifest, repo, true)
+ if not ok then
+ return nil, err
+ end
+
+ local writer = require("luarocks.manif.writer")
+ return writer.add_to_manifest(name, version, nil, deps_mode)
+end
+
+local function add_to_double_checks(double_checks, name, version)
+ double_checks[name] = double_checks[name] or {}
+ double_checks[name][version] = true
+end
+
+local function double_check_all(double_checks, repo)
+ local errs = {}
+ for next_name, versions in pairs(double_checks) do
+ for next_version in pairs(versions) do
+ local rock_manifest, load_err = manif.load_rock_manifest(next_name, next_version)
+ local ok, err = repos.check_everything_is_installed(next_name, next_version, rock_manifest, repo, true)
+ if not ok then
+ table.insert(errs, err)
+ end
+ end
+ end
+ if next(errs) then
+ return nil, table.concat(errs, "\n")
+ end
+ return true
+end
+
+--- Delete a package from the local repository.
+-- @param name string: name of package
+-- @param version string: exact package version in string format
+-- @param deps_mode: string: Which trees to check dependencies for:
+-- "one" for the current default tree, "all" for all trees,
+-- "order" for all trees with priority >= the current default, "none" for no trees.
+-- @param quick boolean: do not try to fix the versioned name
+-- of another version that provides the same module that
+-- was deleted. This is used during 'purge', as every module
+-- will be eventually deleted.
+function repos.delete_version(name, version, deps_mode, quick)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+ assert(type(deps_mode) == "string")
+
+ local rock_manifest, load_err = manif.load_rock_manifest(name, version)
+ if not rock_manifest then
+ if not quick then
+ local writer = require("luarocks.manif.writer")
+ writer.remove_from_manifest(name, version, nil, deps_mode)
+ return nil, "rock_manifest file not found for "..name.." "..version.." - removed entry from the manifest"
+ end
+ return nil, load_err
+ end
+
+ local repo = cfg.root_dir
+ local renames = {}
+ local deletes = {}
+
+ local double_checks = {}
+
+ if rock_manifest.bin then
+ repos.recurse_rock_manifest_entry(rock_manifest.bin, function(file_path)
+ local paths = get_deploy_paths(name, version, "bin", file_path, repo)
+ local mode, cur_name, cur_version, item_name = check_spot_if_available(name, version, "bin", file_path)
+ if mode == "v" then
+ table.insert(deletes, { name = paths.v, suffix = cfg.wrapper_suffix })
+ else
+ table.insert(deletes, { name = paths.nv, suffix = cfg.wrapper_suffix })
+
+ local next_name, next_version = manif.get_next_provider("command", item_name)
+ if next_name then
+ add_to_double_checks(double_checks, next_name, next_version)
+ local next_paths = get_deploy_paths(next_name, next_version, "bin", file_path, repo)
+ table.insert(renames, { src = next_paths.v, dst = next_paths.nv, suffix = cfg.wrapper_suffix })
+ end
+ end
+ end)
+ end
+
+ if rock_manifest.lua then
+ repos.recurse_rock_manifest_entry(rock_manifest.lua, function(file_path)
+ local paths = get_deploy_paths(name, version, "lua", file_path, repo)
+ local mode, cur_name, cur_version, item_name = check_spot_if_available(name, version, "lua", file_path)
+ if mode == "v" then
+ table.insert(deletes, { name = paths.v })
+ else
+ table.insert(deletes, { name = paths.nv })
+
+ local next_name, next_version = manif.get_next_provider("module", item_name)
+ if next_name then
+ add_to_double_checks(double_checks, next_name, next_version)
+ local next_lua_paths = get_deploy_paths(next_name, next_version, "lua", file_path, repo)
+ table.insert(renames, { src = next_lua_paths.v, dst = next_lua_paths.nv })
+ local next_lib_paths = get_deploy_paths(next_name, next_version, "lib", file_path:gsub("%.[^.]+$", ".lua"), repo)
+ table.insert(renames, { src = next_lib_paths.v, dst = next_lib_paths.nv })
+ end
+ end
+ end)
+ end
+
+ if rock_manifest.lib then
+ repos.recurse_rock_manifest_entry(rock_manifest.lib, function(file_path)
+ local paths = get_deploy_paths(name, version, "lib", file_path, repo)
+ local mode, cur_name, cur_version, item_name = check_spot_if_available(name, version, "lib", file_path)
+ if mode == "v" then
+ table.insert(deletes, { name = paths.v })
+ else
+ table.insert(deletes, { name = paths.nv })
+
+ local next_name, next_version = manif.get_next_provider("module", item_name)
+ if next_name then
+ add_to_double_checks(double_checks, next_name, next_version)
+ local next_lua_paths = get_deploy_paths(next_name, next_version, "lua", file_path:gsub("%.[^.]+$", ".lua"), repo)
+ table.insert(renames, { src = next_lua_paths.v, dst = next_lua_paths.nv })
+ local next_lib_paths = get_deploy_paths(next_name, next_version, "lib", file_path, repo)
+ table.insert(renames, { src = next_lib_paths.v, dst = next_lib_paths.nv })
+ end
+ end
+ end)
+ end
+
+ local op_delete, done_op_delete = prepare_op_delete()
+ for _, op in ipairs(deletes) do
+ op_delete(op)
+ end
+ done_op_delete()
+
+ if not quick then
+ for _, op in ipairs(renames) do
+ op_rename(op)
+ end
+
+ local ok, err = double_check_all(double_checks, repo)
+ if not ok then
+ return nil, err
+ end
+ end
+
+ fs.delete(path.install_dir(name, version))
+ if not get_installed_versions(name) then
+ fs.delete(dir.path(cfg.rocks_dir, name))
+ end
+
+ if quick then
+ return true
+ end
+
+ local writer = require("luarocks.manif.writer")
+ return writer.remove_from_manifest(name, version, nil, deps_mode)
+end
+
+return repos
diff --git a/src/luarocks/require.lua b/src/luarocks/require.lua
new file mode 100644
index 0000000..902bd1a
--- /dev/null
+++ b/src/luarocks/require.lua
@@ -0,0 +1,2 @@
+--- Retained for compatibility reasons only. Use luarocks.loader instead.
+return require("luarocks.loader")
diff --git a/src/luarocks/results.lua b/src/luarocks/results.lua
new file mode 100644
index 0000000..c14862d
--- /dev/null
+++ b/src/luarocks/results.lua
@@ -0,0 +1,62 @@
+local results = {}
+
+local vers = require("luarocks.core.vers")
+local util = require("luarocks.util")
+
+local result_mt = {}
+
+result_mt.__index = result_mt
+
+function result_mt.type()
+ return "result"
+end
+
+function results.new(name, version, repo, arch, namespace)
+ assert(type(name) == "string" and not name:match("/"))
+ assert(type(version) == "string")
+ assert(type(repo) == "string")
+ assert(type(arch) == "string" or not arch)
+ assert(type(namespace) == "string" or not namespace)
+
+ if not namespace then
+ name, namespace = util.split_namespace(name)
+ end
+
+ local self = {
+ name = name,
+ version = version,
+ namespace = namespace,
+ arch = arch,
+ repo = repo,
+ }
+
+ return setmetatable(self, result_mt)
+end
+
+--- Test the name field of a query.
+-- If query has a boolean field substring set to true,
+-- then substring match is performed; otherwise, exact string
+-- comparison is done.
+-- @param query table: A query in dependency table format.
+-- @param name string: A package name.
+-- @return boolean: True if names match, false otherwise.
+local function match_name(query, name)
+ if query.substring then
+ return name:find(query.name, 0, true) and true or false
+ else
+ return name == query.name
+ end
+end
+
+--- Returns true if the result satisfies a given query.
+-- @param query: a query.
+-- @return boolean.
+function result_mt:satisfies(query)
+ assert(query:type() == "query")
+ return match_name(query, self.name)
+ and (query.arch[self.arch] or query.arch["any"])
+ and ((not query.namespace) or (query.namespace == self.namespace))
+ and vers.match_constraints(vers.parse_version(self.version), query.constraints)
+end
+
+return results
diff --git a/src/luarocks/rockspecs.lua b/src/luarocks/rockspecs.lua
new file mode 100644
index 0000000..454bab7
--- /dev/null
+++ b/src/luarocks/rockspecs.lua
@@ -0,0 +1,183 @@
+local rockspecs = {}
+
+local cfg = require("luarocks.core.cfg")
+local dir = require("luarocks.dir")
+local path = require("luarocks.path")
+local queries = require("luarocks.queries")
+local type_rockspec = require("luarocks.type.rockspec")
+local util = require("luarocks.util")
+local vers = require("luarocks.core.vers")
+
+local vendored_build_type_set = {
+ ["builtin"] = true,
+ ["cmake"] = true,
+ ["command"] = true,
+ ["make"] = true,
+ ["module"] = true, -- compatibility alias
+ ["none"] = true,
+}
+
+local rockspec_mt = {}
+
+rockspec_mt.__index = rockspec_mt
+
+function rockspec_mt.type()
+ return "rockspec"
+end
+
+--- Perform platform-specific overrides on a table.
+-- Overrides values of table with the contents of the appropriate
+-- subset of its "platforms" field. The "platforms" field should
+-- be a table containing subtables keyed with strings representing
+-- platform names. Names that match the contents of the global
+-- detected platforms setting are used. For example, if
+-- platform "unix" is detected, then the fields of
+-- tbl.platforms.unix will overwrite those of tbl with the same
+-- names. For table values, the operation is performed recursively
+-- (tbl.platforms.foo.x.y.z overrides tbl.x.y.z; other contents of
+-- tbl.x are preserved).
+-- @param tbl table or nil: Table which may contain a "platforms" field;
+-- if it doesn't (or if nil is passed), this function does nothing.
+local function platform_overrides(tbl)
+ assert(type(tbl) == "table" or not tbl)
+
+ if not tbl then return end
+
+ if tbl.platforms then
+ for platform in cfg.each_platform() do
+ local platform_tbl = tbl.platforms[platform]
+ if platform_tbl then
+ util.deep_merge(tbl, platform_tbl)
+ end
+ end
+ end
+ tbl.platforms = nil
+end
+
+local function convert_dependencies(rockspec, key)
+ if rockspec[key] then
+ for i = 1, #rockspec[key] do
+ local parsed, err = queries.from_dep_string(rockspec[key][i])
+ if not parsed then
+ return nil, "Parse error processing dependency '"..rockspec[key][i].."': "..tostring(err)
+ end
+ rockspec[key][i] = parsed
+ end
+ else
+ rockspec[key] = {}
+ end
+ return true
+end
+
+--- Set up path-related variables for a given rock.
+-- Create a "variables" table in the rockspec table, containing
+-- adjusted variables according to the configuration file.
+-- @param rockspec table: The rockspec table.
+local function configure_paths(rockspec)
+ local vars = {}
+ for k,v in pairs(cfg.variables) do
+ vars[k] = v
+ end
+ local name, version = rockspec.name, rockspec.version
+ vars.PREFIX = path.install_dir(name, version)
+ vars.LUADIR = path.lua_dir(name, version)
+ vars.LIBDIR = path.lib_dir(name, version)
+ vars.CONFDIR = path.conf_dir(name, version)
+ vars.BINDIR = path.bin_dir(name, version)
+ vars.DOCDIR = path.doc_dir(name, version)
+ rockspec.variables = vars
+end
+
+function rockspecs.from_persisted_table(filename, rockspec, globals, quick)
+ assert(type(rockspec) == "table")
+ assert(type(globals) == "table" or globals == nil)
+ assert(type(filename) == "string")
+ assert(type(quick) == "boolean" or quick == nil)
+
+ if rockspec.rockspec_format then
+ if vers.compare_versions(rockspec.rockspec_format, type_rockspec.rockspec_format) then
+ return nil, "Rockspec format "..rockspec.rockspec_format.." is not supported, please upgrade LuaRocks."
+ end
+ end
+
+ if not quick then
+ local ok, err = type_rockspec.check(rockspec, globals or {})
+ if not ok then
+ return nil, err
+ end
+ end
+
+ --- Check if rockspec format version satisfies version requirement.
+ -- @param rockspec table: The rockspec table.
+ -- @param version string: required version.
+ -- @return boolean: true if rockspec format matches version or is newer, false otherwise.
+ do
+ local parsed_format = vers.parse_version(rockspec.rockspec_format or "1.0")
+ rockspec.format_is_at_least = function(self, version)
+ return parsed_format >= vers.parse_version(version)
+ end
+ end
+
+ platform_overrides(rockspec.build)
+ platform_overrides(rockspec.dependencies)
+ platform_overrides(rockspec.build_dependencies)
+ platform_overrides(rockspec.test_dependencies)
+ platform_overrides(rockspec.external_dependencies)
+ platform_overrides(rockspec.source)
+ platform_overrides(rockspec.hooks)
+ platform_overrides(rockspec.test)
+
+ rockspec.name = rockspec.package:lower()
+
+ local protocol, pathname = dir.split_url(rockspec.source.url)
+ if dir.is_basic_protocol(protocol) then
+ rockspec.source.file = rockspec.source.file or dir.base_name(rockspec.source.url)
+ end
+ rockspec.source.protocol, rockspec.source.pathname = protocol, pathname
+
+ -- Temporary compatibility
+ if rockspec.source.cvs_module then rockspec.source.module = rockspec.source.cvs_module end
+ if rockspec.source.cvs_tag then rockspec.source.tag = rockspec.source.cvs_tag end
+
+ rockspec.local_abs_filename = filename
+ rockspec.source.dir_set = rockspec.source.dir ~= nil
+ rockspec.source.dir = rockspec.source.dir or rockspec.source.module
+
+ rockspec.rocks_provided = util.get_rocks_provided(rockspec)
+
+ for _, key in ipairs({"dependencies", "build_dependencies", "test_dependencies"}) do
+ local ok, err = convert_dependencies(rockspec, key)
+ if not ok then
+ return nil, err
+ end
+ end
+
+ if rockspec.build
+ and rockspec.build.type
+ and not vendored_build_type_set[rockspec.build.type] then
+ local build_pkg_name = "luarocks-build-" .. rockspec.build.type
+ if not rockspec.build_dependencies then
+ rockspec.build_dependencies = {}
+ end
+
+ local found = false
+ for _, dep in ipairs(rockspec.build_dependencies) do
+ if dep.name == build_pkg_name then
+ found = true
+ break
+ end
+ end
+
+ if not found then
+ table.insert(rockspec.build_dependencies, queries.from_dep_string(build_pkg_name))
+ end
+ end
+
+ if not quick then
+ configure_paths(rockspec)
+ end
+
+ return setmetatable(rockspec, rockspec_mt)
+end
+
+return rockspecs
diff --git a/src/luarocks/search.lua b/src/luarocks/search.lua
new file mode 100644
index 0000000..180f8f4
--- /dev/null
+++ b/src/luarocks/search.lua
@@ -0,0 +1,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
+
diff --git a/src/luarocks/signing.lua b/src/luarocks/signing.lua
new file mode 100644
index 0000000..cb91643
--- /dev/null
+++ b/src/luarocks/signing.lua
@@ -0,0 +1,48 @@
+local signing = {}
+
+local cfg = require("luarocks.core.cfg")
+local fs = require("luarocks.fs")
+
+local function get_gpg()
+ local vars = cfg.variables
+ local gpg = vars.GPG
+ local gpg_ok, err = fs.is_tool_available(gpg, "gpg")
+ if not gpg_ok then
+ return nil, err
+ end
+ return gpg
+end
+
+function signing.signature_url(url)
+ return url .. ".asc"
+end
+
+function signing.sign_file(file)
+ local gpg, err = get_gpg()
+ if not gpg then
+ return nil, err
+ end
+
+ local sigfile = file .. ".asc"
+ if fs.execute(gpg, "--armor", "--output", sigfile, "--detach-sign", file) then
+ return sigfile
+ else
+ return nil, "failed running " .. gpg .. " to sign " .. file
+ end
+end
+
+function signing.verify_signature(file, sigfile)
+ local gpg, err = get_gpg()
+ if not gpg then
+ return nil, err
+ end
+
+ if fs.execute(gpg, "--verify", sigfile, file) then
+ return true
+ else
+ return nil, "GPG returned a verification error"
+ end
+
+end
+
+return signing
diff --git a/src/luarocks/test.lua b/src/luarocks/test.lua
new file mode 100644
index 0000000..d074b95
--- /dev/null
+++ b/src/luarocks/test.lua
@@ -0,0 +1,100 @@
+
+local test = {}
+
+local fetch = require("luarocks.fetch")
+local deps = require("luarocks.deps")
+local util = require("luarocks.util")
+
+local test_types = {
+ "busted",
+ "command",
+}
+
+local test_modules = {}
+
+for _, test_type in ipairs(test_types) do
+ local mod = require("luarocks.test." .. test_type)
+ table.insert(test_modules, mod)
+ test_modules[test_type] = mod
+ test_modules[mod] = test_type
+end
+
+local function get_test_type(rockspec)
+ if rockspec.test and rockspec.test.type then
+ return rockspec.test.type
+ end
+
+ for _, test_module in ipairs(test_modules) do
+ if test_module.detect_type() then
+ return test_modules[test_module]
+ end
+ end
+
+ return nil, "could not detect test type -- no test suite for " .. rockspec.package .. "?"
+end
+
+-- Run test suite as configured in rockspec in the current directory.
+function test.run_test_suite(rockspec_arg, test_type, args, prepare)
+ local rockspec
+ if type(rockspec_arg) == "string" then
+ local err, errcode
+ rockspec, err, errcode = fetch.load_rockspec(rockspec_arg)
+ if err then
+ return nil, err, errcode
+ end
+ else
+ assert(type(rockspec_arg) == "table")
+ rockspec = rockspec_arg
+ end
+
+ if not test_type then
+ local err
+ test_type, err = get_test_type(rockspec, test_type)
+ if not test_type then
+ return nil, err
+ end
+ end
+ assert(test_type)
+
+ local all_deps = {
+ "dependencies",
+ "build_dependencies",
+ "test_dependencies",
+ }
+ for _, dep_kind in ipairs(all_deps) do
+ if rockspec[dep_kind] and next(rockspec[dep_kind]) then
+ local ok, err, errcode = deps.fulfill_dependencies(rockspec, dep_kind, "all")
+ if err then
+ return nil, err, errcode
+ end
+ end
+ end
+
+ local mod_name = "luarocks.test." .. test_type
+ local pok, test_mod = pcall(require, mod_name)
+ if not pok then
+ return nil, "failed loading test execution module " .. mod_name
+ end
+
+ if prepare then
+ if test_type == "busted" then
+ return test_mod.run_tests(rockspec_arg, {"--version"})
+ else
+ return true
+ end
+ else
+ local flags = rockspec.test and rockspec.test.flags
+ if type(flags) == "table" then
+ util.variable_substitutions(flags, rockspec.variables)
+
+ -- insert any flags given in test.flags at the front of args
+ for i = 1, #flags do
+ table.insert(args, i, flags[i])
+ end
+ end
+
+ return test_mod.run_tests(rockspec.test, args)
+ end
+end
+
+return test
diff --git a/src/luarocks/test/busted.lua b/src/luarocks/test/busted.lua
new file mode 100644
index 0000000..c73909c
--- /dev/null
+++ b/src/luarocks/test/busted.lua
@@ -0,0 +1,53 @@
+
+local busted = {}
+
+local fs = require("luarocks.fs")
+local deps = require("luarocks.deps")
+local path = require("luarocks.path")
+local dir = require("luarocks.dir")
+local queries = require("luarocks.queries")
+
+local unpack = table.unpack or unpack
+
+function busted.detect_type()
+ if fs.exists(".busted") then
+ return true
+ end
+ return false
+end
+
+function busted.run_tests(test, args)
+ if not test then
+ test = {}
+ end
+
+ local ok, bustedver, where = deps.fulfill_dependency(queries.new("busted"), nil, nil, nil, "test_dependencies")
+ if not ok then
+ return nil, bustedver
+ end
+
+ local busted_exe
+ if test.busted_executable then
+ busted_exe = test.busted_executable
+ else
+ busted_exe = dir.path(path.root_dir(where), "bin", "busted")
+
+ -- Windows fallback
+ local busted_bat = dir.path(path.root_dir(where), "bin", "busted.bat")
+
+ if not fs.exists(busted_exe) and not fs.exists(busted_bat) then
+ return nil, "'busted' executable failed to be installed"
+ end
+ end
+
+ local err
+ ok, err = fs.execute(busted_exe, unpack(args))
+ if ok then
+ return true
+ else
+ return nil, err or "test suite failed."
+ end
+end
+
+
+return busted
diff --git a/src/luarocks/test/command.lua b/src/luarocks/test/command.lua
new file mode 100644
index 0000000..bed6744
--- /dev/null
+++ b/src/luarocks/test/command.lua
@@ -0,0 +1,52 @@
+
+local command = {}
+
+local fs = require("luarocks.fs")
+local cfg = require("luarocks.core.cfg")
+
+local unpack = table.unpack or unpack
+
+function command.detect_type()
+ if fs.exists("test.lua") then
+ return true
+ end
+ return false
+end
+
+function command.run_tests(test, args)
+ if not test then
+ test = {
+ script = "test.lua"
+ }
+ end
+
+ if not test.script and not test.command then
+ test.script = "test.lua"
+ end
+
+ local ok
+
+ if test.script then
+ if type(test.script) ~= "string" then
+ return nil, "Malformed rockspec: 'script' expects a string"
+ end
+ if not fs.exists(test.script) then
+ return nil, "Test script " .. test.script .. " does not exist"
+ end
+ local lua = fs.Q(cfg.variables["LUA"]) -- get lua interpreter configured
+ ok = fs.execute(lua, test.script, unpack(args))
+ elseif test.command then
+ if type(test.command) ~= "string" then
+ return nil, "Malformed rockspec: 'command' expects a string"
+ end
+ ok = fs.execute(test.command, unpack(args))
+ end
+
+ if ok then
+ return true
+ else
+ return nil, "tests failed with non-zero exit code"
+ end
+end
+
+return command
diff --git a/src/luarocks/tools/patch.lua b/src/luarocks/tools/patch.lua
new file mode 100644
index 0000000..6f36d71
--- /dev/null
+++ b/src/luarocks/tools/patch.lua
@@ -0,0 +1,716 @@
+--- Patch utility to apply unified diffs.
+--
+-- http://lua-users.org/wiki/LuaPatch
+--
+-- (c) 2008 David Manura, Licensed under the same terms as Lua (MIT license).
+-- Code is heavily based on the Python-based patch.py version 8.06-1
+-- Copyright (c) 2008 rainforce.org, MIT License
+-- Project home: http://code.google.com/p/python-patch/ .
+-- Version 0.1
+
+local patch = {}
+
+local fs = require("luarocks.fs")
+local fun = require("luarocks.fun")
+
+local io = io
+local os = os
+local string = string
+local table = table
+local format = string.format
+
+-- logging
+local debugmode = false
+local function debug(_) end
+local function info(_) end
+local function warning(s) io.stderr:write(s .. '\n') end
+
+-- Returns boolean whether string s2 starts with string s.
+local function startswith(s, s2)
+ return s:sub(1, #s2) == s2
+end
+
+-- Returns boolean whether string s2 ends with string s.
+local function endswith(s, s2)
+ return #s >= #s2 and s:sub(#s-#s2+1) == s2
+end
+
+-- Returns string s after filtering out any new-line characters from end.
+local function endlstrip(s)
+ return s:gsub('[\r\n]+$', '')
+end
+
+-- Returns shallow copy of table t.
+local function table_copy(t)
+ local t2 = {}
+ for k,v in pairs(t) do t2[k] = v end
+ return t2
+end
+
+local function exists(filename)
+ local fh = io.open(filename)
+ local result = fh ~= nil
+ if fh then fh:close() end
+ return result
+end
+local function isfile() return true end --FIX?
+
+local function string_as_file(s)
+ return {
+ at = 0,
+ str = s,
+ len = #s,
+ eof = false,
+ read = function(self, n)
+ if self.eof then return nil end
+ local chunk = self.str:sub(self.at, self.at + n - 1)
+ self.at = self.at + n
+ if self.at > self.len then
+ self.eof = true
+ end
+ return chunk
+ end,
+ close = function(self)
+ self.eof = true
+ end,
+ }
+end
+
+--
+-- file_lines(f) is similar to f:lines() for file f.
+-- The main difference is that read_lines includes
+-- new-line character sequences ("\n", "\r\n", "\r"),
+-- if any, at the end of each line. Embedded "\0" are also handled.
+-- Caution: The newline behavior can depend on whether f is opened
+-- in binary or ASCII mode.
+-- (file_lines - version 20080913)
+--
+local function file_lines(f)
+ local CHUNK_SIZE = 1024
+ local buffer = ""
+ local pos_beg = 1
+ return function()
+ local pos, chars
+ while 1 do
+ pos, chars = buffer:match('()([\r\n].)', pos_beg)
+ if pos or not f then
+ break
+ elseif f then
+ local chunk = f:read(CHUNK_SIZE)
+ if chunk then
+ buffer = buffer:sub(pos_beg) .. chunk
+ pos_beg = 1
+ else
+ f = nil
+ end
+ end
+ end
+ if not pos then
+ pos = #buffer
+ elseif chars == '\r\n' then
+ pos = pos + 1
+ end
+ local line = buffer:sub(pos_beg, pos)
+ pos_beg = pos + 1
+ if #line > 0 then
+ return line
+ end
+ end
+end
+
+local function match_linerange(line)
+ local m1, m2, m3, m4 = line:match("^@@ %-(%d+),(%d+) %+(%d+),(%d+)")
+ if not m1 then m1, m3, m4 = line:match("^@@ %-(%d+) %+(%d+),(%d+)") end
+ if not m1 then m1, m2, m3 = line:match("^@@ %-(%d+),(%d+) %+(%d+)") end
+ if not m1 then m1, m3 = line:match("^@@ %-(%d+) %+(%d+)") end
+ return m1, m2, m3, m4
+end
+
+local function match_epoch(str)
+ return str:match("[^0-9]1969[^0-9]") or str:match("[^0-9]1970[^0-9]")
+end
+
+function patch.read_patch(filename, data)
+ -- define possible file regions that will direct the parser flow
+ local state = 'header'
+ -- 'header' - comments before the patch body
+ -- 'filenames' - lines starting with --- and +++
+ -- 'hunkhead' - @@ -R +R @@ sequence
+ -- 'hunkbody'
+ -- 'hunkskip' - skipping invalid hunk mode
+
+ local all_ok = true
+ local lineends = {lf=0, crlf=0, cr=0}
+ local files = {source={}, target={}, epoch={}, hunks={}, fileends={}, hunkends={}}
+ local nextfileno = 0
+ local nexthunkno = 0 --: even if index starts with 0 user messages
+ -- number hunks from 1
+
+ -- hunkinfo holds parsed values, hunkactual - calculated
+ local hunkinfo = {
+ startsrc=nil, linessrc=nil, starttgt=nil, linestgt=nil,
+ invalid=false, text={}
+ }
+ local hunkactual = {linessrc=nil, linestgt=nil}
+
+ info(format("reading patch %s", filename))
+
+ local fp
+ if data then
+ fp = string_as_file(data)
+ else
+ fp = filename == '-' and io.stdin or assert(io.open(filename, "rb"))
+ end
+ local lineno = 0
+
+ for line in file_lines(fp) do
+ lineno = lineno + 1
+ if state == 'header' then
+ if startswith(line, "--- ") then
+ state = 'filenames'
+ end
+ -- state is 'header' or 'filenames'
+ end
+ if state == 'hunkbody' then
+ -- skip hunkskip and hunkbody code until definition of hunkhead read
+
+ if line:match"^[\r\n]*$" then
+ -- prepend space to empty lines to interpret them as context properly
+ line = " " .. line
+ end
+
+ -- process line first
+ if line:match"^[- +\\]" then
+ -- gather stats about line endings
+ local he = files.hunkends[nextfileno]
+ if endswith(line, "\r\n") then
+ he.crlf = he.crlf + 1
+ elseif endswith(line, "\n") then
+ he.lf = he.lf + 1
+ elseif endswith(line, "\r") then
+ he.cr = he.cr + 1
+ end
+ if startswith(line, "-") then
+ hunkactual.linessrc = hunkactual.linessrc + 1
+ elseif startswith(line, "+") then
+ hunkactual.linestgt = hunkactual.linestgt + 1
+ elseif startswith(line, "\\") then
+ -- nothing
+ else
+ hunkactual.linessrc = hunkactual.linessrc + 1
+ hunkactual.linestgt = hunkactual.linestgt + 1
+ end
+ table.insert(hunkinfo.text, line)
+ -- todo: handle \ No newline cases
+ else
+ warning(format("invalid hunk no.%d at %d for target file %s",
+ nexthunkno, lineno, files.target[nextfileno]))
+ -- add hunk status node
+ table.insert(files.hunks[nextfileno], table_copy(hunkinfo))
+ files.hunks[nextfileno][nexthunkno].invalid = true
+ all_ok = false
+ state = 'hunkskip'
+ end
+
+ -- check exit conditions
+ if hunkactual.linessrc > hunkinfo.linessrc or
+ hunkactual.linestgt > hunkinfo.linestgt
+ then
+ warning(format("extra hunk no.%d lines at %d for target %s",
+ nexthunkno, lineno, files.target[nextfileno]))
+ -- add hunk status node
+ table.insert(files.hunks[nextfileno], table_copy(hunkinfo))
+ files.hunks[nextfileno][nexthunkno].invalid = true
+ state = 'hunkskip'
+ elseif hunkinfo.linessrc == hunkactual.linessrc and
+ hunkinfo.linestgt == hunkactual.linestgt
+ then
+ table.insert(files.hunks[nextfileno], table_copy(hunkinfo))
+ state = 'hunkskip'
+
+ -- detect mixed window/unix line ends
+ local ends = files.hunkends[nextfileno]
+ if (ends.cr~=0 and 1 or 0) + (ends.crlf~=0 and 1 or 0) +
+ (ends.lf~=0 and 1 or 0) > 1
+ then
+ warning(format("inconsistent line ends in patch hunks for %s",
+ files.source[nextfileno]))
+ end
+ end
+ -- state is 'hunkbody' or 'hunkskip'
+ end
+
+ if state == 'hunkskip' then
+ if match_linerange(line) then
+ state = 'hunkhead'
+ elseif startswith(line, "--- ") then
+ state = 'filenames'
+ if debugmode and #files.source > 0 then
+ debug(format("- %2d hunks for %s", #files.hunks[nextfileno],
+ files.source[nextfileno]))
+ end
+ end
+ -- state is 'hunkskip', 'hunkhead', or 'filenames'
+ end
+ local advance
+ if state == 'filenames' then
+ if startswith(line, "--- ") then
+ if fun.contains(files.source, nextfileno) then
+ all_ok = false
+ warning(format("skipping invalid patch for %s",
+ files.source[nextfileno+1]))
+ table.remove(files.source, nextfileno+1)
+ -- double source filename line is encountered
+ -- attempt to restart from this second line
+ end
+ -- Accept a space as a terminator, like GNU patch does.
+ -- Breaks patches containing filenames with spaces...
+ -- FIXME Figure out what does GNU patch do in those cases.
+ local match, rest = line:match("^%-%-%- ([^ \t\r\n]+)(.*)")
+ if not match then
+ all_ok = false
+ warning(format("skipping invalid filename at line %d", lineno+1))
+ state = 'header'
+ else
+ if match_epoch(rest) then
+ files.epoch[nextfileno + 1] = true
+ end
+ table.insert(files.source, match)
+ end
+ elseif not startswith(line, "+++ ") then
+ if fun.contains(files.source, nextfileno) then
+ all_ok = false
+ warning(format("skipping invalid patch with no target for %s",
+ files.source[nextfileno+1]))
+ table.remove(files.source, nextfileno+1)
+ else
+ -- this should be unreachable
+ warning("skipping invalid target patch")
+ end
+ state = 'header'
+ else
+ if fun.contains(files.target, nextfileno) then
+ all_ok = false
+ warning(format("skipping invalid patch - double target at line %d",
+ lineno+1))
+ table.remove(files.source, nextfileno+1)
+ table.remove(files.target, nextfileno+1)
+ nextfileno = nextfileno - 1
+ -- double target filename line is encountered
+ -- switch back to header state
+ state = 'header'
+ else
+ -- Accept a space as a terminator, like GNU patch does.
+ -- Breaks patches containing filenames with spaces...
+ -- FIXME Figure out what does GNU patch do in those cases.
+ local re_filename = "^%+%+%+ ([^ \t\r\n]+)(.*)$"
+ local match, rest = line:match(re_filename)
+ if not match then
+ all_ok = false
+ warning(format(
+ "skipping invalid patch - no target filename at line %d",
+ lineno+1))
+ state = 'header'
+ else
+ table.insert(files.target, match)
+ nextfileno = nextfileno + 1
+ if match_epoch(rest) then
+ files.epoch[nextfileno] = true
+ end
+ nexthunkno = 0
+ table.insert(files.hunks, {})
+ table.insert(files.hunkends, table_copy(lineends))
+ table.insert(files.fileends, table_copy(lineends))
+ state = 'hunkhead'
+ advance = true
+ end
+ end
+ end
+ -- state is 'filenames', 'header', or ('hunkhead' with advance)
+ end
+ if not advance and state == 'hunkhead' then
+ local m1, m2, m3, m4 = match_linerange(line)
+ if not m1 then
+ if not fun.contains(files.hunks, nextfileno-1) then
+ all_ok = false
+ warning(format("skipping invalid patch with no hunks for file %s",
+ files.target[nextfileno]))
+ end
+ state = 'header'
+ else
+ hunkinfo.startsrc = tonumber(m1)
+ hunkinfo.linessrc = tonumber(m2 or 1)
+ hunkinfo.starttgt = tonumber(m3)
+ hunkinfo.linestgt = tonumber(m4 or 1)
+ hunkinfo.invalid = false
+ hunkinfo.text = {}
+
+ hunkactual.linessrc = 0
+ hunkactual.linestgt = 0
+
+ state = 'hunkbody'
+ nexthunkno = nexthunkno + 1
+ end
+ -- state is 'header' or 'hunkbody'
+ end
+ end
+ if state ~= 'hunkskip' then
+ warning(format("patch file incomplete - %s", filename))
+ all_ok = false
+ -- os.exit(?)
+ else
+ -- duplicated message when an eof is reached
+ if debugmode and #files.source > 0 then
+ debug(format("- %2d hunks for %s", #files.hunks[nextfileno],
+ files.source[nextfileno]))
+ end
+ end
+
+ local sum = 0; for _,hset in ipairs(files.hunks) do sum = sum + #hset end
+ info(format("total files: %d total hunks: %d", #files.source, sum))
+ fp:close()
+ return files, all_ok
+end
+
+local function find_hunk(file, h, hno)
+ for fuzz=0,2 do
+ local lineno = h.startsrc
+ for i=0,#file do
+ local found = true
+ local location = lineno
+ for l, hline in ipairs(h.text) do
+ if l > fuzz then
+ -- todo: \ No newline at the end of file
+ if startswith(hline, " ") or startswith(hline, "-") then
+ local line = file[lineno]
+ lineno = lineno + 1
+ if not line or #line == 0 then
+ found = false
+ break
+ end
+ if endlstrip(line) ~= endlstrip(hline:sub(2)) then
+ found = false
+ break
+ end
+ end
+ end
+ end
+ if found then
+ local offset = location - h.startsrc - fuzz
+ if offset ~= 0 then
+ warning(format("Hunk %d found at offset %d%s...", hno, offset, fuzz == 0 and "" or format(" (fuzz %d)", fuzz)))
+ end
+ h.startsrc = location
+ h.starttgt = h.starttgt + offset
+ for _=1,fuzz do
+ table.remove(h.text, 1)
+ table.remove(h.text, #h.text)
+ end
+ return true
+ end
+ lineno = i
+ end
+ end
+ return false
+end
+
+local function load_file(filename)
+ local fp = assert(io.open(filename))
+ local file = {}
+ local readline = file_lines(fp)
+ while true do
+ local line = readline()
+ if not line then break end
+ table.insert(file, line)
+ end
+ fp:close()
+ return file
+end
+
+local function find_hunks(file, hunks)
+ for hno, h in ipairs(hunks) do
+ find_hunk(file, h, hno)
+ end
+end
+
+local function check_patched(file, hunks)
+ local lineno = 1
+ local ok, err = pcall(function()
+ if #file == 0 then
+ error('nomatch', 0)
+ end
+ for hno, h in ipairs(hunks) do
+ -- skip to line just before hunk starts
+ if #file < h.starttgt then
+ error('nomatch', 0)
+ end
+ lineno = h.starttgt
+ for _, hline in ipairs(h.text) do
+ -- todo: \ No newline at the end of file
+ if not startswith(hline, "-") and not startswith(hline, "\\") then
+ local line = file[lineno]
+ lineno = lineno + 1
+ if #line == 0 then
+ error('nomatch', 0)
+ end
+ if endlstrip(line) ~= endlstrip(hline:sub(2)) then
+ warning(format("file is not patched - failed hunk: %d", hno))
+ error('nomatch', 0)
+ end
+ end
+ end
+ end
+ end)
+ -- todo: display failed hunk, i.e. expected/found
+ return err ~= 'nomatch'
+end
+
+local function patch_hunks(srcname, tgtname, hunks)
+ local src = assert(io.open(srcname, "rb"))
+ local tgt = assert(io.open(tgtname, "wb"))
+
+ local src_readline = file_lines(src)
+
+ -- todo: detect linefeeds early - in apply_files routine
+ -- to handle cases when patch starts right from the first
+ -- line and no lines are processed. At the moment substituted
+ -- lineends may not be the same at the start and at the end
+ -- of patching. Also issue a warning about mixed lineends
+
+ local srclineno = 1
+ local lineends = {['\n']=0, ['\r\n']=0, ['\r']=0}
+ for hno, h in ipairs(hunks) do
+ debug(format("processing hunk %d for file %s", hno, tgtname))
+ -- skip to line just before hunk starts
+ while srclineno < h.startsrc do
+ local line = src_readline()
+ -- Python 'U' mode works only with text files
+ if endswith(line, "\r\n") then
+ lineends["\r\n"] = lineends["\r\n"] + 1
+ elseif endswith(line, "\n") then
+ lineends["\n"] = lineends["\n"] + 1
+ elseif endswith(line, "\r") then
+ lineends["\r"] = lineends["\r"] + 1
+ end
+ tgt:write(line)
+ srclineno = srclineno + 1
+ end
+
+ for _,hline in ipairs(h.text) do
+ -- todo: check \ No newline at the end of file
+ if startswith(hline, "-") or startswith(hline, "\\") then
+ src_readline()
+ srclineno = srclineno + 1
+ else
+ if not startswith(hline, "+") then
+ src_readline()
+ srclineno = srclineno + 1
+ end
+ local line2write = hline:sub(2)
+ -- detect if line ends are consistent in source file
+ local sum = 0
+ for _,v in pairs(lineends) do if v > 0 then sum=sum+1 end end
+ if sum == 1 then
+ local newline
+ for k,v in pairs(lineends) do if v ~= 0 then newline = k end end
+ tgt:write(endlstrip(line2write) .. newline)
+ else -- newlines are mixed or unknown
+ tgt:write(line2write)
+ end
+ end
+ end
+ end
+ for line in src_readline do
+ tgt:write(line)
+ end
+ tgt:close()
+ src:close()
+ return true
+end
+
+local function strip_dirs(filename, strip)
+ if strip == nil then return filename end
+ for _=1,strip do
+ filename=filename:gsub("^[^/]*/", "")
+ end
+ return filename
+end
+
+local function write_new_file(filename, hunk)
+ local fh = io.open(filename, "wb")
+ if not fh then return false end
+ for _, hline in ipairs(hunk.text) do
+ local c = hline:sub(1,1)
+ if c ~= "+" and c ~= "-" and c ~= " " then
+ return false, "malformed patch"
+ end
+ fh:write(hline:sub(2))
+ end
+ fh:close()
+ return true
+end
+
+local function patch_file(source, target, epoch, hunks, strip, create_delete)
+ local create_file = false
+ if create_delete then
+ local is_src_epoch = epoch and #hunks == 1 and hunks[1].startsrc == 0 and hunks[1].linessrc == 0
+ if is_src_epoch or source == "/dev/null" then
+ info(format("will create %s", target))
+ create_file = true
+ end
+ end
+ if create_file then
+ return write_new_file(fs.absolute_name(strip_dirs(target, strip)), hunks[1])
+ end
+ source = strip_dirs(source, strip)
+ local f2patch = source
+ if not exists(f2patch) then
+ f2patch = strip_dirs(target, strip)
+ f2patch = fs.absolute_name(f2patch)
+ if not exists(f2patch) then --FIX:if f2patch nil
+ warning(format("source/target file does not exist\n--- %s\n+++ %s",
+ source, f2patch))
+ return false
+ end
+ end
+ if not isfile(f2patch) then
+ warning(format("not a file - %s", f2patch))
+ return false
+ end
+
+ source = f2patch
+
+ -- validate before patching
+ local file = load_file(source)
+ local hunkno = 1
+ local hunk = hunks[hunkno]
+ local hunkfind = {}
+ local validhunks = 0
+ local canpatch = false
+ local hunklineno
+ if not file then
+ return nil, "failed reading file " .. source
+ end
+
+ if create_delete then
+ if epoch and #hunks == 1 and hunks[1].starttgt == 0 and hunks[1].linestgt == 0 then
+ local ok = os.remove(source)
+ if not ok then
+ return false
+ end
+ info(format("successfully removed %s", source))
+ return true
+ end
+ end
+
+ find_hunks(file, hunks)
+
+ local function process_line(line, lineno)
+ if not hunk or lineno < hunk.startsrc then
+ return false
+ end
+ if lineno == hunk.startsrc then
+ hunkfind = {}
+ for _,x in ipairs(hunk.text) do
+ if x:sub(1,1) == ' ' or x:sub(1,1) == '-' then
+ hunkfind[#hunkfind+1] = endlstrip(x:sub(2))
+ end
+ end
+ hunklineno = 1
+
+ -- todo \ No newline at end of file
+ end
+ -- check hunks in source file
+ if lineno < hunk.startsrc + #hunkfind - 1 then
+ if endlstrip(line) == hunkfind[hunklineno] then
+ hunklineno = hunklineno + 1
+ else
+ debug(format("hunk no.%d doesn't match source file %s",
+ hunkno, source))
+ -- file may be already patched, but check other hunks anyway
+ hunkno = hunkno + 1
+ if hunkno <= #hunks then
+ hunk = hunks[hunkno]
+ return false
+ else
+ return true
+ end
+ end
+ end
+ -- check if processed line is the last line
+ if lineno == hunk.startsrc + #hunkfind - 1 then
+ debug(format("file %s hunk no.%d -- is ready to be patched",
+ source, hunkno))
+ hunkno = hunkno + 1
+ validhunks = validhunks + 1
+ if hunkno <= #hunks then
+ hunk = hunks[hunkno]
+ else
+ if validhunks == #hunks then
+ -- patch file
+ canpatch = true
+ return true
+ end
+ end
+ end
+ return false
+ end
+
+ local done = false
+ for lineno, line in ipairs(file) do
+ done = process_line(line, lineno)
+ if done then
+ break
+ end
+ end
+ if not done then
+ if hunkno <= #hunks and not create_file then
+ warning(format("premature end of source file %s at hunk %d",
+ source, hunkno))
+ return false
+ end
+ end
+ if validhunks < #hunks then
+ if check_patched(file, hunks) then
+ warning(format("already patched %s", source))
+ elseif not create_file then
+ warning(format("source file is different - %s", source))
+ return false
+ end
+ end
+ if not canpatch then
+ return true
+ end
+ local backupname = source .. ".orig"
+ if exists(backupname) then
+ warning(format("can't backup original file to %s - aborting",
+ backupname))
+ return false
+ end
+ local ok = os.rename(source, backupname)
+ if not ok then
+ warning(format("failed backing up %s when patching", source))
+ return false
+ end
+ patch_hunks(backupname, source, hunks)
+ info(format("successfully patched %s", source))
+ os.remove(backupname)
+ return true
+end
+
+function patch.apply_patch(the_patch, strip, create_delete)
+ local all_ok = true
+ local total = #the_patch.source
+ for fileno, source in ipairs(the_patch.source) do
+ local target = the_patch.target[fileno]
+ local hunks = the_patch.hunks[fileno]
+ local epoch = the_patch.epoch[fileno]
+ info(format("processing %d/%d:\t %s", fileno, total, source))
+ local ok = patch_file(source, target, epoch, hunks, strip, create_delete)
+ all_ok = all_ok and ok
+ end
+ -- todo: check for premature eof
+ return all_ok
+end
+
+return patch
diff --git a/src/luarocks/tools/tar.lua b/src/luarocks/tools/tar.lua
new file mode 100644
index 0000000..bac7b2a
--- /dev/null
+++ b/src/luarocks/tools/tar.lua
@@ -0,0 +1,191 @@
+
+--- A pure-Lua implementation of untar (unpacking .tar archives)
+local tar = {}
+
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+local fun = require("luarocks.fun")
+
+local blocksize = 512
+
+local function get_typeflag(flag)
+ if flag == "0" or flag == "\0" then return "file"
+ elseif flag == "1" then return "link"
+ elseif flag == "2" then return "symlink" -- "reserved" in POSIX, "symlink" in GNU
+ elseif flag == "3" then return "character"
+ elseif flag == "4" then return "block"
+ elseif flag == "5" then return "directory"
+ elseif flag == "6" then return "fifo"
+ elseif flag == "7" then return "contiguous" -- "reserved" in POSIX, "contiguous" in GNU
+ elseif flag == "x" then return "next file"
+ elseif flag == "g" then return "global extended header"
+ elseif flag == "L" then return "long name"
+ elseif flag == "K" then return "long link name"
+ end
+ return "unknown"
+end
+
+local function octal_to_number(octal)
+ local exp = 0
+ local number = 0
+ octal = octal:gsub("%s", "")
+ for i = #octal,1,-1 do
+ local digit = tonumber(octal:sub(i,i))
+ if not digit then
+ break
+ end
+ number = number + (digit * 8^exp)
+ exp = exp + 1
+ end
+ return number
+end
+
+local function checksum_header(block)
+ local sum = 256
+
+ if block:byte(1) == 0 then
+ return 0
+ end
+
+ for i = 1,148 do
+ local b = block:byte(i) or 0
+ sum = sum + b
+ end
+ for i = 157,500 do
+ local b = block:byte(i) or 0
+ sum = sum + b
+ end
+
+ return sum
+end
+
+local function nullterm(s)
+ return s:match("^[^%z]*")
+end
+
+local function read_header_block(block)
+ local header = {}
+ header.name = nullterm(block:sub(1,100))
+ header.mode = nullterm(block:sub(101,108)):gsub(" ", "")
+ header.uid = octal_to_number(nullterm(block:sub(109,116)))
+ header.gid = octal_to_number(nullterm(block:sub(117,124)))
+ header.size = octal_to_number(nullterm(block:sub(125,136)))
+ header.mtime = octal_to_number(nullterm(block:sub(137,148)))
+ header.chksum = octal_to_number(nullterm(block:sub(149,156)))
+ header.typeflag = get_typeflag(block:sub(157,157))
+ header.linkname = nullterm(block:sub(158,257))
+ header.magic = block:sub(258,263)
+ header.version = block:sub(264,265)
+ header.uname = nullterm(block:sub(266,297))
+ header.gname = nullterm(block:sub(298,329))
+ header.devmajor = octal_to_number(nullterm(block:sub(330,337)))
+ header.devminor = octal_to_number(nullterm(block:sub(338,345)))
+ header.prefix = block:sub(346,500)
+
+ -- if header.magic ~= "ustar " and header.magic ~= "ustar\0" then
+ -- return false, ("Invalid header magic %6x"):format(bestring_to_number(header.magic))
+ -- end
+ -- if header.version ~= "00" and header.version ~= " \0" then
+ -- return false, "Unknown version "..header.version
+ -- end
+ if header.typeflag == "unknown" then
+ if checksum_header(block) ~= header.chksum then
+ return false, "Failed header checksum"
+ end
+ end
+ return header
+end
+
+function tar.untar(filename, destdir)
+ assert(type(filename) == "string")
+ assert(type(destdir) == "string")
+
+ local tar_handle = io.open(filename, "rb")
+ if not tar_handle then return nil, "Error opening file "..filename end
+
+ local long_name, long_link_name
+ local ok, err
+ local make_dir = fun.memoize(fs.make_dir)
+ while true do
+ local block
+ repeat
+ block = tar_handle:read(blocksize)
+ until (not block) or block:byte(1) > 0
+ if not block then break end
+ if #block < blocksize then
+ ok, err = nil, "Invalid block size -- corrupted file?"
+ break
+ end
+
+ local header
+ header, err = read_header_block(block)
+ if not header then
+ ok = false
+ break
+ end
+
+ local file_data = ""
+ if header.size > 0 then
+ local nread = math.ceil(header.size / blocksize) * blocksize
+ file_data = tar_handle:read(header.size)
+ if nread > header.size then
+ tar_handle:seek("cur", nread - header.size)
+ end
+ end
+
+ if header.typeflag == "long name" then
+ long_name = nullterm(file_data)
+ elseif header.typeflag == "long link name" then
+ long_link_name = nullterm(file_data)
+ else
+ if long_name then
+ header.name = long_name
+ long_name = nil
+ end
+ if long_link_name then
+ header.name = long_link_name
+ long_link_name = nil
+ end
+ end
+ local pathname = dir.path(destdir, header.name)
+ pathname = fs.absolute_name(pathname)
+ if header.typeflag == "directory" then
+ ok, err = make_dir(pathname)
+ if not ok then
+ break
+ end
+ elseif header.typeflag == "file" then
+ local dirname = dir.dir_name(pathname)
+ if dirname ~= "" then
+ ok, err = make_dir(dirname)
+ if not ok then
+ break
+ end
+ end
+ local file_handle
+ file_handle, err = io.open(pathname, "wb")
+ if not file_handle then
+ ok = nil
+ break
+ end
+ file_handle:write(file_data)
+ file_handle:close()
+ fs.set_time(pathname, header.mtime)
+ if header.mode:match("[75]") then
+ fs.set_permissions(pathname, "exec", "all")
+ else
+ fs.set_permissions(pathname, "read", "all")
+ end
+ end
+ --[[
+ for k,v in pairs(header) do
+ util.printout("[\""..tostring(k).."\"] = "..(type(v)=="number" and v or "\""..v:gsub("%z", "\\0").."\""))
+ end
+ util.printout()
+ --]]
+ end
+ tar_handle:close()
+ return ok, err
+end
+
+return tar
diff --git a/src/luarocks/tools/zip.lua b/src/luarocks/tools/zip.lua
new file mode 100644
index 0000000..82d582f
--- /dev/null
+++ b/src/luarocks/tools/zip.lua
@@ -0,0 +1,531 @@
+
+--- A Lua implementation of .zip and .gz file compression and decompression,
+-- using only lzlib or lua-lzib.
+local zip = {}
+
+local zlib = require("zlib")
+local fs = require("luarocks.fs")
+local fun = require("luarocks.fun")
+local dir = require("luarocks.dir")
+
+local pack = table.pack or function(...) return { n = select("#", ...), ... } end
+
+local function shr(n, m)
+ return math.floor(n / 2^m)
+end
+
+local function shl(n, m)
+ return n * 2^m
+end
+local function lowbits(n, m)
+ return n % 2^m
+end
+
+local function mode_to_windowbits(mode)
+ if mode == "gzip" then
+ return 31
+ elseif mode == "zlib" then
+ return 0
+ elseif mode == "raw" then
+ return -15
+ end
+end
+
+-- zlib module can be provided by both lzlib and lua-lzib packages.
+-- Create a compatibility layer.
+local zlib_compress, zlib_uncompress, zlib_crc32
+if zlib._VERSION:match "^lua%-zlib" then
+ function zlib_compress(data, mode)
+ return (zlib.deflate(6, mode_to_windowbits(mode))(data, "finish"))
+ end
+
+ function zlib_uncompress(data, mode)
+ return (zlib.inflate(mode_to_windowbits(mode))(data))
+ end
+
+ function zlib_crc32(data)
+ return zlib.crc32()(data)
+ end
+elseif zlib._VERSION:match "^lzlib" then
+ function zlib_compress(data, mode)
+ return zlib.compress(data, -1, nil, mode_to_windowbits(mode))
+ end
+
+ function zlib_uncompress(data, mode)
+ return zlib.decompress(data, mode_to_windowbits(mode))
+ end
+
+ function zlib_crc32(data)
+ return zlib.crc32(zlib.crc32(), data)
+ end
+else
+ error("unknown zlib library", 0)
+end
+
+local function number_to_lestring(number, nbytes)
+ local out = {}
+ for _ = 1, nbytes do
+ local byte = number % 256
+ table.insert(out, string.char(byte))
+ number = (number - byte) / 256
+ end
+ return table.concat(out)
+end
+
+local function lestring_to_number(str)
+ local n = 0
+ local bytes = { string.byte(str, 1, #str) }
+ for b = 1, #str do
+ n = n + shl(bytes[b], (b-1)*8)
+ end
+ return math.floor(n)
+end
+
+local LOCAL_FILE_HEADER_SIGNATURE = number_to_lestring(0x04034b50, 4)
+local DATA_DESCRIPTOR_SIGNATURE = number_to_lestring(0x08074b50, 4)
+local CENTRAL_DIRECTORY_SIGNATURE = number_to_lestring(0x02014b50, 4)
+local END_OF_CENTRAL_DIR_SIGNATURE = number_to_lestring(0x06054b50, 4)
+
+--- Begin a new file to be stored inside the zipfile.
+-- @param self handle of the zipfile being written.
+-- @param filename filenome of the file to be added to the zipfile.
+-- @return true if succeeded, nil in case of failure.
+local function zipwriter_open_new_file_in_zip(self, filename)
+ if self.in_open_file then
+ self:close_file_in_zip()
+ return nil
+ end
+ local lfh = {}
+ self.local_file_header = lfh
+ lfh.last_mod_file_time = 0 -- TODO
+ lfh.last_mod_file_date = 0 -- TODO
+ lfh.file_name_length = #filename
+ lfh.extra_field_length = 0
+ lfh.file_name = filename:gsub("\\", "/")
+ lfh.external_attr = shl(493, 16) -- TODO proper permissions
+ self.in_open_file = true
+ return true
+end
+
+--- Write data to the file currently being stored in the zipfile.
+-- @param self handle of the zipfile being written.
+-- @param data string containing full contents of the file.
+-- @return true if succeeded, nil in case of failure.
+local function zipwriter_write_file_in_zip(self, data)
+ if not self.in_open_file then
+ return nil
+ end
+ local lfh = self.local_file_header
+ local compressed = zlib_compress(data, "raw")
+ lfh.crc32 = zlib_crc32(data)
+ lfh.compressed_size = #compressed
+ lfh.uncompressed_size = #data
+ self.data = compressed
+ return true
+end
+
+--- Complete the writing of a file stored in the zipfile.
+-- @param self handle of the zipfile being written.
+-- @return true if succeeded, nil in case of failure.
+local function zipwriter_close_file_in_zip(self)
+ local zh = self.ziphandle
+
+ if not self.in_open_file then
+ return nil
+ end
+
+ -- Local file header
+ local lfh = self.local_file_header
+ lfh.offset = zh:seek()
+ zh:write(LOCAL_FILE_HEADER_SIGNATURE)
+ zh:write(number_to_lestring(20, 2)) -- version needed to extract: 2.0
+ zh:write(number_to_lestring(4, 2)) -- general purpose bit flag
+ zh:write(number_to_lestring(8, 2)) -- compression method: deflate
+ zh:write(number_to_lestring(lfh.last_mod_file_time, 2))
+ zh:write(number_to_lestring(lfh.last_mod_file_date, 2))
+ zh:write(number_to_lestring(lfh.crc32, 4))
+ zh:write(number_to_lestring(lfh.compressed_size, 4))
+ zh:write(number_to_lestring(lfh.uncompressed_size, 4))
+ zh:write(number_to_lestring(lfh.file_name_length, 2))
+ zh:write(number_to_lestring(lfh.extra_field_length, 2))
+ zh:write(lfh.file_name)
+
+ -- File data
+ zh:write(self.data)
+
+ -- Data descriptor
+ zh:write(DATA_DESCRIPTOR_SIGNATURE)
+ zh:write(number_to_lestring(lfh.crc32, 4))
+ zh:write(number_to_lestring(lfh.compressed_size, 4))
+ zh:write(number_to_lestring(lfh.uncompressed_size, 4))
+
+ table.insert(self.files, lfh)
+ self.in_open_file = false
+
+ return true
+end
+
+-- @return boolean or (boolean, string): true on success,
+-- false and an error message on failure.
+local function zipwriter_add(self, file)
+ local fin
+ local ok, err = self:open_new_file_in_zip(file)
+ if not ok then
+ err = "error in opening "..file.." in zipfile"
+ else
+ fin = io.open(fs.absolute_name(file), "rb")
+ if not fin then
+ ok = false
+ err = "error opening "..file.." for reading"
+ end
+ end
+ if ok then
+ local data = fin:read("*a")
+ if not data then
+ err = "error reading "..file
+ ok = false
+ else
+ ok = self:write_file_in_zip(data)
+ if not ok then
+ err = "error in writing "..file.." in the zipfile"
+ end
+ end
+ end
+ if fin then
+ fin:close()
+ end
+ if ok then
+ ok = self:close_file_in_zip()
+ if not ok then
+ err = "error in writing "..file.." in the zipfile"
+ end
+ end
+ return ok == true, err
+end
+
+--- Complete the writing of the zipfile.
+-- @param self handle of the zipfile being written.
+-- @return true if succeeded, nil in case of failure.
+local function zipwriter_close(self)
+ local zh = self.ziphandle
+
+ local central_directory_offset = zh:seek()
+
+ local size_of_central_directory = 0
+ -- Central directory structure
+ for _, lfh in ipairs(self.files) do
+ zh:write(CENTRAL_DIRECTORY_SIGNATURE) -- signature
+ zh:write(number_to_lestring(3, 2)) -- version made by: UNIX
+ zh:write(number_to_lestring(20, 2)) -- version needed to extract: 2.0
+ zh:write(number_to_lestring(0, 2)) -- general purpose bit flag
+ zh:write(number_to_lestring(8, 2)) -- compression method: deflate
+ zh:write(number_to_lestring(lfh.last_mod_file_time, 2))
+ zh:write(number_to_lestring(lfh.last_mod_file_date, 2))
+ zh:write(number_to_lestring(lfh.crc32, 4))
+ zh:write(number_to_lestring(lfh.compressed_size, 4))
+ zh:write(number_to_lestring(lfh.uncompressed_size, 4))
+ zh:write(number_to_lestring(lfh.file_name_length, 2))
+ zh:write(number_to_lestring(lfh.extra_field_length, 2))
+ zh:write(number_to_lestring(0, 2)) -- file comment length
+ zh:write(number_to_lestring(0, 2)) -- disk number start
+ zh:write(number_to_lestring(0, 2)) -- internal file attributes
+ zh:write(number_to_lestring(lfh.external_attr, 4)) -- external file attributes
+ zh:write(number_to_lestring(lfh.offset, 4)) -- relative offset of local header
+ zh:write(lfh.file_name)
+ size_of_central_directory = size_of_central_directory + 46 + lfh.file_name_length
+ end
+
+ -- End of central directory record
+ zh:write(END_OF_CENTRAL_DIR_SIGNATURE) -- signature
+ zh:write(number_to_lestring(0, 2)) -- number of this disk
+ zh:write(number_to_lestring(0, 2)) -- number of disk with start of central directory
+ zh:write(number_to_lestring(#self.files, 2)) -- total number of entries in the central dir on this disk
+ zh:write(number_to_lestring(#self.files, 2)) -- total number of entries in the central dir
+ zh:write(number_to_lestring(size_of_central_directory, 4))
+ zh:write(number_to_lestring(central_directory_offset, 4))
+ zh:write(number_to_lestring(0, 2)) -- zip file comment length
+ zh:close()
+
+ return true
+end
+
+--- Return a zip handle open for writing.
+-- @param name filename of the zipfile to be created.
+-- @return a zip handle, or nil in case of error.
+function zip.new_zipwriter(name)
+
+ local zw = {}
+
+ zw.ziphandle = io.open(fs.absolute_name(name), "wb")
+ if not zw.ziphandle then
+ return nil
+ end
+ zw.files = {}
+ zw.in_open_file = false
+
+ zw.add = zipwriter_add
+ zw.close = zipwriter_close
+ zw.open_new_file_in_zip = zipwriter_open_new_file_in_zip
+ zw.write_file_in_zip = zipwriter_write_file_in_zip
+ zw.close_file_in_zip = zipwriter_close_file_in_zip
+
+ return zw
+end
+
+--- Compress files in a .zip archive.
+-- @param zipfile string: pathname of .zip archive to be created.
+-- @param ... Filenames to be stored in the archive are given as
+-- additional arguments.
+-- @return boolean or (boolean, string): true on success,
+-- false and an error message on failure.
+function zip.zip(zipfile, ...)
+ local zw = zip.new_zipwriter(zipfile)
+ if not zw then
+ return nil, "error opening "..zipfile
+ end
+
+ local args = pack(...)
+ local ok, err
+ for i=1, args.n do
+ local file = args[i]
+ if fs.is_dir(file) then
+ for _, entry in pairs(fs.find(file)) do
+ local fullname = dir.path(file, entry)
+ if fs.is_file(fullname) then
+ ok, err = zw:add(fullname)
+ if not ok then break end
+ end
+ end
+ else
+ ok, err = zw:add(file)
+ if not ok then break end
+ end
+ end
+
+ zw:close()
+ return ok, err
+end
+
+
+local function ziptime_to_luatime(ztime, zdate)
+ local date = {
+ year = shr(zdate, 9) + 1980,
+ month = shr(lowbits(zdate, 9), 5),
+ day = lowbits(zdate, 5),
+ hour = shr(ztime, 11),
+ min = shr(lowbits(ztime, 11), 5),
+ sec = lowbits(ztime, 5) * 2,
+ }
+
+ if date.month == 0 then date.month = 1 end
+ if date.day == 0 then date.day = 1 end
+
+ return date
+end
+
+local function read_file_in_zip(zh, cdr)
+ local sig = zh:read(4)
+ if sig ~= LOCAL_FILE_HEADER_SIGNATURE then
+ return nil, "failed reading Local File Header signature"
+ end
+
+ -- Skip over the rest of the zip file header. See
+ -- zipwriter_close_file_in_zip for the format.
+ zh:seek("cur", 22)
+ local file_name_length = lestring_to_number(zh:read(2))
+ local extra_field_length = lestring_to_number(zh:read(2))
+ zh:read(file_name_length)
+ zh:read(extra_field_length)
+
+ local data = zh:read(cdr.compressed_size)
+
+ local uncompressed
+ if cdr.compression_method == 8 then
+ uncompressed = zlib_uncompress(data, "raw")
+ elseif cdr.compression_method == 0 then
+ uncompressed = data
+ else
+ return nil, "unknown compression method " .. cdr.compression_method
+ end
+
+ if #uncompressed ~= cdr.uncompressed_size then
+ return nil, "uncompressed size doesn't match"
+ end
+ if cdr.crc32 ~= zlib_crc32(uncompressed) then
+ return nil, "crc32 failed (expected " .. cdr.crc32 .. ") - data: " .. uncompressed
+ end
+
+ return uncompressed
+end
+
+local function process_end_of_central_dir(zh)
+ local at, err = zh:seek("end", -22)
+ if not at then
+ return nil, err
+ end
+
+ while true do
+ local sig = zh:read(4)
+ if sig == END_OF_CENTRAL_DIR_SIGNATURE then
+ break
+ end
+ at = at - 1
+ local at1, err = zh:seek("set", at)
+ if at1 ~= at then
+ return nil, "Could not find End of Central Directory signature"
+ end
+ end
+
+ -- number of this disk (2 bytes)
+ -- number of the disk with the start of the central directory (2 bytes)
+ -- total number of entries in the central directory on this disk (2 bytes)
+ -- total number of entries in the central directory (2 bytes)
+ zh:seek("cur", 6)
+
+ local central_directory_entries = lestring_to_number(zh:read(2))
+
+ -- central directory size (4 bytes)
+ zh:seek("cur", 4)
+
+ local central_directory_offset = lestring_to_number(zh:read(4))
+
+ return central_directory_entries, central_directory_offset
+end
+
+local function process_central_dir(zh, cd_entries)
+
+ local files = {}
+
+ for i = 1, cd_entries do
+ local sig = zh:read(4)
+ if sig ~= CENTRAL_DIRECTORY_SIGNATURE then
+ return nil, "failed reading Central Directory signature"
+ end
+
+ local cdr = {}
+ files[i] = cdr
+
+ cdr.version_made_by = lestring_to_number(zh:read(2))
+ cdr.version_needed = lestring_to_number(zh:read(2))
+ cdr.bitflag = lestring_to_number(zh:read(2))
+ cdr.compression_method = lestring_to_number(zh:read(2))
+ cdr.last_mod_file_time = lestring_to_number(zh:read(2))
+ cdr.last_mod_file_date = lestring_to_number(zh:read(2))
+ cdr.last_mod_luatime = ziptime_to_luatime(cdr.last_mod_file_time, cdr.last_mod_file_date)
+ cdr.crc32 = lestring_to_number(zh:read(4))
+ cdr.compressed_size = lestring_to_number(zh:read(4))
+ cdr.uncompressed_size = lestring_to_number(zh:read(4))
+ cdr.file_name_length = lestring_to_number(zh:read(2))
+ cdr.extra_field_length = lestring_to_number(zh:read(2))
+ cdr.file_comment_length = lestring_to_number(zh:read(2))
+ cdr.disk_number_start = lestring_to_number(zh:read(2))
+ cdr.internal_attr = lestring_to_number(zh:read(2))
+ cdr.external_attr = lestring_to_number(zh:read(4))
+ cdr.offset = lestring_to_number(zh:read(4))
+ cdr.file_name = zh:read(cdr.file_name_length)
+ cdr.extra_field = zh:read(cdr.extra_field_length)
+ cdr.file_comment = zh:read(cdr.file_comment_length)
+ end
+ return files
+end
+
+--- Uncompress files from a .zip archive.
+-- @param zipfile string: pathname of .zip archive to be created.
+-- @return boolean or (boolean, string): true on success,
+-- false and an error message on failure.
+function zip.unzip(zipfile)
+ zipfile = fs.absolute_name(zipfile)
+ local zh, err = io.open(zipfile, "rb")
+ if not zh then
+ return nil, err
+ end
+
+ local cd_entries, cd_offset = process_end_of_central_dir(zh)
+ if not cd_entries then
+ return nil, cd_offset
+ end
+
+ local ok, err = zh:seek("set", cd_offset)
+ if not ok then
+ return nil, err
+ end
+
+ local files, err = process_central_dir(zh, cd_entries)
+ if not files then
+ return nil, err
+ end
+
+ for _, cdr in ipairs(files) do
+ local file = cdr.file_name
+ if file:sub(#file) == "/" then
+ local ok, err = fs.make_dir(dir.path(fs.current_dir(), file))
+ if not ok then
+ return nil, err
+ end
+ else
+ local base = dir.dir_name(file)
+ if base ~= "" then
+ base = dir.path(fs.current_dir(), base)
+ if not fs.is_dir(base) then
+ local ok, err = fs.make_dir(base)
+ if not ok then
+ return nil, err
+ end
+ end
+ end
+
+ local ok, err = zh:seek("set", cdr.offset)
+ if not ok then
+ return nil, err
+ end
+
+ local contents, err = read_file_in_zip(zh, cdr)
+ if not contents then
+ return nil, err
+ end
+ local pathname = dir.path(fs.current_dir(), file)
+ local wf, err = io.open(pathname, "wb")
+ if not wf then
+ zh:close()
+ return nil, err
+ end
+ wf:write(contents)
+ wf:close()
+
+ if cdr.external_attr > 0 then
+ fs.set_permissions(pathname, "exec", "all")
+ else
+ fs.set_permissions(pathname, "read", "all")
+ end
+ fs.set_time(pathname, cdr.last_mod_luatime)
+ end
+ end
+ zh:close()
+ return true
+end
+
+function zip.gzip(input_filename, output_filename)
+ assert(type(input_filename) == "string")
+ assert(output_filename == nil or type(output_filename) == "string")
+
+ if not output_filename then
+ output_filename = input_filename .. ".gz"
+ end
+
+ local fn = fun.partial(fun.flip(zlib_compress), "gzip")
+ return fs.filter_file(fn, input_filename, output_filename)
+end
+
+function zip.gunzip(input_filename, output_filename)
+ assert(type(input_filename) == "string")
+ assert(output_filename == nil or type(output_filename) == "string")
+
+ if not output_filename then
+ output_filename = input_filename:gsub("%.gz$", "")
+ end
+
+ local fn = fun.partial(fun.flip(zlib_uncompress), "gzip")
+ return fs.filter_file(fn, input_filename, output_filename)
+end
+
+return zip
diff --git a/src/luarocks/type/manifest.lua b/src/luarocks/type/manifest.lua
new file mode 100644
index 0000000..043366e
--- /dev/null
+++ b/src/luarocks/type/manifest.lua
@@ -0,0 +1,80 @@
+local type_manifest = {}
+
+local type_check = require("luarocks.type_check")
+
+local manifest_formats = type_check.declare_schemas({
+ ["3.0"] = {
+ repository = {
+ _mandatory = true,
+ -- packages
+ _any = {
+ -- versions
+ _any = {
+ -- items
+ _any = {
+ arch = { _type = "string", _mandatory = true },
+ modules = { _any = { _type = "string" } },
+ commands = { _any = { _type = "string" } },
+ dependencies = { _any = { _type = "string" } },
+ -- TODO: to be extended with more metadata.
+ }
+ }
+ }
+ },
+ modules = {
+ _mandatory = true,
+ -- modules
+ _any = {
+ -- providers
+ _any = { _type = "string" }
+ }
+ },
+ commands = {
+ _mandatory = true,
+ -- modules
+ _any = {
+ -- commands
+ _any = { _type = "string" }
+ }
+ },
+ dependencies = {
+ -- each module
+ _any = {
+ -- each version
+ _any = {
+ -- each dependency
+ _any = {
+ name = { _type = "string" },
+ namespace = { _type = "string" },
+ constraints = {
+ _any = {
+ no_upgrade = { _type = "boolean" },
+ op = { _type = "string" },
+ version = {
+ string = { _type = "string" },
+ _any = { _type = "number" },
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+})
+
+--- Type check a manifest table.
+-- Verify the correctness of elements from a
+-- manifest table, reporting on unknown fields and type
+-- mismatches.
+-- @return boolean or (nil, string): true if type checking
+-- succeeded, or nil and an error message if it failed.
+function type_manifest.check(manifest, globals)
+ assert(type(manifest) == "table")
+ local format = manifest_formats["3.0"]
+ local ok, err = type_check.check_undeclared_globals(globals, format)
+ if not ok then return nil, err end
+ return type_check.type_check_table("3.0", manifest, format, "")
+end
+
+return type_manifest
diff --git a/src/luarocks/type/rockspec.lua b/src/luarocks/type/rockspec.lua
new file mode 100644
index 0000000..0b4b5dc
--- /dev/null
+++ b/src/luarocks/type/rockspec.lua
@@ -0,0 +1,199 @@
+local type_rockspec = {}
+
+local type_check = require("luarocks.type_check")
+
+type_rockspec.rockspec_format = "3.0"
+
+-- Syntax for type-checking tables:
+--
+-- A type-checking table describes typing data for a value.
+-- Any key starting with an underscore has a special meaning:
+-- _type (string) is the Lua type of the value. Default is "table".
+-- _mandatory (boolean) indicates if the value is a mandatory key in its container table. Default is false.
+-- For "string" types only:
+-- _pattern (string) is the string-matching pattern, valid for string types only. Default is ".*".
+-- For "table" types only:
+-- _any (table) is the type-checking table for unspecified keys, recursively checked.
+-- _more (boolean) indicates that the table accepts unspecified keys and does not type-check them.
+-- Any other string keys that don't start with an underscore represent known keys and are type-checking tables, recursively checked.
+
+local rockspec_formats, versions = type_check.declare_schemas({
+ ["1.0"] = {
+ rockspec_format = { _type = "string" },
+ package = { _type = "string", _mandatory = true },
+ version = { _type = "string", _pattern = "[%w.]+-[%d]+", _mandatory = true },
+ description = {
+ summary = { _type = "string" },
+ detailed = { _type = "string" },
+ homepage = { _type = "string" },
+ license = { _type = "string" },
+ maintainer = { _type = "string" },
+ },
+ dependencies = {
+ platforms = type_check.MAGIC_PLATFORMS,
+ _any = {
+ _type = "string",
+ _name = "a valid dependency string",
+ _pattern = "%s*([a-zA-Z0-9][a-zA-Z0-9%.%-%_]*)%s*([^/]*)",
+ },
+ },
+ supported_platforms = {
+ _any = { _type = "string" },
+ },
+ external_dependencies = {
+ platforms = type_check.MAGIC_PLATFORMS,
+ _any = {
+ program = { _type = "string" },
+ header = { _type = "string" },
+ library = { _type = "string" },
+ }
+ },
+ source = {
+ _mandatory = true,
+ platforms = type_check.MAGIC_PLATFORMS,
+ url = { _type = "string", _mandatory = true },
+ md5 = { _type = "string" },
+ file = { _type = "string" },
+ dir = { _type = "string" },
+ tag = { _type = "string" },
+ branch = { _type = "string" },
+ module = { _type = "string" },
+ cvs_tag = { _type = "string" },
+ cvs_module = { _type = "string" },
+ },
+ build = {
+ platforms = type_check.MAGIC_PLATFORMS,
+ type = { _type = "string" },
+ install = {
+ lua = {
+ _more = true
+ },
+ lib = {
+ _more = true
+ },
+ conf = {
+ _more = true
+ },
+ bin = {
+ _more = true
+ }
+ },
+ copy_directories = {
+ _any = { _type = "string" },
+ },
+ _more = true,
+ _mandatory = true
+ },
+ hooks = {
+ platforms = type_check.MAGIC_PLATFORMS,
+ post_install = { _type = "string" },
+ },
+ },
+
+ ["1.1"] = {
+ deploy = {
+ wrap_bin_scripts = { _type = "boolean" },
+ }
+ },
+
+ ["3.0"] = {
+ description = {
+ labels = {
+ _any = { _type = "string" }
+ },
+ issues_url = { _type = "string" },
+ },
+ dependencies = {
+ _any = {
+ _pattern = "%s*([a-zA-Z0-9%.%-%_]*/?[a-zA-Z0-9][a-zA-Z0-9%.%-%_]*)%s*([^/]*)",
+ },
+ },
+ build_dependencies = {
+ platforms = type_check.MAGIC_PLATFORMS,
+ _any = {
+ _type = "string",
+ _name = "a valid dependency string",
+ _pattern = "%s*([a-zA-Z0-9%.%-%_]*/?[a-zA-Z0-9][a-zA-Z0-9%.%-%_]*)%s*([^/]*)",
+ },
+ },
+ test_dependencies = {
+ platforms = type_check.MAGIC_PLATFORMS,
+ _any = {
+ _type = "string",
+ _name = "a valid dependency string",
+ _pattern = "%s*([a-zA-Z0-9%.%-%_]*/?[a-zA-Z0-9][a-zA-Z0-9%.%-%_]*)%s*([^/]*)",
+ },
+ },
+ build = {
+ _mandatory = false,
+ },
+ test = {
+ platforms = type_check.MAGIC_PLATFORMS,
+ type = { _type = "string" },
+ _more = true,
+ },
+ }
+})
+
+type_rockspec.order = {"rockspec_format", "package", "version",
+ { "source", { "url", "tag", "branch", "md5" } },
+ { "description", {"summary", "detailed", "homepage", "license" } },
+ "supported_platforms", "dependencies", "build_dependencies", "external_dependencies",
+ { "build", {"type", "modules", "copy_directories", "platforms"} },
+ "test_dependencies", { "test", {"type"} },
+ "hooks"}
+
+local function check_rockspec_using_version(rockspec, globals, version)
+ local schema = rockspec_formats[version]
+ if not schema then
+ return nil, "unknown rockspec format " .. version
+ end
+ local ok, err = type_check.check_undeclared_globals(globals, schema)
+ if ok then
+ ok, err = type_check.type_check_table(version, rockspec, schema, "")
+ end
+ if ok then
+ return true
+ else
+ return nil, err
+ end
+end
+
+--- Type check a rockspec table.
+-- Verify the correctness of elements from a
+-- rockspec table, reporting on unknown fields and type
+-- mismatches.
+-- @return boolean or (nil, string): true if type checking
+-- succeeded, or nil and an error message if it failed.
+function type_rockspec.check(rockspec, globals)
+ assert(type(rockspec) == "table")
+
+ local version = rockspec.rockspec_format or "1.0"
+ local ok, err = check_rockspec_using_version(rockspec, globals, version)
+ if ok then
+ return true
+ end
+
+ -- Rockspec parsing failed.
+ -- Let's see if it would pass using a later version.
+
+ local found = false
+ for _, v in ipairs(versions) do
+ if not found then
+ if v == version then
+ found = true
+ end
+ else
+ local v_ok, v_err = check_rockspec_using_version(rockspec, globals, v)
+ if v_ok then
+ return nil, err .. " (using rockspec format " .. version .. " -- " ..
+ [[adding 'rockspec_format = "]] .. v .. [["' to the rockspec ]] ..
+ [[will fix this)]]
+ end
+ end
+ end
+
+ return nil, err .. " (using rockspec format " .. version .. ")"
+end
+
+return type_rockspec
diff --git a/src/luarocks/type_check.lua b/src/luarocks/type_check.lua
new file mode 100644
index 0000000..21085ef
--- /dev/null
+++ b/src/luarocks/type_check.lua
@@ -0,0 +1,213 @@
+
+local type_check = {}
+
+local cfg = require("luarocks.core.cfg")
+local fun = require("luarocks.fun")
+local util = require("luarocks.util")
+local vers = require("luarocks.core.vers")
+--------------------------------------------------------------------------------
+
+-- A magic constant that is not used anywhere in a schema definition
+-- and retains equality when the table is deep-copied.
+type_check.MAGIC_PLATFORMS = 0xEBABEFAC
+
+do
+ local function fill_in_version(tbl, version)
+ for _, v in pairs(tbl) do
+ if type(v) == "table" then
+ if v._version == nil then
+ v._version = version
+ end
+ fill_in_version(v)
+ end
+ end
+ end
+
+ local function expand_magic_platforms(tbl)
+ for k,v in pairs(tbl) do
+ if v == type_check.MAGIC_PLATFORMS then
+ tbl[k] = {
+ _any = util.deep_copy(tbl)
+ }
+ tbl[k]._any[k] = nil
+ elseif type(v) == "table" then
+ expand_magic_platforms(v)
+ end
+ end
+ end
+
+ -- Build a table of schemas.
+ -- @param versions a table where each key is a version number as a string,
+ -- and the value is a schema specification. Schema versions are considered
+ -- incremental: version "2.0" only needs to specify what's new/changed from
+ -- version "1.0".
+ function type_check.declare_schemas(inputs)
+ local schemas = {}
+ local parent_version
+
+ local versions = fun.reverse_in(fun.sort_in(util.keys(inputs), vers.compare_versions))
+
+ for _, version in ipairs(versions) do
+ local schema = inputs[version]
+ if parent_version ~= nil then
+ local copy = util.deep_copy(schemas[parent_version])
+ util.deep_merge(copy, schema)
+ schema = copy
+ end
+ fill_in_version(schema, version)
+ expand_magic_platforms(schema)
+ parent_version = version
+ schemas[version] = schema
+ end
+
+ return schemas, versions
+ end
+end
+
+--------------------------------------------------------------------------------
+
+local function check_version(version, typetbl, context)
+ local typetbl_version = typetbl._version or "1.0"
+ if vers.compare_versions(typetbl_version, version) then
+ if context == "" then
+ return nil, "Invalid rockspec_format version number in rockspec? Please fix rockspec accordingly."
+ else
+ return nil, context.." is not supported in rockspec format "..version.." (requires version "..typetbl_version.."), please fix the rockspec_format field accordingly."
+ end
+ end
+ return true
+end
+
+--- Type check an object.
+-- The object is compared against an archetypical value
+-- matching the expected type -- the actual values don't matter,
+-- only their types. Tables are type checked recursively.
+-- @param version string: The version of the item.
+-- @param item any: The object being checked.
+-- @param typetbl any: The type-checking table for the object.
+-- @param context string: A string indicating the "context" where the
+-- error occurred (the full table path), for error messages.
+-- @return boolean or (nil, string): true if type checking
+-- succeeded, or nil and an error message if it failed.
+-- @see type_check_table
+local function type_check_item(version, item, typetbl, context)
+ assert(type(version) == "string")
+
+ if typetbl._version and typetbl._version ~= "1.0" then
+ local ok, err = check_version(version, typetbl, context)
+ if not ok then
+ return nil, err
+ end
+ end
+
+ local item_type = type(item) or "nil"
+ local expected_type = typetbl._type or "table"
+
+ if expected_type == "number" then
+ if not tonumber(item) then
+ return nil, "Type mismatch on field "..context..": expected a number"
+ end
+ elseif expected_type == "string" then
+ if item_type ~= "string" then
+ return nil, "Type mismatch on field "..context..": expected a string, got "..item_type
+ end
+ local pattern = typetbl._pattern
+ if pattern then
+ if not item:match("^"..pattern.."$") then
+ local what = typetbl._name or ("'"..pattern.."'")
+ return nil, "Type mismatch on field "..context..": invalid value '"..item.."' does not match " .. what
+ end
+ end
+ elseif expected_type == "table" then
+ if item_type ~= expected_type then
+ return nil, "Type mismatch on field "..context..": expected a table"
+ else
+ return type_check.type_check_table(version, item, typetbl, context)
+ end
+ elseif item_type ~= expected_type then
+ return nil, "Type mismatch on field "..context..": expected "..expected_type
+ end
+ return true
+end
+
+local function mkfield(context, field)
+ if context == "" then
+ return tostring(field)
+ elseif type(field) == "string" then
+ return context.."."..field
+ else
+ return context.."["..tostring(field).."]"
+ end
+end
+
+--- Type check the contents of a table.
+-- The table's contents are compared against a reference table,
+-- which contains the recognized fields, with archetypical values
+-- matching the expected types -- the actual values of items in the
+-- reference table don't matter, only their types (ie, for field x
+-- in tbl that is correctly typed, type(tbl.x) == type(types.x)).
+-- If the reference table contains a field called MORE, then
+-- unknown fields in the checked table are accepted.
+-- If it contains a field called ANY, then its type will be
+-- used to check any unknown fields. If a field is prefixed
+-- with MUST_, it is mandatory; its absence from the table is
+-- a type error.
+-- Tables are type checked recursively.
+-- @param version string: The version of tbl.
+-- @param tbl table: The table to be type checked.
+-- @param typetbl table: The type-checking table, containing
+-- values for recognized fields in the checked table.
+-- @param context string: A string indicating the "context" where the
+-- error occurred (such as the name of the table the item is a part of),
+-- to be used by error messages.
+-- @return boolean or (nil, string): true if type checking
+-- succeeded, or nil and an error message if it failed.
+function type_check.type_check_table(version, tbl, typetbl, context)
+ assert(type(version) == "string")
+ assert(type(tbl) == "table")
+ assert(type(typetbl) == "table")
+
+ local ok, err = check_version(version, typetbl, context)
+ if not ok then
+ return nil, err
+ end
+
+ for k, v in pairs(tbl) do
+ local t = typetbl[k] or typetbl._any
+ if t then
+ local ok, err = type_check_item(version, v, t, mkfield(context, k))
+ if not ok then return nil, err end
+ elseif typetbl._more then
+ -- Accept unknown field
+ else
+ if not cfg.accept_unknown_fields then
+ return nil, "Unknown field "..k
+ end
+ end
+ end
+ for k, v in pairs(typetbl) do
+ if k:sub(1,1) ~= "_" and v._mandatory then
+ if not tbl[k] then
+ return nil, "Mandatory field "..mkfield(context, k).." is missing."
+ end
+ end
+ end
+ return true
+end
+
+function type_check.check_undeclared_globals(globals, typetbl)
+ local undeclared = {}
+ for glob, _ in pairs(globals) do
+ if not (typetbl[glob] or typetbl["MUST_"..glob]) then
+ table.insert(undeclared, glob)
+ end
+ end
+ if #undeclared == 1 then
+ return nil, "Unknown variable: "..undeclared[1]
+ elseif #undeclared > 1 then
+ return nil, "Unknown variables: "..table.concat(undeclared, ", ")
+ end
+ return true
+end
+
+return type_check
diff --git a/src/luarocks/upload/api.lua b/src/luarocks/upload/api.lua
new file mode 100644
index 0000000..e141370
--- /dev/null
+++ b/src/luarocks/upload/api.lua
@@ -0,0 +1,265 @@
+
+local api = {}
+
+local cfg = require("luarocks.core.cfg")
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+local util = require("luarocks.util")
+local persist = require("luarocks.persist")
+local multipart = require("luarocks.upload.multipart")
+local json = require("luarocks.vendor.dkjson")
+local dir_sep = package.config:sub(1, 1)
+
+local Api = {}
+
+local function upload_config_file()
+ if not cfg.config_files.user.file then
+ return nil
+ end
+ return (cfg.config_files.user.file:gsub("[\\/][^\\/]+$", dir_sep .. "upload_config.lua"))
+end
+
+function Api:load_config()
+ local upload_conf = upload_config_file()
+ if not upload_conf then return nil end
+ local config, err = persist.load_into_table(upload_conf)
+ return config
+end
+
+function Api:save_config()
+ -- Test configuration before saving it.
+ local res, err = self:raw_method("status")
+ if not res then
+ return nil, err
+ end
+ if res.errors then
+ util.printerr("Server says: " .. tostring(res.errors[1]))
+ return
+ end
+ local upload_conf = upload_config_file()
+ if not upload_conf then return nil end
+ local ok, err = fs.make_dir(dir.dir_name(upload_conf))
+ if not ok then
+ return nil, err
+ end
+ persist.save_from_table(upload_conf, self.config)
+ fs.set_permissions(upload_conf, "read", "user")
+end
+
+function Api:check_version()
+ if not self._server_tool_version then
+ local tool_version = cfg.upload.tool_version
+ local res, err = self:request(tostring(self.config.server) .. "/api/tool_version", {
+ current = tool_version
+ })
+ if not res then
+ return nil, err
+ end
+ if not res.version then
+ return nil, "failed to fetch tool version"
+ end
+ self._server_tool_version = res.version
+ if res.force_update then
+ return nil, "Your upload client is too out of date to continue, please upgrade LuaRocks."
+ end
+ if res.version ~= tool_version then
+ util.warning("your LuaRocks is out of date, consider upgrading.")
+ end
+ end
+ return true
+end
+
+function Api:method(...)
+ local res, err = self:raw_method(...)
+ if not res then
+ return nil, err
+ end
+ if res.errors then
+ if res.errors[1] == "Invalid key" then
+ return nil, res.errors[1] .. " (use the --api-key flag to change)"
+ end
+ local msg = table.concat(res.errors, ", ")
+ return nil, "API Failed: " .. msg
+ end
+ return res
+end
+
+function Api:raw_method(path, ...)
+ self:check_version()
+ local url = tostring(self.config.server) .. "/api/" .. tostring(cfg.upload.api_version) .. "/" .. tostring(self.config.key) .. "/" .. tostring(path)
+ return self:request(url, ...)
+end
+
+local function encode_query_string(t, sep)
+ if sep == nil then
+ sep = "&"
+ end
+ local i = 0
+ local buf = { }
+ for k, v in pairs(t) do
+ if type(k) == "number" and type(v) == "table" then
+ k, v = v[1], v[2]
+ end
+ buf[i + 1] = multipart.url_escape(k)
+ buf[i + 2] = "="
+ buf[i + 3] = multipart.url_escape(v)
+ buf[i + 4] = sep
+ i = i + 4
+ end
+ buf[i] = nil
+ return table.concat(buf)
+end
+
+local function redact_api_url(url)
+ url = tostring(url)
+ return (url:gsub(".*/api/[^/]+/[^/]+", "")) or ""
+end
+
+local ltn12_ok, ltn12 = pcall(require, "ltn12")
+if not ltn12_ok then -- If not using LuaSocket and/or LuaSec...
+
+function Api:request(url, params, post_params)
+ local vars = cfg.variables
+
+ if fs.which_tool("downloader") == "wget" then
+ local curl_ok, err = fs.is_tool_available(vars.CURL, "curl")
+ if not curl_ok then
+ return nil, err
+ end
+ end
+
+ if not self.config.key then
+ return nil, "Must have API key before performing any actions."
+ end
+ if params and next(params) then
+ url = url .. ("?" .. encode_query_string(params))
+ end
+ local method = "GET"
+ local out
+ local tmpfile = fs.tmpname()
+ if post_params then
+ method = "POST"
+ local curl_cmd = vars.CURL.." "..vars.CURLNOCERTFLAG.." -f -L --silent --user-agent \""..cfg.user_agent.." via curl\" "
+ for k,v in pairs(post_params) do
+ local var = v
+ if type(v) == "table" then
+ var = "@"..v.fname
+ end
+ curl_cmd = curl_cmd .. "--form \""..k.."="..var.."\" "
+ end
+ if cfg.connection_timeout and cfg.connection_timeout > 0 then
+ curl_cmd = curl_cmd .. "--connect-timeout "..tonumber(cfg.connection_timeout).." "
+ end
+ local ok = fs.execute_string(curl_cmd..fs.Q(url).." -o "..fs.Q(tmpfile))
+ if not ok then
+ return nil, "API failure: " .. redact_api_url(url)
+ end
+ else
+ local ok, err = fs.download(url, tmpfile)
+ if not ok then
+ return nil, "API failure: " .. tostring(err) .. " - " .. redact_api_url(url)
+ end
+ end
+
+ local tmpfd = io.open(tmpfile)
+ if not tmpfd then
+ os.remove(tmpfile)
+ return nil, "API failure reading temporary file - " .. redact_api_url(url)
+ end
+ out = tmpfd:read("*a")
+ tmpfd:close()
+ os.remove(tmpfile)
+
+ if self.debug then
+ util.printout("[" .. tostring(method) .. " via curl] " .. redact_api_url(url) .. " ... ")
+ end
+
+ return json.decode(out)
+end
+
+else -- use LuaSocket and LuaSec
+
+local warned_luasec = false
+
+function Api:request(url, params, post_params)
+ local server = tostring(self.config.server)
+ local http_ok, http
+ local via = "luasocket"
+ if server:match("^https://") then
+ http_ok, http = pcall(require, "ssl.https")
+ if http_ok then
+ via = "luasec"
+ else
+ if not warned_luasec then
+ util.printerr("LuaSec is not available; using plain HTTP. Install 'luasec' to enable HTTPS.")
+ warned_luasec = true
+ end
+ http_ok, http = pcall(require, "socket.http")
+ url = url:gsub("^https", "http")
+ via = "luasocket"
+ end
+ else
+ http_ok, http = pcall(require, "socket.http")
+ end
+ if not http_ok then
+ return nil, "Failed loading socket library!"
+ end
+
+ if not self.config.key then
+ return nil, "Must have API key before performing any actions."
+ end
+ local body
+ local headers = {}
+ if params and next(params) then
+ url = url .. ("?" .. encode_query_string(params))
+ end
+ if post_params then
+ local boundary
+ body, boundary = multipart.encode(post_params)
+ headers["Content-length"] = #body
+ headers["Content-type"] = "multipart/form-data; boundary=" .. tostring(boundary)
+ end
+ local method = post_params and "POST" or "GET"
+ if self.debug then
+ util.printout("[" .. tostring(method) .. " via "..via.."] " .. redact_api_url(url) .. " ... ")
+ end
+ local out = {}
+ local _, status = http.request({
+ url = url,
+ headers = headers,
+ method = method,
+ sink = ltn12.sink.table(out),
+ source = body and ltn12.source.string(body)
+ })
+ if self.debug then
+ util.printout(tostring(status))
+ end
+ local pok, ret, err = pcall(json.decode, table.concat(out))
+ if pok and ret then
+ return ret
+ end
+ return nil, "API returned " .. tostring(status) .. " - " .. redact_api_url(url)
+end
+
+end
+
+function api.new(args)
+ local self = {}
+ setmetatable(self, { __index = Api })
+ self.config = self:load_config() or {}
+ self.config.server = args.server or self.config.server or cfg.upload.server
+ self.config.version = self.config.version or cfg.upload.version
+ self.config.key = args.temp_key or args.api_key or self.config.key
+ self.debug = args.debug
+ if not self.config.key then
+ return nil, "You need an API key to upload rocks.\n" ..
+ "Navigate to "..self.config.server.."/settings to get a key\n" ..
+ "and then pass it through the --api-key=<key> flag."
+ end
+ if args.api_key then
+ self:save_config()
+ end
+ return self
+end
+
+return api
diff --git a/src/luarocks/upload/multipart.lua b/src/luarocks/upload/multipart.lua
new file mode 100644
index 0000000..56ae873
--- /dev/null
+++ b/src/luarocks/upload/multipart.lua
@@ -0,0 +1,109 @@
+
+local multipart = {}
+
+local File = {}
+
+local unpack = unpack or table.unpack
+
+-- socket.url.escape(s) from LuaSocket 3.0rc1
+function multipart.url_escape(s)
+ return (string.gsub(s, "([^A-Za-z0-9_])", function(c)
+ return string.format("%%%02x", string.byte(c))
+ end))
+end
+
+function File:mime()
+ if not self.mimetype then
+ local mimetypes_ok, mimetypes = pcall(require, "mimetypes")
+ if mimetypes_ok then
+ self.mimetype = mimetypes.guess(self.fname)
+ end
+ self.mimetype = self.mimetype or "application/octet-stream"
+ end
+ return self.mimetype
+end
+
+function File:content()
+ local fd = io.open(self.fname, "rb")
+ if not fd then
+ return nil, "Failed to open file: "..self.fname
+ end
+ local data = fd:read("*a")
+ fd:close()
+ return data
+end
+
+local function rand_string(len)
+ local shuffled = {}
+ for i = 1, len do
+ local r = math.random(97, 122)
+ if math.random() >= 0.5 then
+ r = r - 32
+ end
+ shuffled[i] = r
+ end
+ return string.char(unpack(shuffled))
+end
+
+-- multipart encodes params
+-- returns encoded string,boundary
+-- params is an a table of tuple tables:
+-- params = {
+-- {key1, value2},
+-- {key2, value2},
+-- key3: value3
+-- }
+function multipart.encode(params)
+ local tuples = { }
+ for i = 1, #params do
+ tuples[i] = params[i]
+ end
+ for k,v in pairs(params) do
+ if type(k) == "string" then
+ table.insert(tuples, {k, v})
+ end
+ end
+ local chunks = {}
+ for _, tuple in ipairs(tuples) do
+ local k,v = unpack(tuple)
+ k = multipart.url_escape(k)
+ local buffer = { 'Content-Disposition: form-data; name="' .. k .. '"' }
+ local content
+ if type(v) == "table" and v.__class == File then
+ buffer[1] = buffer[1] .. ('; filename="' .. v.fname:gsub(".*/", "") .. '"')
+ table.insert(buffer, "Content-type: " .. v:mime())
+ content = v:content()
+ else
+ content = v
+ end
+ table.insert(buffer, "")
+ table.insert(buffer, content)
+ table.insert(chunks, table.concat(buffer, "\r\n"))
+ end
+ local boundary
+ while not boundary do
+ boundary = "Boundary" .. rand_string(16)
+ for _, chunk in ipairs(chunks) do
+ if chunk:find(boundary) then
+ boundary = nil
+ break
+ end
+ end
+ end
+ local inner = "\r\n--" .. boundary .. "\r\n"
+ return table.concat({ "--", boundary, "\r\n",
+ table.concat(chunks, inner),
+ "\r\n", "--", boundary, "--", "\r\n" }), boundary
+end
+
+function multipart.new_file(fname, mime)
+ local self = {}
+ setmetatable(self, { __index = File })
+ self.__class = File
+ self.fname = fname
+ self.mimetype = mime
+ return self
+end
+
+return multipart
+
diff --git a/src/luarocks/util.lua b/src/luarocks/util.lua
new file mode 100644
index 0000000..de9157f
--- /dev/null
+++ b/src/luarocks/util.lua
@@ -0,0 +1,634 @@
+
+--- Assorted utilities for managing tables, plus a scheduler for rollback functions.
+-- Does not requires modules directly (only as locals
+-- inside specific functions) to avoid interdependencies,
+-- as this is used in the bootstrapping stage of luarocks.core.cfg.
+
+local util = {}
+
+local core = require("luarocks.core.util")
+
+util.cleanup_path = core.cleanup_path
+util.split_string = core.split_string
+util.sortedpairs = core.sortedpairs
+util.deep_merge = core.deep_merge
+util.deep_merge_under = core.deep_merge_under
+util.popen_read = core.popen_read
+util.show_table = core.show_table
+util.printerr = core.printerr
+util.warning = core.warning
+util.keys = core.keys
+
+local unpack = unpack or table.unpack
+local pack = table.pack or function(...) return { n = select("#", ...), ... } end
+
+local scheduled_functions = {}
+
+--- Schedule a function to be executed upon program termination.
+-- This is useful for actions such as deleting temporary directories
+-- or failure rollbacks.
+-- @param f function: Function to be executed.
+-- @param ... arguments to be passed to function.
+-- @return table: A token representing the scheduled execution,
+-- which can be used to remove the item later from the list.
+function util.schedule_function(f, ...)
+ assert(type(f) == "function")
+
+ local item = { fn = f, args = pack(...) }
+ table.insert(scheduled_functions, item)
+ return item
+end
+
+--- Unschedule a function.
+-- This is useful for cancelling a rollback of a completed operation.
+-- @param item table: The token representing the scheduled function that was
+-- returned from the schedule_function call.
+function util.remove_scheduled_function(item)
+ for k, v in pairs(scheduled_functions) do
+ if v == item then
+ table.remove(scheduled_functions, k)
+ return
+ end
+ end
+end
+
+--- Execute scheduled functions.
+-- Some calls create temporary files and/or directories and register
+-- corresponding cleanup functions. Calling this function will run
+-- these function, erasing temporaries.
+-- Functions are executed in the inverse order they were scheduled.
+function util.run_scheduled_functions()
+ local fs = require("luarocks.fs")
+ if fs.change_dir_to_root then
+ fs.change_dir_to_root()
+ end
+ for i = #scheduled_functions, 1, -1 do
+ local item = scheduled_functions[i]
+ item.fn(unpack(item.args, 1, item.args.n))
+ end
+end
+
+--- Produce a Lua pattern that matches precisely the given string
+-- (this is suitable to be concatenating to other patterns,
+-- so it does not include beginning- and end-of-string markers (^$)
+-- @param s string: The input string
+-- @return string: The equivalent pattern
+function util.matchquote(s)
+ return (s:gsub("[?%-+*%[%].%%()$^]","%%%1"))
+end
+
+local var_format_pattern = "%$%((%a[%a%d_]+)%)"
+
+-- Check if a set of needed variables are referenced
+-- somewhere in a list of definitions, warning the user
+-- about any unused ones. Each key in needed_set should
+-- appear as a $(XYZ) variable at least once as a
+-- substring of some value of var_defs.
+-- @param var_defs: a table with string keys and string
+-- values, containing variable definitions.
+-- @param needed_set: a set where keys are the names of
+-- needed variables.
+-- @param msg string: the warning message to display.
+function util.warn_if_not_used(var_defs, needed_set, msg)
+ local seen = {}
+ for _, val in pairs(var_defs) do
+ for used in val:gmatch(var_format_pattern) do
+ seen[used] = true
+ end
+ end
+ for var, _ in pairs(needed_set) do
+ if not seen[var] then
+ util.warning(msg:format(var))
+ end
+ end
+end
+
+-- Output any entries that might remain in $(XYZ) format,
+-- warning the user that substitutions have failed.
+-- @param line string: the input string
+local function warn_failed_matches(line)
+ local any_failed = false
+ if line:match(var_format_pattern) then
+ for unmatched in line:gmatch(var_format_pattern) do
+ util.warning("unmatched variable " .. unmatched)
+ any_failed = true
+ end
+ end
+ return any_failed
+end
+
+--- Perform make-style variable substitutions on string values of a table.
+-- For every string value tbl.x which contains a substring of the format
+-- "$(XYZ)" will have this substring replaced by vars["XYZ"], if that field
+-- exists in vars. Only string values are processed; this function
+-- does not scan subtables recursively.
+-- @param tbl table: Table to have its string values modified.
+-- @param vars table: Table containing string-string key-value pairs
+-- representing variables to replace in the strings values of tbl.
+function util.variable_substitutions(tbl, vars)
+ assert(type(tbl) == "table")
+ assert(type(vars) == "table")
+
+ local updated = {}
+ for k, v in pairs(tbl) do
+ if type(v) == "string" then
+ updated[k] = v:gsub(var_format_pattern, vars)
+ if warn_failed_matches(updated[k]) then
+ updated[k] = updated[k]:gsub(var_format_pattern, "")
+ end
+ end
+ end
+ for k, v in pairs(updated) do
+ tbl[k] = v
+ end
+end
+
+function util.lua_versions(sort)
+ local versions = { "5.1", "5.2", "5.3", "5.4" }
+ local i = 0
+ if sort == "descending" then
+ i = #versions + 1
+ return function()
+ i = i - 1
+ return versions[i]
+ end
+ else
+ return function()
+ i = i + 1
+ return versions[i]
+ end
+ end
+end
+
+function util.lua_path_variables()
+ local cfg = require("luarocks.core.cfg")
+ local lpath_var = "LUA_PATH"
+ local lcpath_var = "LUA_CPATH"
+
+ local lv = cfg.lua_version:gsub("%.", "_")
+ if lv ~= "5_1" then
+ if os.getenv("LUA_PATH_" .. lv) then
+ lpath_var = "LUA_PATH_" .. lv
+ end
+ if os.getenv("LUA_CPATH_" .. lv) then
+ lcpath_var = "LUA_CPATH_" .. lv
+ end
+ end
+ return lpath_var, lcpath_var
+end
+
+function util.starts_with(s, prefix)
+ return s:sub(1,#prefix) == prefix
+end
+
+--- Print a line to standard output
+function util.printout(...)
+ io.stdout:write(table.concat({...},"\t"))
+ io.stdout:write("\n")
+end
+
+function util.title(msg, porcelain, underline)
+ if porcelain then return end
+ util.printout()
+ util.printout(msg)
+ util.printout((underline or "-"):rep(#msg))
+ util.printout()
+end
+
+function util.this_program(default)
+ local i = 1
+ local last, cur = default, default
+ while i do
+ local dbg = debug and debug.getinfo(i,"S")
+ if not dbg then break end
+ last = cur
+ cur = dbg.source
+ i=i+1
+ end
+ local prog = last:sub(1,1) == "@" and last:sub(2) or last
+
+ -- Check if we found the true path of a script that has a wrapper
+ local lrdir, binpath = prog:match("^(.*)/lib/luarocks/rocks%-[0-9.]*/[^/]+/[^/]+(/bin/[^/]+)$")
+ if lrdir then
+ -- Return the wrapper instead
+ return lrdir .. binpath
+ end
+
+ return prog
+end
+
+function util.format_rock_name(name, namespace, version)
+ return (namespace and namespace.."/" or "")..name..(version and " "..version or "")
+end
+
+function util.deps_mode_option(parser, program)
+ local cfg = require("luarocks.core.cfg")
+
+ parser:option("--deps-mode", "How to handle dependencies. Four modes are supported:\n"..
+ "* all - use all trees from the rocks_trees list for finding dependencies\n"..
+ "* one - use only the current tree (possibly set with --tree)\n"..
+ "* order - use trees based on order (use the current tree and all "..
+ "trees below it on the rocks_trees list)\n"..
+ "* none - ignore dependencies altogether.\n"..
+ "The default mode may be set with the deps_mode entry in the configuration file.\n"..
+ 'The current default is "'..cfg.deps_mode..'".\n'..
+ "Type '"..util.this_program(program or "luarocks").."' with no "..
+ "arguments to see your list of rocks trees.")
+ :argname("<mode>")
+ :choices({"all", "one", "order", "none"})
+ parser:flag("--nodeps"):hidden(true)
+end
+
+function util.see_help(command, program)
+ return "See '"..util.this_program(program or "luarocks")..' help'..(command and " "..command or "").."'."
+end
+
+function util.see_also(text)
+ local see_also = "See also:\n"
+ if text then
+ see_also = see_also..text.."\n"
+ end
+ return see_also.." '"..util.this_program("luarocks").." help' for general options and configuration."
+end
+
+function util.announce_install(rockspec)
+ local cfg = require("luarocks.core.cfg")
+ local path = require("luarocks.path")
+
+ local suffix = ""
+ if rockspec.description and rockspec.description.license then
+ suffix = " (license: "..rockspec.description.license..")"
+ end
+
+ util.printout(rockspec.name.." "..rockspec.version.." is now installed in "..path.root_dir(cfg.root_dir)..suffix)
+ util.printout()
+end
+
+--- Collect rockspecs located in a subdirectory.
+-- @param versions table: A table mapping rock names to newest rockspec versions.
+-- @param paths table: A table mapping rock names to newest rockspec paths.
+-- @param unnamed_paths table: An array of rockspec paths that don't contain rock
+-- name and version in regular format.
+-- @param subdir string: path to subdirectory.
+local function collect_rockspecs(versions, paths, unnamed_paths, subdir)
+ local fs = require("luarocks.fs")
+ local dir = require("luarocks.dir")
+ local path = require("luarocks.path")
+ local vers = require("luarocks.core.vers")
+
+ if fs.is_dir(subdir) then
+ for file in fs.dir(subdir) do
+ file = dir.path(subdir, file)
+
+ if file:match("rockspec$") and fs.is_file(file) then
+ local rock, version = path.parse_name(file)
+
+ if rock then
+ if not versions[rock] or vers.compare_versions(version, versions[rock]) then
+ versions[rock] = version
+ paths[rock] = file
+ end
+ else
+ table.insert(unnamed_paths, file)
+ end
+ end
+ end
+ end
+end
+
+--- Get default rockspec name for commands that take optional rockspec name.
+-- @return string or (nil, string): path to the rockspec or nil and error message.
+function util.get_default_rockspec()
+ local versions, paths, unnamed_paths = {}, {}, {}
+ -- Look for rockspecs in some common locations.
+ collect_rockspecs(versions, paths, unnamed_paths, ".")
+ collect_rockspecs(versions, paths, unnamed_paths, "rockspec")
+ collect_rockspecs(versions, paths, unnamed_paths, "rockspecs")
+
+ if #unnamed_paths > 0 then
+ -- There are rockspecs not following "name-version.rockspec" format.
+ -- More than one are ambiguous.
+ if #unnamed_paths > 1 then
+ return nil, "Please specify which rockspec file to use."
+ else
+ return unnamed_paths[1]
+ end
+ else
+ local fs = require("luarocks.fs")
+ local dir = require("luarocks.dir")
+ local basename = dir.base_name(fs.current_dir())
+
+ if paths[basename] then
+ return paths[basename]
+ end
+
+ local rock = next(versions)
+
+ if rock then
+ -- If there are rockspecs for multiple rocks it's ambiguous.
+ if next(versions, rock) then
+ return nil, "Please specify which rockspec file to use."
+ else
+ return paths[rock]
+ end
+ else
+ return nil, "Argument missing: please specify a rockspec to use on current directory."
+ end
+ end
+end
+
+-- Quote Lua string, analogous to fs.Q.
+-- @param s A string, such as "hello"
+-- @return string: A quoted string, such as '"hello"'
+function util.LQ(s)
+ return ("%q"):format(s)
+end
+
+-- Split name and namespace of a package name.
+-- @param ns_name a name that may be in "namespace/name" format
+-- @return string, string? - name and optionally a namespace
+function util.split_namespace(ns_name)
+ local p1, p2 = ns_name:match("^([^/]+)/([^/]+)$")
+ if p1 then
+ return p2, p1
+ end
+ return ns_name
+end
+
+--- Argparse action callback for namespaced rock arguments.
+function util.namespaced_name_action(args, target, ns_name)
+ assert(type(args) == "table")
+ assert(type(target) == "string")
+ assert(type(ns_name) == "string" or not ns_name)
+
+ if not ns_name then
+ return
+ end
+
+ if ns_name:match("%.rockspec$") or ns_name:match("%.rock$") then
+ args[target] = ns_name
+ else
+ local name, namespace = util.split_namespace(ns_name)
+ args[target] = name:lower()
+ if namespace then
+ args.namespace = namespace:lower()
+ end
+ end
+end
+
+function util.deep_copy(tbl)
+ local copy = {}
+ for k, v in pairs(tbl) do
+ if type(v) == "table" then
+ copy[k] = util.deep_copy(v)
+ else
+ copy[k] = v
+ end
+ end
+ return copy
+end
+
+-- A portable version of fs.exists that can be used at early startup,
+-- before the platform has been determined and luarocks.fs has been
+-- initialized.
+function util.exists(file)
+ local fd, _, code = io.open(file, "r")
+ if code == 13 then
+ -- code 13 means "Permission denied" on both Unix and Windows
+ -- io.open on folders always fails with code 13 on Windows
+ return true
+ end
+ if fd then
+ fd:close()
+ return true
+ end
+ return false
+end
+
+do
+ local function Q(pathname)
+ if pathname:match("^.:") then
+ return pathname:sub(1, 2) .. '"' .. pathname:sub(3) .. '"'
+ end
+ return '"' .. pathname .. '"'
+ end
+
+ function util.check_lua_version(lua, luaver)
+ if not util.exists(lua) then
+ return nil
+ end
+ local lv, err = util.popen_read(Q(lua) .. ' -e "io.write(_VERSION:sub(5))"')
+ if lv == "" then
+ return nil
+ end
+ if luaver and luaver ~= lv then
+ return nil
+ end
+ return lv
+ end
+
+ function util.get_luajit_version()
+ local cfg = require("luarocks.core.cfg")
+ if cfg.cache.luajit_version_checked then
+ return cfg.cache.luajit_version
+ end
+ cfg.cache.luajit_version_checked = true
+
+ if not cfg.variables.LUA then
+ return nil
+ end
+
+ local ljv
+ if cfg.lua_version == "5.1" then
+ -- Ignores extra version info for custom builds, e.g. "LuaJIT 2.1.0-beta3 some-other-version-info"
+ ljv = util.popen_read(Q(cfg.variables.LUA) .. ' -e "io.write(tostring(jit and jit.version:gsub([[^%S+ (%S+).*]], [[%1]])))"')
+ if ljv == "nil" then
+ ljv = nil
+ end
+ end
+ cfg.cache.luajit_version = ljv
+ return ljv
+ end
+
+ local find_lua_bindir
+ do
+ local exe_suffix = (package.config:sub(1, 1) == "\\" and ".exe" or "")
+
+ local function insert_lua_variants(names, luaver)
+ local variants = {
+ "lua" .. luaver .. exe_suffix,
+ "lua" .. luaver:gsub("%.", "") .. exe_suffix,
+ "lua-" .. luaver .. exe_suffix,
+ "lua-" .. luaver:gsub("%.", "") .. exe_suffix,
+ }
+ for _, name in ipairs(variants) do
+ names[name] = luaver
+ table.insert(names, name)
+ end
+ end
+
+ find_lua_bindir = function(prefix, luaver, verbose)
+ local names = {}
+ if luaver then
+ insert_lua_variants(names, luaver)
+ else
+ for v in util.lua_versions("descending") do
+ insert_lua_variants(names, v)
+ end
+ end
+ if luaver == "5.1" or not luaver then
+ table.insert(names, "luajit" .. exe_suffix)
+ end
+ table.insert(names, "lua" .. exe_suffix)
+
+ local tried = {}
+ local dir_sep = package.config:sub(1, 1)
+ for _, d in ipairs({ prefix .. dir_sep .. "bin", prefix }) do
+ for _, name in ipairs(names) do
+ local lua = d .. dir_sep .. name
+ local is_wrapper, err = util.lua_is_wrapper(lua)
+ if is_wrapper == false then
+ local lv = util.check_lua_version(lua, luaver)
+ if lv then
+ return lua, d, lv
+ end
+ elseif is_wrapper == true or err == nil then
+ table.insert(tried, lua)
+ else
+ table.insert(tried, string.format("%-13s (%s)", lua, err))
+ end
+ end
+ end
+ local interp = luaver
+ and ("Lua " .. luaver .. " interpreter")
+ or "Lua interpreter"
+ return nil, interp .. " not found at " .. prefix .. "\n" ..
+ (verbose and "Tried:\t" .. table.concat(tried, "\n\t") or "")
+ end
+ end
+
+ function util.find_lua(prefix, luaver, verbose)
+ local lua, bindir
+ lua, bindir, luaver = find_lua_bindir(prefix, luaver, verbose)
+ if not lua then
+ return nil, bindir
+ end
+
+ return {
+ lua_version = luaver,
+ lua = lua,
+ lua_dir = prefix,
+ lua_bindir = bindir,
+ }
+ end
+end
+
+function util.lua_is_wrapper(interp)
+ local fd, err = io.open(interp, "r")
+ if not fd then
+ return nil, err
+ end
+ local data, err = fd:read(1000)
+ fd:close()
+ if not data then
+ return nil, err
+ end
+ return not not data:match("LUAROCKS_SYSCONFDIR")
+end
+
+function util.opts_table(type_name, valid_opts)
+ local opts_mt = {}
+
+ opts_mt.__index = opts_mt
+
+ function opts_mt.type()
+ return type_name
+ end
+
+ return function(opts)
+ for k, v in pairs(opts) do
+ local tv = type(v)
+ if not valid_opts[k] then
+ error("invalid option: "..k)
+ end
+ local vo, optional = valid_opts[k]:match("^(.-)(%??)$")
+ if not (tv == vo or (optional == "?" and tv == nil)) then
+ error("invalid type option: "..k.." - got "..tv..", expected "..vo)
+ end
+ end
+ for k, v in pairs(valid_opts) do
+ if (not v:find("?", 1, true)) and opts[k] == nil then
+ error("missing option: "..k)
+ end
+ end
+ return setmetatable(opts, opts_mt)
+ end
+end
+
+--- Return a table of modules that are already provided by the VM, which
+-- can be specified as dependencies without having to install an actual rock.
+-- @param rockspec (optional) a rockspec table, so that rockspec format
+-- version compatibility can be checked. If not given, maximum compatibility
+-- is assumed.
+-- @return a table with rock names as keys and versions and values,
+-- specifying modules that are already provided by the VM (including
+-- "lua" for the Lua version and, for format 3.0+, "luajit" if detected).
+function util.get_rocks_provided(rockspec)
+ local cfg = require("luarocks.core.cfg")
+
+ if not rockspec and cfg.cache.rocks_provided then
+ return cfg.cache.rocks_provided
+ end
+
+ local rocks_provided = {}
+
+ local lv = cfg.lua_version
+
+ rocks_provided["lua"] = lv.."-1"
+
+ if lv == "5.2" then
+ rocks_provided["bit32"] = lv.."-1"
+ end
+
+ if lv == "5.3" or lv == "5.4" then
+ rocks_provided["utf8"] = lv.."-1"
+ end
+
+ if lv == "5.1" then
+ local ljv = util.get_luajit_version()
+ if ljv then
+ rocks_provided["luabitop"] = ljv.."-1"
+ if (not rockspec) or rockspec:format_is_at_least("3.0") then
+ rocks_provided["luajit"] = ljv.."-1"
+ end
+ end
+ end
+
+ if cfg.rocks_provided then
+ util.deep_merge_under(rocks_provided, cfg.rocks_provided)
+ end
+
+ if not rockspec then
+ cfg.cache.rocks_provided = rocks_provided
+ end
+
+ return rocks_provided
+end
+
+function util.remove_doc_dir(name, version)
+ local path = require("luarocks.path")
+ local fs = require("luarocks.fs")
+ local dir = require("luarocks.dir")
+
+ local install_dir = path.install_dir(name, version)
+ for _, f in ipairs(fs.list_dir(install_dir)) do
+ local doc_dirs = { "doc", "docs" }
+ for _, d in ipairs(doc_dirs) do
+ if f == d then
+ fs.delete(dir.path(install_dir, f))
+ end
+ end
+ end
+end
+
+return util
diff --git a/src/luarocks/vendor/argparse.lua b/src/luarocks/vendor/argparse.lua
new file mode 100644
index 0000000..2c2585d
--- /dev/null
+++ b/src/luarocks/vendor/argparse.lua
@@ -0,0 +1,2103 @@
+-- The MIT License (MIT)
+
+-- Copyright (c) 2013 - 2018 Peter Melnichenko
+-- 2019 Paul Ouellette
+
+-- Permission is hereby granted, free of charge, to any person obtaining a copy of
+-- this software and associated documentation files (the "Software"), to deal in
+-- the Software without restriction, including without limitation the rights to
+-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+-- the Software, and to permit persons to whom the Software is furnished to do so,
+-- subject to the following conditions:
+
+-- The above copyright notice and this permission notice shall be included in all
+-- copies or substantial portions of the Software.
+
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+local function deep_update(t1, t2)
+ for k, v in pairs(t2) do
+ if type(v) == "table" then
+ v = deep_update({}, v)
+ end
+
+ t1[k] = v
+ end
+
+ return t1
+end
+
+-- A property is a tuple {name, callback}.
+-- properties.args is number of properties that can be set as arguments
+-- when calling an object.
+local function class(prototype, properties, parent)
+ -- Class is the metatable of its instances.
+ local cl = {}
+ cl.__index = cl
+
+ if parent then
+ cl.__prototype = deep_update(deep_update({}, parent.__prototype), prototype)
+ else
+ cl.__prototype = prototype
+ end
+
+ if properties then
+ local names = {}
+
+ -- Create setter methods and fill set of property names.
+ for _, property in ipairs(properties) do
+ local name, callback = property[1], property[2]
+
+ cl[name] = function(self, value)
+ if not callback(self, value) then
+ self["_" .. name] = value
+ end
+
+ return self
+ end
+
+ names[name] = true
+ end
+
+ function cl.__call(self, ...)
+ -- When calling an object, if the first argument is a table,
+ -- interpret keys as property names, else delegate arguments
+ -- to corresponding setters in order.
+ if type((...)) == "table" then
+ for name, value in pairs((...)) do
+ if names[name] then
+ self[name](self, value)
+ end
+ end
+ else
+ local nargs = select("#", ...)
+
+ for i, property in ipairs(properties) do
+ if i > nargs or i > properties.args then
+ break
+ end
+
+ local arg = select(i, ...)
+
+ if arg ~= nil then
+ self[property[1]](self, arg)
+ end
+ end
+ end
+
+ return self
+ end
+ end
+
+ -- If indexing class fails, fallback to its parent.
+ local class_metatable = {}
+ class_metatable.__index = parent
+
+ function class_metatable.__call(self, ...)
+ -- Calling a class returns its instance.
+ -- Arguments are delegated to the instance.
+ local object = deep_update({}, self.__prototype)
+ setmetatable(object, self)
+ return object(...)
+ end
+
+ return setmetatable(cl, class_metatable)
+end
+
+local function typecheck(name, types, value)
+ for _, type_ in ipairs(types) do
+ if type(value) == type_ then
+ return true
+ end
+ end
+
+ error(("bad property '%s' (%s expected, got %s)"):format(name, table.concat(types, " or "), type(value)))
+end
+
+local function typechecked(name, ...)
+ local types = {...}
+ return {name, function(_, value) typecheck(name, types, value) end}
+end
+
+local multiname = {"name", function(self, value)
+ typecheck("name", {"string"}, value)
+
+ for alias in value:gmatch("%S+") do
+ self._name = self._name or alias
+ table.insert(self._aliases, alias)
+ table.insert(self._public_aliases, alias)
+ -- If alias contains '_', accept '-' also.
+ if alias:find("_", 1, true) then
+ table.insert(self._aliases, (alias:gsub("_", "-")))
+ end
+ end
+
+ -- Do not set _name as with other properties.
+ return true
+end}
+
+local multiname_hidden = {"hidden_name", function(self, value)
+ typecheck("hidden_name", {"string"}, value)
+
+ for alias in value:gmatch("%S+") do
+ table.insert(self._aliases, alias)
+ if alias:find("_", 1, true) then
+ table.insert(self._aliases, (alias:gsub("_", "-")))
+ end
+ end
+
+ return true
+end}
+
+local function parse_boundaries(str)
+ if tonumber(str) then
+ return tonumber(str), tonumber(str)
+ end
+
+ if str == "*" then
+ return 0, math.huge
+ end
+
+ if str == "+" then
+ return 1, math.huge
+ end
+
+ if str == "?" then
+ return 0, 1
+ end
+
+ if str:match "^%d+%-%d+$" then
+ local min, max = str:match "^(%d+)%-(%d+)$"
+ return tonumber(min), tonumber(max)
+ end
+
+ if str:match "^%d+%+$" then
+ local min = str:match "^(%d+)%+$"
+ return tonumber(min), math.huge
+ end
+end
+
+local function boundaries(name)
+ return {name, function(self, value)
+ typecheck(name, {"number", "string"}, value)
+
+ local min, max = parse_boundaries(value)
+
+ if not min then
+ error(("bad property '%s'"):format(name))
+ end
+
+ self["_min" .. name], self["_max" .. name] = min, max
+ end}
+end
+
+local actions = {}
+
+local option_action = {"action", function(_, value)
+ typecheck("action", {"function", "string"}, value)
+
+ if type(value) == "string" and not actions[value] then
+ error(("unknown action '%s'"):format(value))
+ end
+end}
+
+local option_init = {"init", function(self)
+ self._has_init = true
+end}
+
+local option_default = {"default", function(self, value)
+ if type(value) ~= "string" then
+ self._init = value
+ self._has_init = true
+ return true
+ end
+end}
+
+local add_help = {"add_help", function(self, value)
+ typecheck("add_help", {"boolean", "string", "table"}, value)
+
+ if self._help_option_idx then
+ table.remove(self._options, self._help_option_idx)
+ self._help_option_idx = nil
+ end
+
+ if value then
+ local help = self:flag()
+ :description "Show this help message and exit."
+ :action(function()
+ print(self:get_help())
+ os.exit(0)
+ end)
+
+ if value ~= true then
+ help = help(value)
+ end
+
+ if not help._name then
+ help "-h" "--help"
+ end
+
+ self._help_option_idx = #self._options
+ end
+end}
+
+local Parser = class({
+ _arguments = {},
+ _options = {},
+ _commands = {},
+ _mutexes = {},
+ _groups = {},
+ _require_command = true,
+ _handle_options = true
+}, {
+ args = 3,
+ typechecked("name", "string"),
+ typechecked("description", "string"),
+ typechecked("epilog", "string"),
+ typechecked("usage", "string"),
+ typechecked("help", "string"),
+ typechecked("require_command", "boolean"),
+ typechecked("handle_options", "boolean"),
+ typechecked("action", "function"),
+ typechecked("command_target", "string"),
+ typechecked("help_vertical_space", "number"),
+ typechecked("usage_margin", "number"),
+ typechecked("usage_max_width", "number"),
+ typechecked("help_usage_margin", "number"),
+ typechecked("help_description_margin", "number"),
+ typechecked("help_max_width", "number"),
+ add_help
+})
+
+local Command = class({
+ _aliases = {},
+ _public_aliases = {}
+}, {
+ args = 3,
+ multiname,
+ typechecked("description", "string"),
+ typechecked("epilog", "string"),
+ multiname_hidden,
+ typechecked("summary", "string"),
+ typechecked("target", "string"),
+ typechecked("usage", "string"),
+ typechecked("help", "string"),
+ typechecked("require_command", "boolean"),
+ typechecked("handle_options", "boolean"),
+ typechecked("action", "function"),
+ typechecked("command_target", "string"),
+ typechecked("help_vertical_space", "number"),
+ typechecked("usage_margin", "number"),
+ typechecked("usage_max_width", "number"),
+ typechecked("help_usage_margin", "number"),
+ typechecked("help_description_margin", "number"),
+ typechecked("help_max_width", "number"),
+ typechecked("hidden", "boolean"),
+ add_help
+}, Parser)
+
+local Argument = class({
+ _minargs = 1,
+ _maxargs = 1,
+ _mincount = 1,
+ _maxcount = 1,
+ _defmode = "unused",
+ _show_default = true
+}, {
+ args = 5,
+ typechecked("name", "string"),
+ typechecked("description", "string"),
+ option_default,
+ typechecked("convert", "function", "table"),
+ boundaries("args"),
+ typechecked("target", "string"),
+ typechecked("defmode", "string"),
+ typechecked("show_default", "boolean"),
+ typechecked("argname", "string", "table"),
+ typechecked("choices", "table"),
+ typechecked("hidden", "boolean"),
+ option_action,
+ option_init
+})
+
+local Option = class({
+ _aliases = {},
+ _public_aliases = {},
+ _mincount = 0,
+ _overwrite = true
+}, {
+ args = 6,
+ multiname,
+ typechecked("description", "string"),
+ option_default,
+ typechecked("convert", "function", "table"),
+ boundaries("args"),
+ boundaries("count"),
+ multiname_hidden,
+ typechecked("target", "string"),
+ typechecked("defmode", "string"),
+ typechecked("show_default", "boolean"),
+ typechecked("overwrite", "boolean"),
+ typechecked("argname", "string", "table"),
+ typechecked("choices", "table"),
+ typechecked("hidden", "boolean"),
+ option_action,
+ option_init
+}, Argument)
+
+function Parser:_inherit_property(name, default)
+ local element = self
+
+ while true do
+ local value = element["_" .. name]
+
+ if value ~= nil then
+ return value
+ end
+
+ if not element._parent then
+ return default
+ end
+
+ element = element._parent
+ end
+end
+
+function Argument:_get_argument_list()
+ local buf = {}
+ local i = 1
+
+ while i <= math.min(self._minargs, 3) do
+ local argname = self:_get_argname(i)
+
+ if self._default and self._defmode:find "a" then
+ argname = "[" .. argname .. "]"
+ end
+
+ table.insert(buf, argname)
+ i = i+1
+ end
+
+ while i <= math.min(self._maxargs, 3) do
+ table.insert(buf, "[" .. self:_get_argname(i) .. "]")
+ i = i+1
+
+ if self._maxargs == math.huge then
+ break
+ end
+ end
+
+ if i < self._maxargs then
+ table.insert(buf, "...")
+ end
+
+ return buf
+end
+
+function Argument:_get_usage()
+ local usage = table.concat(self:_get_argument_list(), " ")
+
+ if self._default and self._defmode:find "u" then
+ if self._maxargs > 1 or (self._minargs == 1 and not self._defmode:find "a") then
+ usage = "[" .. usage .. "]"
+ end
+ end
+
+ return usage
+end
+
+function actions.store_true(result, target)
+ result[target] = true
+end
+
+function actions.store_false(result, target)
+ result[target] = false
+end
+
+function actions.store(result, target, argument)
+ result[target] = argument
+end
+
+function actions.count(result, target, _, overwrite)
+ if not overwrite then
+ result[target] = result[target] + 1
+ end
+end
+
+function actions.append(result, target, argument, overwrite)
+ result[target] = result[target] or {}
+ table.insert(result[target], argument)
+
+ if overwrite then
+ table.remove(result[target], 1)
+ end
+end
+
+function actions.concat(result, target, arguments, overwrite)
+ if overwrite then
+ error("'concat' action can't handle too many invocations")
+ end
+
+ result[target] = result[target] or {}
+
+ for _, argument in ipairs(arguments) do
+ table.insert(result[target], argument)
+ end
+end
+
+function Argument:_get_action()
+ local action, init
+
+ if self._maxcount == 1 then
+ if self._maxargs == 0 then
+ action, init = "store_true", nil
+ else
+ action, init = "store", nil
+ end
+ else
+ if self._maxargs == 0 then
+ action, init = "count", 0
+ else
+ action, init = "append", {}
+ end
+ end
+
+ if self._action then
+ action = self._action
+ end
+
+ if self._has_init then
+ init = self._init
+ end
+
+ if type(action) == "string" then
+ action = actions[action]
+ end
+
+ return action, init
+end
+
+-- Returns placeholder for `narg`-th argument.
+function Argument:_get_argname(narg)
+ local argname = self._argname or self:_get_default_argname()
+
+ if type(argname) == "table" then
+ return argname[narg]
+ else
+ return argname
+ end
+end
+
+function Argument:_get_choices_list()
+ return "{" .. table.concat(self._choices, ",") .. "}"
+end
+
+function Argument:_get_default_argname()
+ if self._choices then
+ return self:_get_choices_list()
+ else
+ return "<" .. self._name .. ">"
+ end
+end
+
+function Option:_get_default_argname()
+ if self._choices then
+ return self:_get_choices_list()
+ else
+ return "<" .. self:_get_default_target() .. ">"
+ end
+end
+
+-- Returns labels to be shown in the help message.
+function Argument:_get_label_lines()
+ if self._choices then
+ return {self:_get_choices_list()}
+ else
+ return {self._name}
+ end
+end
+
+function Option:_get_label_lines()
+ local argument_list = self:_get_argument_list()
+
+ if #argument_list == 0 then
+ -- Don't put aliases for simple flags like `-h` on different lines.
+ return {table.concat(self._public_aliases, ", ")}
+ end
+
+ local longest_alias_length = -1
+
+ for _, alias in ipairs(self._public_aliases) do
+ longest_alias_length = math.max(longest_alias_length, #alias)
+ end
+
+ local argument_list_repr = table.concat(argument_list, " ")
+ local lines = {}
+
+ for i, alias in ipairs(self._public_aliases) do
+ local line = (" "):rep(longest_alias_length - #alias) .. alias .. " " .. argument_list_repr
+
+ if i ~= #self._public_aliases then
+ line = line .. ","
+ end
+
+ table.insert(lines, line)
+ end
+
+ return lines
+end
+
+function Command:_get_label_lines()
+ return {table.concat(self._public_aliases, ", ")}
+end
+
+function Argument:_get_description()
+ if self._default and self._show_default then
+ if self._description then
+ return ("%s (default: %s)"):format(self._description, self._default)
+ else
+ return ("default: %s"):format(self._default)
+ end
+ else
+ return self._description or ""
+ end
+end
+
+function Command:_get_description()
+ return self._summary or self._description or ""
+end
+
+function Option:_get_usage()
+ local usage = self:_get_argument_list()
+ table.insert(usage, 1, self._name)
+ usage = table.concat(usage, " ")
+
+ if self._mincount == 0 or self._default then
+ usage = "[" .. usage .. "]"
+ end
+
+ return usage
+end
+
+function Argument:_get_default_target()
+ return self._name
+end
+
+function Option:_get_default_target()
+ local res
+
+ for _, alias in ipairs(self._public_aliases) do
+ if alias:sub(1, 1) == alias:sub(2, 2) then
+ res = alias:sub(3)
+ break
+ end
+ end
+
+ res = res or self._name:sub(2)
+ return (res:gsub("-", "_"))
+end
+
+function Option:_is_vararg()
+ return self._maxargs ~= self._minargs
+end
+
+function Parser:_get_fullname(exclude_root)
+ local parent = self._parent
+ if exclude_root and not parent then
+ return ""
+ end
+ local buf = {self._name}
+
+ while parent do
+ if not exclude_root or parent._parent then
+ table.insert(buf, 1, parent._name)
+ end
+ parent = parent._parent
+ end
+
+ return table.concat(buf, " ")
+end
+
+function Parser:_update_charset(charset)
+ charset = charset or {}
+
+ for _, command in ipairs(self._commands) do
+ command:_update_charset(charset)
+ end
+
+ for _, option in ipairs(self._options) do
+ for _, alias in ipairs(option._aliases) do
+ charset[alias:sub(1, 1)] = true
+ end
+ end
+
+ return charset
+end
+
+function Parser:argument(...)
+ local argument = Argument(...)
+ table.insert(self._arguments, argument)
+ return argument
+end
+
+function Parser:option(...)
+ local option = Option(...)
+ table.insert(self._options, option)
+ return option
+end
+
+function Parser:flag(...)
+ return self:option():args(0)(...)
+end
+
+function Parser:command(...)
+ local command = Command():add_help(true)(...)
+ command._parent = self
+ table.insert(self._commands, command)
+ return command
+end
+
+function Parser:mutex(...)
+ local elements = {...}
+
+ for i, element in ipairs(elements) do
+ local mt = getmetatable(element)
+ assert(mt == Option or mt == Argument, ("bad argument #%d to 'mutex' (Option or Argument expected)"):format(i))
+ end
+
+ table.insert(self._mutexes, elements)
+ return self
+end
+
+function Parser:group(name, ...)
+ assert(type(name) == "string", ("bad argument #1 to 'group' (string expected, got %s)"):format(type(name)))
+
+ local group = {name = name, ...}
+
+ for i, element in ipairs(group) do
+ local mt = getmetatable(element)
+ assert(mt == Option or mt == Argument or mt == Command,
+ ("bad argument #%d to 'group' (Option or Argument or Command expected)"):format(i + 1))
+ end
+
+ table.insert(self._groups, group)
+ return self
+end
+
+local usage_welcome = "Usage: "
+
+function Parser:get_usage()
+ if self._usage then
+ return self._usage
+ end
+
+ local usage_margin = self:_inherit_property("usage_margin", #usage_welcome)
+ local max_usage_width = self:_inherit_property("usage_max_width", 70)
+ local lines = {usage_welcome .. self:_get_fullname()}
+
+ local function add(s)
+ if #lines[#lines]+1+#s <= max_usage_width then
+ lines[#lines] = lines[#lines] .. " " .. s
+ else
+ lines[#lines+1] = (" "):rep(usage_margin) .. s
+ end
+ end
+
+ -- Normally options are before positional arguments in usage messages.
+ -- However, vararg options should be after, because they can't be reliable used
+ -- before a positional argument.
+ -- Mutexes come into play, too, and are shown as soon as possible.
+ -- Overall, output usages in the following order:
+ -- 1. Mutexes that don't have positional arguments or vararg options.
+ -- 2. Options that are not in any mutexes and are not vararg.
+ -- 3. Positional arguments - on their own or as a part of a mutex.
+ -- 4. Remaining mutexes.
+ -- 5. Remaining options.
+
+ local elements_in_mutexes = {}
+ local added_elements = {}
+ local added_mutexes = {}
+ local argument_to_mutexes = {}
+
+ local function add_mutex(mutex, main_argument)
+ if added_mutexes[mutex] then
+ return
+ end
+
+ added_mutexes[mutex] = true
+ local buf = {}
+
+ for _, element in ipairs(mutex) do
+ if not element._hidden and not added_elements[element] then
+ if getmetatable(element) == Option or element == main_argument then
+ table.insert(buf, element:_get_usage())
+ added_elements[element] = true
+ end
+ end
+ end
+
+ if #buf == 1 then
+ add(buf[1])
+ elseif #buf > 1 then
+ add("(" .. table.concat(buf, " | ") .. ")")
+ end
+ end
+
+ local function add_element(element)
+ if not element._hidden and not added_elements[element] then
+ add(element:_get_usage())
+ added_elements[element] = true
+ end
+ end
+
+ for _, mutex in ipairs(self._mutexes) do
+ local is_vararg = false
+ local has_argument = false
+
+ for _, element in ipairs(mutex) do
+ if getmetatable(element) == Option then
+ if element:_is_vararg() then
+ is_vararg = true
+ end
+ else
+ has_argument = true
+ argument_to_mutexes[element] = argument_to_mutexes[element] or {}
+ table.insert(argument_to_mutexes[element], mutex)
+ end
+
+ elements_in_mutexes[element] = true
+ end
+
+ if not is_vararg and not has_argument then
+ add_mutex(mutex)
+ end
+ end
+
+ for _, option in ipairs(self._options) do
+ if not elements_in_mutexes[option] and not option:_is_vararg() then
+ add_element(option)
+ end
+ end
+
+ -- Add usages for positional arguments, together with one mutex containing them, if they are in a mutex.
+ for _, argument in ipairs(self._arguments) do
+ -- Pick a mutex as a part of which to show this argument, take the first one that's still available.
+ local mutex
+
+ if elements_in_mutexes[argument] then
+ for _, argument_mutex in ipairs(argument_to_mutexes[argument]) do
+ if not added_mutexes[argument_mutex] then
+ mutex = argument_mutex
+ end
+ end
+ end
+
+ if mutex then
+ add_mutex(mutex, argument)
+ else
+ add_element(argument)
+ end
+ end
+
+ for _, mutex in ipairs(self._mutexes) do
+ add_mutex(mutex)
+ end
+
+ for _, option in ipairs(self._options) do
+ add_element(option)
+ end
+
+ if #self._commands > 0 then
+ if self._require_command then
+ add("<command>")
+ else
+ add("[<command>]")
+ end
+
+ add("...")
+ end
+
+ return table.concat(lines, "\n")
+end
+
+local function split_lines(s)
+ if s == "" then
+ return {}
+ end
+
+ local lines = {}
+
+ if s:sub(-1) ~= "\n" then
+ s = s .. "\n"
+ end
+
+ for line in s:gmatch("([^\n]*)\n") do
+ table.insert(lines, line)
+ end
+
+ return lines
+end
+
+local function autowrap_line(line, max_length)
+ -- Algorithm for splitting lines is simple and greedy.
+ local result_lines = {}
+
+ -- Preserve original indentation of the line, put this at the beginning of each result line.
+ -- If the first word looks like a list marker ('*', '+', or '-'), add spaces so that starts
+ -- of the second and the following lines vertically align with the start of the second word.
+ local indentation = line:match("^ *")
+
+ if line:find("^ *[%*%+%-]") then
+ indentation = indentation .. " " .. line:match("^ *[%*%+%-]( *)")
+ end
+
+ -- Parts of the last line being assembled.
+ local line_parts = {}
+
+ -- Length of the current line.
+ local line_length = 0
+
+ -- Index of the next character to consider.
+ local index = 1
+
+ while true do
+ local word_start, word_finish, word = line:find("([^ ]+)", index)
+
+ if not word_start then
+ -- Ignore trailing spaces, if any.
+ break
+ end
+
+ local preceding_spaces = line:sub(index, word_start - 1)
+ index = word_finish + 1
+
+ if (#line_parts == 0) or (line_length + #preceding_spaces + #word <= max_length) then
+ -- Either this is the very first word or it fits as an addition to the current line, add it.
+ table.insert(line_parts, preceding_spaces) -- For the very first word this adds the indentation.
+ table.insert(line_parts, word)
+ line_length = line_length + #preceding_spaces + #word
+ else
+ -- Does not fit, finish current line and put the word into a new one.
+ table.insert(result_lines, table.concat(line_parts))
+ line_parts = {indentation, word}
+ line_length = #indentation + #word
+ end
+ end
+
+ if #line_parts > 0 then
+ table.insert(result_lines, table.concat(line_parts))
+ end
+
+ if #result_lines == 0 then
+ -- Preserve empty lines.
+ result_lines[1] = ""
+ end
+
+ return result_lines
+end
+
+-- Automatically wraps lines within given array,
+-- attempting to limit line length to `max_length`.
+-- Existing line splits are preserved.
+local function autowrap(lines, max_length)
+ local result_lines = {}
+
+ for _, line in ipairs(lines) do
+ local autowrapped_lines = autowrap_line(line, max_length)
+
+ for _, autowrapped_line in ipairs(autowrapped_lines) do
+ table.insert(result_lines, autowrapped_line)
+ end
+ end
+
+ return result_lines
+end
+
+function Parser:_get_element_help(element)
+ local label_lines = element:_get_label_lines()
+ local description_lines = split_lines(element:_get_description())
+
+ local result_lines = {}
+
+ -- All label lines should have the same length (except the last one, it has no comma).
+ -- If too long, start description after all the label lines.
+ -- Otherwise, combine label and description lines.
+
+ local usage_margin_len = self:_inherit_property("help_usage_margin", 3)
+ local usage_margin = (" "):rep(usage_margin_len)
+ local description_margin_len = self:_inherit_property("help_description_margin", 25)
+ local description_margin = (" "):rep(description_margin_len)
+
+ local help_max_width = self:_inherit_property("help_max_width")
+
+ if help_max_width then
+ local description_max_width = math.max(help_max_width - description_margin_len, 10)
+ description_lines = autowrap(description_lines, description_max_width)
+ end
+
+ if #label_lines[1] >= (description_margin_len - usage_margin_len) then
+ for _, label_line in ipairs(label_lines) do
+ table.insert(result_lines, usage_margin .. label_line)
+ end
+
+ for _, description_line in ipairs(description_lines) do
+ table.insert(result_lines, description_margin .. description_line)
+ end
+ else
+ for i = 1, math.max(#label_lines, #description_lines) do
+ local label_line = label_lines[i]
+ local description_line = description_lines[i]
+
+ local line = ""
+
+ if label_line then
+ line = usage_margin .. label_line
+ end
+
+ if description_line and description_line ~= "" then
+ line = line .. (" "):rep(description_margin_len - #line) .. description_line
+ end
+
+ table.insert(result_lines, line)
+ end
+ end
+
+ return table.concat(result_lines, "\n")
+end
+
+local function get_group_types(group)
+ local types = {}
+
+ for _, element in ipairs(group) do
+ types[getmetatable(element)] = true
+ end
+
+ return types
+end
+
+function Parser:_add_group_help(blocks, added_elements, label, elements)
+ local buf = {label}
+
+ for _, element in ipairs(elements) do
+ if not element._hidden and not added_elements[element] then
+ added_elements[element] = true
+ table.insert(buf, self:_get_element_help(element))
+ end
+ end
+
+ if #buf > 1 then
+ table.insert(blocks, table.concat(buf, ("\n"):rep(self:_inherit_property("help_vertical_space", 0) + 1)))
+ end
+end
+
+function Parser:get_help()
+ if self._help then
+ return self._help
+ end
+
+ local blocks = {self:get_usage()}
+
+ local help_max_width = self:_inherit_property("help_max_width")
+
+ if self._description then
+ local description = self._description
+
+ if help_max_width then
+ description = table.concat(autowrap(split_lines(description), help_max_width), "\n")
+ end
+
+ table.insert(blocks, description)
+ end
+
+ -- 1. Put groups containing arguments first, then other arguments.
+ -- 2. Put remaining groups containing options, then other options.
+ -- 3. Put remaining groups containing commands, then other commands.
+ -- Assume that an element can't be in several groups.
+ local groups_by_type = {
+ [Argument] = {},
+ [Option] = {},
+ [Command] = {}
+ }
+
+ for _, group in ipairs(self._groups) do
+ local group_types = get_group_types(group)
+
+ for _, mt in ipairs({Argument, Option, Command}) do
+ if group_types[mt] then
+ table.insert(groups_by_type[mt], group)
+ break
+ end
+ end
+ end
+
+ local default_groups = {
+ {name = "Arguments", type = Argument, elements = self._arguments},
+ {name = "Options", type = Option, elements = self._options},
+ {name = "Commands", type = Command, elements = self._commands}
+ }
+
+ local added_elements = {}
+
+ for _, default_group in ipairs(default_groups) do
+ local type_groups = groups_by_type[default_group.type]
+
+ for _, group in ipairs(type_groups) do
+ self:_add_group_help(blocks, added_elements, group.name .. ":", group)
+ end
+
+ local default_label = default_group.name .. ":"
+
+ if #type_groups > 0 then
+ default_label = "Other " .. default_label:gsub("^.", string.lower)
+ end
+
+ self:_add_group_help(blocks, added_elements, default_label, default_group.elements)
+ end
+
+ if self._epilog then
+ local epilog = self._epilog
+
+ if help_max_width then
+ epilog = table.concat(autowrap(split_lines(epilog), help_max_width), "\n")
+ end
+
+ table.insert(blocks, epilog)
+ end
+
+ return table.concat(blocks, "\n\n")
+end
+
+function Parser:add_help_command(value)
+ if value then
+ assert(type(value) == "string" or type(value) == "table",
+ ("bad argument #1 to 'add_help_command' (string or table expected, got %s)"):format(type(value)))
+ end
+
+ local help = self:command()
+ :description "Show help for commands."
+ help:argument "command"
+ :description "The command to show help for."
+ :args "?"
+ :action(function(_, _, cmd)
+ if not cmd then
+ print(self:get_help())
+ os.exit(0)
+ else
+ for _, command in ipairs(self._commands) do
+ for _, alias in ipairs(command._aliases) do
+ if alias == cmd then
+ print(command:get_help())
+ os.exit(0)
+ end
+ end
+ end
+ end
+ help:error(("unknown command '%s'"):format(cmd))
+ end)
+
+ if value then
+ help = help(value)
+ end
+
+ if not help._name then
+ help "help"
+ end
+
+ help._is_help_command = true
+ return self
+end
+
+function Parser:_is_shell_safe()
+ if self._basename then
+ if self._basename:find("[^%w_%-%+%.]") then
+ return false
+ end
+ else
+ for _, alias in ipairs(self._aliases) do
+ if alias:find("[^%w_%-%+%.]") then
+ return false
+ end
+ end
+ end
+ for _, option in ipairs(self._options) do
+ for _, alias in ipairs(option._aliases) do
+ if alias:find("[^%w_%-%+%.]") then
+ return false
+ end
+ end
+ if option._choices then
+ for _, choice in ipairs(option._choices) do
+ if choice:find("[%s'\"]") then
+ return false
+ end
+ end
+ end
+ end
+ for _, argument in ipairs(self._arguments) do
+ if argument._choices then
+ for _, choice in ipairs(argument._choices) do
+ if choice:find("[%s'\"]") then
+ return false
+ end
+ end
+ end
+ end
+ for _, command in ipairs(self._commands) do
+ if not command:_is_shell_safe() then
+ return false
+ end
+ end
+ return true
+end
+
+function Parser:add_complete(value)
+ if value then
+ assert(type(value) == "string" or type(value) == "table",
+ ("bad argument #1 to 'add_complete' (string or table expected, got %s)"):format(type(value)))
+ end
+
+ local complete = self:option()
+ :description "Output a shell completion script for the specified shell."
+ :args(1)
+ :choices {"bash", "zsh", "fish"}
+ :action(function(_, _, shell)
+ io.write(self["get_" .. shell .. "_complete"](self))
+ os.exit(0)
+ end)
+
+ if value then
+ complete = complete(value)
+ end
+
+ if not complete._name then
+ complete "--completion"
+ end
+
+ return self
+end
+
+function Parser:add_complete_command(value)
+ if value then
+ assert(type(value) == "string" or type(value) == "table",
+ ("bad argument #1 to 'add_complete_command' (string or table expected, got %s)"):format(type(value)))
+ end
+
+ local complete = self:command()
+ :description "Output a shell completion script."
+ complete:argument "shell"
+ :description "The shell to output a completion script for."
+ :choices {"bash", "zsh", "fish"}
+ :action(function(_, _, shell)
+ io.write(self["get_" .. shell .. "_complete"](self))
+ os.exit(0)
+ end)
+
+ if value then
+ complete = complete(value)
+ end
+
+ if not complete._name then
+ complete "completion"
+ end
+
+ return self
+end
+
+local function base_name(pathname)
+ return pathname:gsub("[/\\]*$", ""):match(".*[/\\]([^/\\]*)") or pathname
+end
+
+local function get_short_description(element)
+ local short = element:_get_description():match("^(.-)%.%s")
+ return short or element:_get_description():match("^(.-)%.?$")
+end
+
+function Parser:_get_options()
+ local options = {}
+ for _, option in ipairs(self._options) do
+ for _, alias in ipairs(option._aliases) do
+ table.insert(options, alias)
+ end
+ end
+ return table.concat(options, " ")
+end
+
+function Parser:_get_commands()
+ local commands = {}
+ for _, command in ipairs(self._commands) do
+ for _, alias in ipairs(command._aliases) do
+ table.insert(commands, alias)
+ end
+ end
+ return table.concat(commands, " ")
+end
+
+function Parser:_bash_option_args(buf, indent)
+ local opts = {}
+ for _, option in ipairs(self._options) do
+ if option._choices or option._minargs > 0 then
+ local compreply
+ if option._choices then
+ compreply = 'COMPREPLY=($(compgen -W "' .. table.concat(option._choices, " ") .. '" -- "$cur"))'
+ else
+ compreply = 'COMPREPLY=($(compgen -f -- "$cur"))'
+ end
+ table.insert(opts, (" "):rep(indent + 4) .. table.concat(option._aliases, "|") .. ")")
+ table.insert(opts, (" "):rep(indent + 8) .. compreply)
+ table.insert(opts, (" "):rep(indent + 8) .. "return 0")
+ table.insert(opts, (" "):rep(indent + 8) .. ";;")
+ end
+ end
+
+ if #opts > 0 then
+ table.insert(buf, (" "):rep(indent) .. 'case "$prev" in')
+ table.insert(buf, table.concat(opts, "\n"))
+ table.insert(buf, (" "):rep(indent) .. "esac\n")
+ end
+end
+
+function Parser:_bash_get_cmd(buf, indent)
+ if #self._commands == 0 then
+ return
+ end
+
+ table.insert(buf, (" "):rep(indent) .. 'args=("${args[@]:1}")')
+ table.insert(buf, (" "):rep(indent) .. 'for arg in "${args[@]}"; do')
+ table.insert(buf, (" "):rep(indent + 4) .. 'case "$arg" in')
+
+ for _, command in ipairs(self._commands) do
+ table.insert(buf, (" "):rep(indent + 8) .. table.concat(command._aliases, "|") .. ")")
+ if self._parent then
+ table.insert(buf, (" "):rep(indent + 12) .. 'cmd="$cmd ' .. command._name .. '"')
+ else
+ table.insert(buf, (" "):rep(indent + 12) .. 'cmd="' .. command._name .. '"')
+ end
+ table.insert(buf, (" "):rep(indent + 12) .. 'opts="$opts ' .. command:_get_options() .. '"')
+ command:_bash_get_cmd(buf, indent + 12)
+ table.insert(buf, (" "):rep(indent + 12) .. "break")
+ table.insert(buf, (" "):rep(indent + 12) .. ";;")
+ end
+
+ table.insert(buf, (" "):rep(indent + 4) .. "esac")
+ table.insert(buf, (" "):rep(indent) .. "done")
+end
+
+function Parser:_bash_cmd_completions(buf)
+ local cmd_buf = {}
+ if self._parent then
+ self:_bash_option_args(cmd_buf, 12)
+ end
+ if #self._commands > 0 then
+ table.insert(cmd_buf, (" "):rep(12) .. 'COMPREPLY=($(compgen -W "' .. self:_get_commands() .. '" -- "$cur"))')
+ elseif self._is_help_command then
+ table.insert(cmd_buf, (" "):rep(12)
+ .. 'COMPREPLY=($(compgen -W "'
+ .. self._parent:_get_commands()
+ .. '" -- "$cur"))')
+ end
+ if #cmd_buf > 0 then
+ table.insert(buf, (" "):rep(8) .. "'" .. self:_get_fullname(true) .. "')")
+ table.insert(buf, table.concat(cmd_buf, "\n"))
+ table.insert(buf, (" "):rep(12) .. ";;")
+ end
+
+ for _, command in ipairs(self._commands) do
+ command:_bash_cmd_completions(buf)
+ end
+end
+
+function Parser:get_bash_complete()
+ self._basename = base_name(self._name)
+ assert(self:_is_shell_safe())
+ local buf = {([[
+_%s() {
+ local IFS=$' \t\n'
+ local args cur prev cmd opts arg
+ args=("${COMP_WORDS[@]}")
+ cur="${COMP_WORDS[COMP_CWORD]}"
+ prev="${COMP_WORDS[COMP_CWORD-1]}"
+ opts="%s"
+]]):format(self._basename, self:_get_options())}
+
+ self:_bash_option_args(buf, 4)
+ self:_bash_get_cmd(buf, 4)
+ if #self._commands > 0 then
+ table.insert(buf, "")
+ table.insert(buf, (" "):rep(4) .. 'case "$cmd" in')
+ self:_bash_cmd_completions(buf)
+ table.insert(buf, (" "):rep(4) .. "esac\n")
+ end
+
+ table.insert(buf, ([=[
+ if [[ "$cur" = -* ]]; then
+ COMPREPLY=($(compgen -W "$opts" -- "$cur"))
+ fi
+}
+
+complete -F _%s -o bashdefault -o default %s
+]=]):format(self._basename, self._basename))
+
+ return table.concat(buf, "\n")
+end
+
+function Parser:_zsh_arguments(buf, cmd_name, indent)
+ if self._parent then
+ table.insert(buf, (" "):rep(indent) .. "options=(")
+ table.insert(buf, (" "):rep(indent + 2) .. "$options")
+ else
+ table.insert(buf, (" "):rep(indent) .. "local -a options=(")
+ end
+
+ for _, option in ipairs(self._options) do
+ local line = {}
+ if #option._aliases > 1 then
+ if option._maxcount > 1 then
+ table.insert(line, '"*"')
+ end
+ table.insert(line, "{" .. table.concat(option._aliases, ",") .. '}"')
+ else
+ table.insert(line, '"')
+ if option._maxcount > 1 then
+ table.insert(line, "*")
+ end
+ table.insert(line, option._name)
+ end
+ if option._description then
+ local description = get_short_description(option):gsub('["%]:`$]', "\\%0")
+ table.insert(line, "[" .. description .. "]")
+ end
+ if option._maxargs == math.huge then
+ table.insert(line, ":*")
+ end
+ if option._choices then
+ table.insert(line, ": :(" .. table.concat(option._choices, " ") .. ")")
+ elseif option._maxargs > 0 then
+ table.insert(line, ": :_files")
+ end
+ table.insert(line, '"')
+ table.insert(buf, (" "):rep(indent + 2) .. table.concat(line))
+ end
+
+ table.insert(buf, (" "):rep(indent) .. ")")
+ table.insert(buf, (" "):rep(indent) .. "_arguments -s -S \\")
+ table.insert(buf, (" "):rep(indent + 2) .. "$options \\")
+
+ if self._is_help_command then
+ table.insert(buf, (" "):rep(indent + 2) .. '": :(' .. self._parent:_get_commands() .. ')" \\')
+ else
+ for _, argument in ipairs(self._arguments) do
+ local spec
+ if argument._choices then
+ spec = ": :(" .. table.concat(argument._choices, " ") .. ")"
+ else
+ spec = ": :_files"
+ end
+ if argument._maxargs == math.huge then
+ table.insert(buf, (" "):rep(indent + 2) .. '"*' .. spec .. '" \\')
+ break
+ end
+ for _ = 1, argument._maxargs do
+ table.insert(buf, (" "):rep(indent + 2) .. '"' .. spec .. '" \\')
+ end
+ end
+
+ if #self._commands > 0 then
+ table.insert(buf, (" "):rep(indent + 2) .. '": :_' .. cmd_name .. '_cmds" \\')
+ table.insert(buf, (" "):rep(indent + 2) .. '"*:: :->args" \\')
+ end
+ end
+
+ table.insert(buf, (" "):rep(indent + 2) .. "&& return 0")
+end
+
+function Parser:_zsh_cmds(buf, cmd_name)
+ table.insert(buf, "\n_" .. cmd_name .. "_cmds() {")
+ table.insert(buf, " local -a commands=(")
+
+ for _, command in ipairs(self._commands) do
+ local line = {}
+ if #command._aliases > 1 then
+ table.insert(line, "{" .. table.concat(command._aliases, ",") .. '}"')
+ else
+ table.insert(line, '"' .. command._name)
+ end
+ if command._description then
+ table.insert(line, ":" .. get_short_description(command):gsub('["`$]', "\\%0"))
+ end
+ table.insert(buf, " " .. table.concat(line) .. '"')
+ end
+
+ table.insert(buf, ' )\n _describe "command" commands\n}')
+end
+
+function Parser:_zsh_complete_help(buf, cmds_buf, cmd_name, indent)
+ if #self._commands == 0 then
+ return
+ end
+
+ self:_zsh_cmds(cmds_buf, cmd_name)
+ table.insert(buf, "\n" .. (" "):rep(indent) .. "case $words[1] in")
+
+ for _, command in ipairs(self._commands) do
+ local name = cmd_name .. "_" .. command._name
+ table.insert(buf, (" "):rep(indent + 2) .. table.concat(command._aliases, "|") .. ")")
+ command:_zsh_arguments(buf, name, indent + 4)
+ command:_zsh_complete_help(buf, cmds_buf, name, indent + 4)
+ table.insert(buf, (" "):rep(indent + 4) .. ";;\n")
+ end
+
+ table.insert(buf, (" "):rep(indent) .. "esac")
+end
+
+function Parser:get_zsh_complete()
+ self._basename = base_name(self._name)
+ assert(self:_is_shell_safe())
+ local buf = {("#compdef %s\n"):format(self._basename)}
+ local cmds_buf = {}
+ table.insert(buf, "_" .. self._basename .. "() {")
+ if #self._commands > 0 then
+ table.insert(buf, " local context state state_descr line")
+ table.insert(buf, " typeset -A opt_args\n")
+ end
+ self:_zsh_arguments(buf, self._basename, 2)
+ self:_zsh_complete_help(buf, cmds_buf, self._basename, 2)
+ table.insert(buf, "\n return 1")
+ table.insert(buf, "}")
+
+ local result = table.concat(buf, "\n")
+ if #cmds_buf > 0 then
+ result = result .. "\n" .. table.concat(cmds_buf, "\n")
+ end
+ return result .. "\n\n_" .. self._basename .. "\n"
+end
+
+local function fish_escape(string)
+ return string:gsub("[\\']", "\\%0")
+end
+
+function Parser:_fish_get_cmd(buf, indent)
+ if #self._commands == 0 then
+ return
+ end
+
+ table.insert(buf, (" "):rep(indent) .. "set -e cmdline[1]")
+ table.insert(buf, (" "):rep(indent) .. "for arg in $cmdline")
+ table.insert(buf, (" "):rep(indent + 4) .. "switch $arg")
+
+ for _, command in ipairs(self._commands) do
+ table.insert(buf, (" "):rep(indent + 8) .. "case " .. table.concat(command._aliases, " "))
+ table.insert(buf, (" "):rep(indent + 12) .. "set cmd $cmd " .. command._name)
+ command:_fish_get_cmd(buf, indent + 12)
+ table.insert(buf, (" "):rep(indent + 12) .. "break")
+ end
+
+ table.insert(buf, (" "):rep(indent + 4) .. "end")
+ table.insert(buf, (" "):rep(indent) .. "end")
+end
+
+function Parser:_fish_complete_help(buf, basename)
+ local prefix = "complete -c " .. basename
+ table.insert(buf, "")
+
+ for _, command in ipairs(self._commands) do
+ local aliases = table.concat(command._aliases, " ")
+ local line
+ if self._parent then
+ line = ("%s -n '__fish_%s_using_command %s' -xa '%s'")
+ :format(prefix, basename, self:_get_fullname(true), aliases)
+ else
+ line = ("%s -n '__fish_%s_using_command' -xa '%s'"):format(prefix, basename, aliases)
+ end
+ if command._description then
+ line = ("%s -d '%s'"):format(line, fish_escape(get_short_description(command)))
+ end
+ table.insert(buf, line)
+ end
+
+ if self._is_help_command then
+ local line = ("%s -n '__fish_%s_using_command %s' -xa '%s'")
+ :format(prefix, basename, self:_get_fullname(true), self._parent:_get_commands())
+ table.insert(buf, line)
+ end
+
+ for _, option in ipairs(self._options) do
+ local parts = {prefix}
+
+ if self._parent then
+ table.insert(parts, "-n '__fish_" .. basename .. "_seen_command " .. self:_get_fullname(true) .. "'")
+ end
+
+ for _, alias in ipairs(option._aliases) do
+ if alias:match("^%-.$") then
+ table.insert(parts, "-s " .. alias:sub(2))
+ elseif alias:match("^%-%-.+") then
+ table.insert(parts, "-l " .. alias:sub(3))
+ end
+ end
+
+ if option._choices then
+ table.insert(parts, "-xa '" .. table.concat(option._choices, " ") .. "'")
+ elseif option._minargs > 0 then
+ table.insert(parts, "-r")
+ end
+
+ if option._description then
+ table.insert(parts, "-d '" .. fish_escape(get_short_description(option)) .. "'")
+ end
+
+ table.insert(buf, table.concat(parts, " "))
+ end
+
+ for _, command in ipairs(self._commands) do
+ command:_fish_complete_help(buf, basename)
+ end
+end
+
+function Parser:get_fish_complete()
+ self._basename = base_name(self._name)
+ assert(self:_is_shell_safe())
+ local buf = {}
+
+ if #self._commands > 0 then
+ table.insert(buf, ([[
+function __fish_%s_print_command
+ set -l cmdline (commandline -poc)
+ set -l cmd]]):format(self._basename))
+ self:_fish_get_cmd(buf, 4)
+ table.insert(buf, ([[
+ echo "$cmd"
+end
+
+function __fish_%s_using_command
+ test (__fish_%s_print_command) = "$argv"
+ and return 0
+ or return 1
+end
+
+function __fish_%s_seen_command
+ string match -q "$argv*" (__fish_%s_print_command)
+ and return 0
+ or return 1
+end]]):format(self._basename, self._basename, self._basename, self._basename))
+ end
+
+ self:_fish_complete_help(buf, self._basename)
+ return table.concat(buf, "\n") .. "\n"
+end
+
+local function get_tip(context, wrong_name)
+ local context_pool = {}
+ local possible_name
+ local possible_names = {}
+
+ for name in pairs(context) do
+ if type(name) == "string" then
+ for i = 1, #name do
+ possible_name = name:sub(1, i - 1) .. name:sub(i + 1)
+
+ if not context_pool[possible_name] then
+ context_pool[possible_name] = {}
+ end
+
+ table.insert(context_pool[possible_name], name)
+ end
+ end
+ end
+
+ for i = 1, #wrong_name + 1 do
+ possible_name = wrong_name:sub(1, i - 1) .. wrong_name:sub(i + 1)
+
+ if context[possible_name] then
+ possible_names[possible_name] = true
+ elseif context_pool[possible_name] then
+ for _, name in ipairs(context_pool[possible_name]) do
+ possible_names[name] = true
+ end
+ end
+ end
+
+ local first = next(possible_names)
+
+ if first then
+ if next(possible_names, first) then
+ local possible_names_arr = {}
+
+ for name in pairs(possible_names) do
+ table.insert(possible_names_arr, "'" .. name .. "'")
+ end
+
+ table.sort(possible_names_arr)
+ return "\nDid you mean one of these: " .. table.concat(possible_names_arr, " ") .. "?"
+ else
+ return "\nDid you mean '" .. first .. "'?"
+ end
+ else
+ return ""
+ end
+end
+
+local ElementState = class({
+ invocations = 0
+})
+
+function ElementState:__call(state, element)
+ self.state = state
+ self.result = state.result
+ self.element = element
+ self.target = element._target or element:_get_default_target()
+ self.action, self.result[self.target] = element:_get_action()
+ return self
+end
+
+function ElementState:error(fmt, ...)
+ self.state:error(fmt, ...)
+end
+
+function ElementState:convert(argument, index)
+ local converter = self.element._convert
+
+ if converter then
+ local ok, err
+
+ if type(converter) == "function" then
+ ok, err = converter(argument)
+ elseif type(converter[index]) == "function" then
+ ok, err = converter[index](argument)
+ else
+ ok = converter[argument]
+ end
+
+ if ok == nil then
+ self:error(err and "%s" or "malformed argument '%s'", err or argument)
+ end
+
+ argument = ok
+ end
+
+ return argument
+end
+
+function ElementState:default(mode)
+ return self.element._defmode:find(mode) and self.element._default
+end
+
+local function bound(noun, min, max, is_max)
+ local res = ""
+
+ if min ~= max then
+ res = "at " .. (is_max and "most" or "least") .. " "
+ end
+
+ local number = is_max and max or min
+ return res .. tostring(number) .. " " .. noun .. (number == 1 and "" or "s")
+end
+
+function ElementState:set_name(alias)
+ self.name = ("%s '%s'"):format(alias and "option" or "argument", alias or self.element._name)
+end
+
+function ElementState:invoke()
+ self.open = true
+ self.overwrite = false
+
+ if self.invocations >= self.element._maxcount then
+ if self.element._overwrite then
+ self.overwrite = true
+ else
+ local num_times_repr = bound("time", self.element._mincount, self.element._maxcount, true)
+ self:error("%s must be used %s", self.name, num_times_repr)
+ end
+ else
+ self.invocations = self.invocations + 1
+ end
+
+ self.args = {}
+
+ if self.element._maxargs <= 0 then
+ self:close()
+ end
+
+ return self.open
+end
+
+function ElementState:check_choices(argument)
+ if self.element._choices then
+ for _, choice in ipairs(self.element._choices) do
+ if argument == choice then
+ return
+ end
+ end
+ local choices_list = "'" .. table.concat(self.element._choices, "', '") .. "'"
+ local is_option = getmetatable(self.element) == Option
+ self:error("%s%s must be one of %s", is_option and "argument for " or "", self.name, choices_list)
+ end
+end
+
+function ElementState:pass(argument)
+ self:check_choices(argument)
+ argument = self:convert(argument, #self.args + 1)
+ table.insert(self.args, argument)
+
+ if #self.args >= self.element._maxargs then
+ self:close()
+ end
+
+ return self.open
+end
+
+function ElementState:complete_invocation()
+ while #self.args < self.element._minargs do
+ self:pass(self.element._default)
+ end
+end
+
+function ElementState:close()
+ if self.open then
+ self.open = false
+
+ if #self.args < self.element._minargs then
+ if self:default("a") then
+ self:complete_invocation()
+ else
+ if #self.args == 0 then
+ if getmetatable(self.element) == Argument then
+ self:error("missing %s", self.name)
+ elseif self.element._maxargs == 1 then
+ self:error("%s requires an argument", self.name)
+ end
+ end
+
+ self:error("%s requires %s", self.name, bound("argument", self.element._minargs, self.element._maxargs))
+ end
+ end
+
+ local args
+
+ if self.element._maxargs == 0 then
+ args = self.args[1]
+ elseif self.element._maxargs == 1 then
+ if self.element._minargs == 0 and self.element._mincount ~= self.element._maxcount then
+ args = self.args
+ else
+ args = self.args[1]
+ end
+ else
+ args = self.args
+ end
+
+ self.action(self.result, self.target, args, self.overwrite)
+ end
+end
+
+local ParseState = class({
+ result = {},
+ options = {},
+ arguments = {},
+ argument_i = 1,
+ element_to_mutexes = {},
+ mutex_to_element_state = {},
+ command_actions = {}
+})
+
+function ParseState:__call(parser, error_handler)
+ self.parser = parser
+ self.error_handler = error_handler
+ self.charset = parser:_update_charset()
+ self:switch(parser)
+ return self
+end
+
+function ParseState:error(fmt, ...)
+ self.error_handler(self.parser, fmt:format(...))
+end
+
+function ParseState:switch(parser)
+ self.parser = parser
+
+ if parser._action then
+ table.insert(self.command_actions, {action = parser._action, name = parser._name})
+ end
+
+ for _, option in ipairs(parser._options) do
+ option = ElementState(self, option)
+ table.insert(self.options, option)
+
+ for _, alias in ipairs(option.element._aliases) do
+ self.options[alias] = option
+ end
+ end
+
+ for _, mutex in ipairs(parser._mutexes) do
+ for _, element in ipairs(mutex) do
+ if not self.element_to_mutexes[element] then
+ self.element_to_mutexes[element] = {}
+ end
+
+ table.insert(self.element_to_mutexes[element], mutex)
+ end
+ end
+
+ for _, argument in ipairs(parser._arguments) do
+ argument = ElementState(self, argument)
+ table.insert(self.arguments, argument)
+ argument:set_name()
+ argument:invoke()
+ end
+
+ self.handle_options = parser._handle_options
+ self.argument = self.arguments[self.argument_i]
+ self.commands = parser._commands
+
+ for _, command in ipairs(self.commands) do
+ for _, alias in ipairs(command._aliases) do
+ self.commands[alias] = command
+ end
+ end
+end
+
+function ParseState:get_option(name)
+ local option = self.options[name]
+
+ if not option then
+ self:error("unknown option '%s'%s", name, get_tip(self.options, name))
+ else
+ return option
+ end
+end
+
+function ParseState:get_command(name)
+ local command = self.commands[name]
+
+ if not command then
+ if #self.commands > 0 then
+ self:error("unknown command '%s'%s", name, get_tip(self.commands, name))
+ else
+ self:error("too many arguments")
+ end
+ else
+ return command
+ end
+end
+
+function ParseState:check_mutexes(element_state)
+ if self.element_to_mutexes[element_state.element] then
+ for _, mutex in ipairs(self.element_to_mutexes[element_state.element]) do
+ local used_element_state = self.mutex_to_element_state[mutex]
+
+ if used_element_state and used_element_state ~= element_state then
+ self:error("%s can not be used together with %s", element_state.name, used_element_state.name)
+ else
+ self.mutex_to_element_state[mutex] = element_state
+ end
+ end
+ end
+end
+
+function ParseState:invoke(option, name)
+ self:close()
+ option:set_name(name)
+ self:check_mutexes(option, name)
+
+ if option:invoke() then
+ self.option = option
+ end
+end
+
+function ParseState:pass(arg)
+ if self.option then
+ if not self.option:pass(arg) then
+ self.option = nil
+ end
+ elseif self.argument then
+ self:check_mutexes(self.argument)
+
+ if not self.argument:pass(arg) then
+ self.argument_i = self.argument_i + 1
+ self.argument = self.arguments[self.argument_i]
+ end
+ else
+ local command = self:get_command(arg)
+ self.result[command._target or command._name] = true
+
+ if self.parser._command_target then
+ self.result[self.parser._command_target] = command._name
+ end
+
+ self:switch(command)
+ end
+end
+
+function ParseState:close()
+ if self.option then
+ self.option:close()
+ self.option = nil
+ end
+end
+
+function ParseState:finalize()
+ self:close()
+
+ for i = self.argument_i, #self.arguments do
+ local argument = self.arguments[i]
+ if #argument.args == 0 and argument:default("u") then
+ argument:complete_invocation()
+ else
+ argument:close()
+ end
+ end
+
+ if self.parser._require_command and #self.commands > 0 then
+ self:error("a command is required")
+ end
+
+ for _, option in ipairs(self.options) do
+ option.name = option.name or ("option '%s'"):format(option.element._name)
+
+ if option.invocations == 0 then
+ if option:default("u") then
+ option:invoke()
+ option:complete_invocation()
+ option:close()
+ end
+ end
+
+ local mincount = option.element._mincount
+
+ if option.invocations < mincount then
+ if option:default("a") then
+ while option.invocations < mincount do
+ option:invoke()
+ option:close()
+ end
+ elseif option.invocations == 0 then
+ self:error("missing %s", option.name)
+ else
+ self:error("%s must be used %s", option.name, bound("time", mincount, option.element._maxcount))
+ end
+ end
+ end
+
+ for i = #self.command_actions, 1, -1 do
+ self.command_actions[i].action(self.result, self.command_actions[i].name)
+ end
+end
+
+function ParseState:parse(args)
+ for _, arg in ipairs(args) do
+ local plain = true
+
+ if self.handle_options then
+ local first = arg:sub(1, 1)
+
+ if self.charset[first] then
+ if #arg > 1 then
+ plain = false
+
+ if arg:sub(2, 2) == first then
+ if #arg == 2 then
+ if self.options[arg] then
+ local option = self:get_option(arg)
+ self:invoke(option, arg)
+ else
+ self:close()
+ end
+
+ self.handle_options = false
+ else
+ local equals = arg:find "="
+ if equals then
+ local name = arg:sub(1, equals - 1)
+ local option = self:get_option(name)
+
+ if option.element._maxargs <= 0 then
+ self:error("option '%s' does not take arguments", name)
+ end
+
+ self:invoke(option, name)
+ self:pass(arg:sub(equals + 1))
+ else
+ local option = self:get_option(arg)
+ self:invoke(option, arg)
+ end
+ end
+ else
+ for i = 2, #arg do
+ local name = first .. arg:sub(i, i)
+ local option = self:get_option(name)
+ self:invoke(option, name)
+
+ if i ~= #arg and option.element._maxargs > 0 then
+ self:pass(arg:sub(i + 1))
+ break
+ end
+ end
+ end
+ end
+ end
+ end
+
+ if plain then
+ self:pass(arg)
+ end
+ end
+
+ self:finalize()
+ return self.result
+end
+
+function Parser:error(msg)
+ io.stderr:write(("%s\n\nError: %s\n"):format(self:get_usage(), msg))
+ os.exit(1)
+end
+
+-- Compatibility with strict.lua and other checkers:
+local default_cmdline = rawget(_G, "arg") or {}
+
+function Parser:_parse(args, error_handler)
+ return ParseState(self, error_handler):parse(args or default_cmdline)
+end
+
+function Parser:parse(args)
+ return self:_parse(args, self.error)
+end
+
+local function xpcall_error_handler(err)
+ if not debug then
+ return tostring(err)
+ end
+ return tostring(err) .. "\noriginal " .. debug.traceback("", 2):sub(2)
+end
+
+function Parser:pparse(args)
+ local parse_error
+
+ local ok, result = xpcall(function()
+ return self:_parse(args, function(_, err)
+ parse_error = err
+ error(err, 0)
+ end)
+ end, xpcall_error_handler)
+
+ if ok then
+ return true, result
+ elseif not parse_error then
+ error(result, 0)
+ else
+ return false, parse_error
+ end
+end
+
+local argparse = {}
+
+argparse.version = "0.7.0"
+
+setmetatable(argparse, {__call = function(_, ...)
+ return Parser(default_cmdline[0]):add_help(true)(...)
+end})
+
+return argparse
diff --git a/src/luarocks/vendor/dkjson.lua b/src/luarocks/vendor/dkjson.lua
new file mode 100644
index 0000000..7a86724
--- /dev/null
+++ b/src/luarocks/vendor/dkjson.lua
@@ -0,0 +1,749 @@
+-- Module options:
+local always_use_lpeg = false
+local register_global_module_table = false
+local global_module_name = 'json'
+
+--[==[
+
+David Kolf's JSON module for Lua 5.1 - 5.4
+
+Version 2.7
+
+
+For the documentation see the corresponding readme.txt or visit
+<http://dkolf.de/src/dkjson-lua.fsl/>.
+
+You can contact the author by sending an e-mail to 'david' at the
+domain 'dkolf.de'.
+
+
+Copyright (C) 2010-2024 David Heiko Kolf
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+--]==]
+
+-- global dependencies:
+local pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset =
+ pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset
+local error, require, pcall, select = error, require, pcall, select
+local floor, huge = math.floor, math.huge
+local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat =
+ string.rep, string.gsub, string.sub, string.byte, string.char,
+ string.find, string.len, string.format
+local strmatch = string.match
+local concat = table.concat
+
+local json = { version = "dkjson 2.7" }
+
+local jsonlpeg = {}
+
+if register_global_module_table then
+ if always_use_lpeg then
+ _G[global_module_name] = jsonlpeg
+ else
+ _G[global_module_name] = json
+ end
+end
+
+local _ENV = nil -- blocking globals in Lua 5.2 and later
+
+pcall (function()
+ -- Enable access to blocked metatables.
+ -- Don't worry, this module doesn't change anything in them.
+ local debmeta = require "debug".getmetatable
+ if debmeta then getmetatable = debmeta end
+end)
+
+json.null = setmetatable ({}, {
+ __tojson = function () return "null" end
+})
+
+local function isarray (tbl)
+ local max, n, arraylen = 0, 0, 0
+ for k,v in pairs (tbl) do
+ if k == 'n' and type(v) == 'number' then
+ arraylen = v
+ if v > max then
+ max = v
+ end
+ else
+ if type(k) ~= 'number' or k < 1 or floor(k) ~= k then
+ return false
+ end
+ if k > max then
+ max = k
+ end
+ n = n + 1
+ end
+ end
+ if max > 10 and max > arraylen and max > n * 2 then
+ return false -- don't create an array with too many holes
+ end
+ return true, max
+end
+
+local escapecodes = {
+ ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f",
+ ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t"
+}
+
+local function escapeutf8 (uchar)
+ local value = escapecodes[uchar]
+ if value then
+ return value
+ end
+ local a, b, c, d = strbyte (uchar, 1, 4)
+ a, b, c, d = a or 0, b or 0, c or 0, d or 0
+ if a <= 0x7f then
+ value = a
+ elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then
+ value = (a - 0xc0) * 0x40 + b - 0x80
+ elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then
+ value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80
+ elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then
+ value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80
+ else
+ return ""
+ end
+ if value <= 0xffff then
+ return strformat ("\\u%.4x", value)
+ elseif value <= 0x10ffff then
+ -- encode as UTF-16 surrogate pair
+ value = value - 0x10000
+ local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400)
+ return strformat ("\\u%.4x\\u%.4x", highsur, lowsur)
+ else
+ return ""
+ end
+end
+
+local function fsub (str, pattern, repl)
+ -- gsub always builds a new string in a buffer, even when no match
+ -- exists. First using find should be more efficient when most strings
+ -- don't contain the pattern.
+ if strfind (str, pattern) then
+ return gsub (str, pattern, repl)
+ else
+ return str
+ end
+end
+
+local function quotestring (value)
+ -- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js
+ value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8)
+ if strfind (value, "[\194\216\220\225\226\239]") then
+ value = fsub (value, "\194[\128-\159\173]", escapeutf8)
+ value = fsub (value, "\216[\128-\132]", escapeutf8)
+ value = fsub (value, "\220\143", escapeutf8)
+ value = fsub (value, "\225\158[\180\181]", escapeutf8)
+ value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8)
+ value = fsub (value, "\226\129[\160-\175]", escapeutf8)
+ value = fsub (value, "\239\187\191", escapeutf8)
+ value = fsub (value, "\239\191[\176-\191]", escapeutf8)
+ end
+ return "\"" .. value .. "\""
+end
+json.quotestring = quotestring
+
+local function replace(str, o, n)
+ local i, j = strfind (str, o, 1, true)
+ if i then
+ return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1)
+ else
+ return str
+ end
+end
+
+-- locale independent num2str and str2num functions
+local decpoint, numfilter
+
+local function updatedecpoint ()
+ decpoint = strmatch(tostring(0.5), "([^05+])")
+ -- build a filter that can be used to remove group separators
+ numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+"
+end
+
+updatedecpoint()
+
+local function num2str (num)
+ return replace(fsub(tostring(num), numfilter, ""), decpoint, ".")
+end
+
+local function str2num (str)
+ local num = tonumber(replace(str, ".", decpoint))
+ if not num then
+ updatedecpoint()
+ num = tonumber(replace(str, ".", decpoint))
+ end
+ return num
+end
+
+local function addnewline2 (level, buffer, buflen)
+ buffer[buflen+1] = "\n"
+ buffer[buflen+2] = strrep (" ", level)
+ buflen = buflen + 2
+ return buflen
+end
+
+function json.addnewline (state)
+ if state.indent then
+ state.bufferlen = addnewline2 (state.level or 0,
+ state.buffer, state.bufferlen or #(state.buffer))
+ end
+end
+
+local encode2 -- forward declaration
+
+local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state)
+ local kt = type (key)
+ if kt ~= 'string' and kt ~= 'number' then
+ return nil, "type '" .. kt .. "' is not supported as a key by JSON."
+ end
+ if prev then
+ buflen = buflen + 1
+ buffer[buflen] = ","
+ end
+ if indent then
+ buflen = addnewline2 (level, buffer, buflen)
+ end
+ buffer[buflen+1] = quotestring (key)
+ buffer[buflen+2] = ":"
+ return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state)
+end
+
+local function appendcustom(res, buffer, state)
+ local buflen = state.bufferlen
+ if type (res) == 'string' then
+ buflen = buflen + 1
+ buffer[buflen] = res
+ end
+ return buflen
+end
+
+local function exception(reason, value, state, buffer, buflen, defaultmessage)
+ defaultmessage = defaultmessage or reason
+ local handler = state.exception
+ if not handler then
+ return nil, defaultmessage
+ else
+ state.bufferlen = buflen
+ local ret, msg = handler (reason, value, state, defaultmessage)
+ if not ret then return nil, msg or defaultmessage end
+ return appendcustom(ret, buffer, state)
+ end
+end
+
+function json.encodeexception(reason, value, state, defaultmessage)
+ return quotestring("<" .. defaultmessage .. ">")
+end
+
+encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state)
+ local valtype = type (value)
+ local valmeta = getmetatable (value)
+ valmeta = type (valmeta) == 'table' and valmeta -- only tables
+ local valtojson = valmeta and valmeta.__tojson
+ if valtojson then
+ if tables[value] then
+ return exception('reference cycle', value, state, buffer, buflen)
+ end
+ tables[value] = true
+ state.bufferlen = buflen
+ local ret, msg = valtojson (value, state)
+ if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end
+ tables[value] = nil
+ buflen = appendcustom(ret, buffer, state)
+ elseif value == nil then
+ buflen = buflen + 1
+ buffer[buflen] = "null"
+ elseif valtype == 'number' then
+ local s
+ if value ~= value or value >= huge or -value >= huge then
+ -- This is the behaviour of the original JSON implementation.
+ s = "null"
+ else
+ s = num2str (value)
+ end
+ buflen = buflen + 1
+ buffer[buflen] = s
+ elseif valtype == 'boolean' then
+ buflen = buflen + 1
+ buffer[buflen] = value and "true" or "false"
+ elseif valtype == 'string' then
+ buflen = buflen + 1
+ buffer[buflen] = quotestring (value)
+ elseif valtype == 'table' then
+ if tables[value] then
+ return exception('reference cycle', value, state, buffer, buflen)
+ end
+ tables[value] = true
+ level = level + 1
+ local isa, n = isarray (value)
+ if n == 0 and valmeta and valmeta.__jsontype == 'object' then
+ isa = false
+ end
+ local msg
+ if isa then -- JSON array
+ buflen = buflen + 1
+ buffer[buflen] = "["
+ for i = 1, n do
+ buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state)
+ if not buflen then return nil, msg end
+ if i < n then
+ buflen = buflen + 1
+ buffer[buflen] = ","
+ end
+ end
+ buflen = buflen + 1
+ buffer[buflen] = "]"
+ else -- JSON object
+ local prev = false
+ buflen = buflen + 1
+ buffer[buflen] = "{"
+ local order = valmeta and valmeta.__jsonorder or globalorder
+ if order then
+ local used = {}
+ n = #order
+ for i = 1, n do
+ local k = order[i]
+ local v = value[k]
+ if v ~= nil then
+ used[k] = true
+ buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
+ if not buflen then return nil, msg end
+ prev = true -- add a seperator before the next element
+ end
+ end
+ for k,v in pairs (value) do
+ if not used[k] then
+ buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
+ if not buflen then return nil, msg end
+ prev = true -- add a seperator before the next element
+ end
+ end
+ else -- unordered
+ for k,v in pairs (value) do
+ buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
+ if not buflen then return nil, msg end
+ prev = true -- add a seperator before the next element
+ end
+ end
+ if indent then
+ buflen = addnewline2 (level - 1, buffer, buflen)
+ end
+ buflen = buflen + 1
+ buffer[buflen] = "}"
+ end
+ tables[value] = nil
+ else
+ return exception ('unsupported type', value, state, buffer, buflen,
+ "type '" .. valtype .. "' is not supported by JSON.")
+ end
+ return buflen
+end
+
+function json.encode (value, state)
+ state = state or {}
+ local oldbuffer = state.buffer
+ local buffer = oldbuffer or {}
+ state.buffer = buffer
+ updatedecpoint()
+ local ret, msg = encode2 (value, state.indent, state.level or 0,
+ buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state)
+ if not ret then
+ error (msg, 2)
+ elseif oldbuffer == buffer then
+ state.bufferlen = ret
+ return true
+ else
+ state.bufferlen = nil
+ state.buffer = nil
+ return concat (buffer)
+ end
+end
+
+local function loc (str, where)
+ local line, pos, linepos = 1, 1, 0
+ while true do
+ pos = strfind (str, "\n", pos, true)
+ if pos and pos < where then
+ line = line + 1
+ linepos = pos
+ pos = pos + 1
+ else
+ break
+ end
+ end
+ return "line " .. line .. ", column " .. (where - linepos)
+end
+
+local function unterminated (str, what, where)
+ return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where)
+end
+
+local function scanwhite (str, pos)
+ while true do
+ pos = strfind (str, "%S", pos)
+ if not pos then return nil end
+ local sub2 = strsub (str, pos, pos + 1)
+ if sub2 == "\239\187" and strsub (str, pos + 2, pos + 2) == "\191" then
+ -- UTF-8 Byte Order Mark
+ pos = pos + 3
+ elseif sub2 == "//" then
+ pos = strfind (str, "[\n\r]", pos + 2)
+ if not pos then return nil end
+ elseif sub2 == "/*" then
+ pos = strfind (str, "*/", pos + 2)
+ if not pos then return nil end
+ pos = pos + 2
+ else
+ return pos
+ end
+ end
+end
+
+local escapechars = {
+ ["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f",
+ ["n"] = "\n", ["r"] = "\r", ["t"] = "\t"
+}
+
+local function unichar (value)
+ if value < 0 then
+ return nil
+ elseif value <= 0x007f then
+ return strchar (value)
+ elseif value <= 0x07ff then
+ return strchar (0xc0 + floor(value/0x40),
+ 0x80 + (floor(value) % 0x40))
+ elseif value <= 0xffff then
+ return strchar (0xe0 + floor(value/0x1000),
+ 0x80 + (floor(value/0x40) % 0x40),
+ 0x80 + (floor(value) % 0x40))
+ elseif value <= 0x10ffff then
+ return strchar (0xf0 + floor(value/0x40000),
+ 0x80 + (floor(value/0x1000) % 0x40),
+ 0x80 + (floor(value/0x40) % 0x40),
+ 0x80 + (floor(value) % 0x40))
+ else
+ return nil
+ end
+end
+
+local function scanstring (str, pos)
+ local lastpos = pos + 1
+ local buffer, n = {}, 0
+ while true do
+ local nextpos = strfind (str, "[\"\\]", lastpos)
+ if not nextpos then
+ return unterminated (str, "string", pos)
+ end
+ if nextpos > lastpos then
+ n = n + 1
+ buffer[n] = strsub (str, lastpos, nextpos - 1)
+ end
+ if strsub (str, nextpos, nextpos) == "\"" then
+ lastpos = nextpos + 1
+ break
+ else
+ local escchar = strsub (str, nextpos + 1, nextpos + 1)
+ local value
+ if escchar == "u" then
+ value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16)
+ if value then
+ local value2
+ if 0xD800 <= value and value <= 0xDBff then
+ -- we have the high surrogate of UTF-16. Check if there is a
+ -- low surrogate escaped nearby to combine them.
+ if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then
+ value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16)
+ if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then
+ value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000
+ else
+ value2 = nil -- in case it was out of range for a low surrogate
+ end
+ end
+ end
+ value = value and unichar (value)
+ if value then
+ if value2 then
+ lastpos = nextpos + 12
+ else
+ lastpos = nextpos + 6
+ end
+ end
+ end
+ end
+ if not value then
+ value = escapechars[escchar] or escchar
+ lastpos = nextpos + 2
+ end
+ n = n + 1
+ buffer[n] = value
+ end
+ end
+ if n == 1 then
+ return buffer[1], lastpos
+ elseif n > 1 then
+ return concat (buffer), lastpos
+ else
+ return "", lastpos
+ end
+end
+
+local scanvalue -- forward declaration
+
+local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta)
+ local len = strlen (str)
+ local tbl, n = {}, 0
+ local pos = startpos + 1
+ if what == 'object' then
+ setmetatable (tbl, objectmeta)
+ else
+ setmetatable (tbl, arraymeta)
+ end
+ while true do
+ pos = scanwhite (str, pos)
+ if not pos then return unterminated (str, what, startpos) end
+ local char = strsub (str, pos, pos)
+ if char == closechar then
+ return tbl, pos + 1
+ end
+ local val1, err
+ val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
+ if err then return nil, pos, err end
+ pos = scanwhite (str, pos)
+ if not pos then return unterminated (str, what, startpos) end
+ char = strsub (str, pos, pos)
+ if char == ":" then
+ if val1 == nil then
+ return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")"
+ end
+ pos = scanwhite (str, pos + 1)
+ if not pos then return unterminated (str, what, startpos) end
+ local val2
+ val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
+ if err then return nil, pos, err end
+ tbl[val1] = val2
+ pos = scanwhite (str, pos)
+ if not pos then return unterminated (str, what, startpos) end
+ char = strsub (str, pos, pos)
+ else
+ n = n + 1
+ tbl[n] = val1
+ end
+ if char == "," then
+ pos = pos + 1
+ end
+ end
+end
+
+scanvalue = function (str, pos, nullval, objectmeta, arraymeta)
+ pos = pos or 1
+ pos = scanwhite (str, pos)
+ if not pos then
+ return nil, strlen (str) + 1, "no valid JSON value (reached the end)"
+ end
+ local char = strsub (str, pos, pos)
+ if char == "{" then
+ return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta)
+ elseif char == "[" then
+ return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta)
+ elseif char == "\"" then
+ return scanstring (str, pos)
+ else
+ local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos)
+ if pstart then
+ local number = str2num (strsub (str, pstart, pend))
+ if number then
+ return number, pend + 1
+ end
+ end
+ pstart, pend = strfind (str, "^%a%w*", pos)
+ if pstart then
+ local name = strsub (str, pstart, pend)
+ if name == "true" then
+ return true, pend + 1
+ elseif name == "false" then
+ return false, pend + 1
+ elseif name == "null" then
+ return nullval, pend + 1
+ end
+ end
+ return nil, pos, "no valid JSON value at " .. loc (str, pos)
+ end
+end
+
+local function optionalmetatables(...)
+ if select("#", ...) > 0 then
+ return ...
+ else
+ return {__jsontype = 'object'}, {__jsontype = 'array'}
+ end
+end
+
+function json.decode (str, pos, nullval, ...)
+ local objectmeta, arraymeta = optionalmetatables(...)
+ return scanvalue (str, pos, nullval, objectmeta, arraymeta)
+end
+
+function json.use_lpeg ()
+ local g = require ("lpeg")
+
+ if type(g.version) == 'function' and g.version() == "0.11" then
+ error "due to a bug in LPeg 0.11, it cannot be used for JSON matching"
+ end
+
+ local pegmatch = g.match
+ local P, S, R = g.P, g.S, g.R
+
+ local function ErrorCall (str, pos, msg, state)
+ if not state.msg then
+ state.msg = msg .. " at " .. loc (str, pos)
+ state.pos = pos
+ end
+ return false
+ end
+
+ local function Err (msg)
+ return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall)
+ end
+
+ local function ErrorUnterminatedCall (str, pos, what, state)
+ return ErrorCall (str, pos - 1, "unterminated " .. what, state)
+ end
+
+ local SingleLineComment = P"//" * (1 - S"\n\r")^0
+ local MultiLineComment = P"/*" * (1 - P"*/")^0 * P"*/"
+ local Space = (S" \n\r\t" + P"\239\187\191" + SingleLineComment + MultiLineComment)^0
+
+ local function ErrUnterminated (what)
+ return g.Cmt (g.Cc (what) * g.Carg (2), ErrorUnterminatedCall)
+ end
+
+ local PlainChar = 1 - S"\"\\\n\r"
+ local EscapeSequence = (P"\\" * g.C (S"\"\\/bfnrt" + Err "unsupported escape sequence")) / escapechars
+ local HexDigit = R("09", "af", "AF")
+ local function UTF16Surrogate (match, pos, high, low)
+ high, low = tonumber (high, 16), tonumber (low, 16)
+ if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then
+ return true, unichar ((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)
+ else
+ return false
+ end
+ end
+ local function UTF16BMP (hex)
+ return unichar (tonumber (hex, 16))
+ end
+ local U16Sequence = (P"\\u" * g.C (HexDigit * HexDigit * HexDigit * HexDigit))
+ local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP
+ local Char = UnicodeEscape + EscapeSequence + PlainChar
+ local String = P"\"" * (g.Cs (Char ^ 0) * P"\"" + ErrUnterminated "string")
+ local Integer = P"-"^(-1) * (P"0" + (R"19" * R"09"^0))
+ local Fractal = P"." * R"09"^0
+ local Exponent = (S"eE") * (S"+-")^(-1) * R"09"^1
+ local Number = (Integer * Fractal^(-1) * Exponent^(-1))/str2num
+ local Constant = P"true" * g.Cc (true) + P"false" * g.Cc (false) + P"null" * g.Carg (1)
+ local SimpleValue = Number + String + Constant
+ local ArrayContent, ObjectContent
+
+ -- The functions parsearray and parseobject parse only a single value/pair
+ -- at a time and store them directly to avoid hitting the LPeg limits.
+ local function parsearray (str, pos, nullval, state)
+ local obj, cont
+ local start = pos
+ local npos
+ local t, nt = {}, 0
+ repeat
+ obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state)
+ if cont == 'end' then
+ return ErrorUnterminatedCall (str, start, "array", state)
+ end
+ pos = npos
+ if cont == 'cont' or cont == 'last' then
+ nt = nt + 1
+ t[nt] = obj
+ end
+ until cont ~= 'cont'
+ return pos, setmetatable (t, state.arraymeta)
+ end
+
+ local function parseobject (str, pos, nullval, state)
+ local obj, key, cont
+ local start = pos
+ local npos
+ local t = {}
+ repeat
+ key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state)
+ if cont == 'end' then
+ return ErrorUnterminatedCall (str, start, "object", state)
+ end
+ pos = npos
+ if cont == 'cont' or cont == 'last' then
+ t[key] = obj
+ end
+ until cont ~= 'cont'
+ return pos, setmetatable (t, state.objectmeta)
+ end
+
+ local Array = P"[" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray)
+ local Object = P"{" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject)
+ local Value = Space * (Array + Object + SimpleValue)
+ local ExpectedValue = Value + Space * Err "value expected"
+ local ExpectedKey = String + Err "key expected"
+ local End = P(-1) * g.Cc'end'
+ local ErrInvalid = Err "invalid JSON"
+ ArrayContent = (Value * Space * (P"," * g.Cc'cont' + P"]" * g.Cc'last'+ End + ErrInvalid) + g.Cc(nil) * (P"]" * g.Cc'empty' + End + ErrInvalid)) * g.Cp()
+ local Pair = g.Cg (Space * ExpectedKey * Space * (P":" + Err "colon expected") * ExpectedValue)
+ ObjectContent = (g.Cc(nil) * g.Cc(nil) * P"}" * g.Cc'empty' + End + (Pair * Space * (P"," * g.Cc'cont' + P"}" * g.Cc'last' + End + ErrInvalid) + ErrInvalid)) * g.Cp()
+ local DecodeValue = ExpectedValue * g.Cp ()
+
+ jsonlpeg.version = json.version
+ jsonlpeg.encode = json.encode
+ jsonlpeg.null = json.null
+ jsonlpeg.quotestring = json.quotestring
+ jsonlpeg.addnewline = json.addnewline
+ jsonlpeg.encodeexception = json.encodeexception
+ jsonlpeg.using_lpeg = true
+
+ function jsonlpeg.decode (str, pos, nullval, ...)
+ local state = {}
+ state.objectmeta, state.arraymeta = optionalmetatables(...)
+ local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state)
+ if state.msg then
+ return nil, state.pos, state.msg
+ else
+ return obj, retpos
+ end
+ end
+
+ -- cache result of this function:
+ json.use_lpeg = function () return jsonlpeg end
+ jsonlpeg.use_lpeg = json.use_lpeg
+
+ return jsonlpeg
+end
+
+if always_use_lpeg then
+ return json.use_lpeg()
+end
+
+return json
+