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