--- /dev/null
+--
+-- 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("\e[%sm%s\e[0m", 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
+