summaryrefslogtreecommitdiff
path: root/src/luarocks/fs/lua.lua
diff options
context:
space:
mode:
Diffstat (limited to 'src/luarocks/fs/lua.lua')
-rw-r--r--src/luarocks/fs/lua.lua1307
1 files changed, 1307 insertions, 0 deletions
diff --git a/src/luarocks/fs/lua.lua b/src/luarocks/fs/lua.lua
new file mode 100644
index 0000000..4016ddc
--- /dev/null
+++ b/src/luarocks/fs/lua.lua
@@ -0,0 +1,1307 @@
+
+--- Native Lua implementation of filesystem and platform abstractions,
+-- using LuaFileSystem, LuaSocket, LuaSec, lua-zlib, LuaPosix, MD5.
+-- module("luarocks.fs.lua")
+local fs_lua = {}
+
+local fs = require("luarocks.fs")
+
+local cfg = require("luarocks.core.cfg")
+local dir = require("luarocks.dir")
+local util = require("luarocks.util")
+local vers = require("luarocks.core.vers")
+
+local pack = table.pack or function(...) return { n = select("#", ...), ... } end
+
+local socket_ok, zip_ok, lfs_ok, md5_ok, posix_ok, bz2_ok, _
+local http, ftp, zip, lfs, md5, posix, bz2
+
+if cfg.fs_use_modules then
+ socket_ok, http = pcall(require, "socket.http")
+ _, ftp = pcall(require, "socket.ftp")
+ zip_ok, zip = pcall(require, "luarocks.tools.zip")
+ bz2_ok, bz2 = pcall(require, "bz2")
+ lfs_ok, lfs = pcall(require, "lfs")
+ md5_ok, md5 = pcall(require, "md5")
+ posix_ok, posix = pcall(require, "posix")
+end
+
+local patch = require("luarocks.tools.patch")
+local tar = require("luarocks.tools.tar")
+
+local dir_sep = package.config:sub(1, 1)
+
+local dir_stack = {}
+
+--- Test is file/dir is writable.
+-- Warning: testing if a file/dir is writable does not guarantee
+-- that it will remain writable and therefore it is no replacement
+-- for checking the result of subsequent operations.
+-- @param file string: filename to test
+-- @return boolean: true if file exists, false otherwise.
+function fs_lua.is_writable(file)
+ assert(file)
+ file = dir.normalize(file)
+ local result
+ if fs.is_dir(file) then
+ local file2 = dir.path(file, '.tmpluarockstestwritable')
+ local fh = io.open(file2, 'wb')
+ result = fh ~= nil
+ if fh then fh:close() end
+ os.remove(file2)
+ else
+ local fh = io.open(file, 'r+b')
+ result = fh ~= nil
+ if fh then fh:close() end
+ end
+ return result
+end
+
+function fs_lua.quote_args(command, ...)
+ local out = { command }
+ local args = pack(...)
+ for i=1, args.n do
+ local arg = args[i]
+ assert(type(arg) == "string")
+ out[#out+1] = fs.Q(arg)
+ end
+ return table.concat(out, " ")
+end
+
+--- Run the given command, quoting its arguments.
+-- The command is executed in the current directory in the dir stack.
+-- @param command string: The command to be executed. No quoting/escaping
+-- is applied.
+-- @param ... Strings containing additional arguments, which are quoted.
+-- @return boolean: true if command succeeds (status code 0), false
+-- otherwise.
+function fs_lua.execute(command, ...)
+ assert(type(command) == "string")
+ return fs.execute_string(fs.quote_args(command, ...))
+end
+
+--- Run the given command, quoting its arguments, silencing its output.
+-- The command is executed in the current directory in the dir stack.
+-- Silencing is omitted if 'verbose' mode is enabled.
+-- @param command string: The command to be executed. No quoting/escaping
+-- is applied.
+-- @param ... Strings containing additional arguments, which will be quoted.
+-- @return boolean: true if command succeeds (status code 0), false
+-- otherwise.
+function fs_lua.execute_quiet(command, ...)
+ assert(type(command) == "string")
+ if cfg.verbose then -- omit silencing output
+ return fs.execute_string(fs.quote_args(command, ...))
+ else
+ return fs.execute_string(fs.quiet(fs.quote_args(command, ...)))
+ end
+end
+
+function fs_lua.execute_env(env, command, ...)
+ assert(type(command) == "string")
+ local envstr = {}
+ for var, val in pairs(env) do
+ table.insert(envstr, fs.export_cmd(var, val))
+ end
+ return fs.execute_string(table.concat(envstr, "\n") .. "\n" .. fs.quote_args(command, ...))
+end
+
+local tool_available_cache = {}
+
+function fs_lua.set_tool_available(tool_name, value)
+ assert(type(value) == "boolean")
+ tool_available_cache[tool_name] = value
+end
+
+--- Checks if the given tool is available.
+-- The tool is executed using a flag, usually just to ask its version.
+-- @param tool_cmd string: The command to be used to check the tool's presence (e.g. hg in case of Mercurial)
+-- @param tool_name string: The actual name of the tool (e.g. Mercurial)
+function fs_lua.is_tool_available(tool_cmd, tool_name)
+ assert(type(tool_cmd) == "string")
+ assert(type(tool_name) == "string")
+
+ local ok
+ if tool_available_cache[tool_name] ~= nil then
+ ok = tool_available_cache[tool_name]
+ else
+ local tool_cmd_no_args = tool_cmd:gsub(" [^\"]*$", "")
+
+ -- if it looks like the tool has a pathname, try that first
+ if tool_cmd_no_args:match("[/\\]") then
+ local tool_cmd_no_args_normalized = dir.path(tool_cmd_no_args)
+ local fd = io.open(tool_cmd_no_args_normalized, "r")
+ if fd then
+ fd:close()
+ ok = true
+ end
+ end
+
+ if not ok then
+ ok = fs.search_in_path(tool_cmd_no_args)
+ end
+
+ tool_available_cache[tool_name] = (ok == true)
+ end
+
+ if ok then
+ return true
+ else
+ local msg = "'%s' program not found. Make sure %s is installed and is available in your PATH " ..
+ "(or you may want to edit the 'variables.%s' value in file '%s')"
+ return nil, msg:format(tool_cmd, tool_name, tool_name:upper(), cfg.config_files.nearest)
+ end
+end
+
+--- Check the MD5 checksum for a file.
+-- @param file string: The file to be checked.
+-- @param md5sum string: The string with the expected MD5 checksum.
+-- @return boolean: true if the MD5 checksum for 'file' equals 'md5sum', false + msg if not
+-- or if it could not perform the check for any reason.
+function fs_lua.check_md5(file, md5sum)
+ file = dir.normalize(file)
+ local computed, msg = fs.get_md5(file)
+ if not computed then
+ return false, msg
+ end
+ if computed:match("^"..md5sum) then
+ return true
+ else
+ return false, "Mismatch MD5 hash for file "..file
+ end
+end
+
+--- List the contents of a directory.
+-- @param at string or nil: directory to list (will be the current
+-- directory if none is given).
+-- @return table: an array of strings with the filenames representing
+-- the contents of a directory.
+function fs_lua.list_dir(at)
+ local result = {}
+ for file in fs.dir(at) do
+ result[#result+1] = file
+ end
+ return result
+end
+
+--- Iterate over the contents of a directory.
+-- @param at string or nil: directory to list (will be the current
+-- directory if none is given).
+-- @return function: an iterator function suitable for use with
+-- the for statement.
+function fs_lua.dir(at)
+ if not at then
+ at = fs.current_dir()
+ end
+ at = dir.normalize(at)
+ if not fs.is_dir(at) then
+ return function() end
+ end
+ return coroutine.wrap(function() fs.dir_iterator(at) end)
+end
+
+--- List the Lua modules at a specific require path.
+-- eg. `modules("luarocks.cmd")` would return a list of all LuaRocks command
+-- modules, in the current Lua path.
+function fs_lua.modules(at)
+ at = at or ""
+ if #at > 0 then
+ -- turn require path into file path
+ at = at:gsub("%.", package.config:sub(1,1)) .. package.config:sub(1,1)
+ end
+
+ local path = package.path:sub(-1, -1) == ";" and package.path or package.path .. ";"
+ local paths = {}
+ for location in path:gmatch("(.-);") do
+ if location:lower() == "?.lua" then
+ location = "./?.lua"
+ end
+ local _, q_count = location:gsub("%?", "") -- only use the ones with a single '?'
+ if location:match("%?%.[lL][uU][aA]$") and q_count == 1 then -- only use when ending with "?.lua"
+ location = location:gsub("%?%.[lL][uU][aA]$", at)
+ table.insert(paths, location)
+ end
+ end
+
+ if #paths == 0 then
+ return {}
+ end
+
+ local modules = {}
+ local is_duplicate = {}
+ for _, path in ipairs(paths) do -- luacheck: ignore 421
+ local files = fs.list_dir(path)
+ for _, filename in ipairs(files or {}) do
+ if filename:match("%.[lL][uU][aA]$") then
+ filename = filename:sub(1,-5) -- drop the extension
+ if not is_duplicate[filename] then
+ is_duplicate[filename] = true
+ table.insert(modules, filename)
+ end
+ end
+ end
+ end
+
+ return modules
+end
+
+function fs_lua.filter_file(fn, input_filename, output_filename)
+ local fd, err = io.open(input_filename, "rb")
+ if not fd then
+ return nil, err
+ end
+
+ local input, err = fd:read("*a")
+ fd:close()
+ if not input then
+ return nil, err
+ end
+
+ local output, err = fn(input)
+ if not output then
+ return nil, err
+ end
+
+ fd, err = io.open(output_filename, "wb")
+ if not fd then
+ return nil, err
+ end
+
+ local ok, err = fd:write(output)
+ fd:close()
+ if not ok then
+ return nil, err
+ end
+
+ return true
+end
+
+function fs_lua.system_temp_dir()
+ return os.getenv("TMPDIR") or os.getenv("TEMP") or "/tmp"
+end
+
+local function temp_dir_pattern(name_pattern)
+ return dir.path(fs.system_temp_dir(),
+ "luarocks_" .. dir.normalize(name_pattern):gsub("[/\\]", "_") .. "-")
+end
+
+---------------------------------------------------------------------
+-- LuaFileSystem functions
+---------------------------------------------------------------------
+
+if lfs_ok then
+
+function fs_lua.file_age(filename)
+ local attr = lfs.attributes(filename)
+ if attr and attr.change then
+ return os.difftime(os.time(), attr.change)
+ end
+ return math.huge
+end
+
+function fs_lua.lock_access(dirname, force)
+ fs.make_dir(dirname)
+ local lockfile = dir.path(dirname, "lockfile.lfs")
+
+ -- drop stale lock, older than 1 hour
+ local age = fs.file_age(lockfile)
+ if age > 3600 and age < math.huge then
+ force = true
+ end
+
+ if force then
+ os.remove(lockfile)
+ end
+ return lfs.lock_dir(dirname)
+end
+
+function fs_lua.unlock_access(lock)
+ return lock:free()
+end
+
+--- Run the given command.
+-- The command is executed in the current directory in the dir stack.
+-- @param cmd string: No quoting/escaping is applied to the command.
+-- @return boolean: true if command succeeds (status code 0), false
+-- otherwise.
+function fs_lua.execute_string(cmd)
+ local code = os.execute(cmd)
+ return (code == 0 or code == true)
+end
+
+--- Obtain current directory.
+-- Uses the module's internal dir stack.
+-- @return string: the absolute pathname of the current directory.
+function fs_lua.current_dir()
+ return lfs.currentdir()
+end
+
+--- Change the current directory.
+-- Uses the module's internal dir 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 d string: The directory to switch to.
+function fs_lua.change_dir(d)
+ table.insert(dir_stack, lfs.currentdir())
+ d = dir.normalize(d)
+ return lfs.chdir(d)
+end
+
+--- Change directory to root.
+-- Allows leaving a directory (e.g. for deleting it) in
+-- a crossplatform way.
+function fs_lua.change_dir_to_root()
+ local current = lfs.currentdir()
+ if not current or current == "" then
+ return false
+ end
+ table.insert(dir_stack, current)
+ lfs.chdir("/") -- works on Windows too
+ return true
+end
+
+--- Change working directory to the previous in the dir stack.
+-- @return true if a pop occurred, false if the stack was empty.
+function fs_lua.pop_dir()
+ local d = table.remove(dir_stack)
+ if d then
+ lfs.chdir(d)
+ return true
+ else
+ return false
+ end
+end
+
+--- Create a directory if it does not already exist.
+-- If any of the higher levels in the path name do not exist
+-- too, they are created as well.
+-- @param directory string: pathname of directory to create.
+-- @return boolean or (boolean, string): true on success or (false, error message) on failure.
+function fs_lua.make_dir(directory)
+ assert(type(directory) == "string")
+ directory = dir.normalize(directory)
+ local path = nil
+ if directory:sub(2, 2) == ":" then
+ path = directory:sub(1, 2)
+ directory = directory:sub(4)
+ else
+ if directory:match("^" .. dir_sep) then
+ path = ""
+ end
+ end
+ for d in directory:gmatch("([^" .. dir_sep .. "]+)" .. dir_sep .. "*") do
+ path = path and path .. dir_sep .. d or d
+ local mode = lfs.attributes(path, "mode")
+ if not mode then
+ local ok, err = lfs.mkdir(path)
+ if not ok then
+ return false, err
+ end
+ if cfg.is_platform("unix") then
+ ok, err = fs.set_permissions(path, "exec", "all")
+ if not ok then
+ return false, err
+ end
+ end
+ elseif mode ~= "directory" then
+ return false, path.." is not a directory"
+ end
+ end
+ return true
+end
+
+--- Remove a directory if it is empty.
+-- Does not return errors (for example, if directory is not empty or
+-- if already does not exist)
+-- @param d string: pathname of directory to remove.
+function fs_lua.remove_dir_if_empty(d)
+ assert(d)
+ d = dir.normalize(d)
+ lfs.rmdir(d)
+end
+
+--- Remove a directory if it is empty.
+-- Does not return errors (for example, if directory is not empty or
+-- if already does not exist)
+-- @param d string: pathname of directory to remove.
+function fs_lua.remove_dir_tree_if_empty(d)
+ assert(d)
+ d = dir.normalize(d)
+ for i=1,10 do
+ lfs.rmdir(d)
+ d = dir.dir_name(d)
+ end
+end
+
+local function are_the_same_file(f1, f2)
+ if f1 == f2 then
+ return true
+ end
+ if cfg.is_platform("unix") then
+ local i1 = lfs.attributes(f1, "ino")
+ local i2 = lfs.attributes(f2, "ino")
+ if i1 ~= nil and i1 == i2 then
+ return true
+ end
+ end
+ return false
+end
+
+--- Copy a file.
+-- @param src string: Pathname of source
+-- @param dest string: Pathname of destination
+-- @param perms string ("read" or "exec") or nil: Permissions for destination
+-- file or nil to use the source file permissions
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message.
+function fs_lua.copy(src, dest, perms)
+ assert(src and dest)
+ src = dir.normalize(src)
+ dest = dir.normalize(dest)
+ local destmode = lfs.attributes(dest, "mode")
+ if destmode == "directory" then
+ dest = dir.path(dest, dir.base_name(src))
+ end
+ if are_the_same_file(src, dest) then
+ return nil, "The source and destination are the same files"
+ end
+ local src_h, err = io.open(src, "rb")
+ if not src_h then return nil, err end
+ local dest_h, err = io.open(dest, "w+b")
+ if not dest_h then src_h:close() return nil, err end
+ while true do
+ local block = src_h:read(8192)
+ if not block then break end
+ local ok, err = dest_h:write(block)
+ if not ok then return nil, err end
+ end
+ src_h:close()
+ dest_h:close()
+
+ local fullattrs
+ if not perms then
+ fullattrs = lfs.attributes(src, "permissions")
+ end
+ if fullattrs and posix_ok then
+ return posix.chmod(dest, fullattrs)
+ else
+ if cfg.is_platform("unix") then
+ if not perms then
+ perms = fullattrs:match("x") and "exec" or "read"
+ end
+ return fs.set_permissions(dest, perms, "all")
+ else
+ return true
+ end
+ end
+end
+
+--- Implementation function for recursive copy of directory contents.
+-- Assumes paths are normalized.
+-- @param src string: Pathname of source
+-- @param dest string: Pathname of destination
+-- @param perms string ("read" or "exec") or nil: Optional permissions.
+-- If not given, permissions of the source are copied over to the destination.
+-- @return boolean or (boolean, string): true on success, false on failure
+local function recursive_copy(src, dest, perms)
+ local srcmode = lfs.attributes(src, "mode")
+
+ if srcmode == "file" then
+ local ok = fs.copy(src, dest, perms)
+ if not ok then return false end
+ elseif srcmode == "directory" then
+ local subdir = dir.path(dest, dir.base_name(src))
+ local ok, err = fs.make_dir(subdir)
+ if not ok then return nil, err end
+ if pcall(lfs.dir, src) == false then
+ return false
+ end
+ for file in lfs.dir(src) do
+ if file ~= "." and file ~= ".." then
+ local ok = recursive_copy(dir.path(src, file), subdir, perms)
+ if not ok then return false end
+ end
+ end
+ end
+ return true
+end
+
+--- Recursively copy the contents of a directory.
+-- @param src string: Pathname of source
+-- @param dest string: Pathname of destination
+-- @param perms string ("read" or "exec") or nil: Optional permissions.
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message.
+function fs_lua.copy_contents(src, dest, perms)
+ assert(src)
+ assert(dest)
+ src = dir.normalize(src)
+ dest = dir.normalize(dest)
+ if not fs.is_dir(src) then
+ return false, src .. " is not a directory"
+ end
+ if pcall(lfs.dir, src) == false then
+ return false, "Permission denied"
+ end
+ for file in lfs.dir(src) do
+ if file ~= "." and file ~= ".." then
+ local ok = recursive_copy(dir.path(src, file), dest, perms)
+ if not ok then
+ return false, "Failed copying "..src.." to "..dest
+ end
+ end
+ end
+ return true
+end
+
+--- Implementation function for recursive removal of directories.
+-- Assumes paths are normalized.
+-- @param name string: Pathname of file
+-- @return boolean or (boolean, string): true on success,
+-- or nil and an error message on failure.
+local function recursive_delete(name)
+ local ok = os.remove(name)
+ if ok then return true end
+ local pok, ok, err = pcall(function()
+ for file in lfs.dir(name) do
+ if file ~= "." and file ~= ".." then
+ local ok, err = recursive_delete(dir.path(name, file))
+ if not ok then return nil, err end
+ end
+ end
+ local ok, err = lfs.rmdir(name)
+ return ok, (not ok) and err
+ end)
+ if pok then
+ return ok, err
+ else
+ return pok, ok
+ end
+end
+
+--- Delete a file or a directory and all its contents.
+-- @param name string: Pathname of source
+-- @return nil
+function fs_lua.delete(name)
+ name = dir.normalize(name)
+ recursive_delete(name)
+end
+
+--- Internal implementation function for fs.dir.
+-- Yields a filename on each iteration.
+-- @param at string: directory to list
+-- @return nil or (nil and string): an error message on failure
+function fs_lua.dir_iterator(at)
+ local pok, iter, arg = pcall(lfs.dir, at)
+ if not pok then
+ return nil, iter
+ end
+ for file in iter, arg do
+ if file ~= "." and file ~= ".." then
+ coroutine.yield(file)
+ end
+ end
+end
+
+--- Implementation function for recursive find.
+-- Assumes paths are normalized.
+-- @param cwd string: Current working directory in recursion.
+-- @param prefix string: Auxiliary prefix string to form pathname.
+-- @param result table: Array of strings where results are collected.
+local function recursive_find(cwd, prefix, result)
+ local pok, iter, arg = pcall(lfs.dir, cwd)
+ if not pok then
+ return nil
+ end
+ for file in iter, arg do
+ if file ~= "." and file ~= ".." then
+ local item = prefix .. file
+ table.insert(result, item)
+ local pathname = dir.path(cwd, file)
+ if lfs.attributes(pathname, "mode") == "directory" then
+ recursive_find(pathname, item .. dir_sep, result)
+ end
+ end
+ end
+end
+
+--- Recursively scan the contents of a directory.
+-- @param at string or nil: directory to scan (will be the current
+-- directory if none is given).
+-- @return table: an array of strings with the filenames representing
+-- the contents of a directory.
+function fs_lua.find(at)
+ assert(type(at) == "string" or not at)
+ if not at then
+ at = fs.current_dir()
+ end
+ at = dir.normalize(at)
+ local result = {}
+ recursive_find(at, "", result)
+ return result
+end
+
+--- Test for existence of a file.
+-- @param file string: filename to test
+-- @return boolean: true if file exists, false otherwise.
+function fs_lua.exists(file)
+ assert(file)
+ file = dir.normalize(file)
+ return type(lfs.attributes(file)) == "table"
+end
+
+--- Test is pathname is a directory.
+-- @param file string: pathname to test
+-- @return boolean: true if it is a directory, false otherwise.
+function fs_lua.is_dir(file)
+ assert(file)
+ file = dir.normalize(file)
+ return lfs.attributes(file, "mode") == "directory"
+end
+
+--- Test is pathname is a regular file.
+-- @param file string: pathname to test
+-- @return boolean: true if it is a file, false otherwise.
+function fs_lua.is_file(file)
+ assert(file)
+ file = dir.normalize(file)
+ return lfs.attributes(file, "mode") == "file"
+end
+
+-- Set access and modification times for a file.
+-- @param filename File to set access and modification times for.
+-- @param time may be a number containing the format returned
+-- by os.time, or a table ready to be processed via os.time; if
+-- nil, current time is assumed.
+function fs_lua.set_time(file, time)
+ assert(time == nil or type(time) == "table" or type(time) == "number")
+ file = dir.normalize(file)
+ if type(time) == "table" then
+ time = os.time(time)
+ end
+ return lfs.touch(file, time)
+end
+
+else -- if not lfs_ok
+
+function fs_lua.exists(file)
+ assert(file)
+ -- check if file exists by attempting to open it
+ return util.exists(fs.absolute_name(file))
+end
+
+function fs_lua.file_age(_)
+ return math.huge
+end
+
+end
+
+---------------------------------------------------------------------
+-- lua-bz2 functions
+---------------------------------------------------------------------
+
+if bz2_ok then
+
+local function bunzip2_string(data)
+ local decompressor = bz2.initDecompress()
+ local output, err = decompressor:update(data)
+ if not output then
+ return nil, err
+ end
+ decompressor:close()
+ return output
+end
+
+--- Uncompresses a .bz2 file.
+-- @param infile string: pathname of .bz2 file to be extracted.
+-- @param outfile string or nil: pathname of output file to be produced.
+-- If not given, name is derived from input file.
+-- @return boolean: true on success; nil and error message on failure.
+function fs_lua.bunzip2(infile, outfile)
+ assert(type(infile) == "string")
+ assert(outfile == nil or type(outfile) == "string")
+ if not outfile then
+ outfile = infile:gsub("%.bz2$", "")
+ end
+
+ return fs.filter_file(bunzip2_string, infile, outfile)
+end
+
+end
+
+---------------------------------------------------------------------
+-- luarocks.tools.zip functions
+---------------------------------------------------------------------
+
+if zip_ok then
+
+function fs_lua.zip(zipfile, ...)
+ return zip.zip(zipfile, ...)
+end
+
+function fs_lua.unzip(zipfile)
+ return zip.unzip(zipfile)
+end
+
+function fs_lua.gunzip(infile, outfile)
+ return zip.gunzip(infile, outfile)
+end
+
+end
+
+---------------------------------------------------------------------
+-- LuaSocket functions
+---------------------------------------------------------------------
+
+if socket_ok then
+
+local ltn12 = require("ltn12")
+local luasec_ok, https = pcall(require, "ssl.https")
+
+if luasec_ok and not vers.compare_versions(https._VERSION, "1.0.3") then
+ luasec_ok = false
+ https = nil
+end
+
+local redirect_protocols = {
+ http = http,
+ https = luasec_ok and https,
+}
+
+local function request(url, method, http, loop_control) -- luacheck: ignore 431
+ local result = {}
+
+ if cfg.verbose then
+ print(method, url)
+ end
+
+ local proxy = os.getenv("http_proxy")
+ if type(proxy) ~= "string" then proxy = nil end
+ -- LuaSocket's http.request crashes when given URLs missing the scheme part.
+ if proxy and not proxy:find("://") then
+ proxy = "http://" .. proxy
+ end
+
+ if cfg.show_downloads then
+ io.write(method.." "..url.." ...\n")
+ end
+ local dots = 0
+ if cfg.connection_timeout and cfg.connection_timeout > 0 then
+ http.TIMEOUT = cfg.connection_timeout
+ end
+ local res, status, headers, err = http.request {
+ url = url,
+ proxy = proxy,
+ method = method,
+ redirect = false,
+ sink = ltn12.sink.table(result),
+ step = cfg.show_downloads and function(...)
+ io.write(".")
+ io.flush()
+ dots = dots + 1
+ if dots == 70 then
+ io.write("\n")
+ dots = 0
+ end
+ return ltn12.pump.step(...)
+ end,
+ headers = {
+ ["user-agent"] = cfg.user_agent.." via LuaSocket"
+ },
+ }
+ if cfg.show_downloads then
+ io.write("\n")
+ end
+ if not res then
+ return nil, status
+ elseif status == 301 or status == 302 then
+ local location = headers.location
+ if location then
+ local protocol, rest = dir.split_url(location)
+ if redirect_protocols[protocol] then
+ if not loop_control then
+ loop_control = {}
+ elseif loop_control[location] then
+ return nil, "Redirection loop -- broken URL?"
+ end
+ loop_control[url] = true
+ return request(location, method, redirect_protocols[protocol], loop_control)
+ else
+ return nil, "URL redirected to unsupported protocol - install luasec >= 1.1 to get HTTPS support.", "https"
+ end
+ end
+ return nil, err
+ elseif status ~= 200 then
+ return nil, err
+ else
+ return result, status, headers, err
+ end
+end
+
+local function write_timestamp(filename, data)
+ local fd = io.open(filename, "w")
+ if fd then
+ fd:write(data)
+ fd:close()
+ end
+end
+
+local function read_timestamp(filename)
+ local fd = io.open(filename, "r")
+ if fd then
+ local data = fd:read("*a")
+ fd:close()
+ return data
+ end
+end
+
+local function fail_with_status(filename, status, headers)
+ write_timestamp(filename .. ".unixtime", os.time())
+ write_timestamp(filename .. ".status", status)
+ return nil, status, headers
+end
+
+-- @param url string: URL to fetch.
+-- @param filename string: local filename of the file to fetch.
+-- @param http table: The library to use (http from LuaSocket or LuaSec)
+-- @param cache boolean: Whether to use a `.timestamp` file to check
+-- via the HTTP Last-Modified header if the full download is needed.
+-- @return (boolean | (nil, string, string?)): True if successful, or
+-- nil, error message and optionally HTTPS error in case of errors.
+local function http_request(url, filename, http, cache) -- luacheck: ignore 431
+ if cache then
+ local status = read_timestamp(filename..".status")
+ local timestamp = read_timestamp(filename..".timestamp")
+ if status or timestamp then
+ local unixtime = read_timestamp(filename..".unixtime")
+ if tonumber(unixtime) then
+ local diff = os.time() - tonumber(unixtime)
+ if status then
+ if diff < cfg.cache_fail_timeout then
+ return nil, status, {}
+ end
+ else
+ if diff < cfg.cache_timeout then
+ return true, nil, nil, true
+ end
+ end
+ end
+
+ local result, status, headers, err = request(url, "HEAD", http) -- luacheck: ignore 421
+ if not result then
+ return fail_with_status(filename, status, headers)
+ end
+ if status == 200 and headers["last-modified"] == timestamp then
+ write_timestamp(filename .. ".unixtime", os.time())
+ return true, nil, nil, true
+ end
+ end
+ end
+ local result, status, headers, err = request(url, "GET", http)
+ if not result then
+ if status then
+ return fail_with_status(filename, status, headers)
+ end
+ end
+ if cache and headers["last-modified"] then
+ write_timestamp(filename .. ".timestamp", headers["last-modified"])
+ write_timestamp(filename .. ".unixtime", os.time())
+ end
+ local file = io.open(filename, "wb")
+ if not file then return nil, 0, {} end
+ for _, data in ipairs(result) do
+ file:write(data)
+ end
+ file:close()
+ return true
+end
+
+local function ftp_request(url, filename)
+ local content, err = ftp.get(url)
+ if not content then
+ return false, err
+ end
+ local file = io.open(filename, "wb")
+ if not file then return false, err end
+ file:write(content)
+ file:close()
+ return true
+end
+
+local downloader_warning = false
+
+--- 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.
+-- @return (boolean, string, boolean):
+-- In case of success:
+-- * true
+-- * a string with the filename
+-- * true if the file was retrieved from local cache
+-- In case of failure:
+-- * false
+-- * error message
+function fs_lua.download(url, filename, cache)
+ assert(type(url) == "string")
+ assert(type(filename) == "string" or not filename)
+
+ filename = fs.absolute_name(filename or dir.base_name(url))
+
+ -- delegate to the configured downloader so we don't have to deal with whitelists
+ if os.getenv("no_proxy") then
+ return fs.use_downloader(url, filename, cache)
+ end
+
+ local ok, err, https_err, from_cache
+ if util.starts_with(url, "http:") then
+ ok, err, https_err, from_cache = http_request(url, filename, http, cache)
+ elseif util.starts_with(url, "ftp:") then
+ ok, err = ftp_request(url, filename)
+ elseif util.starts_with(url, "https:") then
+ -- skip LuaSec when proxy is enabled since it is not supported
+ if luasec_ok and not os.getenv("https_proxy") then
+ local _
+ ok, err, _, from_cache = http_request(url, filename, https, cache)
+ else
+ https_err = true
+ end
+ else
+ err = "Unsupported protocol"
+ end
+ if https_err then
+ local downloader, err = fs.which_tool("downloader")
+ if not downloader then
+ return nil, err
+ end
+ if not downloader_warning then
+ util.warning("falling back to "..downloader.." - install luasec >= 1.1 to get native HTTPS support")
+ downloader_warning = true
+ end
+ return fs.use_downloader(url, filename, cache)
+ elseif not ok then
+ return nil, err, "network"
+ end
+ return true, filename, from_cache
+end
+
+else --...if socket_ok == false then
+
+function fs_lua.download(url, filename, cache)
+ return fs.use_downloader(url, filename, cache)
+end
+
+end
+---------------------------------------------------------------------
+-- MD5 functions
+---------------------------------------------------------------------
+
+if md5_ok then
+
+-- Support the interface of lmd5 by lhf in addition to md5 by Roberto
+-- and the keplerproject.
+if not md5.sumhexa and md5.digest then
+ md5.sumhexa = function(msg)
+ return md5.digest(msg)
+ end
+end
+
+if md5.sumhexa then
+
+--- Get the MD5 checksum for a file.
+-- @param file string: The file to be computed.
+-- @return string: The MD5 checksum or nil + error
+function fs_lua.get_md5(file)
+ file = fs.absolute_name(file)
+ local file_handler = io.open(file, "rb")
+ if not file_handler then return nil, "Failed to open file for reading: "..file end
+ local computed = md5.sumhexa(file_handler:read("*a"))
+ file_handler:close()
+ if computed then return computed end
+ return nil, "Failed to compute MD5 hash for file "..file
+end
+
+end
+end
+
+---------------------------------------------------------------------
+-- POSIX functions
+---------------------------------------------------------------------
+
+function fs_lua._unix_rwx_to_number(rwx, neg)
+ local num = 0
+ neg = neg or false
+ for i = 1, 9 do
+ local c = rwx:sub(10 - i, 10 - i) == "-"
+ if neg == c then
+ num = num + 2^(i-1)
+ end
+ end
+ return math.floor(num)
+end
+
+if posix_ok then
+
+local octal_to_rwx = {
+ ["0"] = "---",
+ ["1"] = "--x",
+ ["2"] = "-w-",
+ ["3"] = "-wx",
+ ["4"] = "r--",
+ ["5"] = "r-x",
+ ["6"] = "rw-",
+ ["7"] = "rwx",
+}
+
+do
+ local umask_cache
+ function fs_lua._unix_umask()
+ if umask_cache then
+ return umask_cache
+ end
+ -- LuaPosix (as of 34.0.4) only returns the umask as rwx
+ local rwx = posix.umask()
+ local num = fs_lua._unix_rwx_to_number(rwx, true)
+ umask_cache = ("%03o"):format(num)
+ return umask_cache
+ end
+end
+
+function fs_lua.set_permissions(filename, mode, scope)
+ local perms, err = fs._unix_mode_scope_to_perms(mode, scope)
+ if err then
+ return false, err
+ end
+
+ -- LuaPosix (as of 5.1.15) does not support octal notation...
+ local new_perms = {}
+ for c in perms:sub(-3):gmatch(".") do
+ table.insert(new_perms, octal_to_rwx[c])
+ end
+ perms = table.concat(new_perms)
+ local err = posix.chmod(filename, perms)
+ return err == 0
+end
+
+function fs_lua.current_user()
+ return posix.getpwuid(posix.geteuid()).pw_name
+end
+
+function fs_lua.is_superuser()
+ return posix.geteuid() == 0
+end
+
+-- This call is not available on all systems, see #677
+if posix.mkdtemp then
+
+--- Create a temporary directory.
+-- @param name_pattern string: name pattern to use for avoiding conflicts
+-- when creating temporary directory.
+-- @return string or (nil, string): name of temporary directory or (nil, error message) on failure.
+function fs_lua.make_temp_dir(name_pattern)
+ assert(type(name_pattern) == "string")
+
+ return posix.mkdtemp(temp_dir_pattern(name_pattern) .. "-XXXXXX")
+end
+
+end -- if posix.mkdtemp
+
+end
+
+---------------------------------------------------------------------
+-- Other functions
+---------------------------------------------------------------------
+
+if not fs_lua.make_temp_dir then
+
+function fs_lua.make_temp_dir(name_pattern)
+ assert(type(name_pattern) == "string")
+
+ local ok, err
+ for _ = 1, 3 do
+ local name = temp_dir_pattern(name_pattern) .. tostring(math.random(10000000))
+ ok, err = fs.make_dir(name)
+ if ok then
+ return name
+ end
+ end
+
+ return nil, err
+end
+
+end
+
+--- Apply a patch.
+-- @param patchname string: The filename of the patch.
+-- @param patchdata string or nil: The actual patch as a string.
+-- @param create_delete boolean: Support creating and deleting files in a patch.
+function fs_lua.apply_patch(patchname, patchdata, create_delete)
+ local p, all_ok = patch.read_patch(patchname, patchdata)
+ if not all_ok then
+ return nil, "Failed reading patch "..patchname
+ end
+ if p then
+ return patch.apply_patch(p, 1, create_delete)
+ end
+end
+
+--- Move a file.
+-- @param src string: Pathname of source
+-- @param dest string: Pathname of destination
+-- @param perms string ("read" or "exec") or nil: Permissions for destination
+-- file or nil to use the source file permissions.
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message.
+function fs_lua.move(src, dest, perms)
+ assert(src and dest)
+ if fs.exists(dest) and not fs.is_dir(dest) then
+ return false, "File already exists: "..dest
+ end
+ local ok, err = fs.copy(src, dest, perms)
+ if not ok then
+ return false, err
+ end
+ fs.delete(src)
+ if fs.exists(src) then
+ return false, "Failed move: could not delete "..src.." after copy."
+ end
+ return true
+end
+
+local function get_local_tree()
+ for _, tree in ipairs(cfg.rocks_trees) do
+ if type(tree) == "table" and tree.name == "user" then
+ return fs.absolute_name(tree.root)
+ end
+ end
+end
+
+local function is_local_tree_in_env(local_tree)
+ local lua_path
+ if _VERSION == "Lua 5.1" then
+ lua_path = os.getenv("LUA_PATH")
+ else
+ lua_path = os.getenv("LUA_PATH_" .. _VERSION:sub(5):gsub("%.", "_"))
+ or os.getenv("LUA_PATH")
+ end
+ if lua_path and lua_path:match(local_tree, 1, true) then
+ return true
+ end
+end
+
+--- Check if user has write permissions for the command.
+-- Assumes the configuration variables under cfg have been previously set up.
+-- @param args table: the args table passed to run() drivers.
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message.
+function fs_lua.check_command_permissions(args)
+ local ok = true
+ local err = ""
+ if args._command_permissions_checked then
+ return true
+ end
+ for _, directory in ipairs { cfg.rocks_dir, cfg.deploy_lua_dir, cfg.deploy_bin_dir, cfg.deploy_lua_dir } do
+ if fs.exists(directory) then
+ if not fs.is_writable(directory) then
+ ok = false
+ err = "Your user does not have write permissions in " .. directory
+ break
+ end
+ else
+ local root = fs.root_of(directory)
+ local parent = directory
+ repeat
+ parent = dir.dir_name(parent)
+ if parent == "" then
+ parent = root
+ end
+ until parent == root or fs.exists(parent)
+ if not fs.is_writable(parent) then
+ ok = false
+ err = directory.." does not exist\nand your user does not have write permissions in " .. parent
+ break
+ end
+ end
+ end
+ if ok then
+ args._command_permissions_checked = true
+ return true
+ else
+ if args["local"] or cfg.local_by_default then
+ err = err .. "\n\nPlease check your permissions.\n"
+ else
+ local local_tree = get_local_tree()
+ if local_tree then
+ err = err .. "\n\nYou may want to run as a privileged user,"
+ .. "\nor use --local to install into your local tree at " .. local_tree
+ .. "\nor run 'luarocks config local_by_default true' to make --local the default.\n"
+
+ if not is_local_tree_in_env(local_tree) then
+ err = err .. "\n(You may need to configure your Lua package paths\nto use the local tree, see 'luarocks path --help')\n"
+ end
+ else
+ err = err .. "\n\nYou may want to run as a privileged user.\n"
+ end
+ end
+ return nil, err
+ end
+end
+
+--- Check whether a file is a Lua script
+-- When the file can be successfully compiled by the configured
+-- Lua interpreter, it's considered to be a valid Lua file.
+-- @param filename filename of file to check
+-- @return boolean true, if it is a Lua script, false otherwise
+function fs_lua.is_lua(filename)
+ filename = filename:gsub([[%\]],"/") -- normalize on fw slash to prevent escaping issues
+ local lua = fs.Q(cfg.variables.LUA) -- get lua interpreter configured
+ -- execute on configured interpreter, might not be the same as the interpreter LR is run on
+ local result = fs.execute_string(lua..[[ -e "if loadfile(']]..filename..[[') then os.exit(0) else os.exit(1) end"]])
+ return (result == true)
+end
+
+--- Unpack an archive.
+-- Extract the contents of an archive, detecting its format by
+-- filename extension.
+-- @param archive string: Filename of archive.
+-- @return boolean or (boolean, string): true on success, false and an error message on failure.
+function fs_lua.unpack_archive(archive)
+ assert(type(archive) == "string")
+
+ local ok, err
+ archive = fs.absolute_name(archive)
+ if archive:match("%.tar%.gz$") then
+ local tar_filename = archive:gsub("%.gz$", "")
+ ok, err = fs.gunzip(archive, tar_filename)
+ if ok then
+ ok, err = tar.untar(tar_filename, ".")
+ end
+ elseif archive:match("%.tgz$") then
+ local tar_filename = archive:gsub("%.tgz$", ".tar")
+ ok, err = fs.gunzip(archive, tar_filename)
+ if ok then
+ ok, err = tar.untar(tar_filename, ".")
+ end
+ elseif archive:match("%.tar%.bz2$") then
+ local tar_filename = archive:gsub("%.bz2$", "")
+ ok, err = fs.bunzip2(archive, tar_filename)
+ if ok then
+ ok, err = tar.untar(tar_filename, ".")
+ end
+ elseif archive:match("%.zip$") then
+ ok, err = fs.unzip(archive)
+ elseif archive:match("%.lua$") or archive:match("%.c$") then
+ -- Ignore .lua and .c files; they don't need to be extracted.
+ return true
+ else
+ return false, "Couldn't extract archive "..archive..": unrecognized filename extension"
+ end
+ if not ok then
+ return false, "Failed extracting "..archive..": "..err
+ end
+ return true
+end
+
+return fs_lua