From 703372dcff31de2dd1edcaedbf2aba68f5d572bf Mon Sep 17 00:00:00 2001 From: Hisham Muhammad Date: Tue, 20 Aug 2024 20:05:50 -0300 Subject: convert luarocks.loader --- src/luarocks/core/cfg.d.tl | 1 + src/luarocks/core/path.tl | 4 +- src/luarocks/loader-original.lua | 269 +++++++++++++++++++++++++++++++ src/luarocks/loader.lua | 316 ++++++++++++++++++++++--------------- src/luarocks/loader.tl | 333 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 795 insertions(+), 128 deletions(-) create mode 100644 src/luarocks/loader-original.lua create mode 100644 src/luarocks/loader.tl diff --git a/src/luarocks/core/cfg.d.tl b/src/luarocks/core/cfg.d.tl index 8437a043..03338d9b 100644 --- a/src/luarocks/core/cfg.d.tl +++ b/src/luarocks/core/cfg.d.tl @@ -59,6 +59,7 @@ local record cfg end -- loader init: function(?{string : string}, ?function(string)): boolean, string, string + init_package_paths: function() -- rockspecs each_platform: function(?string): (function():string) -- fetch diff --git a/src/luarocks/core/path.tl b/src/luarocks/core/path.tl index eff15a51..680c3296 100644 --- a/src/luarocks/core/path.tl +++ b/src/luarocks/core/path.tl @@ -73,7 +73,7 @@ function path.deploy_lua_dir(tree: string | Tree): string if tree is string then return dir.path(tree, cfg.lua_modules_path) else - return tree.lua_dir or dir.path(tree.root, cfg.lua_modules_path) + return tree.lua_dir or dir.path(tree.root, cfg.lua_modules_path) end end @@ -95,7 +95,7 @@ local is_src_extension: {string: boolean} = { [".lua"] = true, [".tl"] = true, [ -- @param i number: the index, 1 if version is the current default, > 1 otherwise. -- This is done this way for use by select_module in luarocks.loader. -- @return string: filename of the module (eg. "/usr/local/lib/lua/5.1/socket/core.so") -function path.which_i(file_name: string, name: string, version: string, tree: string, i: number): string +function path.which_i(file_name: string, name: string, version: string, tree: string | Tree, i: number): string local deploy_dir: string local extension = file_name:match("%.[a-z]+$") if is_src_extension[extension] then diff --git a/src/luarocks/loader-original.lua b/src/luarocks/loader-original.lua new file mode 100644 index 00000000..772fdfcb --- /dev/null +++ b/src/luarocks/loader-original.lua @@ -0,0 +1,269 @@ +--- A module which installs a Lua package loader that is LuaRocks-aware. +-- This loader uses dependency information from the LuaRocks tree to load +-- correct versions of modules. It does this by constructing a "context" +-- table in the environment, which records which versions of packages were +-- used to load previous modules, so that the loader chooses versions +-- that are declared to be compatible with the ones loaded earlier. + +-- luacheck: globals luarocks + +local loaders = package.loaders or package.searchers +local require, ipairs, table, type, next, tostring, error = + require, ipairs, table, type, next, tostring, error +local unpack = unpack or table.unpack + +local loader = {} + +local is_clean = not package.loaded["luarocks.core.cfg"] + +-- This loader module depends only on core modules. +local cfg = require("luarocks.core.cfg") +local cfg_ok, err = cfg.init() +if cfg_ok then + cfg.init_package_paths() +end + +local path = require("luarocks.core.path") +local manif = require("luarocks.core.manif") +local vers = require("luarocks.core.vers") +local require = nil -- luacheck: ignore 411 +-------------------------------------------------------------------------------- + +-- Workaround for wrappers produced by older versions of LuaRocks +local temporary_global = false +local status, luarocks_value = pcall(function() return luarocks end) +if status and luarocks_value then + -- The site_config.lua file generated by old versions uses module(), + -- so it produces a global `luarocks` table. Since we have the table, + -- add the `loader` field to make the old wrappers happy. + luarocks.loader = loader +else + -- When a new version is installed on top of an old version, + -- site_config.lua may be replaced, and then it no longer creates + -- a global. + -- Detect when being called via -lluarocks.loader; this is + -- most likely a wrapper. + local info = debug and debug.getinfo(2, "nS") + if info and info.what == "C" and not info.name then + luarocks = { loader = loader } + temporary_global = true + -- For the other half of this hack, + -- see the next use of `temporary_global` below. + end +end + +loader.context = {} + +--- Process the dependencies of a package to determine its dependency +-- chain for loading modules. +-- @param name string: The name of an installed rock. +-- @param version string: The version of the rock, in string format +function loader.add_context(name, version) + -- assert(type(name) == "string") + -- assert(type(version) == "string") + + if temporary_global then + -- The first thing a wrapper does is to call add_context. + -- From here on, it's safe to clean the global environment. + luarocks = nil + temporary_global = false + end + + local tree_manifests = manif.load_rocks_tree_manifests() + if not tree_manifests then + return nil + end + + return manif.scan_dependencies(name, version, tree_manifests, loader.context) +end + +--- Internal sorting function. +-- @param a table: A provider table. +-- @param b table: Another provider table. +-- @return boolean: True if the version of a is greater than that of b. +local function sort_versions(a,b) + return a.version > b.version +end + +--- Request module to be loaded through other loaders, +-- once the proper name of the module has been determined. +-- For example, in case the module "socket.core" has been requested +-- to the LuaRocks loader and it determined based on context that +-- the version 2.0.2 needs to be loaded and it is not the current +-- version, the module requested for the other loaders will be +-- "socket.core_2_0_2". +-- @param module The module name requested by the user, such as "socket.core" +-- @param name The rock name, such as "luasocket" +-- @param version The rock version, such as "2.0.2-1" +-- @param module_name The actual module name, such as "socket.core" or "socket.core_2_0_2". +-- @return table or (nil, string): The module table as returned by some other loader, +-- or nil followed by an error message if no other loader managed to load the module. +local function call_other_loaders(module, name, version, module_name) + for _, a_loader in ipairs(loaders) do + if a_loader ~= loader.luarocks_loader then + local results = { a_loader(module_name) } + if type(results[1]) == "function" then + return unpack(results) + end + end + end + return "Failed loading module "..module.." in LuaRocks rock "..name.." "..version +end + +local function add_providers(providers, entries, tree, module, filter_file_name) + for i, entry in ipairs(entries) do + local name, version = entry:match("^([^/]*)/(.*)$") + local file_name = tree.manifest.repository[name][version][1].modules[module] + if type(file_name) ~= "string" then + error("Invalid data in manifest file for module "..tostring(module).." (invalid data for "..tostring(name).." "..tostring(version)..")") + end + file_name = filter_file_name(file_name, name, version, tree.tree, i) + if loader.context[name] == version then + return name, version, file_name + end + version = vers.parse_version(version) + table.insert(providers, {name = name, version = version, module_name = file_name, tree = tree}) + end +end + +--- Search for a module in the rocks trees +-- @param module string: module name (eg. "socket.core") +-- @param filter_file_name function(string, string, string, string, number): +-- a function that takes the module file name (eg "socket/core.so"), the rock name +-- (eg "luasocket"), the version (eg "2.0.2-1"), the path of the rocks tree +-- (eg "/usr/local"), and the numeric index of the matching entry, so the +-- filter function can know if the matching module was the first entry or not. +-- @return string, string, string, (string or table): +-- * name of the rock containing the module (eg. "luasocket") +-- * version of the rock (eg. "2.0.2-1") +-- * return value of filter_file_name +-- * tree of the module (string or table in `tree_manifests` format) +local function select_module(module, filter_file_name) + --assert(type(module) == "string") + --assert(type(filter_module_name) == "function") + + local tree_manifests = manif.load_rocks_tree_manifests() + if not tree_manifests then + return nil + end + + local providers = {} + local initmodule + for _, tree in ipairs(tree_manifests) do + local entries = tree.manifest.modules[module] + if entries then + local n, v, f = add_providers(providers, entries, tree, module, filter_file_name) + if n then + return n, v, f + end + else + initmodule = initmodule or module .. ".init" + entries = tree.manifest.modules[initmodule] + if entries then + local n, v, f = add_providers(providers, entries, tree, initmodule, filter_file_name) + if n then + return n, v, f + end + end + end + end + + if next(providers) then + table.sort(providers, sort_versions) + local first = providers[1] + return first.name, first.version.string, first.module_name, first.tree + end +end + +--- Search for a module +-- @param module string: module name (eg. "socket.core") +-- @return string, string, string, (string or table): +-- * name of the rock containing the module (eg. "luasocket") +-- * version of the rock (eg. "2.0.2-1") +-- * name of the module (eg. "socket.core", or "socket.core_2_0_2" if file is stored versioned). +-- * tree of the module (string or table in `tree_manifests` format) +local function pick_module(module) + return + select_module(module, function(file_name, name, version, tree, i) + if i > 1 then + file_name = path.versioned_name(file_name, "", name, version) + end + return path.path_to_module(file_name) + end) +end + +--- Return the pathname of the file that would be loaded for a module. +-- @param module string: module name (eg. "socket.core") +-- @param where string: places to look for the module. If `where` contains +-- "l", it will search using the LuaRocks loader; if it contains "p", +-- it will look in the filesystem using package.path and package.cpath. +-- You can use both at the same time. +-- @return If successful, it will return four values. +-- * If found using the LuaRocks loader, it will return: +-- * filename of the module (eg. "/usr/local/lib/lua/5.1/socket/core.so"), +-- * rock name +-- * rock version +-- * "l" to indicate the match comes from the loader. +-- * If found scanning package.path and package.cpath, it will return: +-- * filename of the module (eg. "/usr/local/lib/lua/5.1/socket/core.so"), +-- * "path" or "cpath" +-- * nil +-- * "p" to indicate the match comes from scanning package.path and cpath. +-- If unsuccessful, nothing is returned. +function loader.which(module, where) + where = where or "l" + if where:match("l") then + local rock_name, rock_version, file_name = select_module(module, path.which_i) + if rock_name then + local fd = io.open(file_name) + if fd then + fd:close() + return file_name, rock_name, rock_version, "l" + end + end + end + if where:match("p") then + local modpath = module:gsub("%.", "/") + for _, v in ipairs({"path", "cpath"}) do + for p in package[v]:gmatch("([^;]+)") do + local file_name = p:gsub("%?", modpath) -- luacheck: ignore 421 + local fd = io.open(file_name) + if fd then + fd:close() + return file_name, v, nil, "p" + end + end + end + end +end + +--- Package loader for LuaRocks support. +-- A module is searched in installed rocks that match the +-- current LuaRocks context. If module is not part of the +-- context, or if a context has not yet been set, the module +-- in the package with the highest version is used. +-- @param module string: The module name, like in plain require(). +-- @return table: The module table (typically), like in plain +-- require(). See require() +-- in the Lua reference manual for details. +function loader.luarocks_loader(module) + local name, version, module_name = pick_module(module) + if not name then + return "No LuaRocks module found for "..module + else + loader.add_context(name, version) + return call_other_loaders(module, name, version, module_name) + end +end + +table.insert(loaders, 1, loader.luarocks_loader) + +if is_clean then + for modname, _ in pairs(package.loaded) do + if modname:match("^luarocks%.") then + package.loaded[modname] = nil + end + end +end + +return loader diff --git a/src/luarocks/loader.lua b/src/luarocks/loader.lua index 772fdfcb..37f196f0 100644 --- a/src/luarocks/loader.lua +++ b/src/luarocks/loader.lua @@ -1,24 +1,24 @@ ---- A module which installs a Lua package loader that is LuaRocks-aware. --- This loader uses dependency information from the LuaRocks tree to load --- correct versions of modules. It does this by constructing a "context" --- table in the environment, which records which versions of packages were --- used to load previous modules, so that the loader chooses versions --- that are declared to be compatible with the ones loaded earlier. +local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local debug = _tl_compat and _tl_compat.debug or debug; local io = _tl_compat and _tl_compat.io or io; local ipairs = _tl_compat and _tl_compat.ipairs or ipairs; local package = _tl_compat and _tl_compat.package or package; local pairs = _tl_compat and _tl_compat.pairs or pairs; local pcall = _tl_compat and _tl_compat.pcall or pcall; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table + + + + --- luacheck: globals luarocks local loaders = package.loaders or package.searchers local require, ipairs, table, type, next, tostring, error = - require, ipairs, table, type, next, tostring, error -local unpack = unpack or table.unpack +require, ipairs, table, type, next, tostring, error local loader = {} + + + local is_clean = not package.loaded["luarocks.core.cfg"] --- This loader module depends only on core modules. + local cfg = require("luarocks.core.cfg") -local cfg_ok, err = cfg.init() +local cfg_ok, _err = cfg.init() if cfg_ok then cfg.init_package_paths() end @@ -26,121 +26,175 @@ end local path = require("luarocks.core.path") local manif = require("luarocks.core.manif") local vers = require("luarocks.core.vers") -local require = nil -- luacheck: ignore 411 --------------------------------------------------------------------------------- --- Workaround for wrappers produced by older versions of LuaRocks + + + + + + + + + + + + + + + + + + + + + + + + local temporary_global = false -local status, luarocks_value = pcall(function() return luarocks end) +local status, luarocks_value = pcall(function() + return luarocks +end) if status and luarocks_value then - -- The site_config.lua file generated by old versions uses module(), - -- so it produces a global `luarocks` table. Since we have the table, - -- add the `loader` field to make the old wrappers happy. + + + luarocks.loader = loader else - -- When a new version is installed on top of an old version, - -- site_config.lua may be replaced, and then it no longer creates - -- a global. - -- Detect when being called via -lluarocks.loader; this is - -- most likely a wrapper. + + + + + local info = debug and debug.getinfo(2, "nS") if info and info.what == "C" and not info.name then luarocks = { loader = loader } temporary_global = true - -- For the other half of this hack, - -- see the next use of `temporary_global` below. + + end end + + + + loader.context = {} ---- Process the dependencies of a package to determine its dependency --- chain for loading modules. --- @param name string: The name of an installed rock. --- @param version string: The version of the rock, in string format -function loader.add_context(name, version) - -- assert(type(name) == "string") - -- assert(type(version) == "string") + + + + +function loader.add_context(name, version) if temporary_global then - -- The first thing a wrapper does is to call add_context. - -- From here on, it's safe to clean the global environment. + + luarocks = nil temporary_global = false end local tree_manifests = manif.load_rocks_tree_manifests() if not tree_manifests then - return nil + return end - return manif.scan_dependencies(name, version, tree_manifests, loader.context) + manif.scan_dependencies(name, version, tree_manifests, loader.context) end ---- Internal sorting function. --- @param a table: A provider table. --- @param b table: Another provider table. --- @return boolean: True if the version of a is greater than that of b. -local function sort_versions(a,b) + + + + + + +local function sort_versions(a, b) return a.version > b.version end ---- Request module to be loaded through other loaders, --- once the proper name of the module has been determined. --- For example, in case the module "socket.core" has been requested --- to the LuaRocks loader and it determined based on context that --- the version 2.0.2 needs to be loaded and it is not the current --- version, the module requested for the other loaders will be --- "socket.core_2_0_2". --- @param module The module name requested by the user, such as "socket.core" --- @param name The rock name, such as "luasocket" --- @param version The rock version, such as "2.0.2-1" --- @param module_name The actual module name, such as "socket.core" or "socket.core_2_0_2". --- @return table or (nil, string): The module table as returned by some other loader, --- or nil followed by an error message if no other loader managed to load the module. + + + + + + + + + + + + + + + local function call_other_loaders(module, name, version, module_name) for _, a_loader in ipairs(loaders) do if a_loader ~= loader.luarocks_loader then local results = { a_loader(module_name) } - if type(results[1]) == "function" then - return unpack(results) + local f = results[1] + if type(f) == "function" then + if #results == 2 then + return f, results[2] + else + return f + end end end end - return "Failed loading module "..module.." in LuaRocks rock "..name.." "..version + return "Failed loading module " .. module .. " in LuaRocks rock " .. name .. " " .. version end -local function add_providers(providers, entries, tree, module, filter_file_name) + + + + + + + + + + + + + + +local function add_providers(providers, entries, tree, module, filter_name) for i, entry in ipairs(entries) do local name, version = entry:match("^([^/]*)/(.*)$") + local file_name = tree.manifest.repository[name][version][1].modules[module] if type(file_name) ~= "string" then - error("Invalid data in manifest file for module "..tostring(module).." (invalid data for "..tostring(name).." "..tostring(version)..")") + error("Invalid data in manifest file for module " .. tostring(module) .. " (invalid data for " .. tostring(name) .. " " .. tostring(version) .. ")") end - file_name = filter_file_name(file_name, name, version, tree.tree, i) + + file_name = filter_name(file_name, name, version, tree.tree, i) + if loader.context[name] == version then return name, version, file_name end - version = vers.parse_version(version) - table.insert(providers, {name = name, version = version, module_name = file_name, tree = tree}) + + table.insert(providers, { + name = name, + version = vers.parse_version(version), + module_name = file_name, + tree = tree, + }) end end ---- Search for a module in the rocks trees --- @param module string: module name (eg. "socket.core") --- @param filter_file_name function(string, string, string, string, number): --- a function that takes the module file name (eg "socket/core.so"), the rock name --- (eg "luasocket"), the version (eg "2.0.2-1"), the path of the rocks tree --- (eg "/usr/local"), and the numeric index of the matching entry, so the --- filter function can know if the matching module was the first entry or not. --- @return string, string, string, (string or table): --- * name of the rock containing the module (eg. "luasocket") --- * version of the rock (eg. "2.0.2-1") --- * return value of filter_file_name --- * tree of the module (string or table in `tree_manifests` format) -local function select_module(module, filter_file_name) - --assert(type(module) == "string") - --assert(type(filter_module_name) == "function") + + + + + + + + + + + + +local function select_module(module, filter_name) local tree_manifests = manif.load_rocks_tree_manifests() if not tree_manifests then @@ -152,7 +206,7 @@ local function select_module(module, filter_file_name) for _, tree in ipairs(tree_manifests) do local entries = tree.manifest.modules[module] if entries then - local n, v, f = add_providers(providers, entries, tree, module, filter_file_name) + local n, v, f = add_providers(providers, entries, tree, module, filter_name) if n then return n, v, f end @@ -160,7 +214,7 @@ local function select_module(module, filter_file_name) initmodule = initmodule or module .. ".init" entries = tree.manifest.modules[initmodule] if entries then - local n, v, f = add_providers(providers, entries, tree, initmodule, filter_file_name) + local n, v, f = add_providers(providers, entries, tree, initmodule, filter_name) if n then return n, v, f end @@ -168,48 +222,55 @@ local function select_module(module, filter_file_name) end end - if next(providers) then + if next(providers) ~= nil then table.sort(providers, sort_versions) local first = providers[1] - return first.name, first.version.string, first.module_name, first.tree + return first.name, first.version.string, first.module_name end end ---- Search for a module --- @param module string: module name (eg. "socket.core") --- @return string, string, string, (string or table): --- * name of the rock containing the module (eg. "luasocket") --- * version of the rock (eg. "2.0.2-1") --- * name of the module (eg. "socket.core", or "socket.core_2_0_2" if file is stored versioned). --- * tree of the module (string or table in `tree_manifests` format) + + + + + + + + + + + +local function filter_module_name(file_name, name, version, _tree, i) + if i > 1 then + file_name = path.versioned_name(file_name, "", name, version) + end + return path.path_to_module(file_name) +end + + + + + + + + + local function pick_module(module) - return - select_module(module, function(file_name, name, version, tree, i) - if i > 1 then - file_name = path.versioned_name(file_name, "", name, version) - end - return path.path_to_module(file_name) - end) + return select_module(module, filter_module_name) end ---- Return the pathname of the file that would be loaded for a module. --- @param module string: module name (eg. "socket.core") --- @param where string: places to look for the module. If `where` contains --- "l", it will search using the LuaRocks loader; if it contains "p", --- it will look in the filesystem using package.path and package.cpath. --- You can use both at the same time. --- @return If successful, it will return four values. --- * If found using the LuaRocks loader, it will return: --- * filename of the module (eg. "/usr/local/lib/lua/5.1/socket/core.so"), --- * rock name --- * rock version --- * "l" to indicate the match comes from the loader. --- * If found scanning package.path and package.cpath, it will return: --- * filename of the module (eg. "/usr/local/lib/lua/5.1/socket/core.so"), --- * "path" or "cpath" --- * nil --- * "p" to indicate the match comes from scanning package.path and cpath. --- If unsuccessful, nothing is returned. + + + + + + + + + + + + function loader.which(module, where) where = where or "l" if where:match("l") then @@ -224,9 +285,9 @@ function loader.which(module, where) end if where:match("p") then local modpath = module:gsub("%.", "/") - for _, v in ipairs({"path", "cpath"}) do - for p in package[v]:gmatch("([^;]+)") do - local file_name = p:gsub("%?", modpath) -- luacheck: ignore 421 + for _, v in ipairs({ package.path, package.cpath }) do + for p in v:gmatch("([^;]+)") do + local file_name = p:gsub("%?", modpath) local fd = io.open(file_name) if fd then fd:close() @@ -237,19 +298,22 @@ function loader.which(module, where) end end ---- Package loader for LuaRocks support. --- A module is searched in installed rocks that match the --- current LuaRocks context. If module is not part of the --- context, or if a context has not yet been set, the module --- in the package with the highest version is used. --- @param module string: The module name, like in plain require(). --- @return table: The module table (typically), like in plain --- require(). See require() --- in the Lua reference manual for details. + + + + + + + + + + + + function loader.luarocks_loader(module) local name, version, module_name = pick_module(module) if not name then - return "No LuaRocks module found for "..module + return "No LuaRocks module found for " .. module else loader.add_context(name, version) return call_other_loaders(module, name, version, module_name) diff --git a/src/luarocks/loader.tl b/src/luarocks/loader.tl new file mode 100644 index 00000000..0a5e3c30 --- /dev/null +++ b/src/luarocks/loader.tl @@ -0,0 +1,333 @@ +--- A module which installs a Lua package loader that is LuaRocks-aware. +-- This loader uses dependency information from the LuaRocks tree to load +-- correct versions of modules. It does this by constructing a "context" +-- table in the environment, which records which versions of packages were +-- used to load previous modules, so that the loader chooses versions +-- that are declared to be compatible with the ones loaded earlier. + +local loaders = package.loaders or package.searchers +local require, ipairs, table, type, next, tostring, error = + require, ipairs, table, type, next, tostring, error + +local record loader + context: {string: string} + luarocks_loader: function(string): LoaderFn | string, any +end + +local is_clean = not package.loaded["luarocks.core.cfg"] + +-- This loader module depends only on core modules. +local cfg = require("luarocks.core.cfg") +local cfg_ok, _err = cfg.init() +if cfg_ok then + cfg.init_package_paths() +end + +local path = require("luarocks.core.path") +local manif = require("luarocks.core.manif") +local vers = require("luarocks.core.vers") + +local type Version = require("luarocks.core.types.version").Version +local type TreeManifest = require("luarocks.core.types.manifest").Tree_manifest +local type Tree = require("luarocks.core.types.tree").Tree +local type FilterFn = function(string, string, string, Tree, integer): string +local type LoaderFn = function() + +local record Provider + name: string + version: Version + module_name: string + tree: TreeManifest +end + +-------------------------------------------------------------------------------- +-- Backwards compatibility +-------------------------------------------------------------------------------- + +local record LuaRocksGlobal + loader: loader +end + +global luarocks: LuaRocksGlobal + +-- Workaround for wrappers produced by older versions of LuaRocks +local temporary_global = false +local status, luarocks_value = pcall(function(): LuaRocksGlobal + return luarocks +end) +if status and luarocks_value then + -- The site_config.lua file generated by old versions uses module(), + -- so it produces a global `luarocks` table. Since we have the table, + -- add the `loader` field to make the old wrappers happy. + luarocks.loader = loader +else + -- When a new version is installed on top of an old version, + -- site_config.lua may be replaced, and then it no longer creates + -- a global. + -- Detect when being called via -lluarocks.loader; this is + -- most likely a wrapper. + local info = debug and debug.getinfo(2, "nS") + if info and info.what == "C" and not info.name then + luarocks = { loader = loader } + temporary_global = true + -- For the other half of this hack, + -- see the next use of `temporary_global` below. + end +end + +-------------------------------------------------------------------------------- +-- Context management +-------------------------------------------------------------------------------- + +loader.context = {} + +--- Process the dependencies of a package to determine its dependency +-- chain for loading modules. +-- +-- @param name The name of an installed rock. +-- @param version The version of the rock, in string format +function loader.add_context(name: string, version: string) + if temporary_global then + -- The first thing a wrapper does is to call add_context. + -- From here on, it's safe to clean the global environment. + luarocks = nil + temporary_global = false + end + + local tree_manifests = manif.load_rocks_tree_manifests() + if not tree_manifests then + return + end + + manif.scan_dependencies(name, version, tree_manifests, loader.context) +end + +--- Internal sorting function. +-- +-- @param a A provider table. +-- @param b Another provider table. +-- +-- @return true if the version of a is greater than that of b. +local function sort_versions(a: Provider, b: Provider): boolean + return a.version > b.version +end + +--- Request module to be loaded through other loaders, +-- once the proper name of the module has been determined. +-- For example, in case the module "socket.core" has been requested +-- to the LuaRocks loader and it determined based on context that +-- the version 2.0.2 needs to be loaded and it is not the current +-- version, the module requested for the other loaders will be +-- "socket.core_2_0_2". +-- +-- @param module The module name requested by the user, e.g. "socket.core" +-- @param name The rock name, such as "luasocket" +-- @param version The rock version, such as "2.0.2-1" +-- @param module_name Actual module name, such as "socket.core_2_0_2" +-- +-- @return The loader function returned or an error message. +-- @return Additional loader data, if returned by the loader. +local function call_other_loaders(module: string, name: string, version: string, module_name: string): LoaderFn | string, any + for _, a_loader in ipairs(loaders) do + if a_loader ~= loader.luarocks_loader then + local results: {any} = { a_loader(module_name) } + local f = results[1] + if f is LoaderFn then + if #results == 2 then + return f, results[2] + else + return f + end + end + end + end + return "Failed loading module " .. module .. " in LuaRocks rock " .. name .. " " .. version +end + +--- Find entries which provide the wanted module in the tree, +-- and store them in the array of providers for later sorting. +-- +-- @param providers The array of providers being accumulated into +-- @param entries The packages which provide the module +-- @param tree TreeManifest where filenames can be found. +-- @param module The module name being looked up +-- @param filter_name A filtering function to adjust the filename. +-- +-- @return If the current LuaRocks loader context already resolved this +-- dependency based on other dependencies, return the name of the module +-- for immediate use. +-- @return Version of the module for immediate use, if matched. +-- @return File name of the module for immediate use, if matched. +local function add_providers(providers: {Provider}, entries: {string}, tree: TreeManifest, module: string, filter_name: FilterFn): string, string, string + for i, entry in ipairs(entries) do + local name, version = entry:match("^([^/]*)/(.*)$") + + local file_name = tree.manifest.repository[name][version][1].modules[module] + if type(file_name) ~= "string" then + error("Invalid data in manifest file for module " .. tostring(module) .. " (invalid data for " .. tostring(name) .. " " .. tostring(version) .. ")") + end + + file_name = filter_name(file_name, name, version, tree.tree, i) + + if loader.context[name] == version then + return name, version, file_name + end + + table.insert(providers, { + name = name, + version = vers.parse_version(version), + module_name = file_name, + tree = tree + }) + end +end + +--- Search for a module in the rocks trees. +-- +-- @param module module name (eg. "socket.core") +-- @param filter_name a function that takes the module file name +-- (eg "socket/core.so"), the rock name (eg "luasocket"), +-- the version (eg "2.0.2-1"), the path of the rocks tree +-- (eg "/usr/local"), and the numeric index of the matching entry, so the +-- filter function can know if the matching module was the first entry or not. +-- +-- @return name of the rock containing the module (eg. "luasocket") +-- @return version of the rock (eg. "2.0.2-1") +-- @return return value of filter_name +local function select_module(module: string, filter_name: FilterFn): string, string, string + + local tree_manifests = manif.load_rocks_tree_manifests() + if not tree_manifests then + return nil + end + + local providers: {Provider} = {} + local initmodule: string + for _, tree in ipairs(tree_manifests) do + local entries = tree.manifest.modules[module] + if entries then + local n, v, f = add_providers(providers, entries, tree, module, filter_name) + if n then + return n, v, f + end + else + initmodule = initmodule or module .. ".init" + entries = tree.manifest.modules[initmodule] + if entries then + local n, v, f = add_providers(providers, entries, tree, initmodule, filter_name) + if n then + return n, v, f + end + end + end + end + + if next(providers) ~= nil then + table.sort(providers, sort_versions) + local first = providers[1] + return first.name, first.version.string, first.module_name + end +end + +--- Filter operation for adjusting the versioned names when multiple packages provide +-- the same file. +-- +-- @param file_name The original filename +-- @param name The rock name +-- @param version The rock version +-- @param _tree (unused) +-- @param i The priority index, to determine whether to version the name. +-- +-- @return A filename, which may be plain or versioned. +-- (eg. "socket.core", or "socket.core_2_0_2" if file is stored versioned). +local function filter_module_name(file_name: string, name: string, version: string, _tree: Tree, i: integer): string + if i > 1 then + file_name = path.versioned_name(file_name, "", name, version) + end + return path.path_to_module(file_name) +end + +--- Search for a module. +-- +-- @param module name of the module (eg. "socket.core") +-- +-- @return name of the rock containing the module (eg. "luasocket") +-- @return version of the rock (eg. "2.0.2-1") +-- @return name of the module (eg. "socket.core", or "socket.core_2_0_2" if file is stored versioned). +-- @return tree of the module (string or table in `tree_manifests` format) +local function pick_module(module: string): string, string, string, string | TreeManifest + return select_module(module, filter_module_name) +end + +--- Return the pathname of the file that would be loaded for a module. +-- +-- @param module module name (eg. "socket.core") +-- @param where places to look for the module. If `where` contains +-- "l", it will search using the LuaRocks loader; if it contains "p", +-- it will look in the filesystem using package.path and package.cpath. +-- You can use both at the same time. +-- +-- @return If found, filename of the module (eg. "/usr/local/lib/lua/5.1/socket/core.so"), +-- @return If found via the loader, the rock name; otherwise, "path" or "cpath" +-- @return If found via the loader, the rock version; otherwise, nil +-- @return If found via the loader, "l"; if found via package.path or package.cpath, "p". +function loader.which(module: string, where?: string): string, string, string, string + where = where or "l" + if where:match("l") then + local rock_name, rock_version, file_name = select_module(module, path.which_i) + if rock_name then + local fd = io.open(file_name) + if fd then + fd:close() + return file_name, rock_name, rock_version, "l" + end + end + end + if where:match("p") then + local modpath = module:gsub("%.", "/") + for _, v in ipairs({package.path, package.cpath}) do + for p in v:gmatch("([^;]+)") do + local file_name = p:gsub("%?", modpath) -- luacheck: ignore 421 + local fd = io.open(file_name) + if fd then + fd:close() + return file_name, v, nil, "p" + end + end + end + end +end + +--- Package loader for LuaRocks support. +-- See require() +-- in the Lua reference manual for details on the require() mechanism. +-- The LuaRocks loader works by searching in installed rocks that match the +-- current LuaRocks context. If module is not part of the +-- context, or if a context has not yet been set, the module +-- in the package with the highest version is used. +-- +-- @param module The module name, like in plain require(). +-- +-- @return A function which can load the module found, +-- or a string with an error message. +function loader.luarocks_loader(module: string): LoaderFn | string, any + local name, version, module_name = pick_module(module) + if not name then + return "No LuaRocks module found for " .. module + else + loader.add_context(name, version) + return call_other_loaders(module, name, version, module_name) + end +end + +table.insert(loaders, 1, loader.luarocks_loader) + +if is_clean then + for modname, _ in pairs(package.loaded) do + if modname:match("^luarocks%.") then + package.loaded[modname] = nil + end + end +end + +return loader -- cgit v1.2.3-55-g6feb