]> Dogcows Code - chaz/yoink/blobdiff - build/utility.lua
build system enhancements
[chaz/yoink] / build / utility.lua
diff --git a/build/utility.lua b/build/utility.lua
new file mode 100644 (file)
index 0000000..b94a7ec
--- /dev/null
@@ -0,0 +1,424 @@
+--
+-- 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
+
This page took 0.024032 seconds and 4 git commands to generate.