summaryrefslogtreecommitdiff
path: root/src/luarocks/fs
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
fetch tarballHEADmaster
Diffstat (limited to 'src/luarocks/fs')
-rw-r--r--src/luarocks/fs/linux.lua50
-rw-r--r--src/luarocks/fs/lua.lua1307
-rw-r--r--src/luarocks/fs/macosx.lua50
-rw-r--r--src/luarocks/fs/tools.lua222
-rw-r--r--src/luarocks/fs/unix.lua266
-rw-r--r--src/luarocks/fs/unix/tools.lua353
-rw-r--r--src/luarocks/fs/win32.lua384
-rw-r--r--src/luarocks/fs/win32/tools.lua330
8 files changed, 2962 insertions, 0 deletions
diff --git a/src/luarocks/fs/linux.lua b/src/luarocks/fs/linux.lua
new file mode 100644
index 0000000..c1b057c
--- /dev/null
+++ b/src/luarocks/fs/linux.lua
@@ -0,0 +1,50 @@
+--- Linux-specific implementation of filesystem and platform abstractions.
+local linux = {}
+
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+
+function linux.is_dir(file)
+ file = fs.absolute_name(file)
+ file = dir.normalize(file) .. "/."
+ local fd, _, code = io.open(file, "r")
+ if code == 2 then -- "No such file or directory"
+ return false
+ end
+ if code == 20 then -- "Not a directory", regardless of permissions
+ return false
+ end
+ if code == 13 then -- "Permission denied", but is a directory
+ return true
+ end
+ if fd then
+ local _, _, ecode = fd:read(1)
+ fd:close()
+ if ecode == 21 then -- "Is a directory"
+ return true
+ end
+ end
+ return false
+end
+
+function linux.is_file(file)
+ file = fs.absolute_name(file)
+ if fs.is_dir(file) then
+ return false
+ end
+ file = dir.normalize(file)
+ local fd, _, code = io.open(file, "r")
+ if code == 2 then -- "No such file or directory"
+ return false
+ end
+ if code == 13 then -- "Permission denied", but it exists
+ return true
+ end
+ if fd then
+ fd:close()
+ return true
+ end
+ return false
+end
+
+return linux
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
diff --git a/src/luarocks/fs/macosx.lua b/src/luarocks/fs/macosx.lua
new file mode 100644
index 0000000..b71e7f1
--- /dev/null
+++ b/src/luarocks/fs/macosx.lua
@@ -0,0 +1,50 @@
+--- macOS-specific implementation of filesystem and platform abstractions.
+local macosx = {}
+
+local fs = require("luarocks.fs")
+local dir = require("luarocks.dir")
+
+function macosx.is_dir(file)
+ file = fs.absolute_name(file)
+ file = dir.normalize(file) .. "/."
+ local fd, _, code = io.open(file, "r")
+ if code == 2 then -- "No such file or directory"
+ return false
+ end
+ if code == 20 then -- "Not a directory", regardless of permissions
+ return false
+ end
+ if code == 13 then -- "Permission denied", but is a directory
+ return true
+ end
+ if fd then
+ local _, _, ecode = fd:read(1)
+ fd:close()
+ if ecode == 21 then -- "Is a directory"
+ return true
+ end
+ end
+ return false
+end
+
+function macosx.is_file(file)
+ file = fs.absolute_name(file)
+ if fs.is_dir(file) then
+ return false
+ end
+ file = dir.normalize(file)
+ local fd, _, code = io.open(file, "r")
+ if code == 2 then -- "No such file or directory"
+ return false
+ end
+ if code == 13 then -- "Permission denied", but it exists
+ return true
+ end
+ if fd then
+ fd:close()
+ return true
+ end
+ return false
+end
+
+return macosx
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
diff --git a/src/luarocks/fs/unix.lua b/src/luarocks/fs/unix.lua
new file mode 100644
index 0000000..41a9ba8
--- /dev/null
+++ b/src/luarocks/fs/unix.lua
@@ -0,0 +1,266 @@
+
+--- Unix implementation of filesystem and platform abstractions.
+local unix = {}
+
+local fs = require("luarocks.fs")
+
+local cfg = require("luarocks.core.cfg")
+local dir = require("luarocks.dir")
+local path = require("luarocks.path")
+local util = require("luarocks.util")
+
+--- Annotate command string for quiet execution.
+-- @param cmd string: A command-line string.
+-- @return string: The command-line, with silencing annotation.
+function unix.quiet(cmd)
+ return cmd.." 1> /dev/null 2> /dev/null"
+end
+
+--- Annotate command string for execution with quiet stderr.
+-- @param cmd string: A command-line string.
+-- @return string: The command-line, with stderr silencing annotation.
+function unix.quiet_stderr(cmd)
+ return cmd.." 2> /dev/null"
+end
+
+--- Quote argument for shell processing.
+-- Adds single quotes and escapes.
+-- @param arg string: Unquoted argument.
+-- @return string: Quoted argument.
+function unix.Q(arg)
+ assert(type(arg) == "string")
+ return "'" .. arg:gsub("'", "'\\''") .. "'"
+end
+
+--- Return an absolute pathname from a potentially relative one.
+-- @param pathname string: pathname to convert.
+-- @param relative_to string or nil: path to prepend when making
+-- pathname absolute, or the current dir in the dir stack if
+-- not given.
+-- @return string: The pathname converted to absolute.
+function unix.absolute_name(pathname, relative_to)
+ assert(type(pathname) == "string")
+ assert(type(relative_to) == "string" or not relative_to)
+
+ local unquoted = pathname:match("^['\"](.*)['\"]$")
+ if unquoted then
+ pathname = unquoted
+ end
+
+ relative_to = relative_to or fs.current_dir()
+ if pathname:sub(1,1) == "/" then
+ return dir.normalize(pathname)
+ else
+ return dir.path(relative_to, pathname)
+ end
+end
+
+--- Return the root directory for the given path.
+-- In Unix, root is always "/".
+-- @param pathname string: pathname to use.
+-- @return string: The root of the given pathname.
+function unix.root_of(_)
+ return "/"
+end
+
+--- Create a wrapper to make a script executable from the command-line.
+-- @param script string: Pathname of script to be made executable.
+-- @param target string: wrapper target pathname (without wrapper suffix).
+-- @param name string: rock name to be used in loader context.
+-- @param version string: rock version to be used in loader context.
+-- @return boolean or (nil, string): True if succeeded, or nil and
+-- an error message.
+function unix.wrap_script(script, target, deps_mode, name, version, ...)
+ assert(type(script) == "string" or not script)
+ assert(type(target) == "string")
+ assert(type(deps_mode) == "string")
+ assert(type(name) == "string" or not name)
+ assert(type(version) == "string" or not version)
+
+ local wrapper = io.open(target, "w")
+ if not wrapper then
+ return nil, "Could not open "..target.." for writing."
+ end
+
+ local lpath, lcpath = path.package_paths(deps_mode)
+
+ local luainit = {
+ "package.path="..util.LQ(lpath..";").."..package.path",
+ "package.cpath="..util.LQ(lcpath..";").."..package.cpath",
+ }
+
+ local remove_interpreter = false
+ local base = dir.base_name(target):gsub("%..*$", "")
+ if base == "luarocks" or base == "luarocks-admin" then
+ if cfg.is_binary then
+ remove_interpreter = true
+ end
+ luainit = {
+ "package.path="..util.LQ(package.path),
+ "package.cpath="..util.LQ(package.cpath),
+ }
+ end
+
+ if name and version then
+ local addctx = "local k,l,_=pcall(require,"..util.LQ("luarocks.loader")..") _=k " ..
+ "and l.add_context("..util.LQ(name)..","..util.LQ(version)..")"
+ table.insert(luainit, addctx)
+ end
+
+ local argv = {
+ fs.Q(cfg.variables["LUA"]),
+ "-e",
+ fs.Q(table.concat(luainit, ";")),
+ script and fs.Q(script) or [[$([ "$*" ] || echo -i)]],
+ ...
+ }
+ if remove_interpreter then
+ table.remove(argv, 1)
+ table.remove(argv, 1)
+ table.remove(argv, 1)
+ end
+
+ wrapper:write("#!/bin/sh\n\n")
+ wrapper:write("LUAROCKS_SYSCONFDIR="..fs.Q(cfg.sysconfdir) .. " ")
+ wrapper:write("exec "..table.concat(argv, " ")..' "$@"\n')
+ wrapper:close()
+
+ if fs.set_permissions(target, "exec", "all") then
+ return true
+ else
+ return nil, "Could not make "..target.." executable."
+ end
+end
+
+--- Check if a file (typically inside path.bin_dir) is an actual binary
+-- or a Lua wrapper.
+-- @param filename string: the file name with full path.
+-- @return boolean: returns true if file is an actual binary
+-- (or if it couldn't check) or false if it is a Lua wrapper.
+function unix.is_actual_binary(filename)
+ if filename:match("%.lua$") then
+ return false
+ end
+ local file = io.open(filename)
+ if not file then
+ return true
+ end
+ local first = file:read(2)
+ file:close()
+ if not first then
+ util.warning("could not read "..filename)
+ return true
+ end
+ return first ~= "#!"
+end
+
+function unix.copy_binary(filename, dest)
+ return fs.copy(filename, dest, "exec")
+end
+
+--- Move a file on top of the other.
+-- The new file ceases to exist under its original name,
+-- and takes over the name of the old file.
+-- On Unix this is done through a single rename operation.
+-- @param old_file The name of the original file,
+-- which will be the new name of new_file.
+-- @param new_file The name of the new file,
+-- which will replace old_file.
+-- @return boolean or (nil, string): True if succeeded, or nil and
+-- an error message.
+function unix.replace_file(old_file, new_file)
+ return os.rename(new_file, old_file)
+end
+
+function unix.tmpname()
+ return os.tmpname()
+end
+
+function unix.export_cmd(var, val)
+ return ("export %s='%s'"):format(var, val)
+end
+
+local octal_to_rwx = {
+ ["0"] = "---",
+ ["1"] = "--x",
+ ["2"] = "-w-",
+ ["3"] = "-wx",
+ ["4"] = "r--",
+ ["5"] = "r-x",
+ ["6"] = "rw-",
+ ["7"] = "rwx",
+}
+local rwx_to_octal = {}
+for octal, rwx in pairs(octal_to_rwx) do
+ rwx_to_octal[rwx] = octal
+end
+--- Moderate the given permissions based on the local umask
+-- @param perms string: permissions to moderate
+-- @return string: the moderated permissions
+local function apply_umask(perms)
+ local umask = fs._unix_umask()
+
+ local moderated_perms = ""
+ for i = 1, 3 do
+ local p_rwx = octal_to_rwx[perms:sub(i, i)]
+ local u_rwx = octal_to_rwx[umask:sub(i, i)]
+ local new_perm = ""
+ for j = 1, 3 do
+ local p_val = p_rwx:sub(j, j)
+ local u_val = u_rwx:sub(j, j)
+ if p_val == u_val then
+ new_perm = new_perm .. "-"
+ else
+ new_perm = new_perm .. p_val
+ end
+ end
+ moderated_perms = moderated_perms .. rwx_to_octal[new_perm]
+ end
+ return moderated_perms
+end
+
+function unix._unix_mode_scope_to_perms(mode, scope)
+ local perms
+ if mode == "read" and scope == "user" then
+ perms = apply_umask("600")
+ elseif mode == "exec" and scope == "user" then
+ perms = apply_umask("700")
+ elseif mode == "read" and scope == "all" then
+ perms = apply_umask("666")
+ elseif mode == "exec" and scope == "all" then
+ perms = apply_umask("777")
+ else
+ return false, "Invalid permission " .. mode .. " for " .. scope
+ end
+ return perms
+end
+
+function unix.system_cache_dir()
+ if fs.is_dir("/var/cache") then
+ return "/var/cache"
+ end
+ return dir.path(fs.system_temp_dir(), "cache")
+end
+
+function unix.search_in_path(program)
+ if program:match("/") then
+ local fd = io.open(dir.path(program), "r")
+ if fd then
+ fd:close()
+ return true, program
+ end
+
+ return false
+ end
+
+ for d in (os.getenv("PATH") or ""):gmatch("([^:]+)") do
+ local fd = io.open(dir.path(d, program), "r")
+ if fd then
+ fd:close()
+ return true, d
+ end
+ end
+ return false
+end
+
+return unix
diff --git a/src/luarocks/fs/unix/tools.lua b/src/luarocks/fs/unix/tools.lua
new file mode 100644
index 0000000..d733473
--- /dev/null
+++ b/src/luarocks/fs/unix/tools.lua
@@ -0,0 +1,353 @@
+
+--- fs operations implemented with third-party tools for Unix platform abstractions.
+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 })
+
+--- Adds prefix to command to make it run from a directory.
+-- @param directory string: Path to a directory.
+-- @param cmd string: A command-line string.
+-- @return string: The command-line with prefix.
+function tools.command_at(directory, cmd)
+ return "cd " .. fs.Q(fs.absolute_name(directory)) .. " && " .. cmd
+end
+
+--- Create a directory if it does not already exist.
+-- If any of the higher levels in the path name does not exist
+-- too, they are created as well.
+-- @param directory string: pathname of directory to create.
+-- @return boolean: true on success, false on failure.
+function tools.make_dir(directory)
+ assert(directory)
+ local ok, err = fs.execute(vars.MKDIR.." -p", directory)
+ if not ok then
+ err = "failed making directory "..directory
+ end
+ return ok, err
+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 directory string: pathname of directory to remove.
+function tools.remove_dir_if_empty(directory)
+ assert(directory)
+ fs.execute_quiet(vars.RMDIR, directory)
+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 directory string: pathname of directory to remove.
+function tools.remove_dir_tree_if_empty(directory)
+ assert(directory)
+ fs.execute_quiet(vars.RMDIR, "-p", directory)
+end
+
+--- Copy a file.
+-- @param src string: Pathname of source
+-- @param dest string: Pathname of destination
+-- @param perm string ("read" or "exec") or nil: Permissions for destination
+-- file or nil to use the source permissions
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message.
+function tools.copy(src, dest, perm)
+ assert(src and dest)
+ if fs.execute(vars.CP, src, dest) then
+ if perm then
+ if fs.is_dir(dest) then
+ dest = dir.path(dest, dir.base_name(src))
+ end
+ if fs.set_permissions(dest, perm, "all") then
+ return true
+ else
+ return false, "Failed setting permissions of "..dest
+ end
+ end
+ return true
+ else
+ return false, "Failed copying "..src.." to "..dest
+ end
+end
+
+--- Recursively copy the contents of a directory.
+-- @param src string: Pathname of source
+-- @param dest string: Pathname of destination
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message.
+function tools.copy_contents(src, dest)
+ assert(src and dest)
+ if not fs.is_dir(src) then
+ return false, src .. " is not a directory"
+ end
+ if fs.make_dir(dest) and fs.execute_quiet(vars.CP.." -pPR "..fs.Q(src).."/* "..fs.Q(dest)) then
+ return true
+ else
+ return false, "Failed copying "..src.." to "..dest
+ end
+end
+--- Delete a file or a directory and all its contents.
+-- For safety, this only accepts absolute paths.
+-- @param arg string: Pathname of source
+-- @return nil
+function tools.delete(arg)
+ assert(arg)
+ assert(arg:sub(1,1) == "/")
+ fs.execute_quiet(vars.RM, "-rf", arg)
+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 tools.find(at)
+ assert(type(at) == "string" or not at)
+ if not at then
+ at = fs.current_dir()
+ end
+ if not fs.is_dir(at) then
+ return {}
+ end
+ local result = {}
+ local pipe = io.popen(fs.command_at(at, fs.quiet_stderr(vars.FIND.." *")))
+ for file in pipe:lines() do
+ table.insert(result, file)
+ end
+ pipe:close()
+ return result
+end
+
+--- Compress files in a .zip archive.
+-- @param zipfile string: pathname of .zip archive to be created.
+-- @param ... Filenames to be stored in the archive are given as
+-- additional arguments.
+-- @return boolean: true on success, nil and error message on failure.
+function tools.zip(zipfile, ...)
+ local ok, err = fs.is_tool_available(vars.ZIP, "zip")
+ if not ok then
+ return nil, err
+ end
+ if fs.execute_quiet(vars.ZIP.." -r", zipfile, ...) then
+ return true
+ else
+ return nil, "failed compressing " .. zipfile
+ end
+end
+
+--- Uncompress files from a .zip archive.
+-- @param zipfile string: pathname of .zip archive to be extracted.
+-- @return boolean: true on success, nil and error message on failure.
+function tools.unzip(zipfile)
+ assert(zipfile)
+ local ok, err = fs.is_tool_available(vars.UNZIP, "unzip")
+ if not ok then
+ return nil, err
+ end
+ if fs.execute_quiet(vars.UNZIP, zipfile) then
+ return true
+ else
+ return nil, "failed extracting " .. zipfile
+ end
+end
+
+local function uncompress(default_ext, program, infile, outfile)
+ assert(type(infile) == "string")
+ assert(outfile == nil or type(outfile) == "string")
+ if not outfile then
+ outfile = infile:gsub("%."..default_ext.."$", "")
+ end
+ if fs.execute(fs.Q(program).." -c "..fs.Q(infile).." > "..fs.Q(outfile)) then
+ return true
+ else
+ return nil, "failed extracting " .. infile
+ end
+end
+
+--- Uncompresses a .gz file.
+-- @param infile string: pathname of .gz 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 tools.gunzip(infile, outfile)
+ return uncompress("gz", "gunzip", infile, outfile)
+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 tools.bunzip2(infile, outfile)
+ return uncompress("bz2", "bunzip2", infile, outfile)
+end
+
+do
+ local function rwx_to_octal(rwx)
+ return (rwx:match "r" and 4 or 0)
+ + (rwx:match "w" and 2 or 0)
+ + (rwx:match "x" and 1 or 0)
+ end
+ local umask_cache
+ function tools._unix_umask()
+ if umask_cache then
+ return umask_cache
+ end
+ local fd = assert(io.popen("umask -S"))
+ local umask = assert(fd:read("*a"))
+ fd:close()
+ local u, g, o = umask:match("u=([rwx]*),g=([rwx]*),o=([rwx]*)")
+ if not u then
+ error("invalid umask result")
+ end
+ umask_cache = string.format("%d%d%d",
+ 7 - rwx_to_octal(u),
+ 7 - rwx_to_octal(g),
+ 7 - rwx_to_octal(o))
+ return umask_cache
+ end
+end
+
+--- Set permissions for file or directory
+-- @param filename string: filename whose permissions are to be modified
+-- @param mode string ("read" or "exec"): permissions to set
+-- @param scope string ("user" or "all"): the user(s) to whom the permission applies
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message
+function tools.set_permissions(filename, mode, scope)
+ assert(filename and mode and scope)
+
+ local perms, err = fs._unix_mode_scope_to_perms(mode, scope)
+ if err then
+ return false, err
+ end
+
+ return fs.execute(vars.CHMOD, perms, filename)
+end
+
+function tools.browser(url)
+ return fs.execute(cfg.web_browser, url)
+end
+
+-- Set access and modification times for a file.
+-- @param filename File to set access and modification times for.
+-- @param time may be a string or 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 tools.set_time(file, time)
+ assert(time == nil or type(time) == "table" or type(time) == "number")
+ file = dir.normalize(file)
+ local flag = ""
+ if type(time) == "number" then
+ time = os.date("*t", time)
+ end
+ if type(time) == "table" then
+ flag = ("-t %04d%02d%02d%02d%02d.%02d"):format(time.year, time.month, time.day, time.hour, time.min, time.sec)
+ end
+ return fs.execute(vars.TOUCH .. " " .. flag, file)
+end
+
+--- 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 tools.make_temp_dir(name_pattern)
+ assert(type(name_pattern) == "string")
+ name_pattern = dir.normalize(name_pattern)
+
+ local template = (os.getenv("TMPDIR") or "/tmp") .. "/luarocks_" .. name_pattern:gsub("/", "_") .. "-XXXXXX"
+ local pipe = io.popen(vars.MKTEMP.." -d "..fs.Q(template))
+ local dirname = pipe:read("*l")
+ pipe:close()
+ if dirname and dirname:match("^/") then
+ return dirname
+ end
+ return nil, "Failed to create temporary directory "..tostring(dirname)
+end
+
+--- Test is file/directory exists
+-- @param file string: filename to test
+-- @return boolean: true if file exists, false otherwise.
+function tools.exists(file)
+ assert(file)
+ return fs.execute(vars.TEST, "-e", file)
+end
+
+--- Test is pathname is a directory.
+-- @param file string: pathname to test
+-- @return boolean: true if it is a directory, false otherwise.
+function tools.is_dir(file)
+ assert(file)
+ return fs.execute(vars.TEST, "-d", file)
+end
+
+--- Test is pathname is a regular file.
+-- @param file string: pathname to test
+-- @return boolean: true if it is a regular file, false otherwise.
+function tools.is_file(file)
+ assert(file)
+ return fs.execute(vars.TEST, "-f", file)
+end
+
+function tools.current_user()
+ local user = os.getenv("USER")
+ if user then
+ return user
+ end
+ local pd = io.popen("whoami", "r")
+ if not pd then
+ return ""
+ end
+ user = pd:read("*l")
+ pd:close()
+ return user
+end
+
+function tools.is_superuser()
+ return fs.current_user() == "root"
+end
+
+function tools.lock_access(dirname, force)
+ local ok, err = fs.make_dir(dirname)
+ if not ok then
+ return nil, err
+ end
+
+ local tempfile = dir.path(dirname, ".lock.tmp." .. tostring(math.random(100000000)))
+
+ local fd, fderr = io.open(tempfile, "w")
+ if not fd then
+ return nil, "failed opening temp file " .. tempfile .. " for locking: " .. fderr
+ end
+
+ local ok, werr = fd:write("lock file for " .. dirname)
+ if not ok then
+ return nil, "failed writing temp file " .. tempfile .. " for locking: " .. werr
+ end
+
+ fd:close()
+
+ local lockfile = dir.path(dirname, "lockfile.lfs")
+
+ local force_flag = force and " -f" or ""
+
+ if fs.execute(vars.LN .. force_flag, tempfile, lockfile) then
+ return {
+ tempfile = tempfile,
+ lockfile = lockfile,
+ }
+ else
+ return nil, "File exists" -- same message as luafilesystem
+ end
+end
+
+function tools.unlock_access(lock)
+ os.remove(lock.lockfile)
+ os.remove(lock.tempfile)
+end
+
+return tools
diff --git a/src/luarocks/fs/win32.lua b/src/luarocks/fs/win32.lua
new file mode 100644
index 0000000..bba6873
--- /dev/null
+++ b/src/luarocks/fs/win32.lua
@@ -0,0 +1,384 @@
+--- Windows implementation of filesystem and platform abstractions.
+-- Download http://unxutils.sourceforge.net/ for Windows GNU utilities
+-- used by this module.
+local win32 = {}
+
+local fs = require("luarocks.fs")
+
+local cfg = require("luarocks.core.cfg")
+local dir = require("luarocks.dir")
+local path = require("luarocks.path")
+local util = require("luarocks.util")
+
+-- Monkey patch io.popen and os.execute to make sure quoting
+-- works as expected.
+-- See http://lua-users.org/lists/lua-l/2013-11/msg00367.html
+local _prefix = "type NUL && "
+local _popen, _execute = io.popen, os.execute
+
+-- luacheck: push globals io os
+io.popen = function(cmd, ...) return _popen(_prefix..cmd, ...) end
+os.execute = function(cmd, ...) return _execute(_prefix..cmd, ...) end
+-- luacheck: pop
+
+--- Annotate command string for quiet execution.
+-- @param cmd string: A command-line string.
+-- @return string: The command-line, with silencing annotation.
+function win32.quiet(cmd)
+ return cmd.." 2> NUL 1> NUL"
+end
+
+--- Annotate command string for execution with quiet stderr.
+-- @param cmd string: A command-line string.
+-- @return string: The command-line, with stderr silencing annotation.
+function win32.quiet_stderr(cmd)
+ return cmd.." 2> NUL"
+end
+
+function win32.execute_env(env, command, ...)
+ assert(type(command) == "string")
+ local cmdstr = {}
+ for var, val in pairs(env) do
+ table.insert(cmdstr, fs.export_cmd(var, val))
+ end
+ table.insert(cmdstr, fs.quote_args(command, ...))
+ return fs.execute_string(table.concat(cmdstr, " & "))
+end
+
+-- Split path into drive, root and the rest.
+-- Example: "c:\\hello\\world" becomes "c:" "\\" "hello\\world"
+-- if any part is missing from input, it becomes an empty string.
+local function split_root(pathname)
+ local drive = ""
+ local root = ""
+ local rest
+
+ local unquoted = pathname:match("^['\"](.*)['\"]$")
+ if unquoted then
+ pathname = unquoted
+ end
+
+ if pathname:match("^.:") then
+ drive = pathname:sub(1, 2)
+ pathname = pathname:sub(3)
+ end
+
+ if pathname:match("^[\\/]") then
+ root = pathname:sub(1, 1)
+ rest = pathname:sub(2)
+ else
+ rest = pathname
+ end
+
+ return drive, root, rest
+end
+
+--- Quote argument for shell processing. Fixes paths on Windows.
+-- Adds double quotes and escapes.
+-- @param arg string: Unquoted argument.
+-- @return string: Quoted argument.
+function win32.Q(arg)
+ assert(type(arg) == "string")
+ -- Use Windows-specific directory separator for paths.
+ -- Paths should be converted to absolute by now.
+ local drive, root, rest = split_root(arg)
+ if root ~= "" then
+ arg = arg:gsub("/", "\\")
+ end
+ if arg == "\\" then
+ return '\\' -- CHDIR needs special handling for root dir
+ end
+ -- URLs and anything else
+ arg = arg:gsub('\\(\\*)"', '\\%1%1"')
+ arg = arg:gsub('\\+$', '%0%0')
+ arg = arg:gsub('"', '\\"')
+ arg = arg:gsub('(\\*)%%', '%1%1"%%"')
+ return '"' .. arg .. '"'
+end
+
+--- Quote argument for shell processing in batch files.
+-- Adds double quotes and escapes.
+-- @param arg string: Unquoted argument.
+-- @return string: Quoted argument.
+function win32.Qb(arg)
+ assert(type(arg) == "string")
+ -- Use Windows-specific directory separator for paths.
+ -- Paths should be converted to absolute by now.
+ local drive, root, rest = split_root(arg)
+ if root ~= "" then
+ arg = arg:gsub("/", "\\")
+ end
+ if arg == "\\" then
+ return '\\' -- CHDIR needs special handling for root dir
+ end
+ -- URLs and anything else
+ arg = arg:gsub('\\(\\*)"', '\\%1%1"')
+ arg = arg:gsub('\\+$', '%0%0')
+ arg = arg:gsub('"', '\\"')
+ arg = arg:gsub('%%', '%%%%')
+ return '"' .. arg .. '"'
+end
+
+--- Return an absolute pathname from a potentially relative one.
+-- @param pathname string: pathname to convert.
+-- @param relative_to string or nil: path to prepend when making
+-- pathname absolute, or the current dir in the dir stack if
+-- not given.
+-- @return string: The pathname converted to absolute.
+function win32.absolute_name(pathname, relative_to)
+ assert(type(pathname) == "string")
+ assert(type(relative_to) == "string" or not relative_to)
+
+ relative_to = (relative_to or fs.current_dir()):gsub("[\\/]*$", "")
+ local drive, root, rest = split_root(pathname)
+ if root:match("[\\/]$") then
+ -- It's an absolute path already. Ensure is not quoted.
+ return dir.normalize(drive .. root .. rest)
+ else
+ -- It's a relative path, join it with base path.
+ -- This drops drive letter from paths like "C:foo".
+ return dir.path(relative_to, rest)
+ end
+end
+
+--- Return the root directory for the given path.
+-- For example, for "c:\hello", returns "c:\"
+-- @param pathname string: pathname to use.
+-- @return string: The root of the given pathname.
+function win32.root_of(pathname)
+ local drive, root, rest = split_root(fs.absolute_name(pathname))
+ return drive .. root
+end
+
+--- Create a wrapper to make a script executable from the command-line.
+-- @param script string: Pathname of script to be made executable.
+-- @param target string: wrapper target pathname (without wrapper suffix).
+-- @param name string: rock name to be used in loader context.
+-- @param version string: rock version to be used in loader context.
+-- @return boolean or (nil, string): True if succeeded, or nil and
+-- an error message.
+function win32.wrap_script(script, target, deps_mode, name, version, ...)
+ assert(type(script) == "string" or not script)
+ assert(type(target) == "string")
+ assert(type(deps_mode) == "string")
+ assert(type(name) == "string" or not name)
+ assert(type(version) == "string" or not version)
+
+ local wrapper = io.open(target, "wb")
+ if not wrapper then
+ return nil, "Could not open "..target.." for writing."
+ end
+
+ local lpath, lcpath = path.package_paths(deps_mode)
+
+ local luainit = {
+ "package.path="..util.LQ(lpath..";").."..package.path",
+ "package.cpath="..util.LQ(lcpath..";").."..package.cpath",
+ }
+
+ local remove_interpreter = false
+ local base = dir.base_name(target):gsub("%..*$", "")
+ if base == "luarocks" or base == "luarocks-admin" then
+ if cfg.is_binary then
+ remove_interpreter = true
+ end
+ luainit = {
+ "package.path="..util.LQ(package.path),
+ "package.cpath="..util.LQ(package.cpath),
+ }
+ end
+
+ if name and version then
+ local addctx = "local k,l,_=pcall(require,'luarocks.loader') _=k " ..
+ "and l.add_context('"..name.."','"..version.."')"
+ table.insert(luainit, addctx)
+ end
+
+ local argv = {
+ fs.Qb(cfg.variables["LUA"]),
+ "-e",
+ fs.Qb(table.concat(luainit, ";")),
+ script and fs.Qb(script) or "%I%",
+ ...
+ }
+ if remove_interpreter then
+ table.remove(argv, 1)
+ table.remove(argv, 1)
+ table.remove(argv, 1)
+ end
+
+ wrapper:write("@echo off\r\n")
+ wrapper:write("setlocal\r\n")
+ if not script then
+ wrapper:write([[IF "%*"=="" (set I=-i) ELSE (set I=)]] .. "\r\n")
+ end
+ wrapper:write("set "..fs.Qb("LUAROCKS_SYSCONFDIR="..cfg.sysconfdir) .. "\r\n")
+ wrapper:write(table.concat(argv, " ") .. " %*\r\n")
+ wrapper:write("exit /b %ERRORLEVEL%\r\n")
+ wrapper:close()
+ return true
+end
+
+function win32.is_actual_binary(name)
+ name = name:lower()
+ if name:match("%.bat$") or name:match("%.exe$") then
+ return true
+ end
+ return false
+end
+
+function win32.copy_binary(filename, dest)
+ local ok, err = fs.copy(filename, dest)
+ if not ok then
+ return nil, err
+ end
+ local exe_pattern = "%.[Ee][Xx][Ee]$"
+ local base = dir.base_name(filename)
+ dest = dir.dir_name(dest)
+ if base:match(exe_pattern) then
+ base = base:gsub(exe_pattern, ".lua")
+ local helpname = dest.."\\"..base
+ local helper = io.open(helpname, "w")
+ if not helper then
+ return nil, "Could not open "..helpname.." for writing."
+ end
+ helper:write('package.path=\"'..package.path:gsub("\\","\\\\")..';\"..package.path\n')
+ helper:write('package.cpath=\"'..package.path:gsub("\\","\\\\")..';\"..package.cpath\n')
+ helper:close()
+ end
+ return true
+end
+
+--- Move a file on top of the other.
+-- The new file ceases to exist under its original name,
+-- and takes over the name of the old file.
+-- On Windows this is done by removing the original file and
+-- renaming the new file to its original name.
+-- @param old_file The name of the original file,
+-- which will be the new name of new_file.
+-- @param new_file The name of the new file,
+-- which will replace old_file.
+-- @return boolean or (nil, string): True if succeeded, or nil and
+-- an error message.
+function win32.replace_file(old_file, new_file)
+ os.remove(old_file)
+ return os.rename(new_file, old_file)
+end
+
+function win32.is_dir(file)
+ file = fs.absolute_name(file)
+ file = dir.normalize(file)
+ local fd, _, code = io.open(file, "r")
+ if code == 13 then -- directories return "Permission denied"
+ fd, _, code = io.open(file .. "\\", "r")
+ if code == 2 then -- directories return 2, files return 22
+ return true
+ end
+ end
+ if fd then
+ fd:close()
+ end
+ return false
+end
+
+function win32.is_file(file)
+ file = fs.absolute_name(file)
+ file = dir.normalize(file)
+ local fd, _, code = io.open(file, "r")
+ if code == 13 then -- if "Permission denied"
+ fd, _, code = io.open(file .. "\\", "r")
+ if code == 2 then -- directories return 2, files return 22
+ return false
+ elseif code == 22 then
+ return true
+ end
+ end
+ if fd then
+ fd:close()
+ return true
+ end
+ return false
+end
+
+--- 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 win32.is_writable(file)
+ assert(file)
+ file = dir.normalize(file)
+ local result
+ local tmpname = 'tmpluarockstestwritable.deleteme'
+ if fs.is_dir(file) then
+ local file2 = dir.path(file, tmpname)
+ local fh = io.open(file2, 'wb')
+ result = fh ~= nil
+ if fh then fh:close() end
+ if result then
+ -- the above test might give a false positive when writing to
+ -- c:\program files\ because of VirtualStore redirection on Vista and up
+ -- So check whether it's really there
+ result = fs.exists(file2)
+ 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 win32.tmpname()
+ local name = os.tmpname()
+ local tmp = os.getenv("TMP")
+ if tmp and name:sub(1, #tmp) ~= tmp then
+ name = (tmp .. "\\" .. name):gsub("\\+", "\\")
+ end
+ return name
+end
+
+function win32.current_user()
+ return os.getenv("USERNAME")
+end
+
+function win32.is_superuser()
+ return false
+end
+
+function win32.export_cmd(var, val)
+ return ("SET %s"):format(fs.Q(var.."="..val))
+end
+
+function win32.system_cache_dir()
+ return dir.path(fs.system_temp_dir(), "cache")
+end
+
+function win32.search_in_path(program)
+ if program:match("\\") then
+ local fd = io.open(dir.path(program), "r")
+ if fd then
+ fd:close()
+ return true, program
+ end
+
+ return false
+ end
+
+ if not program:lower():match("exe$") then
+ program = program .. ".exe"
+ end
+
+ for d in (os.getenv("PATH") or ""):gmatch("([^;]+)") do
+ local fd = io.open(dir.path(d, program), "r")
+ if fd then
+ fd:close()
+ return true, d
+ end
+ end
+ return false
+end
+
+return win32
diff --git a/src/luarocks/fs/win32/tools.lua b/src/luarocks/fs/win32/tools.lua
new file mode 100644
index 0000000..86cbb45
--- /dev/null
+++ b/src/luarocks/fs/win32/tools.lua
@@ -0,0 +1,330 @@
+
+--- fs operations implemented with third-party tools for Windows platform abstractions.
+-- Download http://unxutils.sourceforge.net/ for Windows GNU utilities
+-- used by this module.
+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_sep = package.config:sub(1, 1)
+
+--- Adds prefix to command to make it run from a directory.
+-- @param directory string: Path to a directory.
+-- @param cmd string: A command-line string.
+-- @param exit_on_error bool: Exits immediately if entering the directory failed.
+-- @return string: The command-line with prefix.
+function tools.command_at(directory, cmd, exit_on_error)
+ local drive = directory:match("^([A-Za-z]:)")
+ local op = " & "
+ if exit_on_error then
+ op = " && "
+ end
+ local cmd_prefixed = "cd " .. fs.Q(directory) .. op .. cmd
+ if drive then
+ cmd_prefixed = drive .. " & " .. cmd_prefixed
+ end
+ return cmd_prefixed
+end
+
+--- Create a directory if it does not already exist.
+-- If any of the higher levels in the path name does not exist
+-- too, they are created as well.
+-- @param directory string: pathname of directory to create.
+-- @return boolean: true on success, false on failure.
+function tools.make_dir(directory)
+ assert(directory)
+ directory = dir.normalize(directory)
+ fs.execute_quiet(vars.MKDIR, directory)
+ if not fs.is_dir(directory) then
+ return false, "failed making directory "..directory
+ 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 directory string: pathname of directory to remove.
+function tools.remove_dir_if_empty(directory)
+ assert(directory)
+ fs.execute_quiet(vars.RMDIR, directory)
+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 directory string: pathname of directory to remove.
+function tools.remove_dir_tree_if_empty(directory)
+ assert(directory)
+ while true do
+ fs.execute_quiet(vars.RMDIR, directory)
+ local parent = dir.dir_name(directory)
+ if parent ~= directory then
+ directory = parent
+ else
+ break
+ end
+ end
+end
+
+--- Copy a file.
+-- @param src string: Pathname of source
+-- @param dest string: Pathname of destination
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message.
+function tools.copy(src, dest)
+ assert(src and dest)
+ if dest:match("[/\\]$") then dest = dest:sub(1, -2) end
+ local ok = fs.execute(vars.CP, src, dest)
+ if ok then
+ return true
+ else
+ return false, "Failed copying "..src.." to "..dest
+ end
+end
+
+--- Recursively copy the contents of a directory.
+-- @param src string: Pathname of source
+-- @param dest string: Pathname of destination
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message.
+function tools.copy_contents(src, dest)
+ assert(src and dest)
+ if not fs.is_dir(src) then
+ return false, src .. " is not a directory"
+ end
+ if fs.make_dir(dest) and fs.execute_quiet(vars.CP, "-dR", src.."\\*.*", dest) then
+ return true
+ else
+ return false, "Failed copying "..src.." to "..dest
+ end
+end
+
+--- Delete a file or a directory and all its contents.
+-- For safety, this only accepts absolute paths.
+-- @param arg string: Pathname of source
+-- @return nil
+function tools.delete(arg)
+ assert(arg)
+ assert(arg:match("^[a-zA-Z]?:?[\\/]"))
+ fs.execute_quiet("if exist "..fs.Q(arg.."\\*").." ( RMDIR /S /Q "..fs.Q(arg).." ) else ( DEL /Q /F "..fs.Q(arg).." )")
+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. Paths are returned with forward slashes.
+function tools.find(at)
+ assert(type(at) == "string" or not at)
+ if not at then
+ at = fs.current_dir()
+ end
+ if not fs.is_dir(at) then
+ return {}
+ end
+ local result = {}
+ local pipe = io.popen(fs.command_at(at, fs.quiet_stderr(vars.FIND), true))
+ for file in pipe:lines() do
+ -- Windows find is a bit different
+ local first_two = file:sub(1,2)
+ if first_two == ".\\" or first_two == "./" then file=file:sub(3) end
+ if file ~= "." then
+ table.insert(result, (file:gsub("[\\/]", dir_sep)))
+ end
+ end
+ pipe:close()
+ return result
+end
+
+--- Compress files in a .zip archive.
+-- @param zipfile string: pathname of .zip archive to be created.
+-- @param ... Filenames to be stored in the archive are given as
+-- additional arguments.
+-- @return boolean: true on success, nil and error message on failure.
+function tools.zip(zipfile, ...)
+ if fs.execute_quiet(vars.SEVENZ.." -aoa a -tzip", zipfile, ...) then
+ return true
+ else
+ return nil, "failed compressing " .. zipfile
+ end
+end
+
+--- Uncompress files from a .zip archive.
+-- @param zipfile string: pathname of .zip archive to be extracted.
+-- @return boolean: true on success, nil and error message on failure.
+function tools.unzip(zipfile)
+ assert(zipfile)
+ if fs.execute_quiet(vars.SEVENZ.." -aoa x", zipfile) then
+ return true
+ else
+ return nil, "failed extracting " .. zipfile
+ end
+end
+
+local function sevenz(default_ext, infile, outfile)
+ assert(type(infile) == "string")
+ assert(outfile == nil or type(outfile) == "string")
+
+ local dropext = infile:gsub("%."..default_ext.."$", "")
+ local outdir = dir.dir_name(dropext)
+
+ infile = fs.absolute_name(infile)
+
+ local cmdline = vars.SEVENZ.." -aoa -t* -o"..fs.Q(outdir).." x "..fs.Q(infile)
+ local ok, err = fs.execute_quiet(cmdline)
+ if not ok then
+ return nil, "failed extracting " .. infile
+ end
+
+ if outfile then
+ outfile = fs.absolute_name(outfile)
+ dropext = fs.absolute_name(dropext)
+ ok, err = os.rename(dropext, outfile)
+ if not ok then
+ return nil, "failed creating new file " .. outfile
+ end
+ end
+
+ return true
+end
+
+--- Uncompresses a .gz file.
+-- @param infile string: pathname of .gz 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 tools.gunzip(infile, outfile)
+ return sevenz("gz", infile, outfile)
+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 tools.bunzip2(infile, outfile)
+ return sevenz("bz2", infile, outfile)
+end
+
+--- Helper function for fs.set_permissions
+-- @return table: an array of all system users
+local function get_system_users()
+ local exclude = {
+ [""] = true,
+ ["Name"] = true,
+ ["\128\164\172\168\173\168\225\226\224\160\226\174\224"] = true, -- Administrator in cp866
+ ["Administrator"] = true,
+ }
+ local result = {}
+ local fd = assert(io.popen("wmic UserAccount get name"))
+ for user in fd:lines() do
+ user = user:gsub("%s+$", "")
+ if not exclude[user] then
+ table.insert(result, user)
+ end
+ end
+ return result
+end
+
+--- Set permissions for file or directory
+-- @param filename string: filename whose permissions are to be modified
+-- @param mode string ("read" or "exec"): permission to set
+-- @param scope string ("user" or "all"): the user(s) to whom the permission applies
+-- @return boolean or (boolean, string): true on success, false on failure,
+-- plus an error message
+function tools.set_permissions(filename, mode, scope)
+ assert(filename and mode and scope)
+
+ if scope == "user" then
+ local perms
+ if mode == "read" then
+ perms = "(R,W,M)"
+ elseif mode == "exec" then
+ perms = "(F)"
+ end
+
+ local ok
+ -- Take ownership of the given file
+ ok = fs.execute_quiet("takeown /f " .. fs.Q(filename))
+ if not ok then
+ return false, "Could not take ownership of the given file"
+ end
+ local username = os.getenv('USERNAME')
+ -- Grant the current user the proper rights
+ ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /inheritance:d /grant:r " .. fs.Q(username) .. ":" .. perms)
+ if not ok then
+ return false, "Failed setting permission " .. mode .. " for " .. scope
+ end
+ -- Finally, remove all the other users from the ACL in order to deny them access to the file
+ for _, user in pairs(get_system_users()) do
+ if username ~= user then
+ local ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /remove " .. fs.Q(user))
+ if not ok then
+ return false, "Failed setting permission " .. mode .. " for " .. scope
+ end
+ end
+ end
+ elseif scope == "all" then
+ local my_perms, others_perms
+ if mode == "read" then
+ my_perms = "(R,W,M)"
+ others_perms = "(R)"
+ elseif mode == "exec" then
+ my_perms = "(F)"
+ others_perms = "(RX)"
+ end
+
+ local ok
+ -- Grant permissions available to all users
+ ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /inheritance:d /grant:r *S-1-1-0:" .. others_perms)
+ if not ok then
+ return false, "Failed setting permission " .. mode .. " for " .. scope
+ end
+
+ -- Grant permissions available only to the current user
+ ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /inheritance:d /grant \"%USERNAME%\":" .. my_perms)
+
+ -- This may not be necessary if the above syntax is correct,
+ -- but I couldn't really test the extra quotes above, so if that
+ -- fails we try again with the syntax used in previous releases
+ -- just to be on the safe side
+ if not ok then
+ ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /inheritance:d /grant %USERNAME%:" .. my_perms)
+ end
+
+ if not ok then
+ return false, "Failed setting permission " .. mode .. " for " .. scope
+ end
+ end
+
+ return true
+end
+
+function tools.browser(url)
+ return fs.execute(cfg.web_browser..' "Starting docs..." '..fs.Q(url))
+end
+
+-- Set access and modification times for a file.
+-- @param filename File to set access and modification times for.
+-- @param time may be a string or 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 tools.set_time(filename, time)
+ return true -- FIXME
+end
+
+function tools.lock_access(dirname)
+ -- NYI
+ return {}
+end
+
+function tools.unlock_access(lock)
+ -- NYI
+end
+
+return tools