-- eLua build system module( ..., package.seeall ) local lfs = require "lfs" local sf = string.format utils = require "tools.utils" ------------------------------------------------------------------------------- -- Various helpers -- Return the time of the last modification of the file local function get_ftime( path ) local t = lfs.attributes( path, 'modification' ) return t or -1 end -- Check if a given target name is phony local function is_phony( target ) return target:find( "#phony" ) == 1 end -- Return a string with $(key) replaced with 'value' local function expand_key( s, key, value ) if not value then return s end local fmt = sf( "%%$%%(%s%%)", key ) return ( s:gsub( fmt, value ) ) end -- Return a target name considering phony targets local function get_target_name( s ) if not is_phony( s ) then return s end end -- 'Liniarize' a file name by replacing its path separators indicators with '_' local function linearize_fname( s ) return ( s:gsub( "[\\/]", "__" ) ) end -- Helper: transform a table into a string if needed local function table_to_string( t ) if not t then return nil end if type( t ) == "table" then t = table.concat( t, " " ) end return t end -- Helper: return the extended type of an object (takes into account __type) local function exttype( o ) local t = type( o ) if t == "table" and o.__type then t = o:__type() end return t end --------------------------------------- -- Table utils -- (from http://lua-users.org/wiki/TableUtils) function table.val_to_str( v ) if "string" == type( v ) then v = string.gsub( v, "\n", "\\n" ) if string.match( string.gsub(v,"[^'\"]",""), '^"+$' ) then return "'" .. v .. "'" end return '"' .. string.gsub(v,'"', '\\"' ) .. '"' else return "table" == type( v ) and table.tostring( v ) or tostring( v ) end end function table.key_to_str ( k ) if "string" == type( k ) and string.match( k, "^[_%a][_%a%d]*$" ) then return k else return "[" .. table.val_to_str( k ) .. "]" end end function table.tostring( tbl ) local result, done = {}, {} for k, v in ipairs( tbl ) do table.insert( result, table.val_to_str( v ) ) done[ k ] = true end for k, v in pairs( tbl ) do if not done[ k ] then table.insert( result, table.key_to_str( k ) .. "=" .. table.val_to_str( v ) ) end end return "{" .. table.concat( result, "," ) .. "}" end ------------------------------------------------------------------------------- -- Dummy 'builder': simply checks the date of a file local _fbuilder = {} _fbuilder.new = function( target, dep ) local self = {} setmetatable( self, { __index = _fbuilder } ) self.target = target self.dep = dep return self end _fbuilder.build = function( self ) -- Doesn't build anything but returns 'true' if the dependency is newer than -- the target if is_phony( self.target ) then return true else return get_ftime( self.dep ) > get_ftime( self.target ) end end _fbuilder.target_name = function( self ) return get_target_name( self.dep ) end -- Object type _fbuilder.__type = function() return "_fbuilder" end ------------------------------------------------------------------------------- -- Target object local _target = {} _target.new = function( target, dep, command, builder, ttype ) local self = {} setmetatable( self, { __index = _target } ) self.target = target self.command = command self.builder = builder builder:register_target( target, self ) self:set_dependencies( dep ) self.dep = self:_build_dependencies( self.origdep ) self.dont_clean = false self.can_substitute_cmdline = false self._force_rebuild = #self.dep == 0 builder.runlist[ target ] = false self:set_type( ttype ) return self end -- Set dependencies as a string; actual dependencies are computed by _build_dependencies -- (below) when 'build' is called _target.set_dependencies = function( self, dep ) self.origdep = dep end -- Set the target type -- This is only for displaying actions _target.set_type = function( self, ttype ) local atable = { comp = { "[COMPILE]", 'blue' } , dep = { "[DEPENDS]", 'magenta' }, link = { "[LINK]", 'yellow' }, asm = { "[ASM]", 'white' } } local tdata = atable[ ttype ] if not tdata then self.dispstr = is_phony( self.target ) and "[PHONY]" or "[TARGET]" self.dispcol = 'green' else self.dispstr = tdata[ 1 ] self.dispcol = tdata[ 2 ] end end -- Set dependencies -- This uses a proxy table and returns string deps dynamically according -- to the targets currently registered in the builder _target._build_dependencies = function( self, dep ) -- Step 1: start with an array if type( dep ) == "string" then dep = utils.string_to_table( dep ) end -- Step 2: linearize "dep" array keeping targets local filter = function( e ) local t = exttype( e ) return t ~= "_ftarget" and t ~= "_target" end dep = utils.linearize_array( dep, filter ) -- Step 3: strings are turned into _fbuilder objects if not found as targets; -- otherwise the corresponding target object is used for i = 1, #dep do if type( dep[ i ] ) == 'string' then local t = self.builder:get_registered_target( dep[ i ] ) dep[ i ] = t or _fbuilder.new( self.target, dep[ i ] ) end end return dep end -- Set pre-build function _target.set_pre_build_function = function( self, f ) self._pre_build_function = f end -- Set post-build function _target.set_post_build_function = function( self, f ) self._post_build_function = f end -- Force rebuild _target.force_rebuild = function( self, flag ) self._force_rebuild = flag end -- Set additional arguments to send to the builder function if it is a callable _target.set_target_args = function( self, args ) self._target_args = args end -- Function to execute in clean mode _target._cleaner = function( target, deps, tobj, disp_mode ) -- Clean the main target if it is not a phony target local dprint = function( ... ) if disp_mode ~= "minimal" then print( ... ) end end if not is_phony( target ) then if tobj.dont_clean then dprint( sf( "[builder] Target '%s' will not be deleted", target ) ) return 0 end if disp_mode ~= "minimal" then io.write( sf( "[builder] Removing %s ... ", target ) ) end if os.remove( target ) then dprint "done." else dprint "failed!" end end return 0 end -- Build the given target _target.build = function( self ) if self.builder.runlist[ self.target ] then return end local docmd = self:target_name() and lfs.attributes( self:target_name(), "mode" ) ~= "file" docmd = docmd or self.builder.global_force_rebuild local initdocmd = docmd self.dep = self:_build_dependencies( self.origdep ) local depends, dep, previnit = '', self.dep, self.origdep -- Iterate through all dependencies, execute each one in turn local deprunner = function() for i = 1, #dep do local res = dep[ i ]:build() docmd = docmd or res local t = dep[ i ]:target_name() if exttype( dep[ i ] ) == "_target" and t and not is_phony( self.target ) then docmd = docmd or get_ftime( t ) > get_ftime( self.target ) end if t then depends = depends .. t .. " " end end end deprunner() -- Execute the preb-build function if needed if self._pre_build_function then self._pre_build_function( self, docmd ) end -- If the dependencies changed as a result of running the pre-build function -- run through them again if previnit ~= self.origdep then self.dep = self:_build_dependencies( self.origdep ) depends, dep, docmd = '', self.dep, initdocmd deprunner() end -- If at least one dependency is new rebuild the target docmd = docmd or self._force_rebuild or self.builder.clean_mode local keep_flag = true if docmd and self.command then if self.builder.disp_mode ~= 'all' and self.builder.disp_mode ~= "minimal" and not self.builder.clean_mode then io.write( utils.col_funcs[ self.dispcol ]( self.dispstr ) .. " " ) end local cmd, code = self.command if self.builder.clean_mode then cmd = _target._cleaner end if type( cmd ) == 'string' then cmd = expand_key( cmd, "TARGET", self.target ) cmd = expand_key( cmd, "DEPENDS", depends ) cmd = expand_key( cmd, "FIRST", dep[ 1 ]:target_name() ) if self.builder.disp_mode == 'all' then print( cmd ) elseif self.builder.disp_mode ~= "minimal" then print( self.target ) end code = self:execute( cmd ) else if not self.builder.clean_mode and self.builder.disp_mode ~= "all" and self.builder.disp_mode ~= "minimal" then print( self.target ) end code = cmd( self.target, self.dep, self.builder.clean_mode and self or self._target_args, self.builder.disp_mode ) if code == 1 then -- this means "mark target as 'not executed'" keep_flag = false code = 0 end end if code ~= 0 then print( utils.col_red( "[builder] Error building target" ) ) if self.builder.disp_mode ~= 'all' and type( cmd ) == "string" then print( utils.col_red( "[builder] Last executed command was: " ) ) print( cmd ) end os.exit( 1 ) end end -- Execute the post-build function if needed if self._post_build_function then self._post_build_function( self, docmd ) end -- Marked target as "already ran" so it won't run again self.builder.runlist[ self.target ] = true return docmd and keep_flag end -- Return the actual target name (taking into account phony targets) _target.target_name = function( self ) return get_target_name( self.target ) end -- Restrict cleaning this target _target.prevent_clean = function( self, flag ) self.dont_clean = flag end -- Object type _target.__type = function() return "_target" end _target.execute = function( self, cmd ) local code if utils.is_windows() and #cmd > 8190 and self.can_substitute_cmdline then -- Avoid cmd's maximum command line length limitation local t = cmd:find( " " ) f = io.open( "tmpcmdline", "w" ) local rest = cmd:sub( t + 1 ) f:write( ( rest:gsub( "\\", "/" ) ) ) f:close() cmd = cmd:sub( 1, t - 1 ) .. " @tmpcmdline" end local code = os.execute( cmd ) os.remove( "tmpcmdline" ) return code end _target.set_substitute_cmdline = function( self, flag ) self.can_substitute_cmdline = flag end ------------------------------------------------------------------------------- -- Builder public interface builder = { KEEP_DIR = 0, BUILD_DIR_LINEARIZED = 1 } --------------------------------------- -- Initialization and option handling -- Create a new builder object with the output in 'build_dir' and with the -- specified compile, dependencies and link command builder.new = function( build_dir ) self = {} setmetatable( self, { __index = builder } ) self.build_dir = build_dir or ".build" self.exe_extension = utils.is_windows() and "exe" or "" self.clean_mode = false self.opts = utils.options_handler() self.args = {} self.user_args = {} self.build_mode = self.KEEP_DIR self.targets = {} self.targetargs = {} self._tlist = {} self.runlist = {} self.disp_mode = 'all' self.cmdline_macros = {} self.c_targets = {} self.preprocess_mode = false self.asm_mode = false return self end -- Helper: create the build output directory builder._create_build_dir = function( self ) if self.build_dir_created then return end if self.build_mode ~= self.KEEP_DIR then -- Create builds directory if needed local mode = lfs.attributes( self.build_dir, "mode" ) if not mode or mode ~= "directory" then if not utils.full_mkdir( self.build_dir ) then print( "[builder] Unable to create directory " .. self.build_dir ) os.exit( 1 ) end end end self.build_dir_created = true end -- Add an options to the builder builder.add_option = function( self, name, help, default, data ) self.opts:add_option( name, help, default, data ) end -- Initialize builder from the given command line builder.init = function( self, args ) -- Add the default options local opts = self.opts opts:add_option( "build_mode", 'choose location of the object files', self.KEEP_DIR, { keep_dir = self.KEEP_DIR, build_dir_linearized = self.BUILD_DIR_LINEARIZED } ) opts:add_option( "build_dir", 'choose build directory', self.build_dir ) opts:add_option( "disp_mode", 'set builder display mode', 'summary', { 'all', 'summary', 'minimal' } ) -- Apply default values to all options for i = 1, opts:get_num_opts() do local o = opts:get_option( i ) self.args[ o.name:upper() ] = o.default end -- Read and interpret command line for i = 1, #args do local a = args[ i ] if a:upper() == "-C" then -- clean option (-c) self.clean_mode = true elseif a:upper() == '-H' then -- help option (-h) self:_show_help() os.exit( 1 ) elseif a:upper() == "-E" then -- preprocess self.preprocess_mode = true elseif a:upper() == "-S" then -- generate assembler self.asm_mode = true elseif a:find( '-D' ) == 1 and #a > 2 then -- this is a macro definition that will be auomatically added to the compiler flags table.insert( self.cmdline_macros, a:sub( 3 ) ) elseif a:find( '=' ) then -- builder argument (key=value) local k, v = opts:handle_arg( a ) if not k then self:_show_help() os.exit( 1 ) end self.args[ k:upper() ] = v self.user_args[ k:upper() ] = true else -- this must be the target name / target arguments if self.targetname == nil then self.targetname = a else table.insert( self.targetargs, a ) end end end -- Read back the default options self.build_mode = self.args.BUILD_MODE self.build_dir = self.args.BUILD_DIR self.disp_mode = self.args.DISP_MODE end -- Return the value of the option with the given name builder.get_option = function( self, optname ) return self.args[ optname:upper() ] end -- Returns true if the given option was specified by the user on the command line, false otherwise builder.is_user_option = function( self, optname ) return self.user_args[ optname:upper() ] end -- Show builder help builder._show_help = function( self ) print( "[builder] Valid options:" ) print( " -h: help (this text)" ) print( " -c: clean target" ) print( " -E: generate preprocessed output for single file targets" ) print( " -S: generate assembler output for single file targets" ) self.opts:show_help() end --------------------------------------- -- Builder configuration -- Set the compile command builder.set_compile_cmd = function( self, cmd ) self.comp_cmd = cmd end -- Set the link command builder.set_link_cmd = function( self, cmd ) self.link_cmd = cmd end -- Set the assembler command builder.set_asm_cmd = function( self, cmd ) self._asm_cmd = cmd end -- Set (actually force) the object file extension builder.set_object_extension = function( self, ext ) self.obj_extension = ext end -- Set (actually force) the executable file extension builder.set_exe_extension = function( self, ext ) self.exe_extension = ext end -- Set the clean mode builder.set_clean_mode = function( self, isclean ) self.clean_mode = isclean end -- Sets the build mode builder.set_build_mode = function( self, mode ) self.build_mode = mode end -- Set the build directory builder.set_build_dir = function( self, dir ) if self.build_dir_created then print "[builder] Error: build directory already created" os.exit( 1 ) end self.build_dir = dir self:_create_build_dir() end -- Return the current build directory builder.get_build_dir = function( self ) return self.build_dir end -- Return the target arguments builder.get_target_args = function( self ) return self.targetargs end -- Set a specific dependency generation command for the assembler -- Pass 'false' to skip dependency generation for assembler files builder.set_asm_dep_cmd = function( self, asm_dep_cmd ) self.asm_dep_cmd = asm_dep_cmd end -- Set a specific dependency generation command for the compiler -- Pass 'false' to skip dependency generation for C files builder.set_c_dep_cmd = function( self, c_dep_cmd ) self.c_dep_cmd = c_dep_cmd end -- Save the builder configuration for a given component to a string builder._config_to_string = function( self, what ) local ctable = {} local state_fields if what == 'comp' then state_fields = { 'comp_cmd', '_asm_cmd', 'c_dep_cmd', 'asm_dep_cmd', 'obj_extension' } elseif what == 'link' then state_fields = { 'link_cmd' } else print( sf( "Invalid argument '%s' to _config_to_string", what ) ) os.exit( 1 ) end utils.foreach( state_fields, function( k, v ) ctable[ v ] = self[ v ] end ) return table.tostring( ctable ) end -- Check the configuration of the given component against the previous one -- Return true if the configuration has changed builder._compare_config = function( self, what ) local res = false local crtstate = self:_config_to_string( what ) if not self.clean_mode then local fconf = io.open( self.build_dir .. utils.dir_sep .. ".builddata." .. what, "rb" ) if fconf then local oldstate = fconf:read( "*a" ) fconf:close() if oldstate:lower() ~= crtstate:lower() then res = true end end end -- Write state to build dir fconf = io.open( self.build_dir .. utils.dir_sep .. ".builddata." .. what, "wb" ) if fconf then fconf:write( self:_config_to_string( what ) ) fconf:close() end return res end -- Sets the way commands are displayed builder.set_disp_mode = function( self, mode ) mode = mode:lower() if mode ~= 'all' and mode ~= 'summary' and mode ~= "minimal" then print( sf( "[builder] Invalid display mode '%s'", mode ) ) os.exit( 1 ) end self.disp_mode = mode end --------------------------------------- -- Command line builders -- Internal helper builder._generic_cmd = function( self, args ) local compcmd = args.compiler or "gcc" compcmd = compcmd .. " " local flags = type( args.flags ) == 'table' and table_to_string( utils.linearize_array( args.flags ) ) or args.flags local defines = type( args.defines ) == 'table' and table_to_string( utils.linearize_array( args.defines ) ) or args.defines local includes = type( args.includes ) == 'table' and table_to_string( utils.linearize_array( args.includes ) ) or args.includes local comptype = table_to_string( args.comptype ) or "-c" compcmd = compcmd .. utils.prepend_string( defines, "-D" ) compcmd = compcmd .. utils.prepend_string( includes, "-I" ) return compcmd .. flags .. " " .. comptype .. " -o $(TARGET) $(FIRST)" end -- Return a compile command based on the specified args builder.compile_cmd = function( self, args ) args.defines = { args.defines, self.cmdline_macros } if self.preprocess_mode then args.comptype = "-E" elseif self.asm_mode then args.comptype = "-S" else args.comptype = "-c" end return self:_generic_cmd( args ) end -- Return an assembler command based on the specified args builder.asm_cmd = function( self, args ) args.defines = { args.defines, self.cmdline_macros } args.compiler = args.assembler args.comptype = self.preprocess_mode and "-E" or "-c" return self:_generic_cmd( args ) end -- Return a link command based on the specified args builder.link_cmd = function( self, args ) local flags = type( args.flags ) == 'table' and table_to_string( utils.linearize_array( args.flags ) ) or args.flags local libraries = type( args.libraries ) == 'table' and table_to_string( utils.linearize_array( args.libraries ) ) or args.libraries local linkcmd = args.linker or "gcc" linkcmd = linkcmd .. " " .. flags .. " -o $(TARGET) $(DEPENDS)" linkcmd = linkcmd .. " " .. utils.prepend_string( libraries, "-l" ) return linkcmd end --------------------------------------- -- Target handling -- Create a return a new C to object target builder.c_target = function( self, target, deps, comp_cmd ) return _target.new( target, deps, comp_cmd or self.comp_cmd, self, 'comp' ) end -- Create a return a new ASM to object target builder.asm_target = function( self, target, deps, asm_cmd ) return _target.new( target, deps, asm_cmd or self._asm_cmd, self, 'asm' ) end -- Return the name of a dependency file name corresponding to a C source builder.get_dep_filename = function( self, srcname ) return utils.replace_extension( self.build_dir .. utils.dir_sep .. linearize_fname( srcname ), "d" ) end -- Create a return a new C dependency target builder.dep_target = function( self, dep, depdeps, dep_cmd ) local depname = self:get_dep_filename( dep ) return _target.new( depname, depdeps, dep_cmd, self, 'dep' ) end -- Create and return a new link target builder.link_target = function( self, out, dep, link_cmd ) local path, ext = utils.split_ext( out ) if not ext and self.exe_extension and #self.exe_extension > 0 then out = out .. self.exe_extension end local t = _target.new( out, dep, link_cmd or self.link_cmd, self, 'link' ) if self:_compare_config( 'link' ) then t:force_rebuild( true ) end t:set_substitute_cmdline( true ) return t end -- Create and return a new generic target builder.target = function( self, dest_target, deps, cmd ) return _target.new( dest_target, deps, cmd, self ) end -- Register a target (called from _target.new) builder.register_target = function( self, name, obj ) self._tlist[ name:gsub( "\\", "/" ) ] = obj end -- Returns a registered target (nil if not found) builder.get_registered_target = function( self, name ) return self._tlist[ name:gsub( "\\", "/" ) ] end --------------------------------------- -- Actual building functions -- Return the object name corresponding to a source file name builder.obj_name = function( self, name, ext ) local r = ext or self.obj_extension if not r then r = utils.is_windows() and "obj" or "o" end local objname = utils.replace_extension( name, r ) -- KEEP_DIR: object file in the same directory as source file -- BUILD_DIR_LINEARIZED: object file in the build directory, linearized filename if self.build_mode == self.KEEP_DIR then return objname elseif self.build_mode == self.BUILD_DIR_LINEARIZED then return self.build_dir .. utils.dir_sep .. linearize_fname( objname ) end end -- Read and interpret dependencies for each file specified in "ftable" -- "ftable" is either a space-separated string with all the source files or an array builder.read_depends = function( self, ftable ) if type( ftable ) == 'string' then ftable = utils.string_to_table( ftable ) end -- Read dependency data local dtable = {} for i = 1, #ftable do local f = io.open( self:get_dep_filename( ftable[ i ] ), "rb" ) local lines = ftable[ i ] if f then lines = f:read( "*a" ) f:close() lines = lines:gsub( "\n", " " ):gsub( "\\%s+", " " ):gsub( "%s+", " " ):gsub( "^.-: (.*)", "%1" ) end dtable[ ftable[ i ] ] = lines end return dtable end -- Create and return compile targets for the given sources builder.create_compile_targets = function( self, ftable, res ) if type( ftable ) == 'string' then ftable = utils.string_to_table( ftable ) end res = res or {} ccmd, oname = "-c", "o" if self.preprocess_mode then ccmd, oname = '-E', "pre" elseif self.asm_mode then ccmd, oname = '-S', 's' end -- Build dependencies for all targets for i = 1, #ftable do local isasm = ftable[ i ]:find( "%.c$" ) == nil -- Skip assembler targets if 'asm_dep_cmd' is set to 'false' -- Skip C targets if 'c_dep_cmd' is set to 'false' local skip = isasm and self.asm_dep_cmd == false skip = skip or ( not isasm and self.c_dep_cmd == false ) local deps = self:get_dep_filename( ftable[ i ] ) local target if not isasm then local depcmd = skip and self.comp_cmd or ( self.c_dep_cmd or self.comp_cmd:gsub( ccmd .. " ", sf( ccmd .. " -MD -MF %s ", deps ) ) ) target = self:c_target( self:obj_name( ftable[ i ], oname ), { self:get_registered_target( deps ) or ftable[ i ] }, depcmd ) else local depcmd = skip and self._asm_cmd or ( self.asm_dep_cmd or self._asm_cmd:gsub( ccmd .. " ", sf( ccmd .. " -MD -MF %s ", deps ) ) ) target = self:asm_target( self:obj_name( ftable[ i ], oname ), { self:get_registered_target( deps ) or ftable[ i ] }, depcmd ) end -- Pre build step: replace dependencies with the ones from the compiler generated dependency file local dprint = function( ... ) if self.disp_mode ~= "minimal" then print( ... ) end end if not skip then target:set_pre_build_function( function( t, _ ) if not self.clean_mode then local fres = self:read_depends( ftable[ i ] ) local fdeps = fres[ ftable[ i ] ] if #fdeps:gsub( "%s+", "" ) == 0 then fdeps = ftable[ i ] end t:set_dependencies( fdeps ) else if self.disp_mode ~= "minimal" then io.write( sf( "[builder] Removing %s ... ", deps ) ) end if os.remove( deps ) then dprint "done." else dprint "failed!" end end end ) end target.srcname = ftable[ i ] -- TODO: check clean mode? if not isasm then self.c_targets[ #self.c_targets + 1 ] = target end table.insert( res, target ) end return res end -- Add a target to the list of builder targets builder.add_target = function( self, target, help, alias ) self.targets[ target.target ] = { target = target, help = help } alias = alias or {} for _, v in ipairs( alias ) do self.targets[ v ] = { target = target, help = help } end return target end -- Make a target the default one builder.default = function( self, target ) self.deftarget = target.target self.targets.default = { target = target, help = "default target" } end -- Build everything builder.build = function( self, target ) local t = self.targetname or self.deftarget if not t then print( utils.col_red( "[builder] Error: build target not specified" ) ) os.exit( 1 ) end local trg -- Look for single targets (C source files) for _, ct in pairs( self.c_targets ) do if ct.srcname == t then trg = ct break end end if not trg then if not self.targets[ t ] then print( sf( "[builder] Error: target '%s' not found", t ) ) print( "Available targets: " ) print( " all source files" ) for k, v in pairs( self.targets ) do if not is_phony( k ) then print( sf( " %s - %s", k, v.help or "(no help available)" ) ) end end if self.deftarget and not is_phony( self.deftarget ) then print( sf( "Default target is '%s'", self.deftarget ) ) end os.exit( 1 ) else if self.preprocess_mode or self.asm_mode then print( "[builder] Error: preprocess (-E) or asm (-S) works only with single file targets." ) os.exit( 1 ) end trg = self.targets[ t ].target end end self:_create_build_dir() -- At this point check if we have a change in the state that would require a rebuild if self:_compare_config( 'comp' ) then print( utils.col_yellow( "[builder] Forcing rebuild due to configuration change." ) ) self.global_force_rebuild = true else self.global_force_rebuild = false end -- Do the actual build local res = trg:build() if not res then print( utils.col_yellow( sf( '[builder] %s: up to date', t ) ) ) end if self.clean_mode then os.remove( self.build_dir .. utils.dir_sep .. ".builddata.comp" ) os.remove( self.build_dir .. utils.dir_sep .. ".builddata.link" ) end print( utils.col_yellow( "[builder] Done building target." ) ) return res end -- Create dependencies, create object files, link final object builder.make_exe_target = function( self, target, file_list ) local odeps = self:create_compile_targets( file_list ) local exetarget = self:link_target( target, odeps ) self:default( self:add_target( exetarget ) ) return exetarget end ------------------------------------------------------------------------------- -- Other exported functions function new_builder( build_dir ) return builder.new( build_dir ) end