summaryrefslogtreecommitdiff
path: root/src/luarocks/fs/unix/tools.lua
diff options
context:
space:
mode:
authorMike Vink <mike@pionative.com>2025-02-03 21:29:42 +0100
committerMike Vink <mike@pionative.com>2025-02-03 21:29:42 +0100
commit5155816b7b925dec5d5feb1568b1d7ceb00938b9 (patch)
treedeca28ea15e79f6f804c3d90d2ba757881638af5 /src/luarocks/fs/unix/tools.lua
fetch tarballHEADmaster
Diffstat (limited to 'src/luarocks/fs/unix/tools.lua')
-rw-r--r--src/luarocks/fs/unix/tools.lua353
1 files changed, 353 insertions, 0 deletions
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