local writer = {} local cfg = require("luarocks.core.cfg") local search = require("luarocks.search") local repos = require("luarocks.repos") local deps = require("luarocks.deps") local vers = require("luarocks.core.vers") local fs = require("luarocks.fs") local util = require("luarocks.util") local dir = require("luarocks.dir") local fetch = require("luarocks.fetch") local path = require("luarocks.path") local persist = require("luarocks.persist") local manif = require("luarocks.manif") local queries = require("luarocks.queries") --- Update storage table to account for items provided by a package. -- @param storage table: a table storing items in the following format: -- keys are item names and values are arrays of packages providing each item, -- where a package is specified as string `name/version`. -- @param items table: a table mapping item names to paths. -- @param name string: package name. -- @param version string: package version. local function store_package_items(storage, name, version, items) assert(type(storage) == "table") assert(type(items) == "table") assert(type(name) == "string" and not name:match("/")) assert(type(version) == "string") local package_identifier = name.."/"..version for item_name, path in pairs(items) do -- luacheck: ignore 431 if not storage[item_name] then storage[item_name] = {} end table.insert(storage[item_name], package_identifier) end end --- Update storage table removing items provided by a package. -- @param storage table: a table storing items in the following format: -- keys are item names and values are arrays of packages providing each item, -- where a package is specified as string `name/version`. -- @param items table: a table mapping item names to paths. -- @param name string: package name. -- @param version string: package version. local function remove_package_items(storage, name, version, items) assert(type(storage) == "table") assert(type(items) == "table") assert(type(name) == "string" and not name:match("/")) assert(type(version) == "string") local package_identifier = name.."/"..version for item_name, path in pairs(items) do -- luacheck: ignore 431 local key = item_name local all_identifiers = storage[key] if not all_identifiers then key = key .. ".init" all_identifiers = storage[key] end if all_identifiers then for i, identifier in ipairs(all_identifiers) do if identifier == package_identifier then table.remove(all_identifiers, i) break end end if #all_identifiers == 0 then storage[key] = nil end else util.warning("Cannot find entry for " .. item_name .. " in manifest -- corrupted manifest?") end end end --- Process the dependencies of a manifest table to determine its dependency -- chains for loading modules. The manifest dependencies information is filled -- and any dependency inconsistencies or missing dependencies are reported to -- standard error. -- @param manifest table: a manifest table. -- @param deps_mode string: Dependency mode: "one" for the current default tree, -- "all" for all trees, "order" for all trees with priority >= the current default, -- "none" for no trees. local function update_dependencies(manifest, deps_mode) assert(type(manifest) == "table") assert(type(deps_mode) == "string") if not manifest.dependencies then manifest.dependencies = {} end local mdeps = manifest.dependencies for pkg, versions in pairs(manifest.repository) do for version, repositories in pairs(versions) do for _, repo in ipairs(repositories) do if repo.arch == "installed" then local rd = {} repo.dependencies = rd deps.scan_deps(rd, mdeps, pkg, version, deps_mode) rd[pkg] = nil end end end end end --- Sort function for ordering rock identifiers in a manifest's -- modules table. Rocks are ordered alphabetically by name, and then -- by version which greater first. -- @param a string: Version to compare. -- @param b string: Version to compare. -- @return boolean: The comparison result, according to the -- rule outlined above. local function sort_pkgs(a, b) assert(type(a) == "string") assert(type(b) == "string") local na, va = a:match("(.*)/(.*)$") local nb, vb = b:match("(.*)/(.*)$") return (na == nb) and vers.compare_versions(va, vb) or na < nb end --- Sort items of a package matching table by version number (higher versions first). -- @param tbl table: the package matching table: keys should be strings -- and values arrays of strings with packages names in "name/version" format. local function sort_package_matching_table(tbl) assert(type(tbl) == "table") if next(tbl) then for item, pkgs in pairs(tbl) do if #pkgs > 1 then table.sort(pkgs, sort_pkgs) -- Remove duplicates from the sorted array. local prev = nil local i = 1 while pkgs[i] do local curr = pkgs[i] if curr == prev then table.remove(pkgs, i) else prev = curr i = i + 1 end end end end end end --- Filter manifest table by Lua version, removing rockspecs whose Lua version -- does not match. -- @param manifest table: a manifest table. -- @param lua_version string or nil: filter by Lua version -- @param repodir string: directory of repository being scanned -- @param cache table: temporary rockspec cache table local function filter_by_lua_version(manifest, lua_version, repodir, cache) assert(type(manifest) == "table") assert(type(repodir) == "string") assert((not cache) or type(cache) == "table") cache = cache or {} lua_version = vers.parse_version(lua_version) for pkg, versions in pairs(manifest.repository) do local to_remove = {} for version, repositories in pairs(versions) do for _, repo in ipairs(repositories) do if repo.arch == "rockspec" then local pathname = dir.path(repodir, pkg.."-"..version..".rockspec") local rockspec, err = cache[pathname] if not rockspec then rockspec, err = fetch.load_local_rockspec(pathname, true) end if rockspec then cache[pathname] = rockspec for _, dep in ipairs(rockspec.dependencies) do if dep.name == "lua" then if not vers.match_constraints(lua_version, dep.constraints) then table.insert(to_remove, version) end break end end else util.printerr("Error loading rockspec for "..pkg.." "..version..": "..err) end end end end if next(to_remove) then for _, incompat in ipairs(to_remove) do versions[incompat] = nil end if not next(versions) then manifest.repository[pkg] = nil end end end end --- Store search results in a manifest table. -- @param results table: The search results as returned by search.disk_search. -- @param manifest table: A manifest table (must contain repository, modules, commands tables). -- It will be altered to include the search results. -- @return boolean or (nil, string): true in case of success, or nil followed by an error message. local function store_results(results, manifest) assert(type(results) == "table") assert(type(manifest) == "table") for name, versions in pairs(results) do local pkgtable = manifest.repository[name] or {} for version, entries in pairs(versions) do local versiontable = {} for _, entry in ipairs(entries) do local entrytable = {} entrytable.arch = entry.arch if entry.arch == "installed" then local rock_manifest, err = manif.load_rock_manifest(name, version) if not rock_manifest then return nil, err end entrytable.modules = repos.package_modules(name, version) store_package_items(manifest.modules, name, version, entrytable.modules) entrytable.commands = repos.package_commands(name, version) store_package_items(manifest.commands, name, version, entrytable.commands) end table.insert(versiontable, entrytable) end pkgtable[version] = versiontable end manifest.repository[name] = pkgtable end sort_package_matching_table(manifest.modules) sort_package_matching_table(manifest.commands) return true end --- Commit a table to disk in given local path. -- @param where string: The directory where the table should be saved. -- @param name string: The filename. -- @param tbl table: The table to be saved. -- @return boolean or (nil, string): true if successful, or nil and a -- message in case of errors. local function save_table(where, name, tbl) assert(type(where) == "string") assert(type(name) == "string" and not name:match("/")) assert(type(tbl) == "table") local filename = dir.path(where, name) local ok, err = persist.save_from_table(filename..".tmp", tbl) if ok then ok, err = fs.replace_file(filename, filename..".tmp") end return ok, err end function writer.make_rock_manifest(name, version) local install_dir = path.install_dir(name, version) local tree = {} for _, file in ipairs(fs.find(install_dir)) do local full_path = dir.path(install_dir, file) local walk = tree local last local last_name for filename in file:gmatch("[^\\/]+") do local next = walk[filename] if not next then next = {} walk[filename] = next end last = walk last_name = filename walk = next end if fs.is_file(full_path) then local sum, err = fs.get_md5(full_path) if not sum then return nil, "Failed producing checksum: "..tostring(err) end last[last_name] = sum end end local rock_manifest = { rock_manifest=tree } manif.rock_manifest_cache[name.."/"..version] = rock_manifest save_table(install_dir, "rock_manifest", rock_manifest ) return true end -- Writes a 'rock_namespace' file in a locally installed rock directory. -- @param name string: the rock name, without a namespace -- @param version string: the rock version -- @param namespace string?: the namespace -- @return true if successful (or unnecessary, if there is no namespace), -- or nil and an error message. function writer.make_namespace_file(name, version, namespace) assert(type(name) == "string" and not name:match("/")) assert(type(version) == "string") assert(type(namespace) == "string" or not namespace) if not namespace then return true end local fd, err = io.open(path.rock_namespace_file(name, version), "w") if not fd then return nil, err end local ok, err = fd:write(namespace) if not ok then return nil, err end fd:close() return true end --- Scan a LuaRocks repository and output a manifest file. -- A file called 'manifest' will be written in the root of the given -- repository directory. -- @param repo A local repository directory. -- @param deps_mode string: Dependency mode: "one" for the current default tree, -- "all" for all trees, "order" for all trees with priority >= the current default, -- "none" for the default dependency mode from the configuration. -- @param remote boolean: 'true' if making a manifest for a rocks server. -- @return boolean or (nil, string): True if manifest was generated, -- or nil and an error message. function writer.make_manifest(repo, deps_mode, remote) assert(type(repo) == "string") assert(type(deps_mode) == "string") if deps_mode == "none" then deps_mode = cfg.deps_mode end if not fs.is_dir(repo) then return nil, "Cannot access repository at "..repo end local query = queries.all("any") local results = search.disk_search(repo, query) local manifest = { repository = {}, modules = {}, commands = {} } manif.cache_manifest(repo, nil, manifest) local ok, err = store_results(results, manifest) if not ok then return nil, err end if remote then local cache = {} for luaver in util.lua_versions() do local vmanifest = { repository = {}, modules = {}, commands = {} } local ok, err = store_results(results, vmanifest) filter_by_lua_version(vmanifest, luaver, repo, cache) if not cfg.no_manifest then save_table(repo, "manifest-"..luaver, vmanifest) end end else update_dependencies(manifest, deps_mode) end if cfg.no_manifest then -- We want to have cache updated; but exit before save_table is called return true end return save_table(repo, "manifest", manifest) end --- Update manifest file for a local repository -- adding information about a version of a package installed in that repository. -- @param name string: Name of a package from the repository. -- @param version string: Version of a package from the repository. -- @param repo string or nil: Pathname of a local repository. If not given, -- the default local repository is used. -- @param deps_mode string: Dependency mode: "one" for the current default tree, -- "all" for all trees, "order" for all trees with priority >= the current default, -- "none" for using the default dependency mode from the configuration. -- @return boolean or (nil, string): True if manifest was updated successfully, -- or nil and an error message. function writer.add_to_manifest(name, version, repo, deps_mode) assert(type(name) == "string" and not name:match("/")) assert(type(version) == "string") local rocks_dir = path.rocks_dir(repo or cfg.root_dir) assert(type(deps_mode) == "string") if deps_mode == "none" then deps_mode = cfg.deps_mode end local manifest, err = manif.load_manifest(rocks_dir) if not manifest then util.printerr("No existing manifest. Attempting to rebuild...") -- Manifest built by `writer.make_manifest` should already -- include information about given name and version, -- no need to update it. return writer.make_manifest(rocks_dir, deps_mode) end local results = {[name] = {[version] = {{arch = "installed", repo = rocks_dir}}}} local ok, err = store_results(results, manifest) if not ok then return nil, err end update_dependencies(manifest, deps_mode) if cfg.no_manifest then return true end return save_table(rocks_dir, "manifest", manifest) end --- Update manifest file for a local repository -- removing information about a version of a package. -- @param name string: Name of a package removed from the repository. -- @param version string: Version of a package removed from the repository. -- @param repo string or nil: Pathname of a local repository. If not given, -- the default local repository is used. -- @param deps_mode string: Dependency mode: "one" for the current default tree, -- "all" for all trees, "order" for all trees with priority >= the current default, -- "none" for using the default dependency mode from the configuration. -- @return boolean or (nil, string): True if manifest was updated successfully, -- or nil and an error message. function writer.remove_from_manifest(name, version, repo, deps_mode) assert(type(name) == "string" and not name:match("/")) assert(type(version) == "string") local rocks_dir = path.rocks_dir(repo or cfg.root_dir) assert(type(deps_mode) == "string") if deps_mode == "none" then deps_mode = cfg.deps_mode end local manifest, err = manif.load_manifest(rocks_dir) if not manifest then util.printerr("No existing manifest. Attempting to rebuild...") -- Manifest built by `writer.make_manifest` should already -- include up-to-date information, no need to update it. return writer.make_manifest(rocks_dir, deps_mode) end local package_entry = manifest.repository[name] if package_entry == nil or package_entry[version] == nil then -- entry is already missing from repository, no need to do anything return true end local version_entry = package_entry[version][1] if not version_entry then -- manifest looks corrupted, rebuild return writer.make_manifest(rocks_dir, deps_mode) end remove_package_items(manifest.modules, name, version, version_entry.modules) remove_package_items(manifest.commands, name, version, version_entry.commands) package_entry[version] = nil manifest.dependencies[name][version] = nil if not next(package_entry) then -- No more versions of this package. manifest.repository[name] = nil manifest.dependencies[name] = nil end update_dependencies(manifest, deps_mode) if cfg.no_manifest then return true end return save_table(rocks_dir, "manifest", manifest) end --- Report missing dependencies for all rocks installed in a repository. -- @param repo string or nil: Pathname of a local repository. If not given, -- the default local repository is used. -- @param deps_mode string: Dependency mode: "one" for the current default tree, -- "all" for all trees, "order" for all trees with priority >= the current default, -- "none" for using the default dependency mode from the configuration. function writer.check_dependencies(repo, deps_mode) local rocks_dir = path.rocks_dir(repo or cfg.root_dir) assert(type(deps_mode) == "string") if deps_mode == "none" then deps_mode = cfg.deps_mode end local manifest = manif.load_manifest(rocks_dir) if not manifest then return end for name, versions in util.sortedpairs(manifest.repository) do for version, version_entries in util.sortedpairs(versions, vers.compare_versions) do for _, entry in ipairs(version_entries) do if entry.arch == "installed" then if manifest.dependencies[name] and manifest.dependencies[name][version] then deps.report_missing_dependencies(name, version, manifest.dependencies[name][version], deps_mode, util.get_rocks_provided()) end end end end end end return writer