summaryrefslogtreecommitdiff
path: root/src/luarocks/fs/tools.lua
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/fs/tools.lua
fetch tarballHEADmaster
Diffstat (limited to 'src/luarocks/fs/tools.lua')
-rw-r--r--src/luarocks/fs/tools.lua222
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