-- -- Yoink Build System -- A pseudo-random collection of useful Lua functions. -- -- Copyright (c) 2011, Charles McGarvey -- Distributable under the terms and conditions of the 2-clause BSD -- license; see the file COPYING for a complete text of the license. -- local M = { logfile = "config.log" } -- Sometimes it is useful to iterate over an array with gaps or sections of -- nil values, skipping over such empty sections. This function, like the -- standard ipairs, creates an iterator, except the iterator will look n -- indices past any nil key it finds. The argument n should be given as -- the largest possible number of consecutive nils; it defaults to 4. This -- function can typically serve as a drop-in replacement for ipairs. local function npairs(t, n) n = tonumber(n) or 4 return function(t, k) for i = k + 1, k + n do local v = t[i] if v ~= nil then return i, v end end end, t, 0 end M.npairs = npairs -- Makes a copy of an object. For most objects, this just returns the -- object that was passed. For tables, a new table will be created and the -- contents of the original object will either be assigned or copied over, -- depending on the mode. The mode enables deep copies; it is a string -- which may be empty or contain any combination of subsets of "k", "v", -- and "m" for copying keys, values and metatables, respectively. The -- default behavior is to deep copy only the values. The subcopy argument -- is the function to be called when making subcopies during a deep copy. -- It may also be a table with keys equal to one or more of the copy modes -- and values set as the subcopy function to use for the type of object -- represented by the copy mode character identifier. local function copy(object, mode, subcopy) if type(object) ~= "table" then return object end mode = mode or "v" local copykey = function(object) return object end local copyvalue, copymetatable = copykey, copykey if type(subcopy) == "table" then if mode:find("k") and type(subcopy.k) == "function" then copykey = subcopy.k end if mode:find("v") and type(subcopy.v) == "function" then copyvalue = subcopy.v end if mode:find("m") and type(subcopy.m) == "function" then copymetatable = subcopy.m end else if type(subcopy) ~= "function" then subcopy = copy end if mode:find("k") then copykey = subcopy end if mode:find("v") then copyvalue = subcopy end if mode:find("m") then copymetatable = subcopy end end local new = {} for key,value in pairs(object) do new[copykey(key, mode)] = copyvalue(value, mode) end return setmetatable(new, copymetatable(getmetatable(object), mode)) end M.copy = copy -- Like the standard table.concat function in all respects except values -- are transformed by an optional function given; the function defaults to -- tostring, so tables with objects for which the concatenate operator -- cannot apply do not cause an error to be thrown. local function concat(t, s, i, j, f) f = f or tostring return table.concat(copy(t, "v", f), s, i, j) end M.concat = concat -- Escape the argument, preparing it for passage through a shell parse. local function escape(str) return string.format("%q", tostring(str)) end M.escape = escape -- Run the test command with the given arguments. If the test passes, -- returns true; false if the test fails. local function test(...) return os.execute(string.format("test %s", concat(arg, " ", 1, #arg, escape))) == 0 end M.test = test -- Format a string with two pieces of text; the first bit of text is -- printed as-is, and the next bit of text is printed left-justified with -- an indent. The second bit of text is pushed to the right if the first -- bit of text is too big. local function align(text1, text2, indent) text1 = text1 or "" text2 = text2 or "" indent = indent or 8 local numSpaces = math.max(indent - #text1 - 1, 0) for i = 0, numSpaces do text2 = " " .. text2 end return text1 .. text2 end M.align = align -- Remove the whitespace surrounding a string. local function trim(str) return (str:gsub("^%s*(.-)%s*$", "%1")) end M.trim = trim -- Trim the string and convert all sequences of whitespace to a single -- space. local function compact(str) return trim(str:gsub("%s+", " ")) end M.compact = compact -- Execute a command and return its output or nil if the command failed to -- run. Use capture to specify which file descriptor to return. The exit -- code of the command is also returned. local function exec(command, capture) capture = capture or "" local tmpname = os.tmpname() local full = string.format("%s %s>%q", command, tostring(capture), tmpname) local code = os.execute(full) local fd = io.open(tmpname) local output = "" if fd then output = fd:read("*a") fd:close() end os.remove(tmpname) if M.logfile then local fd = io.open(M.logfile, "a") fd:write("\n# ", command, "\n", output, "# exit: ", code, "\n") fd:close() end return trim(output), code end M.exec = exec -- Try to execute a command and return true if the command finished -- successfully (with an exit code of zero). local function try_run(command, dir) if type(dir) == "string" then dir = string.format("cd %q && ", dir) else dir = "" end local output, code= exec(string.format("%s%s", dir, command)) return code == 0 end M.try_run = try_run -- Look for a command from a list that can complete successfully (with exit -- code zero) given some arguments. Returns nil and an error string if -- none were successful. local function find_command(commands, args, dir) if type(commands) ~= "table" then commands = {commands} end if type(args) ~= "string" then args = "" end local paths = os.getenv("PATH") local found = false for _,command in npairs(commands) do if command:byte() == 0x2F then if test("-x", command) then if try_run(command .. " " .. args, dir) then return command end found = true end else for path in paths:gmatch("([^:]*)") do local command = path .. "/" .. command if test("-x", command) then if try_run(command .. " " .. args, dir) then return command end found = true end end end if found then return nil, "command failed" end return nil, "command not found" end end M.find_command = find_command local function make_tempdir(template) local dir, code = exec(string.format("mktemp -d ${TMPDIR:-/tmp}/%q", template)) if code == 0 then return dir end end M.make_tempdir = make_tempdir local function pkgfind(libs, libdir) local cmd = "pkg-config" if type(libdir) == "string" and libdir ~= "" then cmd = string.format("PKG_CONFIG_PATH=%s/pkgconfig:$PKG_CONFIG_PATH %s", libdir, cmd) end local function invoke(package) return exec(cmd .. " " .. package) end local packages, missing = {}, {} for _,list in ipairs(libs:split("%S+")) do list = list:split("[^|]+") local found = false for _,package in ipairs(list) do local flags,code = invoke(package) if code == 0 then table.insert(packages, package) found = true break end end if not found then table.insert(missing, list[1]) end end if #missing == 0 then return concat(packages, " ") end return nil, "One or more required packages are missing: " .. concat(missing, ", ") .. ". Please install them." end M.pkgfind = pkgfind -- A wrapper function for a call out to pkg-config. The first argument is -- a string signifying what kind of flags to get, either CFLAGS, LIBS, or -- LDFLAGS. The second argument is the list of libraries. Finally, the -- last argument can be given as a path to libdir; it is used to add a new -- path to PKG_CONFIG_PATH when searching for "pc" files. local function pkgconfig(what, libs, libdir) local cmd = "pkg-config" if type(libdir) == "string" and libdir ~= "" then cmd = string.format("PKG_CONFIG_PATH=%s/pkgconfig:$PKG_CONFIG_PATH %s", libdir, cmd) end if what == "CFLAGS" or what == "CXXFLAGS" then return exec(cmd .. " --cflags " .. libs) elseif what == "LIBS" then return exec(cmd .. " --libs-only-l " .. libs) elseif what == "LDFLAGS" then local output, code = exec(cmd .. " --libs " .. libs) output = output:gsub("%s%-l%S*", ""):gsub("^%-l%S*%s*", "") return output, code end return exec(cmd .. " " .. libs) end M.pkgconfig = pkgconfig -- Escape special characters of a pattern string. local function escapePattern(str) str = str:gsub("%%", "%%%%") str = str:gsub("%^", "%%^") str = str:gsub("%$", "%%$") str = str:gsub("%(", "%%(") str = str:gsub("%)", "%%)") str = str:gsub("%.", "%%.") str = str:gsub("%[", "%%[") str = str:gsub("%]", "%%[") str = str:gsub("%*", "%%*") str = str:gsub("%+", "%%+") str = str:gsub("%-", "%%-") str = str:gsub("%?", "%%?") return str end -- A basic split function for strings. Give a pattern of a sequence which -- should be matched and returns a table of matches. function string:split(pattern) local t = {} self:gsub(pattern, function(seq) table.insert(t, seq) end) return t end -- Truncates a string to a certain length, appending an ellipsis if any -- part of the string had to be chopped. function string:truncate(length) if length < #self then return self:sub(1, length - 3) .. "..." end return self end -- Append a word (i.e. flag) to the end of the string. function string:appendFlag(...) for _,flag in ipairs(arg) do if self == "" then self = flag else self = string.format("%s %s", self, flag) end end return self end -- Set the flag by appending it to the end of the string unless it already -- exists somewhere else in the string. Note: The command line isn't -- parsed; a simple search is used to determined if a flag is set. function string:setFlag(...) for _,flag in ipairs(arg) do local escaped = escapePattern(flag) if not self:match(escaped) then self = self:appendFlag(flag) end end return self end -- Remove all matching flags from a string. Note: The command line isn't -- parsed; a simple search and replace is used to determine where a flag is -- set. function string:unsetFlag(...) for _,flag in ipairs(arg) do flag = escapePattern(flag) self = self:gsub("^"..flag.."$", "") self = self:gsub("^"..flag.."%s", "") self = self:gsub("%s"..flag.."$", "") self = self:gsub("%s"..flag, "") end return self end -- Replace all matching flags from a string with a new flag. The old flag -- is parsed as a pattern, just like a substitution. Each flag matching -- the pattern is replaced by the new flag, but if no replacements are -- made, the new flag is appended to the string. function string:replaceFlag(old, new) local count self, count = self:gsub(old, new) if count < 1 then self = self:appendFlag(new) end return self end do local colorCount = exec("tput colors") local isTTY = test("-t", 1) -- The logging facility is exported by way of this printer generator. -- Pass a format and attribute string and get a function to print to -- stdout. Attributes will only be used if stdout is a terminal with -- at least eight colors. All arguments are optional; the default -- format mimics the return print function regarding only the first -- argument. local function printer(format, attrib) format = tostring(format or "%s\n") if type(attrib) == "string" and isTTY and 8 <= tonumber(colorCount) then return function(...) io.write(string.format("[%sm%s", attrib, string.format(format, ...))) end else return function(...) io.write(string.format(format, ...)) end end end M.printer = printer end -- Symbolize a name; that is, convert a string such that it would make a -- good filename. The string is converted to lower case and series of -- non-alphanumeric characters are converted to hyphens. local function symbolize(name) return name:lower():gsub("%W+", "-") end M.symbolize = symbolize -- Take a standard version number with three parts and separate the parts -- into major, minor, and revision components. local function splitVersion(version) local vmajor, vminor, vrevis = version:match("^(%d*)%.?(%d*)%.?(%d*)") vmajor = tonumber(vmajor) or 0 vminor = tonumber(vminor) or 0 vrevis = tonumber(vrevis) or 0 return vmajor, vminor, vrevis end M.splitVersion = splitVersion -- Create a new class type. Pass one or more parent classes for -- inheritence. local function class(...) local c = {__init = function() end} local s = {} for _,super in ipairs(arg) do for key,value in pairs(super) do c[key] = value end table.insert(s, super) end c.__index = c c.__super = s local m = {} function m:__call(...) local o = setmetatable({}, c) o:__init(...) return o end function c:isa(c) local function recurse(a, b) if a == b then return true end for _,super in ipairs(a.__super) do if recurse(super, b) then return true end end return false end return recurse(getmetatable(self), c) end return setmetatable(c, m) end M.class = class return M