]> Dogcows Code - chaz/yoink/blob - build/utility.lua
build system enhancements
[chaz/yoink] / build / utility.lua
1 --
2 -- Yoink Build System
3 -- A pseudo-random collection of useful Lua functions.
4 --
5 -- Copyright (c) 2011, Charles McGarvey
6 -- Distributable under the terms and conditions of the 2-clause BSD
7 -- license; see the file COPYING for a complete text of the license.
8 --
9
10 local M = {
11 logfile = "config.log"
12 }
13
14
15 -- Sometimes it is useful to iterate over an array with gaps or sections of
16 -- nil values, skipping over such empty sections. This function, like the
17 -- standard ipairs, creates an iterator, except the iterator will look n
18 -- indices past any nil key it finds. The argument n should be given as
19 -- the largest possible number of consecutive nils; it defaults to 4. This
20 -- function can typically serve as a drop-in replacement for ipairs.
21 local function npairs(t, n)
22 n = tonumber(n) or 4
23 return function(t, k)
24 for i = k + 1, k + n
25 do
26 local v = t[i]
27 if v ~= nil then return i, v end
28 end
29 end, t, 0
30 end
31 M.npairs = npairs
32
33
34 -- Makes a copy of an object. For most objects, this just returns the
35 -- object that was passed. For tables, a new table will be created and the
36 -- contents of the original object will either be assigned or copied over,
37 -- depending on the mode. The mode enables deep copies; it is a string
38 -- which may be empty or contain any combination of subsets of "k", "v",
39 -- and "m" for copying keys, values and metatables, respectively. The
40 -- default behavior is to deep copy only the values. The subcopy argument
41 -- is the function to be called when making subcopies during a deep copy.
42 -- It may also be a table with keys equal to one or more of the copy modes
43 -- and values set as the subcopy function to use for the type of object
44 -- represented by the copy mode character identifier.
45 local function copy(object, mode, subcopy)
46 if type(object) ~= "table" then return object end
47 mode = mode or "v"
48 local copykey = function(object) return object end
49 local copyvalue, copymetatable = copykey, copykey
50 if type(subcopy) == "table"
51 then
52 if mode:find("k") and type(subcopy.k) == "function"
53 then
54 copykey = subcopy.k
55 end
56 if mode:find("v") and type(subcopy.v) == "function"
57 then
58 copyvalue = subcopy.v
59 end
60 if mode:find("m") and type(subcopy.m) == "function"
61 then
62 copymetatable = subcopy.m
63 end
64 else
65 if type(subcopy) ~= "function" then subcopy = copy end
66 if mode:find("k") then copykey = subcopy end
67 if mode:find("v") then copyvalue = subcopy end
68 if mode:find("m") then copymetatable = subcopy end
69 end
70 local new = {}
71 for key,value in pairs(object) do
72 new[copykey(key, mode)] = copyvalue(value, mode)
73 end
74 return setmetatable(new, copymetatable(getmetatable(object), mode))
75 end
76 M.copy = copy
77
78
79 -- Like the standard table.concat function in all respects except values
80 -- are transformed by an optional function given; the function defaults to
81 -- tostring, so tables with objects for which the concatenate operator
82 -- cannot apply do not cause an error to be thrown.
83 local function concat(t, s, i, j, f)
84 f = f or tostring
85 return table.concat(copy(t, "v", f), s, i, j)
86 end
87 M.concat = concat
88
89 -- Escape the argument, preparing it for passage through a shell parse.
90 local function escape(str)
91 return string.format("%q", tostring(str))
92 end
93 M.escape = escape
94
95 -- Run the test command with the given arguments. If the test passes,
96 -- returns true; false if the test fails.
97 local function test(...)
98 return os.execute(string.format("test %s", concat(arg, " ", 1, #arg, escape))) == 0
99 end
100 M.test = test
101
102
103 -- Format a string with two pieces of text; the first bit of text is
104 -- printed as-is, and the next bit of text is printed left-justified with
105 -- an indent. The second bit of text is pushed to the right if the first
106 -- bit of text is too big.
107 local function align(text1, text2, indent)
108 text1 = text1 or ""
109 text2 = text2 or ""
110 indent = indent or 8
111 local numSpaces = math.max(indent - #text1 - 1, 0)
112 for i = 0, numSpaces do text2 = " " .. text2 end
113 return text1 .. text2
114 end
115 M.align = align
116
117
118 -- Remove the whitespace surrounding a string.
119 local function trim(str)
120 return (str:gsub("^%s*(.-)%s*$", "%1"))
121 end
122 M.trim = trim
123
124 -- Trim the string and convert all sequences of whitespace to a single
125 -- space.
126 local function compact(str)
127 return trim(str:gsub("%s+", " "))
128 end
129 M.compact = compact
130
131
132 -- Execute a command and return its output or nil if the command failed to
133 -- run. Use capture to specify which file descriptor to return. The exit
134 -- code of the command is also returned.
135 local function exec(command, capture)
136 capture = capture or ""
137 local tmpname = os.tmpname()
138 local full = string.format("%s %s>%q", command, tostring(capture), tmpname)
139 local code = os.execute(full)
140 local fd = io.open(tmpname)
141 local output = ""
142 if fd then output = fd:read("*a") fd:close() end
143 os.remove(tmpname)
144 if M.logfile
145 then
146 local fd = io.open(M.logfile, "a")
147 fd:write("\n# ", command, "\n", output, "# exit: ", code, "\n")
148 fd:close()
149 end
150 return trim(output), code
151 end
152 M.exec = exec
153
154 -- Try to execute a command and return true if the command finished
155 -- successfully (with an exit code of zero).
156 local function try_run(command, dir)
157 if type(dir) == "string" then dir = string.format("cd %q && ", dir) else dir = "" end
158 local output, code= exec(string.format("%s%s", dir, command))
159 return code == 0
160 end
161 M.try_run = try_run
162
163 -- Look for a command from a list that can complete successfully (with exit
164 -- code zero) given some arguments. Returns nil and an error string if
165 -- none were successful.
166 local function find_command(commands, args, dir)
167 if type(commands) ~= "table" then commands = {commands} end
168 if type(args) ~= "string" then args = "" end
169 local paths = os.getenv("PATH")
170 local found = false
171 for _,command in npairs(commands)
172 do
173 if command:byte() == 0x2F
174 then
175 if test("-x", command)
176 then
177 if try_run(command .. " " .. args, dir) then return command end
178 found = true
179 end
180 else
181 for path in paths:gmatch("([^:]*)")
182 do
183 local command = path .. "/" .. command
184 if test("-x", command)
185 then
186 if try_run(command .. " " .. args, dir) then return command end
187 found = true
188 end
189 end
190 end
191 if found then return nil, "command failed" end
192 return nil, "command not found"
193 end
194 end
195 M.find_command = find_command
196
197
198 local function make_tempdir(template)
199 local dir, code = exec(string.format("mktemp -d ${TMPDIR:-/tmp}/%q", template))
200 if code == 0 then return dir end
201 end
202 M.make_tempdir = make_tempdir
203
204
205 local function pkgfind(libs, libdir)
206 local cmd = "pkg-config"
207 if type(libdir) == "string" and libdir ~= ""
208 then
209 cmd = string.format("PKG_CONFIG_PATH=%s/pkgconfig:$PKG_CONFIG_PATH %s", libdir, cmd)
210 end
211 local function invoke(package) return exec(cmd .. " " .. package) end
212 local packages, missing = {}, {}
213 for _,list in ipairs(libs:split("%S+"))
214 do
215 list = list:split("[^|]+")
216 local found = false
217 for _,package in ipairs(list)
218 do
219 local flags,code = invoke(package)
220 if code == 0
221 then
222 table.insert(packages, package)
223 found = true
224 break
225 end
226 end
227 if not found then table.insert(missing, list[1]) end
228 end
229 if #missing == 0 then return concat(packages, " ") end
230 return nil, "One or more required packages are missing: " .. concat(missing, ", ") .. ". Please install them."
231 end
232 M.pkgfind = pkgfind
233
234 -- A wrapper function for a call out to pkg-config. The first argument is
235 -- a string signifying what kind of flags to get, either CFLAGS, LIBS, or
236 -- LDFLAGS. The second argument is the list of libraries. Finally, the
237 -- last argument can be given as a path to libdir; it is used to add a new
238 -- path to PKG_CONFIG_PATH when searching for "pc" files.
239 local function pkgconfig(what, libs, libdir)
240 local cmd = "pkg-config"
241 if type(libdir) == "string" and libdir ~= ""
242 then
243 cmd = string.format("PKG_CONFIG_PATH=%s/pkgconfig:$PKG_CONFIG_PATH %s", libdir, cmd)
244 end
245 if what == "CFLAGS" or what == "CXXFLAGS"
246 then
247 return exec(cmd .. " --cflags " .. libs)
248 elseif what == "LIBS"
249 then
250 return exec(cmd .. " --libs-only-l " .. libs)
251 elseif what == "LDFLAGS"
252 then
253 local output, code = exec(cmd .. " --libs " .. libs)
254 output = output:gsub("%s%-l%S*", ""):gsub("^%-l%S*%s*", "")
255 return output, code
256 end
257 return exec(cmd .. " " .. libs)
258 end
259 M.pkgconfig = pkgconfig
260
261
262 -- Escape special characters of a pattern string.
263 local function escapePattern(str)
264 str = str:gsub("%%", "%%%%")
265 str = str:gsub("%^", "%%^")
266 str = str:gsub("%$", "%%$")
267 str = str:gsub("%(", "%%(")
268 str = str:gsub("%)", "%%)")
269 str = str:gsub("%.", "%%.")
270 str = str:gsub("%[", "%%[")
271 str = str:gsub("%]", "%%[")
272 str = str:gsub("%*", "%%*")
273 str = str:gsub("%+", "%%+")
274 str = str:gsub("%-", "%%-")
275 str = str:gsub("%?", "%%?")
276 return str
277 end
278
279 -- A basic split function for strings. Give a pattern of a sequence which
280 -- should be matched and returns a table of matches.
281 function string:split(pattern)
282 local t = {}
283 self:gsub(pattern, function(seq) table.insert(t, seq) end)
284 return t
285 end
286
287 -- Truncates a string to a certain length, appending an ellipsis if any
288 -- part of the string had to be chopped.
289 function string:truncate(length)
290 if length < #self then return self:sub(1, length - 3) .. "..." end
291 return self
292 end
293
294
295 -- Append a word (i.e. flag) to the end of the string.
296 function string:appendFlag(...)
297 for _,flag in ipairs(arg)
298 do
299 if self == "" then self = flag
300 else self = string.format("%s %s", self, flag) end
301 end
302 return self
303 end
304
305 -- Set the flag by appending it to the end of the string unless it already
306 -- exists somewhere else in the string. Note: The command line isn't
307 -- parsed; a simple search is used to determined if a flag is set.
308 function string:setFlag(...)
309 for _,flag in ipairs(arg)
310 do
311 local escaped = escapePattern(flag)
312 if not self:match(escaped) then self = self:appendFlag(flag) end
313 end
314 return self
315 end
316
317 -- Remove all matching flags from a string. Note: The command line isn't
318 -- parsed; a simple search and replace is used to determine where a flag is
319 -- set.
320 function string:unsetFlag(...)
321 for _,flag in ipairs(arg)
322 do
323 flag = escapePattern(flag)
324 self = self:gsub("^"..flag.."$", "")
325 self = self:gsub("^"..flag.."%s", "")
326 self = self:gsub("%s"..flag.."$", "")
327 self = self:gsub("%s"..flag, "")
328 end
329 return self
330 end
331
332 -- Replace all matching flags from a string with a new flag. The old flag
333 -- is parsed as a pattern, just like a substitution. Each flag matching
334 -- the pattern is replaced by the new flag, but if no replacements are
335 -- made, the new flag is appended to the string.
336 function string:replaceFlag(old, new)
337 local count
338 self, count = self:gsub(old, new)
339 if count < 1 then self = self:appendFlag(new) end
340 return self
341 end
342
343
344 do
345 local colorCount = exec("tput colors")
346 local isTTY = test("-t", 1)
347
348 -- The logging facility is exported by way of this printer generator.
349 -- Pass a format and attribute string and get a function to print to
350 -- stdout. Attributes will only be used if stdout is a terminal with
351 -- at least eight colors. All arguments are optional; the default
352 -- format mimics the return print function regarding only the first
353 -- argument.
354 local function printer(format, attrib)
355 format = tostring(format or "%s\n")
356 if type(attrib) == "string" and isTTY and 8 <= tonumber(colorCount)
357 then
358 return function(...)
359 io.write(string.format("\e[%sm%s\e[0m", attrib, string.format(format, ...)))
360 end
361 else
362 return function(...)
363 io.write(string.format(format, ...))
364 end
365 end
366 end
367 M.printer = printer
368 end
369
370
371 -- Symbolize a name; that is, convert a string such that it would make a
372 -- good filename. The string is converted to lower case and series of
373 -- non-alphanumeric characters are converted to hyphens.
374 local function symbolize(name)
375 return name:lower():gsub("%W+", "-")
376 end
377 M.symbolize = symbolize
378
379 -- Take a standard version number with three parts and separate the parts
380 -- into major, minor, and revision components.
381 local function splitVersion(version)
382 local vmajor, vminor, vrevis = version:match("^(%d*)%.?(%d*)%.?(%d*)")
383 vmajor = tonumber(vmajor) or 0
384 vminor = tonumber(vminor) or 0
385 vrevis = tonumber(vrevis) or 0
386 return vmajor, vminor, vrevis
387 end
388 M.splitVersion = splitVersion
389
390
391 -- Create a new class type. Pass one or more parent classes for
392 -- inheritence.
393 local function class(...)
394 local c = {__init = function() end}
395 local s = {}
396 for _,super in ipairs(arg)
397 do
398 for key,value in pairs(super) do c[key] = value end
399 table.insert(s, super)
400 end
401 c.__index = c
402 c.__super = s
403 local m = {}
404 function m:__call(...)
405 local o = setmetatable({}, c) o:__init(...) return o
406 end
407 function c:isa(c)
408 local function recurse(a, b)
409 if a == b then return true end
410 for _,super in ipairs(a.__super)
411 do
412 if recurse(super, b) then return true end
413 end
414 return false
415 end
416 return recurse(getmetatable(self), c)
417 end
418 return setmetatable(c, m)
419 end
420 M.class = class
421
422
423 return M
424
This page took 0.051226 seconds and 4 git commands to generate.