/* @file builder.js (WIP) @author David Gallardo @galloscript @brief Builder script to compile and link c++ projects without dependencies with IDE project. Can be launched from an IDE with custom build tool target, but all the dependencies must be handled by the target.js file and builder.js */ /* Example emtest.target.js exports.GetTargetData = function(aEnv) { var lData = {}; lData.TargetName = 'emtest'; lData.TargetType = 'exe'; lData.SrcPath = '.'; return lData; }; */ /* Example build.sh #!/bin/bash DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" BUILDER=$DIR"/../builder.js" node $BUILDER build emtest macosx Release x86_64 $DIR node $BUILDER build emtest emscripten release wasm $DIR */ //- Modules --------------------------------------- const spawn = require('child_process').spawnSync; const fs = require('fs'); //- Constants ------------------------------------- const BUILDER_PLATFORM_WINDOWS = 'mswindows' const BUILDER_PLATFORM_MACOSX = 'macosx' const BUILDER_PLATFORM_EMSCRIPTEN = 'emscripten' //Platform exclusive directories const BUILDER_WINDOWS_SOURCES_DIRNAME = 'win'; const BUILDER_OSX_SOURCES_DIRNAME = 'osx'; const BUILDER_EMSCRIPTEN_SOURCES_DIRNAME = 'ems'; //Target type const BUILDER_STATIC_LIBRARY = 'lib'; const BUILDER_DYNAMIC_LIBRARY = 'dylib'; const BUILDER_EXECUTABLE = 'exe'; //Configurations const BUILDER_CONFIGURATIONS = { BUILDER_DEBUG : 'debug', BUILDER_RELEASE : 'release' }; //Versions const BUILDER_VC_VERSION = '14'; const BUILDER_OSXSDK_VERSION = '10.11'; //- Global Configuration -------------------------- var GlobalConfig = {}; //- Utility --------------------------------------- function CreatePathDirectories(aPath) { aPath.split('/').reduce((path, folder) => { path += folder + '/'; if (!fs.existsSync(path)) { fs.mkdirSync(path); } return path; }, ''); } //Returns a list of relative paths to compilable files function TraverseSourcesDirectoryTree(aFiles, aDir, aExtensions, aExcluded) { //console.log('Traversing '+aDir); var lList = fs.readdirSync(aDir); for(var f = 0; f < lList.length; ++f) { var lFile = aDir + '/' + lList[f]; var lStat = fs.statSync(lFile); if (lStat && lStat.isDirectory()) { TraverseSourcesDirectoryTree(aFiles, lFile, aExtensions, aExcluded); } else { var lExtension = lFile.replace(/.*\./, '').toLowerCase(); if( (aExtensions.length == 0) || (aExtensions.indexOf(lExtension) != -1) ) { aFiles.push(lFile); } } } } function PrepareCommand(aCommandStr, aOutputParamsObj) { //Remove double whitespaces and split into array aCommandStr = aCommandStr.replace(/ +/g, ' '); console.log("Command: "+aCommandStr); //console.log(lSourceFiles[f]); var lCommandArray = aCommandStr.split(' '); //Separate command from parameters var lCommandExecutable = lCommandArray[0]; lCommandArray.shift(); aOutputParamsObj.CommandName = lCommandExecutable; aOutputParamsObj.CommandParameters = lCommandArray; } //- Compiler Configs ------------------------------ function ConfigureShared(aConfiguration, aWorkingDir, aTargetData) { GlobalConfig.WorkingDir = aWorkingDir; GlobalConfig.SourcesExtensions = ['cpp', 'c', 'cc', 'cxx']; GlobalConfig.ExcludedDirectories = [ 'avoid', 'deprecated', BUILDER_WINDOWS_SOURCES_DIRNAME, BUILDER_OSX_SOURCES_DIRNAME ]; GlobalConfig.IntermediatePath = 'intermediate/'+aConfiguration+'/${TARGET_NAME}'; GlobalConfig.IncludePaths = []; GlobalConfig.LibraryPaths = [ GlobalConfig.WorkingDir+'/bin/'+aConfiguration ]; GlobalConfig.Libraries = []; GlobalConfig.Frameworks = []; GlobalConfig.OutputPath = 'bin/'+aConfiguration; } function ConfigureVC(aConfiguration, aTargetData) { GlobalConfig.ExcludedDirectories.splice(GlobalConfig.ExcludedDirectories.indexOf(BUILDER_WINDOWS_SOURCES_DIRNAME), 1); GlobalConfig.ObjExtension = '.obj'; GlobalConfig.StaticLibraryExtension = '.lib'; GlobalConfig.DynamicLibraryExtension = '.dll'; GlobalConfig.ExecutableExtension = '.exe'; GlobalConfig.LibraryPrefix = ''; //TODO: config for visual studio c++ compiler & linker } function ConfigureOSXClang(aConfiguration, aTargetData) { GlobalConfig.SourcesExtensions = GlobalConfig.SourcesExtensions.concat(['m', 'mm']); GlobalConfig.ExcludedDirectories.splice(GlobalConfig.ExcludedDirectories.indexOf(BUILDER_OSX_SOURCES_DIRNAME), 1); GlobalConfig.ObjExtension = '.o'; GlobalConfig.StaticLibraryExtension = '.a'; GlobalConfig.DynamicLibraryExtension = '.dylib'; GlobalConfig.ExecutableExtension = ''; GlobalConfig.LibraryPrefix = 'lib'; GlobalConfig.CompilerCommand = 'clang++ ${SOURCE_FILE} -c -g -Wall -arch x86_64 -mmacosx-version-min='+BUILDER_OSXSDK_VERSION+' -std=gnu++11 -stdlib=libc++ ${COMPILER_PARAMS} -o ${OBJECT_FILE}'; GlobalConfig.CompilerAppendIncludePath = ' -I${PATH}'; switch(aConfiguration) { case BUILDER_CONFIGURATIONS.BUILDER_DEBUG: GlobalConfig.CompilerParams = ' -O0 -DDEBUG=1'; break; case BUILDER_CONFIGURATIONS.BUILDER_RELEASE: default: GlobalConfig.CompilerParams = ' -Os'; break; }; GlobalConfig.CompilerExtraParams = ' -Werror -Wno-missing-field-initializers -Wno-missing-prototypes -Werror=return-type '+ ' -Wdocumentation -Wunreachable-code -Werror=deprecated-objc-isa-usage '+ ' -Werror=objc-root-class -Wno-missing-braces -Wparentheses -Wswitch -Wunused-function '+ ' -Wno-unused-label -Wno-unused-parameter -Wunused-variable -Wunused-value '+ ' -Wempty-body -Wconditional-uninitialized -Wno-unknown-pragmas -Wshadow '+ ' -Wno-four-char-constants -Wno-conversion -Wconstant-conversion -Wint-conversion '+ ' -Wbool-conversion -Wenum-conversion -Wassign-enum -Wsign-compare -Wshorten-64-to-32 '+ ' -Wpointer-sign -Wnewline-eof'; GlobalConfig.LinkerAppendLibrary = ' -l${LIBRARY_NAME}'; GlobalConfig.LinkerAppendLibraryPath = ' -L${LIBRARY_PATH}'; GlobalConfig.LinkerAppendFramework = ' -framework ${FRAMEWORK_NAME}'; GlobalConfig.StaticLibraryCommand = 'Libtool -static ${OBJECT_FILE_LIST} ${DEPENCENCIES} -o ${LIBRARY_FILE}'; GlobalConfig.DynamicLibraryCommand = 'Libtool -dynamic ${OBJECT_FILE_LIST} ${DEPENCENCIES} -o ${LIBRARY_FILE}'; var lSysRoot = '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX'+BUILDER_OSXSDK_VERSION+'.sdk'; GlobalConfig.ExecutableCommand = 'clang++ ${OBJECT_FILE_LIST} -arch x86_64 -isysroot '+lSysRoot+' -mmacosx-version-min='+BUILDER_OSXSDK_VERSION+' ${DEPENCENCIES} -o ${EXECUTABLE_FILE}'; } function ConfigureEmscripten(aConfiguration, aTargetData) { GlobalConfig.ObjExtension = '.bc'; GlobalConfig.StaticLibraryExtension = '.bc'; GlobalConfig.DynamicLibraryExtension = '.bc'; GlobalConfig.ExecutableExtension = '.html'; GlobalConfig.LibraryPrefix = 'lib'; GlobalConfig.CompilerCommand = 'emcc ${SOURCE_FILE} -c -g -Wall ${COMPILER_PARAMS} -o ${OBJECT_FILE}'; GlobalConfig.CompilerAppendIncludePath = ' -I${PATH}'; switch(aConfiguration) { case BUILDER_CONFIGURATIONS.BUILDER_DEBUG: GlobalConfig.CompilerParams = ' -O0 -DDEBUG=1'; break; case BUILDER_CONFIGURATIONS.BUILDER_RELEASE: default: GlobalConfig.CompilerParams = ' -Os'; break; }; GlobalConfig.CompilerExtraParams = ' -Werror -Wno-missing-field-initializers -Wno-missing-prototypes -Werror=return-type '+ ' -Wdocumentation -Wunreachable-code -Werror=deprecated-objc-isa-usage '+ ' -Werror=objc-root-class -Wno-missing-braces -Wparentheses -Wswitch -Wunused-function '+ ' -Wno-unused-label -Wno-unused-parameter -Wunused-variable -Wunused-value '+ ' -Wempty-body -Wconditional-uninitialized -Wno-unknown-pragmas -Wshadow '+ ' -Wno-four-char-constants -Wno-conversion -Wconstant-conversion -Wint-conversion '+ ' -Wbool-conversion -Wenum-conversion -Wassign-enum -Wsign-compare -Wshorten-64-to-32 '+ ' -Wpointer-sign -Wnewline-eof'; GlobalConfig.LinkerAppendLibrary = ' -l${LIBRARY_NAME}'; GlobalConfig.LinkerAppendLibraryPath = ' -L${LIBRARY_PATH}'; GlobalConfig.LinkerAppendFramework = ' -framework ${FRAMEWORK_NAME}'; GlobalConfig.StaticLibraryCommand = 'emcc ${OBJECT_FILE_LIST} ${DEPENCENCIES} -o ${LIBRARY_FILE}'; GlobalConfig.DynamicLibraryCommand = 'emcc ${OBJECT_FILE_LIST} ${DEPENCENCIES} -o ${LIBRARY_FILE}'; GlobalConfig.ExecutableCommand = 'emcc ${OBJECT_FILE_LIST} ${DEPENCENCIES} -o ${EXECUTABLE_FILE}'; } //- Build ----------------------------------------- function Build(aTargetData, aConfiguration, aArchitecture, aPlatform) { //Compiler var lCompilerCommand = GlobalConfig.CompilerCommand; var lCompilerParams = ''; var lIntermediatePath = GlobalConfig.IntermediatePath.replace('${TARGET_NAME}', aTargetData.TargetName); //Appen Include paths for(var i = 0; i < GlobalConfig.IncludePaths.length; ++i) { lCompilerParams += GlobalConfig.CompilerAppendIncludePath.replace('${PATH}', GlobalConfig.IncludePaths[i]); } //Append extra configuracion params lCompilerParams += GlobalConfig.CompilerParams; lCompilerParams += GlobalConfig.CompilerExtraParams; //Replace params lCompilerCommand = lCompilerCommand.replace('${COMPILER_PARAMS}', lCompilerParams); //Iterate files var lSourceFiles = []; var lObjectFiles = []; TraverseSourcesDirectoryTree(lSourceFiles, GlobalConfig.WorkingDir+'/'+aTargetData.SrcPath, GlobalConfig.SourcesExtensions, GlobalConfig.ExcludedDirectories); if(lSourceFiles.length == 0) { console.error('[builder.js] No source files found.'); return 1; } var lOk = true; for(var f = 0; f < lSourceFiles.length; ++f) { //filename withoud working dir path var lRelativePath = lSourceFiles[f].replace(GlobalConfig.WorkingDir+'/', ''); //Object file path var lIntermediateFilePath = GlobalConfig.WorkingDir+'/'+lIntermediatePath+'/'+lRelativePath+GlobalConfig.ObjExtension; var lIntermediateFileDirectory = lIntermediateFilePath.substring(0, lIntermediateFilePath.lastIndexOf("/")); lObjectFiles.push(lIntermediateFilePath); // Create Intermediate directory if it doens't exist CreatePathDirectories(lIntermediateFileDirectory); //Create File command var lFileCommand = lCompilerCommand.replace('${SOURCE_FILE}', GlobalConfig.WorkingDir+'/'+lRelativePath).replace('${OBJECT_FILE}', lIntermediateFilePath); var lCommandData = {CommandName : '', CommandParameters : []}; PrepareCommand(lFileCommand, lCommandData); //TODO: Exclude older object files //TODO: Multythread AND/OR Unity build //Run the compiler command var s = spawn(lCommandData.CommandName, lCommandData.CommandParameters, { stdio: 'inherit' }); lOk = lOk && (s.status == 0); } if(!lOk) return 2; //Create Library if(BUILDER_STATIC_LIBRARY == aTargetData.TargetType) { var lLibraryOutputPath = GlobalConfig.WorkingDir+ '/'+GlobalConfig.OutputPath+ '/'+GlobalConfig.LibraryPrefix +aTargetData.TargetName +GlobalConfig.StaticLibraryExtension; var lLibraryOutputDirectory = lLibraryOutputPath.substring(0, lLibraryOutputPath.lastIndexOf("/")); var lLinkCommand = GlobalConfig.StaticLibraryCommand .replace('${OBJECT_FILE_LIST}', lObjectFiles.join(' ')) .replace('${DEPENCENCIES}', '') .replace('${LIBRARY_FILE}', lLibraryOutputPath); var lCommandData = {CommandName : '', CommandParameters : []}; PrepareCommand(lLinkCommand, lCommandData); // Create Output directory if it doens't exist CreatePathDirectories(lLibraryOutputDirectory); //Run the libtool command var s = spawn(lCommandData.CommandName, lCommandData.CommandParameters, { stdio: 'inherit' }); //(s.status == 0); } //Create Executable else if(BUILDER_EXECUTABLE == aTargetData.TargetType) { var lDepencencies = ''; for(var i = 0; i < GlobalConfig.LibraryPaths.length; ++i) { lDepencencies += GlobalConfig.LinkerAppendLibraryPath.replace('${LIBRARY_PATH}', GlobalConfig.LibraryPaths[i]); } for(var i = 0; i < GlobalConfig.Libraries.length; ++i) { lDepencencies += GlobalConfig.LinkerAppendLibrary.replace('${LIBRARY_NAME}', GlobalConfig.Libraries[i]); } //TODO: append frameworks var lExecutableOutputPath = GlobalConfig.WorkingDir+'/'+GlobalConfig.OutputPath+'/'+aTargetData.TargetName+GlobalConfig.ExecutableExtension; var lExecutableOutputDirectory = lExecutableOutputPath.substring(0, lExecutableOutputPath.lastIndexOf("/")); var lLinkCommand = GlobalConfig.ExecutableCommand .replace('${OBJECT_FILE_LIST}', lObjectFiles.join(' ')) .replace('${DEPENCENCIES}', lDepencencies) .replace('${EXECUTABLE_FILE}', lExecutableOutputPath); var lCommandData = {CommandName : '', CommandParameters : []}; PrepareCommand(lLinkCommand, lCommandData); // Create Output directory if it doens't exist CreatePathDirectories(lExecutableOutputDirectory); //Run the linker command var s = spawn(lCommandData.CommandName, lCommandData.CommandParameters, { stdio: 'inherit' }); //(s.status == 0); } } function Main() { var lAction = process.argv[2]; var lTargetName = process.argv[3]; var lPlatform = process.argv[4]; var lConfiguration = process.argv[5]; var lArchitecture = process.argv[6]; var lWorkingDir = process.argv[7] || process.pwd(); console.log(lWorkingDir); if(!lArchitecture) { console.error('[builder.js] Incorrect number of arguments.'); console.error('[builder.js] node builder.js action[build|clean] target_name platform[macosx|mswindows|emscripten] configuration[debug|release] architecture[x86|x86_64]'); console.error('Current arguments: '); process.argv.forEach(function (val, index, array) { console.error(index + ': ' + val); }); return 1; } lAction = lAction.toLowerCase(); lPlatform = lPlatform.toLowerCase(); lConfiguration = lConfiguration.toLowerCase(); lArchitecture = lArchitecture.toLowerCase(); //Target data var lTargetFileName = lWorkingDir+'/'+lTargetName+'.target.js'; var lTargetData = require(lTargetFileName).GetTargetData(); //Shared configuration ConfigureShared(lConfiguration, lWorkingDir, lTargetData); switch(lPlatform) { case BUILDER_PLATFORM_WINDOWS: ConfigureVC(lConfiguration, lTargetData); break; case BUILDER_PLATFORM_MACOSX: ConfigureOSXClang(lConfiguration, lTargetData); break; case BUILDER_PLATFORM_EMSCRIPTEN: ConfigureEmscripten(lConfiguration, lTargetData); break; } return Build(lTargetData, lConfiguration, lArchitecture, lPlatform); } var r = Main(); process.exit(r);