diff options
| author | Mike Vink <mike@pionative.com> | 2025-02-03 21:29:42 +0100 |
|---|---|---|
| committer | Mike Vink <mike@pionative.com> | 2025-02-03 21:29:42 +0100 |
| commit | 5155816b7b925dec5d5feb1568b1d7ceb00938b9 (patch) | |
| tree | deca28ea15e79f6f804c3d90d2ba757881638af5 /src/luarocks/cmd | |
Diffstat (limited to 'src/luarocks/cmd')
| -rw-r--r-- | src/luarocks/cmd/build.lua | 198 | ||||
| -rw-r--r-- | src/luarocks/cmd/config.lua | 392 | ||||
| -rw-r--r-- | src/luarocks/cmd/doc.lua | 153 | ||||
| -rw-r--r-- | src/luarocks/cmd/download.lua | 51 | ||||
| -rw-r--r-- | src/luarocks/cmd/init.lua | 219 | ||||
| -rw-r--r-- | src/luarocks/cmd/install.lua | 271 | ||||
| -rw-r--r-- | src/luarocks/cmd/lint.lua | 50 | ||||
| -rw-r--r-- | src/luarocks/cmd/list.lua | 96 | ||||
| -rw-r--r-- | src/luarocks/cmd/make.lua | 163 | ||||
| -rw-r--r-- | src/luarocks/cmd/new_version.lua | 228 | ||||
| -rw-r--r-- | src/luarocks/cmd/pack.lua | 36 | ||||
| -rw-r--r-- | src/luarocks/cmd/path.lua | 83 | ||||
| -rw-r--r-- | src/luarocks/cmd/purge.lua | 73 | ||||
| -rw-r--r-- | src/luarocks/cmd/remove.lua | 72 | ||||
| -rw-r--r-- | src/luarocks/cmd/search.lua | 84 | ||||
| -rw-r--r-- | src/luarocks/cmd/show.lua | 314 | ||||
| -rw-r--r-- | src/luarocks/cmd/test.lua | 48 | ||||
| -rw-r--r-- | src/luarocks/cmd/unpack.lua | 169 | ||||
| -rw-r--r-- | src/luarocks/cmd/upload.lua | 128 | ||||
| -rw-r--r-- | src/luarocks/cmd/which.lua | 40 | ||||
| -rw-r--r-- | src/luarocks/cmd/write_rockspec.lua | 408 |
21 files changed, 3276 insertions, 0 deletions
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 |
