426 lines
12 KiB
Lua
426 lines
12 KiB
Lua
|
-- Generic utility functions
|
||
|
|
||
|
module( ..., package.seeall )
|
||
|
|
||
|
local lfs = require "lfs"
|
||
|
local sf = string.format
|
||
|
-- Taken from Lake
|
||
|
dir_sep = package.config:sub( 1, 1 )
|
||
|
is_os_windows = dir_sep == '\\'
|
||
|
|
||
|
-- Converts a string with items separated by 'sep' into a table
|
||
|
string_to_table = function( s, sep )
|
||
|
if type( s ) ~= "string" then return end
|
||
|
sep = sep or ' '
|
||
|
if s:sub( -1, -1 ) ~= sep then s = s .. sep end
|
||
|
s = s:gsub( sf( "^%s*", sep ), "" )
|
||
|
local t = {}
|
||
|
local fmt = sf( "(.-)%s+", sep )
|
||
|
for w in s:gmatch( fmt ) do table.insert( t, w ) end
|
||
|
return t
|
||
|
end
|
||
|
|
||
|
-- Split a file name into 'path part' and 'extension part'
|
||
|
split_ext = function( s )
|
||
|
local pos
|
||
|
for i = #s, 1, -1 do
|
||
|
if s:sub( i, i ) == "." then
|
||
|
pos = i
|
||
|
break
|
||
|
end
|
||
|
end
|
||
|
if not pos or s:find( dir_sep, pos + 1 ) then return s end
|
||
|
return s:sub( 1, pos - 1 ), s:sub( pos )
|
||
|
end
|
||
|
|
||
|
-- Replace the extension of a given file name
|
||
|
replace_extension = function( s, newext )
|
||
|
local p, e = split_ext( s )
|
||
|
if e then
|
||
|
if newext and #newext > 0 then
|
||
|
s = p .. "." .. newext
|
||
|
else
|
||
|
s = p
|
||
|
end
|
||
|
end
|
||
|
return s
|
||
|
end
|
||
|
|
||
|
-- Return 'true' if building from Windows, false otherwise
|
||
|
is_windows = function()
|
||
|
return is_os_windows
|
||
|
end
|
||
|
|
||
|
-- Prepend each component of a 'pat'-separated string with 'prefix'
|
||
|
prepend_string = function( s, prefix, pat )
|
||
|
if not s or #s == 0 then return "" end
|
||
|
pat = pat or ' '
|
||
|
local res = ''
|
||
|
local st = string_to_table( s, pat )
|
||
|
foreach( st, function( k, v ) res = res .. prefix .. v .. " " end )
|
||
|
return res
|
||
|
end
|
||
|
|
||
|
-- Like above, but consider 'prefix' a path
|
||
|
prepend_path = function( s, prefix, pat )
|
||
|
return prepend_string( s, prefix .. dir_sep, pat )
|
||
|
end
|
||
|
|
||
|
-- full mkdir: create all the paths needed for a multipath
|
||
|
full_mkdir = function( path )
|
||
|
local ptables = string_to_table( path, dir_sep )
|
||
|
local p, res = ''
|
||
|
for i = 1, #ptables do
|
||
|
p = ( i ~= 1 and p .. dir_sep or p ) .. ptables[ i ]
|
||
|
res = lfs.mkdir( p )
|
||
|
end
|
||
|
return res
|
||
|
end
|
||
|
|
||
|
-- Concatenate the given paths to form a complete path
|
||
|
concat_path = function( paths )
|
||
|
return table.concat( paths, dir_sep )
|
||
|
end
|
||
|
|
||
|
-- Return true if the given array contains the given element, false otherwise
|
||
|
array_element_index = function( arr, element )
|
||
|
for i = 1, #arr do
|
||
|
if arr[ i ] == element then return i end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Linearize an array with (possibly) embedded arrays into a simple array
|
||
|
_linearize_array = function( arr, res, filter )
|
||
|
if type( arr ) ~= "table" then return end
|
||
|
for i = 1, #arr do
|
||
|
local e = arr[ i ]
|
||
|
if type( e ) == 'table' and filter( e ) then
|
||
|
_linearize_array( e, res, filter )
|
||
|
else
|
||
|
table.insert( res, e )
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
linearize_array = function( arr, filter )
|
||
|
local res = {}
|
||
|
filter = filter or function( v ) return true end
|
||
|
_linearize_array( arr, res, filter )
|
||
|
return res
|
||
|
end
|
||
|
|
||
|
-- Return an array with the keys of a table
|
||
|
table_keys = function( t )
|
||
|
local keys = {}
|
||
|
foreach( t, function( k, v ) table.insert( keys, k ) end )
|
||
|
return keys
|
||
|
end
|
||
|
|
||
|
-- Return an array with the values of a table
|
||
|
table_values = function( t )
|
||
|
local vals = {}
|
||
|
foreach( t, function( k, v ) table.insert( vals, v ) end )
|
||
|
return vals
|
||
|
end
|
||
|
|
||
|
-- Returns true if 'path' is a regular file, false otherwise
|
||
|
is_file = function( path )
|
||
|
return lfs.attributes( path, "mode" ) == "file"
|
||
|
end
|
||
|
|
||
|
-- Returns true if 'path' is a directory, false otherwise
|
||
|
is_dir = function( path )
|
||
|
return lfs.attributes( path, "mode" ) == "directory"
|
||
|
end
|
||
|
|
||
|
-- Return a list of files in the given directory matching a given mask
|
||
|
get_files = function( path, mask, norec, level )
|
||
|
local t = ''
|
||
|
level = level or 0
|
||
|
for f in lfs.dir( path ) do
|
||
|
local fname = path .. dir_sep .. f
|
||
|
if lfs.attributes( fname, "mode" ) == "file" then
|
||
|
local include
|
||
|
if type( mask ) == "string" then
|
||
|
include = fname:find( mask )
|
||
|
else
|
||
|
include = mask( fname )
|
||
|
end
|
||
|
if include then t = t .. ' ' .. fname end
|
||
|
elseif lfs.attributes( fname, "mode" ) == "directory" and not fname:find( "%.+$" ) and not norec then
|
||
|
t = t .. " " .. get_files( fname, mask, norec, level + 1 )
|
||
|
end
|
||
|
end
|
||
|
return level > 0 and t or t:gsub( "^%s+", "" )
|
||
|
end
|
||
|
|
||
|
-- Check if the given command can be executed properly
|
||
|
check_command = function( cmd )
|
||
|
local res = os.execute( cmd .. " > .build.temp 2>&1" )
|
||
|
os.remove( ".build.temp" )
|
||
|
return res
|
||
|
end
|
||
|
|
||
|
-- Execute a command and capture output
|
||
|
-- From: http://stackoverflow.com/a/326715/105950
|
||
|
exec_capture = function( cmd, raw )
|
||
|
local f = assert(io.popen(cmd, 'r'))
|
||
|
local s = assert(f:read('*a'))
|
||
|
f:close()
|
||
|
if raw then return s end
|
||
|
s = string.gsub(s, '^%s+', '')
|
||
|
s = string.gsub(s, '%s+$', '')
|
||
|
s = string.gsub(s, '[\n\r]+', ' ')
|
||
|
return s
|
||
|
end
|
||
|
|
||
|
-- Execute the given command for each value in a table
|
||
|
foreach = function ( t, cmd )
|
||
|
if type( t ) ~= "table" then return end
|
||
|
for k, v in pairs( t ) do cmd( k, v ) end
|
||
|
end
|
||
|
|
||
|
-- Generate header with the given #defines, return result as string
|
||
|
gen_header_string = function( name, defines )
|
||
|
local s = "// eLua " .. name:lower() .. " definition\n\n"
|
||
|
s = s .. "#ifndef __" .. name:upper() .. "_H__\n"
|
||
|
s = s .. "#define __" .. name:upper() .. "_H__\n\n"
|
||
|
|
||
|
for key,value in pairs(defines) do
|
||
|
s = s .. string.format("#define %-25s%-19s\n",key:upper(),value)
|
||
|
end
|
||
|
|
||
|
s = s .. "\n#endif\n"
|
||
|
return s
|
||
|
end
|
||
|
|
||
|
-- Generate header with the given #defines, save result to file
|
||
|
gen_header_file = function( name, defines )
|
||
|
local hname = concat_path{ "inc", name:lower() .. ".h" }
|
||
|
local h = assert( io.open( hname, "w" ) )
|
||
|
h:write( gen_header_string( name, defines ) )
|
||
|
h:close()
|
||
|
end
|
||
|
|
||
|
-- Remove the given elements from an array
|
||
|
remove_array_elements = function( arr, del )
|
||
|
del = istable( del ) and del or { del }
|
||
|
foreach( del, function( k, v )
|
||
|
local pos = array_element_index( arr, v )
|
||
|
if pos then table.remove( arr, pos ) end
|
||
|
end )
|
||
|
end
|
||
|
|
||
|
-- Remove a directory recusively
|
||
|
-- USE WITH CARE!! Doesn't do much checks :)
|
||
|
rmdir_rec = function ( dirname )
|
||
|
if lfs.attributes( dirname, "mode" ) ~= "directory" then return end
|
||
|
for f in lfs.dir( dirname ) do
|
||
|
local ename = string.format( "%s/%s", dirname, f )
|
||
|
local attrs = lfs.attributes( ename )
|
||
|
if attrs.mode == 'directory' and f ~= '.' and f ~= '..' then
|
||
|
rmdir_rec( ename )
|
||
|
elseif attrs.mode == 'file' or attrs.mode == 'named pipe' or attrs.mode == 'link' then
|
||
|
os.remove( ename )
|
||
|
end
|
||
|
end
|
||
|
lfs.rmdir( dirname )
|
||
|
end
|
||
|
|
||
|
-- Concatenates the second table into the first one
|
||
|
concat_tables = function( dst, src )
|
||
|
foreach( src, function( k, v ) dst[ k ] = v end )
|
||
|
end
|
||
|
|
||
|
-------------------------------------------------------------------------------
|
||
|
-- Color-related funtions
|
||
|
-- Currently disabled when running in Windows
|
||
|
-- (they can be enabled by setting WIN_ANSI_TERM)
|
||
|
|
||
|
local dcoltable = { 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white' }
|
||
|
local coltable = {}
|
||
|
foreach( dcoltable, function( k, v ) coltable[ v ] = k - 1 end )
|
||
|
|
||
|
local _col_builder = function( col )
|
||
|
local _col_maker = function( s )
|
||
|
if is_os_windows and not os.getenv( "WIN_ANSI_TERM" ) then
|
||
|
return s
|
||
|
else
|
||
|
return( sf( "\027[%d;1m%s\027[m", coltable[ col ] + 30, s ) )
|
||
|
end
|
||
|
end
|
||
|
return _col_maker
|
||
|
end
|
||
|
|
||
|
col_funcs = {}
|
||
|
foreach( coltable, function( k, v )
|
||
|
local fname = "col_" .. k
|
||
|
_G[ fname ] = _col_builder( k )
|
||
|
col_funcs[ k ] = _G[ fname ]
|
||
|
end )
|
||
|
|
||
|
-------------------------------------------------------------------------------
|
||
|
-- Option handling
|
||
|
|
||
|
local options = {}
|
||
|
|
||
|
options.new = function()
|
||
|
local self = {}
|
||
|
self.options = {}
|
||
|
setmetatable( self, { __index = options } )
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
-- Argument validator: boolean value
|
||
|
options._bool_validator = function( v )
|
||
|
if v == '0' or v:upper() == 'FALSE' then
|
||
|
return false
|
||
|
elseif v == '1' or v:upper() == 'TRUE' then
|
||
|
return true
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Argument validator: choice value
|
||
|
options._choice_validator = function( v, allowed )
|
||
|
for i = 1, #allowed do
|
||
|
if v:upper() == allowed[ i ]:upper() then return allowed[ i ] end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Argument validator: choice map (argument value maps to something)
|
||
|
options._choice_map_validator = function( v, allowed )
|
||
|
for k, value in pairs( allowed ) do
|
||
|
if v:upper() == k:upper() then return value end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Argument validator: string value (no validation)
|
||
|
options._string_validator = function( v )
|
||
|
return v
|
||
|
end
|
||
|
|
||
|
-- Argument printer: boolean value
|
||
|
options._bool_printer = function( o )
|
||
|
return "true|false", o.default and "true" or "false"
|
||
|
end
|
||
|
|
||
|
-- Argument printer: choice value
|
||
|
options._choice_printer = function( o )
|
||
|
local clist, opts = '', o.data
|
||
|
for i = 1, #opts do
|
||
|
clist = clist .. ( i ~= 1 and "|" or "" ) .. opts[ i ]
|
||
|
end
|
||
|
return clist, o.default
|
||
|
end
|
||
|
|
||
|
-- Argument printer: choice map printer
|
||
|
options._choice_map_printer = function( o )
|
||
|
local clist, opts, def = '', o.data
|
||
|
local i = 1
|
||
|
for k, v in pairs( opts ) do
|
||
|
clist = clist .. ( i ~= 1 and "|" or "" ) .. k
|
||
|
if o.default == v then def = k end
|
||
|
i = i + 1
|
||
|
end
|
||
|
return clist, def
|
||
|
end
|
||
|
|
||
|
-- Argument printer: string printer
|
||
|
options._string_printer = function( o )
|
||
|
return nil, o.default
|
||
|
end
|
||
|
|
||
|
-- Add an option of the specified type
|
||
|
options._add_option = function( self, optname, opttype, help, default, data )
|
||
|
local validators =
|
||
|
{
|
||
|
string = options._string_validator, choice = options._choice_validator,
|
||
|
boolean = options._bool_validator, choice_map = options._choice_map_validator
|
||
|
}
|
||
|
local printers =
|
||
|
{
|
||
|
string = options._string_printer, choice = options._choice_printer,
|
||
|
boolean = options._bool_printer, choice_map = options._choice_map_printer
|
||
|
}
|
||
|
if not validators[ opttype ] then
|
||
|
print( sf( "[builder] Invalid option type '%s'", opttype ) )
|
||
|
os.exit( 1 )
|
||
|
end
|
||
|
table.insert( self.options, { name = optname, help = help, validator = validators[ opttype ], printer = printers[ opttype ], data = data, default = default } )
|
||
|
end
|
||
|
|
||
|
-- Find an option with the given name
|
||
|
options._find_option = function( self, optname )
|
||
|
for i = 1, #self.options do
|
||
|
local o = self.options[ i ]
|
||
|
if o.name:upper() == optname:upper() then return self.options[ i ] end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- 'add option' helper (automatically detects option type)
|
||
|
options.add_option = function( self, name, help, default, data )
|
||
|
local otype
|
||
|
if type( default ) == 'boolean' then
|
||
|
otype = 'boolean'
|
||
|
elseif data and type( data ) == 'table' and #data == 0 then
|
||
|
otype = 'choice_map'
|
||
|
elseif data and type( data ) == 'table' then
|
||
|
otype = 'choice'
|
||
|
data = linearize_array( data )
|
||
|
elseif type( default ) == 'string' then
|
||
|
otype = 'string'
|
||
|
else
|
||
|
print( sf( "Error: cannot detect option type for '%s'", name ) )
|
||
|
os.exit( 1 )
|
||
|
end
|
||
|
self:_add_option( name, otype, help, default, data )
|
||
|
end
|
||
|
|
||
|
options.get_num_opts = function( self )
|
||
|
return #self.options
|
||
|
end
|
||
|
|
||
|
options.get_option = function( self, i )
|
||
|
return self.options[ i ]
|
||
|
end
|
||
|
|
||
|
-- Handle an option of type 'key=value'
|
||
|
-- Returns both the key and the value or nil for error
|
||
|
options.handle_arg = function( self, a )
|
||
|
local si, ei, k, v = a:find( "([^=]+)=(.*)$" )
|
||
|
if not k or not v then
|
||
|
print( sf( "Error: invalid syntax in '%s'", a ) )
|
||
|
return
|
||
|
end
|
||
|
local opt = self:_find_option( k )
|
||
|
if not opt then
|
||
|
print( sf( "Error: invalid option '%s'", k ) )
|
||
|
return
|
||
|
end
|
||
|
local optv = opt.validator( v, opt.data )
|
||
|
if optv == nil then
|
||
|
print( sf( "Error: invalid value '%s' for option '%s'", v, k ) )
|
||
|
return
|
||
|
end
|
||
|
return k, optv
|
||
|
end
|
||
|
|
||
|
-- Show help for all the registered options
|
||
|
options.show_help = function( self )
|
||
|
for i = 1, #self.options do
|
||
|
local o = self.options[ i ]
|
||
|
print( sf( "\n %s: %s", o.name, o.help ) )
|
||
|
local values, default = o.printer( o )
|
||
|
if values then
|
||
|
print( sf( " Possible values: %s", values ) )
|
||
|
end
|
||
|
print( sf( " Default value: %s", default or "none (changes at runtime)" ) )
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Create a new option handler
|
||
|
function options_handler()
|
||
|
return options.new()
|
||
|
end
|
||
|
|