From 44f034eb73f5e4d04119d7d2ecbec612d701912b Mon Sep 17 00:00:00 2001 From: V1K1NGbg Date: Thu, 22 Aug 2024 17:49:07 -0300 Subject: Teal: convert luarocks.deps --- src/luarocks/deps.lua | 833 ----------------------------------------------- src/luarocks/deps.tl | 877 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 877 insertions(+), 833 deletions(-) delete mode 100644 src/luarocks/deps.lua create mode 100644 src/luarocks/deps.tl diff --git a/src/luarocks/deps.lua b/src/luarocks/deps.lua deleted file mode 100644 index 344991f6..00000000 --- a/src/luarocks/deps.lua +++ /dev/null @@ -1,833 +0,0 @@ - ---- High-level dependency related functions. -local deps = {} - -local cfg = require("luarocks.core.cfg") -local manif = require("luarocks.manif") -local path = require("luarocks.path") -local dir = require("luarocks.dir") -local fun = require("luarocks.fun") -local util = require("luarocks.util") -local vers = require("luarocks.core.vers") -local queries = require("luarocks.queries") -local deplocks = require("luarocks.deplocks") - ---- Generate a function that matches dep queries against the manifest, --- taking into account rocks_provided, the list of versions to skip, --- and the lockfile. --- @param deps_mode "one", "none", "all" or "order" --- @param rocks_provided a one-level table mapping names to versions, --- listing rocks to consider provided by the VM --- @param rocks_provided table: A table of auto-provided dependencies. --- by this Lua implementation for the given dependency. --- @param depskey key to use when matching the lockfile ("dependencies", --- "build_dependencies", etc.) --- @param skip_set a two-level table mapping names to versions to --- boolean, listing rocks that should not be matched --- @return function(dep): {string}, {string:string}, string, boolean --- * array of matching versions --- * map of versions to locations --- * version matched via lockfile if any --- * true if rock matched via rocks_provided -local function prepare_get_versions(deps_mode, rocks_provided, depskey, skip_set) - assert(type(deps_mode) == "string") - assert(type(rocks_provided) == "table") - assert(type(depskey) == "string") - assert(type(skip_set) == "table" or skip_set == nil) - - return function(dep) - local versions, locations - local provided = rocks_provided[dep.name] - if provided then - -- Provided rocks have higher priority than manifest's rocks. - versions, locations = { provided }, {} - else - if deps_mode == "none" then - deps_mode = "one" - end - versions, locations = manif.get_versions(dep, deps_mode) - end - - if skip_set and skip_set[dep.name] then - for i = #versions, 1, -1 do - local v = versions[i] - if skip_set[dep.name][v] then - table.remove(versions, i) - end - end - end - - local lockversion = deplocks.get(depskey, dep.name) - - return versions, locations, lockversion, provided ~= nil - end -end - ---- Attempt to match a dependency to an installed rock. --- @param get_versions a getter function obtained via prepare_get_versions --- @return (string, string, table) or (nil, nil, table): --- 1. latest installed version of the rock matching the dependency --- 2. location where the installed version is installed --- 3. the 'dep' query table --- 4. true if provided via VM --- or --- 1. nil --- 2. nil --- 3. either 'dep' or an alternative query to be used --- 4. false -local function match_dep(dep, get_versions) - assert(type(dep) == "table") - assert(type(get_versions) == "function") - - local versions, locations, lockversion, provided = get_versions(dep) - - local latest_version - local latest_vstring - for _, vstring in ipairs(versions) do - local version = vers.parse_version(vstring) - if vers.match_constraints(version, dep.constraints) then - if not latest_version or version > latest_version then - latest_version = version - latest_vstring = vstring - end - end - end - - if lockversion and not locations[lockversion] then - local latest_matching_msg = "" - if latest_vstring and latest_vstring ~= lockversion then - latest_matching_msg = " (latest matching is " .. latest_vstring .. ")" - end - util.printout("Forcing " .. dep.name .. " to pinned version " .. lockversion .. latest_matching_msg) - return nil, nil, queries.new(dep.name, dep.namespace, lockversion) - end - - return latest_vstring, locations[latest_vstring], dep, provided -end - -local function match_all_deps(dependencies, get_versions) - assert(type(dependencies) == "table") - assert(type(get_versions) == "function") - - local matched, missing, no_upgrade = {}, {}, {} - - for _, dep in ipairs(dependencies) do - local found, _, provided - found, _, dep, provided = match_dep(dep, get_versions) - if found then - if not provided then - matched[dep] = {name = dep.name, version = found} - end - else - if dep.constraints[1] and dep.constraints[1].no_upgrade then - no_upgrade[dep.name] = dep - else - missing[dep.name] = dep - end - end - end - return matched, missing, no_upgrade -end - ---- Attempt to match dependencies of a rockspec to installed rocks. --- @param dependencies table: The table of dependencies. --- @param rocks_provided table: The table of auto-provided dependencies. --- @param skip_set table or nil: Program versions to not use as valid matches. --- Table where keys are program names and values are tables where keys --- are program versions and values are 'true'. --- @param deps_mode string: Which trees to check dependencies for --- @return table, table, table: A table where keys are dependencies parsed --- in table format and values are tables containing fields 'name' and --- version' representing matches; a table of missing dependencies --- parsed as tables; and a table of "no-upgrade" missing dependencies --- (to be used in plugin modules so that a plugin does not force upgrade of --- its parent application). -function deps.match_deps(dependencies, rocks_provided, skip_set, deps_mode) - assert(type(dependencies) == "table") - assert(type(rocks_provided) == "table") - assert(type(skip_set) == "table" or skip_set == nil) - assert(type(deps_mode) == "string") - - local get_versions = prepare_get_versions(deps_mode, rocks_provided, "dependencies", skip_set) - return match_all_deps(dependencies, get_versions) -end - -local function rock_status(dep, get_versions) - assert(dep:type() == "query") - assert(type(get_versions) == "function") - - local installed, _, _, provided = match_dep(dep, get_versions) - local installation_type = provided and "provided by VM" or "installed" - return installed and installed.." "..installation_type..": success" or "not installed" -end - ---- Check depenendencies of a package and report any missing ones. --- @param name string: package name. --- @param version string: package version. --- @param dependencies table: array of dependencies. --- @param deps_mode string: Which trees to check dependencies for --- @param rocks_provided table: A table of auto-dependencies provided --- by this Lua implementation for the given dependency. --- "one" for the current default tree, "all" for all trees, --- "order" for all trees with priority >= the current default, "none" for no trees. -function deps.report_missing_dependencies(name, version, dependencies, deps_mode, rocks_provided) - assert(type(name) == "string") - assert(type(version) == "string") - assert(type(dependencies) == "table") - assert(type(deps_mode) == "string") - assert(type(rocks_provided) == "table") - - if deps_mode == "none" then - return - end - - local get_versions = prepare_get_versions(deps_mode, rocks_provided, "dependencies") - - local first_missing_dep = true - - for _, dep in ipairs(dependencies) do - local found, _ - found, _, dep = match_dep(dep, get_versions) - if not found then - if first_missing_dep then - util.printout(("Missing dependencies for %s %s:"):format(name, version)) - first_missing_dep = false - end - - util.printout((" %s (%s)"):format(tostring(dep), rock_status(dep, get_versions))) - end - end -end - -function deps.fulfill_dependency(dep, deps_mode, rocks_provided, verify, depskey) - assert(dep:type() == "query") - assert(type(deps_mode) == "string" or deps_mode == nil) - assert(type(rocks_provided) == "table" or rocks_provided == nil) - assert(type(verify) == "boolean" or verify == nil) - assert(type(depskey) == "string") - - deps_mode = deps_mode or "all" - rocks_provided = rocks_provided or {} - - local get_versions = prepare_get_versions(deps_mode, rocks_provided, depskey) - - local found, where - found, where, dep = match_dep(dep, get_versions) - if found then - local tree_manifests = manif.load_rocks_tree_manifests(deps_mode) - manif.scan_dependencies(dep.name, found, tree_manifests, deplocks.proxy(depskey)) - return true, found, where - end - - local search = require("luarocks.search") - local install = require("luarocks.cmd.install") - - local url, search_err = search.find_suitable_rock(dep) - if not url then - return nil, "Could not satisfy dependency "..tostring(dep)..": "..search_err - end - util.printout("Installing "..url) - local install_args = { - rock = url, - deps_mode = deps_mode, - namespace = dep.namespace, - verify = verify, - } - local ok, install_err, errcode = install.command(install_args) - if not ok then - return nil, "Failed installing dependency: "..url.." - "..install_err, errcode - end - - found, where = match_dep(dep, get_versions) - if not found then - return nil, "Repository inconsistency detected (previously unfinished/corrupted installation?)" - end - return true, found, where -end - -local function check_supported_platforms(rockspec) - if rockspec.supported_platforms and next(rockspec.supported_platforms) then - local all_negative = true - local supported = false - for _, plat in pairs(rockspec.supported_platforms) do - local neg - neg, plat = plat:match("^(!?)(.*)") - if neg == "!" then - if cfg.is_platform(plat) then - return nil, "This rockspec for "..rockspec.package.." does not support "..plat.." platforms." - end - else - all_negative = false - if cfg.is_platform(plat) then - supported = true - break - end - end - end - if supported == false and not all_negative then - local plats = cfg.print_platforms() - return nil, "This rockspec for "..rockspec.package.." does not support "..plats.." platforms." - end - end - - return true -end - ---- Check dependencies of a rock and attempt to install any missing ones. --- Packages are installed using the LuaRocks "install" command. --- Aborts the program if a dependency could not be fulfilled. --- @param rockspec table: A rockspec in table format. --- @param depskey string: Rockspec key to fetch to get dependency table --- ("dependencies", "build_dependencies", etc.). --- @param deps_mode string --- @param verify boolean --- @param deplock_dir string: dirname of the deplock file --- @return boolean or (nil, string, [string]): True if no errors occurred, or --- nil and an error message if any test failed, followed by an optional --- error code. -function deps.fulfill_dependencies(rockspec, depskey, deps_mode, verify, deplock_dir) - assert(type(rockspec) == "table") - assert(type(depskey) == "string") - assert(type(deps_mode) == "string") - assert(type(verify) == "boolean" or verify == nil) - assert(type(deplock_dir) == "string" or deplock_dir == nil) - - local name = rockspec.name - local version = rockspec.version - local rocks_provided = rockspec.rocks_provided - - local ok, filename, err = deplocks.load(name, deplock_dir or ".") - if filename then - util.printout("Using dependencies pinned in lockfile: " .. filename) - - local get_versions = prepare_get_versions("none", rocks_provided, depskey) - for dnsname, dversion in deplocks.each(depskey) do - local dname, dnamespace = util.split_namespace(dnsname) - local dep = queries.new(dname, dnamespace, dversion) - - util.printout(("%s %s is pinned to %s (%s)"):format( - name, version, tostring(dep), rock_status(dep, get_versions))) - - local ok, err = deps.fulfill_dependency(dep, "none", rocks_provided, verify, depskey) - if not ok then - return nil, err - end - end - util.printout() - return true - elseif err then - util.warning(err) - end - - ok, err = check_supported_platforms(rockspec) - if not ok then - return nil, err - end - - deps.report_missing_dependencies(name, version, rockspec[depskey], deps_mode, rocks_provided) - - util.printout() - - local get_versions = prepare_get_versions(deps_mode, rocks_provided, depskey) - for _, dep in ipairs(rockspec[depskey]) do - - util.printout(("%s %s depends on %s (%s)"):format( - name, version, tostring(dep), rock_status(dep, get_versions))) - - local ok, found_or_err, _, no_upgrade = deps.fulfill_dependency(dep, deps_mode, rocks_provided, verify, depskey) - if ok then - deplocks.add(depskey, dep.name, found_or_err) - else - if no_upgrade then - util.printerr("This version of "..name.." is designed for use with") - util.printerr(tostring(dep)..", but is configured to avoid upgrading it") - util.printerr("automatically. Please upgrade "..dep.name.." with") - util.printerr(" luarocks install "..dep.name) - util.printerr("or look for a suitable version of "..name.." with") - util.printerr(" luarocks search "..name) - end - return nil, found_or_err - end - end - - return true -end - ---- If filename matches a pattern, return the capture. --- For example, given "libfoo.so" and "lib?.so" is a pattern, --- returns "foo" (which can then be used to build names --- based on other patterns. --- @param file string: a filename --- @param pattern string: a pattern, where ? is to be matched by the filename. --- @return string The pattern, if found, or nil. -local function deconstruct_pattern(file, pattern) - local depattern = "^"..(pattern:gsub("%.", "%%."):gsub("%*", ".*"):gsub("?", "(.*)")).."$" - return (file:match(depattern)) -end - ---- Construct all possible patterns for a name and add to the files array. --- Run through the patterns array replacing all occurrences of "?" --- with the given file name and store them in the files array. --- @param file string A raw name (e.g. "foo") --- @param array of string An array of patterns with "?" as the wildcard --- (e.g. {"?.so", "lib?.so"}) --- @param files The array of constructed names -local function add_all_patterns(file, patterns, files) - for _, pattern in ipairs(patterns) do - table.insert(files, {#files + 1, (pattern:gsub("?", file))}) - end -end - -local function get_external_deps_dirs(mode) - local patterns = cfg.external_deps_patterns - local subdirs = cfg.external_deps_subdirs - if mode == "install" then - patterns = cfg.runtime_external_deps_patterns - subdirs = cfg.runtime_external_deps_subdirs - end - local dirs = { - BINDIR = { subdir = subdirs.bin, testfile = "program", pattern = patterns.bin }, - INCDIR = { subdir = subdirs.include, testfile = "header", pattern = patterns.include }, - LIBDIR = { subdir = subdirs.lib, testfile = "library", pattern = patterns.lib } - } - if mode == "install" then - dirs.INCDIR = nil - end - return dirs -end - -local function resolve_prefix(prefix, dirs) - if type(prefix) == "string" then - return prefix - elseif type(prefix) == "table" then - if prefix.bin then - dirs.BINDIR.subdir = prefix.bin - end - if prefix.include then - if dirs.INCDIR then - dirs.INCDIR.subdir = prefix.include - end - end - if prefix.lib then - dirs.LIBDIR.subdir = prefix.lib - end - return prefix.prefix - end -end - -local function add_patterns_for_file(files, file, patterns) - -- If it doesn't look like it contains a filename extension - if not (file:match("%.[a-z]+$") or file:match("%.[a-z]+%.")) then - add_all_patterns(file, patterns, files) - else - for _, pattern in ipairs(patterns) do - local matched = deconstruct_pattern(file, pattern) - if matched then - add_all_patterns(matched, patterns, files) - end - end - table.insert(files, {#files + 1, file}) - end -end - -local function check_external_dependency_at(prefix, name, ext_files, vars, dirs, err_files, cache) - local fs = require("luarocks.fs") - cache = cache or {} - - for dirname, dirdata in util.sortedpairs(dirs) do - local paths - local path_var_value = vars[name.."_"..dirname] - if path_var_value then - paths = { path_var_value } - elseif type(dirdata.subdir) == "table" then - paths = {} - for i,v in ipairs(dirdata.subdir) do - paths[i] = dir.path(prefix, v) - end - else - paths = { dir.path(prefix, dirdata.subdir) } - end - local file_or_files = ext_files[dirdata.testfile] - if file_or_files then - local files = {} - if type(file_or_files) == "string" then - add_patterns_for_file(files, file_or_files, dirdata.pattern) - elseif type(file_or_files) == "table" then - for _, f in ipairs(file_or_files) do - add_patterns_for_file(files, f, dirdata.pattern) - end - end - - local found = false - table.sort(files, function(a, b) - if (not a[2]:match("%*")) and b[2]:match("%*") then - return true - elseif a[2]:match("%*") and (not b[2]:match("%*")) then - return false - else - return a[1] < b[1] - end - end) - for _, fa in ipairs(files) do - - local f = fa[2] - -- small convenience hack - if f:match("%.so$") or f:match("%.dylib$") or f:match("%.dll$") then - f = f:gsub("%.[^.]+$", "."..cfg.external_lib_extension) - end - - local pattern - if f:match("%*") then - pattern = "^" .. f:gsub("([-.+])", "%%%1"):gsub("%*", ".*") .. "$" - f = "matching "..f - end - - for _, d in ipairs(paths) do - if pattern then - if not cache[d] then - cache[d] = fs.list_dir(d) - end - local match = string.match - for _, entry in ipairs(cache[d]) do - if match(entry, pattern) then - found = true - break - end - end - else - found = fs.is_file(dir.path(d, f)) - end - if found then - dirdata.dir = d - dirdata.file = f - break - else - table.insert(err_files[dirdata.testfile], f.." in "..d) - end - end - if found then - break - end - end - if not found then - return nil, dirname, dirdata.testfile - end - else - -- When we have a set of subdir suffixes, look for one that exists. - -- For these reason, we now put "lib" ahead of "" on Windows in our - -- default set. - dirdata.dir = paths[1] - for _, p in ipairs(paths) do - if fs.exists(p) then - dirdata.dir = p - break - end - end - end - end - - for dirname, dirdata in pairs(dirs) do - vars[name.."_"..dirname] = dirdata.dir - vars[name.."_"..dirname.."_FILE"] = dirdata.file - end - vars[name.."_DIR"] = prefix - return true -end - -local function check_external_dependency(name, ext_files, vars, mode, cache) - local ok - local err_dirname - local err_testfile - local err_files = {program = {}, header = {}, library = {}} - - local dirs = get_external_deps_dirs(mode) - - local prefixes - if vars[name .. "_DIR"] then - prefixes = { vars[name .. "_DIR"] } - elseif vars.DEPS_DIR then - prefixes = { vars.DEPS_DIR } - else - prefixes = cfg.external_deps_dirs - end - - for _, prefix in ipairs(prefixes) do - prefix = resolve_prefix(prefix, dirs) - if cfg.is_platform("mingw32") and name == "LUA" then - dirs.LIBDIR.pattern = fun.filter(util.deep_copy(dirs.LIBDIR.pattern), function(s) - return not s:match("%.a$") - end) - elseif cfg.is_platform("windows") and name == "LUA" then - dirs.LIBDIR.pattern = fun.filter(util.deep_copy(dirs.LIBDIR.pattern), function(s) - return not s:match("%.dll$") - end) - end - ok, err_dirname, err_testfile = check_external_dependency_at(prefix, name, ext_files, vars, dirs, err_files, cache) - if ok then - return true - end - end - - return nil, err_dirname, err_testfile, err_files -end - -function deps.autodetect_external_dependencies(build) - -- only applies to the 'builtin' build type - if not build or not build.modules then - return nil - end - - local extdeps = {} - local any = false - for _, data in pairs(build.modules) do - if type(data) == "table" and data.libraries then - local libraries = data.libraries - if type(libraries) == "string" then - libraries = { libraries } - end - local incdirs = {} - local libdirs = {} - for _, lib in ipairs(libraries) do - local upper = lib:upper():gsub("%+", "P"):gsub("[^%w]", "_") - any = true - extdeps[upper] = { library = lib } - table.insert(incdirs, "$(" .. upper .. "_INCDIR)") - table.insert(libdirs, "$(" .. upper .. "_LIBDIR)") - end - if not data.incdirs then - data.incdirs = incdirs - end - if not data.libdirs then - data.libdirs = libdirs - end - end - end - return any and extdeps or nil -end - ---- Set up path-related variables for external dependencies. --- For each key in the external_dependencies table in the --- rockspec file, four variables are created: _DIR, _BINDIR, --- _INCDIR and _LIBDIR. These are not overwritten --- if already set (e.g. by the LuaRocks config file or through the --- command-line). Values in the external_dependencies table --- are tables that may contain a "header" or a "library" field, --- with filenames to be tested for existence. --- @param rockspec table: The rockspec table. --- @param mode string: if "build" is given, checks all files; --- if "install" is given, do not scan for headers. --- @return boolean or (nil, string): True if no errors occurred, or --- nil and an error message if any test failed. -function deps.check_external_deps(rockspec, mode) - assert(rockspec:type() == "rockspec") - - if not rockspec.external_dependencies then - rockspec.external_dependencies = deps.autodetect_external_dependencies(rockspec.build) - end - if not rockspec.external_dependencies then - return true - end - - for name, ext_files in util.sortedpairs(rockspec.external_dependencies) do - local ok, err_dirname, err_testfile, err_files = check_external_dependency(name, ext_files, rockspec.variables, mode) - if not ok then - local lines = {"Could not find "..err_testfile.." file for "..name} - - local err_paths = {} - for _, err_file in ipairs(err_files[err_testfile]) do - if not err_paths[err_file] then - err_paths[err_file] = true - table.insert(lines, " No file "..err_file) - end - end - - table.insert(lines, "You may have to install "..name.." in your system and/or pass "..name.."_DIR or "..name.."_"..err_dirname.." to the luarocks command.") - table.insert(lines, "Example: luarocks install "..rockspec.name.." "..name.."_DIR=/usr/local") - - return nil, table.concat(lines, "\n"), "dependency" - end - end - return true -end - ---- Recursively add satisfied dependencies of a package to a table, --- to build a transitive closure of all dependent packages. --- Additionally ensures that `dependencies` table of the manifest is up-to-date. --- @param results table: The results table being built, maps package names to versions. --- @param mdeps table: The manifest dependencies table. --- @param name string: Package name. --- @param version string: Package version. -function deps.scan_deps(results, mdeps, name, version, deps_mode) - assert(type(results) == "table") - assert(type(mdeps) == "table") - assert(type(name) == "string" and not name:match("/")) - assert(type(version) == "string") - - local fetch = require("luarocks.fetch") - - if results[name] then - return - end - if not mdeps[name] then mdeps[name] = {} end - local mdn = mdeps[name] - local dependencies = mdn[version] - local rocks_provided - if not dependencies then - local rockspec, err = fetch.load_local_rockspec(path.rockspec_file(name, version), false) - if not rockspec then - return - end - dependencies = rockspec.dependencies - rocks_provided = rockspec.rocks_provided - mdn[version] = dependencies - else - rocks_provided = util.get_rocks_provided() - end - - local get_versions = prepare_get_versions(deps_mode, rocks_provided, "dependencies") - - local matched = match_all_deps(dependencies, get_versions) - results[name] = version - for _, match in pairs(matched) do - deps.scan_deps(results, mdeps, match.name, match.version, deps_mode) - end -end - -local function lua_h_exists(d, luaver) - local major, minor = luaver:match("(%d+)%.(%d+)") - local luanum = ("%s%02d"):format(major, tonumber(minor)) - - local lua_h = dir.path(d, "lua.h") - local fd = io.open(lua_h) - if fd then - local data = fd:read("*a") - fd:close() - if data:match("LUA_VERSION_NUM%s*" .. tostring(luanum)) then - return d - end - return nil, "Lua header lua.h found at " .. d .. " does not match Lua version " .. luaver .. ". You can use `luarocks config variables.LUA_INCDIR ` to set the correct location.", "dependency", 2 - end - - return nil, "Failed finding Lua header lua.h (searched at " .. d .. "). You may need to install Lua development headers. You can use `luarocks config variables.LUA_INCDIR ` to set the correct location.", "dependency", 1 -end - -local function find_lua_incdir(prefix, luaver, luajitver) - luajitver = luajitver and luajitver:gsub("%-.*", "") - local shortv = luaver:gsub("%.", "") - local incdirs = { - prefix .. "/include/lua/" .. luaver, - prefix .. "/include/lua" .. luaver, - prefix .. "/include/lua-" .. luaver, - prefix .. "/include/lua" .. shortv, - prefix .. "/include", - prefix, - luajitver and (prefix .. "/include/luajit-" .. (luajitver:match("^(%d+%.%d+)") or "")), - } - local errprio = 0 - local mainerr - for _, d in ipairs(incdirs) do - local ok, err, _, prio = lua_h_exists(d, luaver) - if ok then - return d - end - if prio > errprio then - mainerr = err - errprio = prio - end - end - - -- not found, will fallback to a default - return nil, mainerr -end - -function deps.check_lua_incdir(vars) - if vars.LUA_INCDIR_OK == true - then return true - end - - local ljv = util.get_luajit_version() - - if vars.LUA_INCDIR then - local ok, err = lua_h_exists(vars.LUA_INCDIR, cfg.lua_version) - if ok then - vars.LUA_INCDIR_OK = true - end - return ok, err - end - - if vars.LUA_DIR then - local d, err = find_lua_incdir(vars.LUA_DIR, cfg.lua_version, ljv) - if d then - vars.LUA_INCDIR = d - vars.LUA_INCDIR_OK = true - return true - end - return nil, err - end - - return nil, "Failed finding Lua headers; neither LUA_DIR or LUA_INCDIR are set. You may need to install them or configure LUA_INCDIR.", "dependency" -end - -function deps.check_lua_libdir(vars) - if vars.LUA_LIBDIR_OK == true - then return true - end - - local fs = require("luarocks.fs") - local ljv = util.get_luajit_version() - - if vars.LUA_LIBDIR and vars.LUALIB and fs.exists(dir.path(vars.LUA_LIBDIR, vars.LUALIB)) then - vars.LUA_LIBDIR_OK = true - return true - end - - local shortv = cfg.lua_version:gsub("%.", "") - local libnames = { - "lua" .. cfg.lua_version, - "lua" .. shortv, - "lua-" .. cfg.lua_version, - "lua-" .. shortv, - "lua", - } - if ljv then - table.insert(libnames, 1, "luajit-" .. cfg.lua_version) - table.insert(libnames, 2, "luajit") - end - local cache = {} - local save_LUA_INCDIR = vars.LUA_INCDIR - local ok, _, _, errfiles = check_external_dependency("LUA", { library = libnames }, vars, "build", cache) - vars.LUA_INCDIR = save_LUA_INCDIR - local err - if ok then - local filename = dir.path(vars.LUA_LIBDIR, vars.LUA_LIBDIR_FILE) - local fd = io.open(filename, "r") - if fd then - if not vars.LUA_LIBDIR_FILE:match((cfg.lua_version:gsub("%.", "%%.?"))) then - -- if filename isn't versioned, check file contents - local txt = fd:read("*a") - ok = txt:match("Lua " .. cfg.lua_version, 1, true) - or txt:match("lua" .. (cfg.lua_version:gsub("%.", "")), 1, true) - if not ok then - err = "Lua library at " .. filename .. " does not match Lua version " .. cfg.lua_version .. ". You can use `luarocks config variables.LUA_LIBDIR ` to set the correct location." - end - end - - fd:close() - end - end - - if ok then - vars.LUALIB = vars.LUA_LIBDIR_FILE - vars.LUA_LIBDIR_OK = true - return true - else - err = err or "Failed finding the Lua library. You can use `luarocks config variables.LUA_LIBDIR ` to set the correct location." - return nil, err, "dependency", errfiles - end -end - -function deps.get_deps_mode(args) - return args.deps_mode or cfg.deps_mode -end - -return deps diff --git a/src/luarocks/deps.tl b/src/luarocks/deps.tl new file mode 100644 index 00000000..e26ba60f --- /dev/null +++ b/src/luarocks/deps.tl @@ -0,0 +1,877 @@ + +--- High-level dependency related functions. +local record deps + installer: function(Args): boolean, string, string +end + +local cfg = require("luarocks.core.cfg") +local manif = require("luarocks.manif") +local path = require("luarocks.path") +local dir = require("luarocks.dir") +local fun = require("luarocks.fun") +local util = require("luarocks.util") +local vers = require("luarocks.core.vers") +local queries = require("luarocks.queries") +local deplocks = require("luarocks.deplocks") + +local type Rockspec = require("luarocks.core.types.rockspec").Rockspec +local type Dependencies = require("luarocks.core.types.rockspec").Dependencies + +local type BuiltinBuild = require("luarocks.core.types.build").BuiltinBuild +local type Module = BuiltinBuild.Module + +local type Build = require("luarocks.core.types.build").Build + +local type Tree = require("luarocks.core.types.tree").Tree + +local type Query = require("luarocks.core.types.query").Query + +local type Version = require("luarocks.core.types.version").Version + +local type PersistableTable = require("luarocks.core.types.persist").PersistableTable + +local type Result = require("luarocks.core.types.result").Result + +local type Dir = require("luarocks.core.types.dir").Dir +local type Dirs = require("luarocks.core.types.dir").Dirs + +local type Args = require("luarocks.core.types.args").Args + +--- Generate a function that matches dep queries against the manifest, +-- taking into account rocks_provided, the list of versions to skip, +-- and the lockfile. +-- @param deps_mode "one", "none", "all" or "order" +-- @param rocks_provided a one-level table mapping names to versions, +-- listing rocks to consider provided by the VM +-- @param rocks_provided table: A table of auto-provided dependencies. +-- by this Lua implementation for the given dependency. +-- @param depskey key to use when matching the lockfile ("dependencies", +-- "build_dependencies", etc.) +-- @param skip_set a two-level table mapping names to versions to +-- boolean, listing rocks that should not be matched +-- @return function(dep): {string}, {string:string}, string, boolean +-- * array of matching versions +-- * map of versions to locations +-- * version matched via lockfile if any +-- * true if rock matched via rocks_provided +local function prepare_get_versions(deps_mode: string, rocks_provided: {string : string}, depskey: string, skip_set?: {string: {string: boolean}}): function(Query): {string}, {string: string | Tree}, string | number | boolean | PersistableTable, boolean + + return function(dep: Query): {string}, {string: string | Tree}, string | number | boolean | PersistableTable, boolean + local versions, locations: {string}, {string: string | Tree} + local provided = rocks_provided[dep.name] + if provided then + -- Provided rocks have higher priority than manifest's rocks. + versions, locations = { provided }, {} + else + if deps_mode == "none" then + deps_mode = "one" + end + versions, locations = manif.get_versions(dep, deps_mode) + end + + if skip_set and skip_set[dep.name] then + for i = #versions, 1, -1 do + local v = versions[i] + if skip_set[dep.name][v] then + table.remove(versions, i) + end + end + end + + local lockversion = deplocks.get(depskey, dep.name) --! cast? + + return versions, locations, lockversion, provided ~= nil + end +end + +--- Attempt to match a dependency to an installed rock. +-- @param get_versions a getter function obtained via prepare_get_versions +-- @return (string, string, table) or (nil, nil, table): +-- 1. latest installed version of the rock matching the dependency +-- 2. location where the installed version is installed +-- 3. the 'dep' query table +-- 4. true if provided via VM +-- or +-- 1. nil +-- 2. nil +-- 3. either 'dep' or an alternative query to be used +-- 4. false +local function match_dep(depq: Query, + get_versions: function(Query): {string}, {string: string | Tree}, string, boolean): string, string | Tree, Query, boolean + + local versions, locations, lockversion, provided = get_versions(depq) + + local latest_version: Version + local latest_vstring: string + for _, vstring in ipairs(versions) do + local version = vers.parse_version(vstring) + if vers.match_constraints(version, depq.constraints) then + if not latest_version or version > latest_version then + latest_version = version + latest_vstring = vstring + end + end + end + + if lockversion and not locations[lockversion] then + local latest_matching_msg = "" + if latest_vstring and latest_vstring ~= lockversion then + latest_matching_msg = " (latest matching is " .. latest_vstring .. ")" + end + util.printout("Forcing " .. depq.name .. " to pinned version " .. lockversion .. latest_matching_msg) + return nil, nil, queries.new(depq.name, depq.namespace, lockversion) + end + + return latest_vstring, locations[latest_vstring], depq, provided +end + +local function match_all_deps(dependencies: {Query}, + get_versions: function(Query): {string}, {string: string | Tree}, string, boolean): {Query: Result}, {string: Query}, {string: Query} + + local matched, missing, no_upgrade = {}, {}, {} + + for _, depq in ipairs(dependencies) do + local found, _, provided: string, string | Tree, boolean + found, _, depq, provided = match_dep(depq, get_versions) + if found then + if not provided then + matched[depq] = {name = depq.name, version = found} as Result + end + else + if depq.constraints and depq.constraints[1] and depq.constraints[1].no_upgrade then + no_upgrade[depq.name] = depq + else + missing[depq.name] = depq + end + end + end + return matched, missing, no_upgrade +end + +--- Attempt to match dependencies of a rockspec to installed rocks. +-- @param dependencies table: The table of dependencies. +-- @param rocks_provided table: The table of auto-provided dependencies. +-- @param skip_set table or nil: Program versions to not use as valid matches. +-- Table where keys are program names and values are tables where keys +-- are program versions and values are 'true'. +-- @param deps_mode string: Which trees to check dependencies for +-- @return table, table, table: A table where keys are dependencies parsed +-- in table format and values are tables containing fields 'name' and +-- version' representing matches; a table of missing dependencies +-- parsed as tables; and a table of "no-upgrade" missing dependencies +-- (to be used in plugin modules so that a plugin does not force upgrade of +-- its parent application). +function deps.match_deps(dependencies: {Query}, rocks_provided: {string: string}, deps_mode: string, skip_set?: {string: {string: boolean}}): {Query : Result}, {string : Query}, {string : Query} + + local get_versions = prepare_get_versions(deps_mode, rocks_provided, "dependencies", skip_set) + return match_all_deps(dependencies, get_versions) +end + +local function rock_status(dep: Query, get_versions: function(Query): {string}, {string : string | Tree}, string, boolean): string, string + local installed, _, _, provided = match_dep(dep, get_versions) + local installation_type = provided and "provided by VM" or "installed" + return installed and installed.." "..installation_type..": success" or "not installed" +end + +--- Check depenendencies of a package and report any missing ones. +-- @param name string: package name. +-- @param version string: package version. +-- @param dependencies table: array of dependencies. +-- @param deps_mode string: Which trees to check dependencies for +-- @param rocks_provided table: A table of auto-dependencies provided +-- by this Lua implementation for the given dependency. +-- "one" for the current default tree, "all" for all trees, +-- "order" for all trees with priority >= the current default, "none" for no trees. +function deps.report_missing_dependencies(name: string, version: string, dependencies: {Query}, deps_mode: string, rocks_provided: {string: string}) + + if deps_mode == "none" then + return + end + + local get_versions = prepare_get_versions(deps_mode, rocks_provided, "dependencies") + + local first_missing_dep = true + + for _, depq in ipairs(dependencies) do + local found, _: string, string | Tree + found, _, depq = match_dep(depq, get_versions) + if not found then + if first_missing_dep then + util.printout(("Missing dependencies for %s %s:"):format(name, version)) + first_missing_dep = false + end + + util.printout((" %s (%s)"):format(tostring(depq), rock_status(depq, get_versions))) + end + end +end + +function deps.fulfill_dependency(dep: Query, deps_mode: string, rocks_provided: {string: string}, verify: boolean, depskey?: string): boolean, string, string | Tree + + deps_mode = deps_mode or "all" + rocks_provided = rocks_provided or {} + + local get_versions = prepare_get_versions(deps_mode, rocks_provided, depskey) + + local found, where: string, string | Tree + found, where, dep = match_dep(dep, get_versions) + if found then + local tree_manifests = manif.load_rocks_tree_manifests(deps_mode) + manif.scan_dependencies(dep.name, found, tree_manifests, deplocks.proxy(depskey)) + return true, found, where + end + + local search = require("luarocks.search") + + local url, search_err = search.find_suitable_rock(dep) + if not url then + return nil, "Could not satisfy dependency "..tostring(dep)..": "..search_err + end + util.printout("Installing "..url) + local install_args = { + rock = url, + deps_mode = deps_mode, + namespace = dep.namespace, + verify = verify, + } + local ok, install_err, errcode = deps.installer(install_args) + if not ok then + return nil, "Failed installing dependency: "..url.." - "..install_err, errcode + end + + found, where = match_dep(dep, get_versions) + if not found then + return nil, "Repository inconsistency detected (previously unfinished/corrupted installation?)" + end + return true, found, where +end + +local function check_supported_platforms(rockspec: Rockspec): boolean, string + if rockspec.supported_platforms and next(rockspec.supported_platforms) then + local all_negative = true + local supported = false + for _, plat in ipairs(rockspec.supported_platforms) do + local neg: string + neg, plat = plat:match("^(!?)(.*)") + if neg == "!" then + if cfg.is_platform(plat) then + return nil, "This rockspec for "..rockspec.package.." does not support "..plat.." platforms." + end + else + all_negative = false + if cfg.is_platform(plat) then + supported = true + break + end + end + end + if supported == false and not all_negative then + local plats = cfg.print_platforms() + return nil, "This rockspec for "..rockspec.package.." does not support "..plats.." platforms." + end + end + + return true +end + +--- Check dependencies of a rock and attempt to install any missing ones. +-- Packages are installed using the LuaRocks "install" command. +-- Aborts the program if a dependency could not be fulfilled. +-- @param rockspec table: A rockspec in table format. +-- @param depskey string: Rockspec key to fetch to get dependency table +-- ("dependencies", "build_dependencies", etc.). +-- @param deps_mode string +-- @param verify boolean +-- @param deplock_dir string: dirname of the deplock file +-- @return boolean or (nil, string, [string]): True if no errors occurred, or +-- nil and an error message if any test failed, followed by an optional +-- error code. +function deps.fulfill_dependencies(rockspec: Rockspec, depskey: string, deps_mode: string, verify?: boolean, deplock_dir?: string): boolean, string, string + local name = rockspec.name + local version = rockspec.version + local rocks_provided = rockspec.rocks_provided + + local ok, filename, err = deplocks.load(name, deplock_dir or ".") + if filename then + util.printout("Using dependencies pinned in lockfile: " .. filename) + + local get_versions = prepare_get_versions("none", rocks_provided, depskey) + local dnsnamestr, dversionstr: string, string + for dnsname, dversion in deplocks.each(depskey) do + if dnsname is string then + dnsnamestr = dnsname + end + if dversion is string then + dversionstr = dversion + end + local dname, dnamespace = util.split_namespace(dnsnamestr) + local depq = queries.new(dname, dnamespace, dversionstr) + + util.printout(("%s %s is pinned to %s (%s)"):format( + name, version, tostring(depq), rock_status(depq, get_versions))) + + local okfullfill, errfullfill = deps.fulfill_dependency(depq, "none", rocks_provided, verify, depskey) + if not okfullfill then + return nil, errfullfill + end + end + util.printout() + return true + elseif err then + util.warning(err) + end + + ok, err = check_supported_platforms(rockspec) + if not ok then + return nil, err + end + + deps.report_missing_dependencies(name, version, (rockspec as {string: Dependencies})[depskey].queries, deps_mode, rocks_provided) + + util.printout() + + local get_versions = prepare_get_versions(deps_mode, rocks_provided, depskey) + for _, depq in ipairs((rockspec as {string: Dependencies})[depskey].queries) do + + util.printout(("%s %s depends on %s (%s)"):format( + name, version, tostring(depq), rock_status(depq, get_versions))) + + local okfulfill, found_or_err, _ = deps.fulfill_dependency(depq, deps_mode, rocks_provided, verify, depskey) + if okfulfill then + deplocks.add(depskey, depq.name, found_or_err) + else + -- if no_upgrade then + -- util.printerr("This version of "..name.." is designed for use with") + -- util.printerr(tostring(dep)..", but is configured to avoid upgrading it") + -- util.printerr("automatically. Please upgrade "..dep.name.." with") + -- util.printerr(" luarocks install "..dep.name) + -- util.printerr("or look for a suitable version of "..name.." with") + -- util.printerr(" luarocks search "..name) + -- end + return nil, found_or_err + end + end + + return true +end + +--- If filename matches a pattern, return the capture. +-- For example, given "libfoo.so" and "lib?.so" is a pattern, +-- returns "foo" (which can then be used to build names +-- based on other patterns. +-- @param file string: a filename +-- @param pattern string: a pattern, where ? is to be matched by the filename. +-- @return string The pattern, if found, or nil. +local function deconstruct_pattern(file: string, pattern: string): string + local depattern = "^"..(pattern:gsub("%.", "%%."):gsub("%*", ".*"):gsub("?", "(.*)")).."$" + return (file:match(depattern)) +end + +--- Construct all possible patterns for a name and add to the files array. +-- Run through the patterns array replacing all occurrences of "?" +-- with the given file name and store them in the files array. +-- @param file string A raw name (e.g. "foo") +-- @param array of string An array of patterns with "?" as the wildcard +-- (e.g. {"?.so", "lib?.so"}) +-- @param files The array of constructed names +local function add_all_patterns(file: string, patterns: {string}, files: {{integer, string}}): Dirs + for _, pattern in ipairs(patterns) do + table.insert(files, {#files + 1, (pattern:gsub("?", file))}) + end +end + +local function get_external_deps_dirs(mode: string): Dirs + local patterns = cfg.external_deps_patterns + local subdirs = cfg.external_deps_subdirs + if mode == "install" then + patterns = cfg.runtime_external_deps_patterns + subdirs = cfg.runtime_external_deps_subdirs + end + local dirs = { + BINDIR = { subdir = subdirs.bin, testfile = "program", pattern = patterns.bin }, + INCDIR = { subdir = subdirs.include, testfile = "header", pattern = patterns.include }, + LIBDIR = { subdir = subdirs.lib, testfile = "library", pattern = patterns.lib } + } + if mode == "install" then + dirs.INCDIR = nil + end + return dirs as Dirs +end + +local function resolve_prefix(prefix: string | {string: string}, dirs: Dirs): string + if prefix is string then + return prefix + elseif prefix is {string: string} then + if prefix.bin then + dirs.BINDIR.subdir = prefix.bin + end + if prefix.include then + if dirs.INCDIR then + dirs.INCDIR.subdir = prefix.include + end + end + if prefix.lib then + dirs.LIBDIR.subdir = prefix.lib + end + return prefix.prefix + end +end + +local function add_patterns_for_file(files: {{integer, string}}, file: string, patterns: {string}) + -- If it doesn't look like it contains a filename extension + if not (file:match("%.[a-z]+$") or file:match("%.[a-z]+%.")) then + add_all_patterns(file, patterns, files) + else + for _, pattern in ipairs(patterns) do + local matched = deconstruct_pattern(file, pattern) + if matched then + add_all_patterns(matched, patterns, files) + end + end + table.insert(files, {#files + 1, file}) + end +end + +local function check_external_dependency_at( + prefix: string, + name: string, + ext_files: {string: string | {string}}, + vars: {string: string}, + dirs: Dirs, + err_files: {string: {string}}, + cache: {string: {string}}): boolean, string, string + + local fs = require("luarocks.fs") + cache = cache or {} + + for dirname, dirdata in util.sortedpairs(dirs) do + local paths: {string} + local path_var_value = vars[name.."_"..dirname] + local dirdatastr = dirdata.subdir + if path_var_value then + paths = { path_var_value } + elseif dirdatastr is {string} then + paths = {} + for i,v in ipairs(dirdatastr) do + paths[i] = dir.path(prefix, v) + end + else + paths = { dir.path(prefix, dirdatastr) } + end + local file_or_files = ext_files[dirdata.testfile] + if file_or_files then + local files = {} + if file_or_files is string then + add_patterns_for_file(files, file_or_files, dirdata.pattern) + elseif file_or_files is {string} then + for _, f in ipairs(file_or_files) do + add_patterns_for_file(files, f, dirdata.pattern) + end + end + + local found = false + table.sort(files, function(a: {integer, string}, b: {integer, string}): boolean + if (not a[2]:match("%*")) and b[2]:match("%*") then + return true + elseif a[2]:match("%*") and (not b[2]:match("%*")) then + return false + else + return a[1] < b[1] + end + end) + for _, fa in ipairs(files) do + + local f = fa[2] + -- small convenience hack + if f:match("%.so$") or f:match("%.dylib$") or f:match("%.dll$") then + f = f:gsub("%.[^.]+$", "."..cfg.external_lib_extension) + end + + local pattern: string + if f:match("%*") then + pattern = "^" .. f:gsub("([-.+])", "%%%1"):gsub("%*", ".*") .. "$" + f = "matching "..f + end + + for _, d in ipairs(paths) do + if pattern then + if not cache[d] then + cache[d] = fs.list_dir(d) + end + local match = string.match + for _, entry in ipairs(cache[d]) do + if match(entry, pattern) then + found = true + break + end + end + else + found = fs.is_file(dir.path(d, f)) + end + if found then + dirdata.dir = d + dirdata.file = f + break + else + table.insert(err_files[dirdata.testfile], f.." in "..d) + end + end + if found then + break + end + end + if not found then + return nil, dirname, dirdata.testfile + end + else + -- When we have a set of subdir suffixes, look for one that exists. + -- For these reason, we now put "lib" ahead of "" on Windows in our + -- default set. + dirdata.dir = paths[1] + for _, p in ipairs(paths) do + if fs.exists(p) then + dirdata.dir = p + break + end + end + end + end + + for dirname, dirdata in pairs(dirs as {string: Dir}) do + vars[name.."_"..dirname] = dirdata.dir + vars[name.."_"..dirname.."_FILE"] = dirdata.file + end + vars[name.."_DIR"] = prefix + return true +end + +local function check_external_dependency( + name: string, + ext_files: {string: string | {string}}, + vars: {string: string}, + mode: string, + cache?: {string : {string}}): boolean, string, string, {string : {string}} + local ok: boolean + local err_dirname: string + local err_testfile: string + local err_files: {string: {string}} = {program = {}, header = {}, library = {}} + + local dirs = get_external_deps_dirs(mode) + + local prefixes: {string} + if vars[name .. "_DIR"] then + prefixes = { vars[name .. "_DIR"] } + elseif vars.DEPS_DIR then + prefixes = { vars.DEPS_DIR } + else + prefixes = cfg.external_deps_dirs + end + + for _, prefix in ipairs(prefixes) do + prefix = resolve_prefix(prefix, dirs) + if cfg.is_platform("mingw32") and name == "LUA" then + dirs.LIBDIR.pattern = fun.filter(util.deep_copy(dirs.LIBDIR.pattern) as {string}, function(s: string): boolean + return not s:match("%.a$") + end) + elseif cfg.is_platform("windows") and name == "LUA" then + dirs.LIBDIR.pattern = fun.filter(util.deep_copy(dirs.LIBDIR.pattern) as {string}, function(s: string): boolean + return not s:match("%.dll$") + end) + end + ok, err_dirname, err_testfile = check_external_dependency_at(prefix, name, ext_files, vars, dirs, err_files, cache) + if ok then + return true + end + end + + return nil, err_dirname, err_testfile, err_files +end + +function deps.autodetect_external_dependencies(build: Build): {string : {string : string}} + -- only applies to the 'builtin' build type + if not build or not (build as BuiltinBuild).modules then + return nil + end + + local extdeps: {string: {string: string}} = {} + local any = false + for _, data in pairs((build as BuiltinBuild).modules) do + if data is Module and data.libraries then + local libraries: {string} + local librariesstr: string | {string} = data.libraries + if librariesstr is string then + libraries = { librariesstr } + else + libraries = librariesstr + end + local incdirs = {} + local libdirs = {} + for _, lib in ipairs(libraries) do + local upper = lib:upper():gsub("%+", "P"):gsub("[^%w]", "_") + any = true + extdeps[upper] = { library = lib } + table.insert(incdirs, "$(" .. upper .. "_INCDIR)") + table.insert(libdirs, "$(" .. upper .. "_LIBDIR)") + end + if not data.incdirs then + data.incdirs = incdirs + end + if not data.libdirs then + data.libdirs = libdirs + end + end + end + return any and extdeps or nil +end + +--- Set up path-related variables for external dependencies. +-- For each key in the external_dependencies table in the +-- rockspec file, four variables are created: _DIR, _BINDIR, +-- _INCDIR and _LIBDIR. These are not overwritten +-- if already set (e.g. by the LuaRocks config file or through the +-- command-line). Values in the external_dependencies table +-- are tables that may contain a "header" or a "library" field, +-- with filenames to be tested for existence. +-- @param rockspec table: The rockspec table. +-- @param mode string: if "build" is given, checks all files; +-- if "install" is given, do not scan for headers. +-- @return boolean or (nil, string): True if no errors occurred, or +-- nil and an error message if any test failed. +function deps.check_external_deps(rockspec: Rockspec, mode: string): boolean, string, string + + if not rockspec.external_dependencies then + rockspec.external_dependencies = deps.autodetect_external_dependencies(rockspec.build) as {string: {string: string}} + end + if not rockspec.external_dependencies then + return true + end + + for name, ext_files in util.sortedpairs(rockspec.external_dependencies) do + local ok, err_dirname, err_testfile, err_files = check_external_dependency(name, ext_files as {string : string | {string}}, rockspec.variables, mode) + if not ok then + local lines = {"Could not find "..err_testfile.." file for "..name} + + local err_paths = {} + for _, err_file in ipairs(err_files[err_testfile]) do + if not err_paths[err_file] then + err_paths[err_file] = true + table.insert(lines, " No file "..err_file) + end + end + + table.insert(lines, "You may have to install "..name.." in your system and/or pass "..name.."_DIR or "..name.."_"..err_dirname.." to the luarocks command.") + table.insert(lines, "Example: luarocks install "..rockspec.name.." "..name.."_DIR=/usr/local") + + return nil, table.concat(lines, "\n"), "dependency" + end + end + return true +end + +--- Recursively add satisfied dependencies of a package to a table, +-- to build a transitive closure of all dependent packages. +-- Additionally ensures that `dependencies` table of the manifest is up-to-date. +-- @param results table: The results table being built, maps package names to versions. +-- @param mdeps table: The manifest dependencies table. +-- @param name string: Package name. +-- @param version string: Package version. +function deps.scan_deps(results: {string: string}, mdeps: {string: {string: {Query}}}, name: string, version: string, deps_mode: string) + assert(not name:match("/")) + + local fetch = require("luarocks.fetch") + + if results[name] then + return + end + if not mdeps[name] then mdeps[name] = {} end + local mdn = mdeps[name] + local dependencies = mdn[version] + local rocks_provided: {string : string} + if not dependencies then + local rockspec = fetch.load_local_rockspec(path.rockspec_file(name, version), false) + if not rockspec then + return + end + dependencies = rockspec.dependencies.queries + rocks_provided = rockspec.rocks_provided + mdn[version] = dependencies + else + rocks_provided = util.get_rocks_provided() + end + + local get_versions = prepare_get_versions(deps_mode, rocks_provided, "dependencies") + + local matched = match_all_deps(dependencies, get_versions) + results[name] = version + for _, match in pairs(matched) do + deps.scan_deps(results, mdeps, match.name, match.version, deps_mode) + end +end + +local function lua_h_exists(d: string, luaver: string): boolean, string, string, integer + local major, minor = luaver:match("(%d+)%.(%d+)") + local luanum = ("%s%02d"):format(major, tonumber(minor)) + + local lua_h = dir.path(d, "lua.h") + local fd = io.open(lua_h) + if fd then + local data = fd:read("*a") + fd:close() + if data:match("LUA_VERSION_NUM%s*" .. tostring(luanum)) then + return d ~= nil + end + return nil, "Lua header lua.h found at " .. d .. " does not match Lua version " .. luaver .. ". You can use `luarocks config variables.LUA_INCDIR ` to set the correct location.", "dependency", 2 + end + + return nil, "Failed finding Lua header lua.h (searched at " .. d .. "). You may need to install Lua development headers. You can use `luarocks config variables.LUA_INCDIR ` to set the correct location.", "dependency", 1 +end + +local function find_lua_incdir(prefix: string, luaver: string, luajitver: string): string, string + luajitver = luajitver and luajitver:gsub("%-.*", "") + local shortv = luaver:gsub("%.", "") + local incdirs = { + prefix .. "/include/lua/" .. luaver, + prefix .. "/include/lua" .. luaver, + prefix .. "/include/lua-" .. luaver, + prefix .. "/include/lua" .. shortv, + prefix .. "/include", + prefix, + luajitver and (prefix .. "/include/luajit-" .. (luajitver:match("^(%d+%.%d+)") or "")), + } + local errprio = 0 + local mainerr: string + for _, d in ipairs(incdirs) do + local ok, err, _, prio = lua_h_exists(d, luaver) + if ok then + return d + end + if prio > errprio then + mainerr = err + errprio = prio + end + end + + -- not found, will fallback to a default + return nil, mainerr +end + +function deps.check_lua_incdir(vars: {string: string}): boolean, string, string + if vars.LUA_INCDIR_OK == "ok" + then return true + end + + local ljv = util.get_luajit_version() + + if vars.LUA_INCDIR then + local ok, err = lua_h_exists(vars.LUA_INCDIR, cfg.lua_version) + if ok then + vars.LUA_INCDIR_OK = "ok" + end + return ok, err + end + + if vars.LUA_DIR then + local d, err = find_lua_incdir(vars.LUA_DIR, cfg.lua_version, ljv) + if d then + vars.LUA_INCDIR = d + vars.LUA_INCDIR_OK = "ok" + return true + end + return nil, err + end + + return nil, "Failed finding Lua headers; neither LUA_DIR or LUA_INCDIR are set. You may need to install them or configure LUA_INCDIR.", "dependency" +end + +function deps.check_lua_libdir(vars: {string: string}): boolean, string, string, {string : {string}} + if vars.LUA_LIBDIR_OK == "ok" + then return true + end + + local fs = require("luarocks.fs") + local ljv = util.get_luajit_version() + + if vars.LUA_LIBDIR and vars.LUALIB and fs.exists(dir.path(vars.LUA_LIBDIR, vars.LUALIB)) then + vars.LUA_LIBDIR_OK = "ok" + return true + end + + local shortv = cfg.lua_version:gsub("%.", "") + local libnames = { + "lua" .. cfg.lua_version, + "lua" .. shortv, + "lua-" .. cfg.lua_version, + "lua-" .. shortv, + "lua", + } + if ljv then + table.insert(libnames, 1, "luajit-" .. cfg.lua_version) + table.insert(libnames, 2, "luajit") + end + local cache = {} + local save_LUA_INCDIR = vars.LUA_INCDIR + local ok, _, _, errfiles = check_external_dependency("LUA", { library = libnames }, vars, "build", cache) + vars.LUA_INCDIR = save_LUA_INCDIR + local err: string + if ok then + local filename = dir.path(vars.LUA_LIBDIR, vars.LUA_LIBDIR_FILE) + local fd = io.open(filename, "r") + if fd then + if not vars.LUA_LIBDIR_FILE:match((cfg.lua_version:gsub("%.", "%%.?"))) then + -- if filename isn't versioned, check file contents + local txt = fd:read("*a") + ok = txt:find("Lua " .. cfg.lua_version, 1, true) + or txt:find("lua" .. (cfg.lua_version:gsub("%.", "")), 1, true) + and true + if not ok then + err = "Lua library at " .. filename .. " does not match Lua version " .. cfg.lua_version .. ". You can use `luarocks config variables.LUA_LIBDIR ` to set the correct location." + end + end + + fd:close() + end + end + + if ok then + vars.LUALIB = vars.LUA_LIBDIR_FILE + vars.LUA_LIBDIR_OK = "ok" + return true + else + err = err or "Failed finding the Lua library. You can use `luarocks config variables.LUA_LIBDIR ` to set the correct location." + return nil, err, "dependency", errfiles + end +end + +function deps.get_deps_mode(args: Args): string + return args.deps_mode or cfg.deps_mode +end + +--- Report missing dependencies for all rocks installed in a repository. +-- @param repo string or nil: Pathname of a local repository. If not given, +-- the default local repository is used. +-- @param deps_mode string: Dependency mode: "one" for the current default tree, +-- "all" for all trees, "order" for all trees with priority >= the current default, +-- "none" for using the default dependency mode from the configuration. +function deps.check_dependencies(repo: string, deps_mode: string) + local rocks_dir = path.rocks_dir(repo or cfg.root_dir) + if deps_mode == "none" then deps_mode = cfg.deps_mode end + + local manifest = manif.load_manifest(rocks_dir) + if not manifest then + return + end + + for name, versions in util.sortedpairs(manifest.repository) do + for version, version_entries in util.sortedpairs(versions, vers.compare_versions) do + for _, entry in ipairs(version_entries) do + if entry.arch == "installed" then + if manifest.dependencies[name] and manifest.dependencies[name][version] then + deps.report_missing_dependencies(name, version, manifest.dependencies[name][version], deps_mode, util.get_rocks_provided()) + end + end + end + end + end +end + +return deps -- cgit v1.2.3-55-g6feb