diff options
| author | Mike Vink <mike@pionative.com> | 2025-02-03 21:29:42 +0100 |
|---|---|---|
| committer | Mike Vink <mike@pionative.com> | 2025-02-03 21:29:42 +0100 |
| commit | 5155816b7b925dec5d5feb1568b1d7ceb00938b9 (patch) | |
| tree | deca28ea15e79f6f804c3d90d2ba757881638af5 /src/luarocks/fs/tools.lua | |
Diffstat (limited to 'src/luarocks/fs/tools.lua')
| -rw-r--r-- | src/luarocks/fs/tools.lua | 222 |
1 files changed, 222 insertions, 0 deletions
diff --git a/src/luarocks/fs/tools.lua b/src/luarocks/fs/tools.lua new file mode 100644 index 0000000..23f2561 --- /dev/null +++ b/src/luarocks/fs/tools.lua @@ -0,0 +1,222 @@ + +--- Common fs operations implemented with third-party tools. +local tools = {} + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local cfg = require("luarocks.core.cfg") + +local vars = setmetatable({}, { __index = function(_,k) return cfg.variables[k] end }) + +local dir_stack = {} + +do + local tool_cache = {} + + local tool_options = { + downloader = { + desc = "downloader", + { var = "WGET", name = "wget" }, + { var = "CURL", name = "curl" }, + }, + md5checker = { + desc = "MD5 checker", + { var = "MD5SUM", name = "md5sum" }, + { var = "OPENSSL", name = "openssl", cmdarg = "md5" }, + { var = "MD5", name = "md5" }, + }, + } + + function tools.which_tool(tooltype) + local tool = tool_cache[tooltype] + local names = {} + if not tool then + for _, opt in ipairs(tool_options[tooltype]) do + table.insert(names, opt.name) + if fs.is_tool_available(vars[opt.var], opt.name) then + tool = opt + tool_cache[tooltype] = opt + break + end + end + end + if not tool then + local tool_names = table.concat(names, ", ", 1, #names - 1) .. " or " .. names[#names] + return nil, "no " .. tool_options[tooltype].desc .. " tool available," .. " please install " .. tool_names .. " in your system" + end + return tool.name, vars[tool.var] .. (tool.cmdarg and " "..tool.cmdarg or "") + end +end + +local current_dir_with_cache +do + local cache_pwd + + current_dir_with_cache = function() + local current = cache_pwd + if not current then + local pipe = io.popen(fs.quiet_stderr(vars.PWD)) + current = pipe:read("*a"):gsub("^%s*", ""):gsub("%s*$", "") + pipe:close() + cache_pwd = current + end + for _, directory in ipairs(dir_stack) do + current = fs.absolute_name(directory, current) + end + return current, cache_pwd + end + + --- Obtain current directory. + -- Uses the module's internal directory stack. + -- @return string: the absolute pathname of the current directory. + function tools.current_dir() + return (current_dir_with_cache()) -- drop second return + end +end + +--- Change the current directory. +-- Uses the module's internal directory stack. This does not have exact +-- semantics of chdir, as it does not handle errors the same way, +-- but works well for our purposes for now. +-- @param directory string: The directory to switch to. +-- @return boolean or (nil, string): true if successful, (nil, error message) if failed. +function tools.change_dir(directory) + assert(type(directory) == "string") + if fs.is_dir(directory) then + table.insert(dir_stack, directory) + return true + end + return nil, "directory not found: "..directory +end + +--- Change directory to root. +-- Allows leaving a directory (e.g. for deleting it) in +-- a crossplatform way. +function tools.change_dir_to_root() + local curr_dir = fs.current_dir() + if not curr_dir or not fs.is_dir(curr_dir) then + return false + end + table.insert(dir_stack, "/") + return true +end + +--- Change working directory to the previous in the directory stack. +function tools.pop_dir() + local directory = table.remove(dir_stack) + return directory ~= nil +end + +--- Run the given command. +-- The command is executed in the current directory in the directory stack. +-- @param cmd string: No quoting/escaping is applied to the command. +-- @return boolean: true if command succeeds (status code 0), false +-- otherwise. +function tools.execute_string(cmd) + local current, cache_pwd = current_dir_with_cache() + if not current then return false end + if current ~= cache_pwd then + cmd = fs.command_at(current, cmd) + end + local code = os.execute(cmd) + if code == 0 or code == true then + return true + else + return false + end +end + +--- Internal implementation function for fs.dir. +-- Yields a filename on each iteration. +-- @param at string: directory to list +-- @return nil +function tools.dir_iterator(at) + local pipe = io.popen(fs.command_at(at, vars.LS, true)) + for file in pipe:lines() do + if file ~= "." and file ~= ".." then + coroutine.yield(file) + end + end + pipe:close() +end + +--- Download a remote file. +-- @param url string: URL to be fetched. +-- @param filename string or nil: this function attempts to detect the +-- resulting local filename of the remote file as the basename of the URL; +-- if that is not correct (due to a redirection, for example), the local +-- filename can be given explicitly as this second argument. +-- @param cache boolean: compare remote timestamps via HTTP HEAD prior to +-- re-downloading the file. +-- @return (boolean, string, string): true and the filename on success, +-- false and the error message and code on failure. +function tools.use_downloader(url, filename, cache) + assert(type(url) == "string") + assert(type(filename) == "string" or not filename) + + filename = fs.absolute_name(filename or dir.base_name(url)) + + local downloader, err = fs.which_tool("downloader") + if not downloader then + return nil, err, "downloader" + end + + local ok = false + if downloader == "wget" then + local wget_cmd = vars.WGET.." "..vars.WGETNOCERTFLAG.." --no-cache --user-agent=\""..cfg.user_agent.." via wget\" --quiet " + if cfg.connection_timeout and cfg.connection_timeout > 0 then + wget_cmd = wget_cmd .. "--timeout="..tostring(cfg.connection_timeout).." --tries=1 " + end + if cache then + -- --timestamping is incompatible with --output-document, + -- but that's not a problem for our use cases. + fs.delete(filename .. ".unixtime") + fs.change_dir(dir.dir_name(filename)) + ok = fs.execute_quiet(wget_cmd.." --timestamping ", url) + fs.pop_dir() + elseif filename then + ok = fs.execute_quiet(wget_cmd.." --output-document ", filename, url) + else + ok = fs.execute_quiet(wget_cmd, url) + end + elseif downloader == "curl" then + local curl_cmd = vars.CURL.." "..vars.CURLNOCERTFLAG.." -f -L --user-agent \""..cfg.user_agent.." via curl\" " + if cfg.connection_timeout and cfg.connection_timeout > 0 then + curl_cmd = curl_cmd .. "--connect-timeout "..tostring(cfg.connection_timeout).." " + end + if cache then + curl_cmd = curl_cmd .. " -R -z \"" .. filename .. "\" " + end + ok = fs.execute_string(fs.quiet_stderr(curl_cmd..fs.Q(url).." --output "..fs.Q(filename))) + end + if ok then + return true, filename + else + os.remove(filename) + return false, "failed downloading " .. url, "network" + end +end + +--- Get the MD5 checksum for a file. +-- @param file string: The file to be computed. +-- @return string: The MD5 checksum or nil + message +function tools.get_md5(file) + local ok, md5checker = fs.which_tool("md5checker") + if not ok then + return false, md5checker + end + + local pipe = io.popen(md5checker.." "..fs.Q(fs.absolute_name(file))) + local computed = pipe:read("*l") + pipe:close() + if computed then + computed = computed:match("("..("%x"):rep(32)..")") + end + if computed then + return computed + else + return nil, "Failed to compute MD5 hash for file "..tostring(fs.absolute_name(file)) + end +end + +return tools |
