diff options
Diffstat (limited to 'src/luarocks/fs')
| -rw-r--r-- | src/luarocks/fs/linux.lua | 50 | ||||
| -rw-r--r-- | src/luarocks/fs/lua.lua | 1307 | ||||
| -rw-r--r-- | src/luarocks/fs/macosx.lua | 50 | ||||
| -rw-r--r-- | src/luarocks/fs/tools.lua | 222 | ||||
| -rw-r--r-- | src/luarocks/fs/unix.lua | 266 | ||||
| -rw-r--r-- | src/luarocks/fs/unix/tools.lua | 353 | ||||
| -rw-r--r-- | src/luarocks/fs/win32.lua | 384 | ||||
| -rw-r--r-- | src/luarocks/fs/win32/tools.lua | 330 |
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 |
