X-Git-Url: https://git.dogcows.com/gitweb?p=chaz%2Fyoink;a=blobdiff_plain;f=build%2Futility.lua;fp=build%2Futility.lua;h=b94a7ec3d302a7ec7f3ed64a72b0c86cfe976464;hp=0000000000000000000000000000000000000000;hb=6c9943707d4f33035830eba0587a61a34eaecbc2;hpb=af88821a172c4dfd138b91b2a5148ae50b502fa2 diff --git a/build/utility.lua b/build/utility.lua new file mode 100644 index 0000000..b94a7ec --- /dev/null +++ b/build/utility.lua @@ -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("[%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 +