diff options
Diffstat (limited to 'src/luarocks')
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 = [[ +• <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, ', ').. '</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..': ' + 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, ', ') .. '<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 + |
