#include #include "../BackendUtils.hpp" #include "Ninja.hpp" #include "../../include/FileUtil.hpp" #include "../../include/Exec.hpp" #include "../../include/PkgConfig.hpp" #include "../../include/GlobalLib.hpp" using namespace std; using namespace sibs; #if OS_FAMILY == OS_FAMILY_POSIX #define nprintf printf #else #define nprintf wprintf #endif namespace backend { string join(const vector &list, const char *joinStr) { if(list.empty()) return ""; string result; long stringSize = 0; long joinStrLen = strlen(joinStr); int i = 0; for(const string &str : list) { stringSize += str.size(); if(!str.empty() && i > 0) stringSize += joinStrLen; ++i; } result.reserve(stringSize); i = 0; for(const string &str : list) { if(i > 0) result += joinStr; result += str; ++i; } return result; } bool endsWith(const string &str, const string &endWithStr) { if(endWithStr.size() > str.size()) return false; else return strncmp(&str[str.size() - endWithStr.size()], &endWithStr[0], endWithStr.size()) == 0; } Ninja::LibraryType getNinjaLibraryType(PackageType packageType) { switch(packageType) { case PackageType::EXECUTABLE: return Ninja::LibraryType::EXECUTABLE; case PackageType::STATIC: return Ninja::LibraryType::STATIC; case PackageType::DYNAMIC: case PackageType::LIBRARY: return Ninja::LibraryType::DYNAMIC; default: assert(false); return (Ninja::LibraryType)-1; } } string getIncludeOptionFlag(Compiler compiler, const string &filepath) { string result; switch (compiler) { case Compiler::GCC: { result = "'-I"; result += filepath; result += "'"; break; } case Compiler::MSVC: { result = "/I \""; result += filepath; result += "\""; break; } } return result; } string getDefineFlag(Compiler compiler, const string &name, const string &value) { string result; switch (compiler) { case Compiler::GCC: { result = "'-D"; result += name; result += "="; result += value; result += "'"; break; } case Compiler::MSVC: { result = "\"/D"; result += name; result += "="; result += value; result += "\""; break; } } return result; } string getObjectFileNameFlag(Compiler compiler, const string &objectFileName) { string result; switch (compiler) { case Compiler::GCC: { result = "-o "; result += objectFileName; break; } case Compiler::MSVC: { result = "/Fo"; result += objectFileName; break; } } return result; } string getLanguageVersionFlag(Compiler compiler, CVersion cVersion) { switch (compiler) { case Compiler::GCC: { switch(cVersion) { case CVersion::C89: return "-std=c89 -pedantic"; case CVersion::C99: return "-std=c99 -pedantic"; case CVersion::C11: return "-std=c11 -pedantic"; } } case Compiler::MSVC: { // TODO: Is it possible to specify c version in msvc? return ""; } } assert(false); return ""; } string getLanguageVersionFlag(Compiler compiler, CPPVersion cppVersion) { switch (compiler) { case Compiler::GCC: { switch(cppVersion) { case CPPVersion::CPP11: return "-std=c++11 -pedantic"; case CPPVersion::CPP14: return "-std=c++14 -pedantic"; case CPPVersion::CPP17: return "-std=c++17 -pedantic"; } } case Compiler::MSVC: { switch(cppVersion) { // Use /Za flag? case CPPVersion::CPP11: return "/std=c++11"; case CPPVersion::CPP14: return "/std=c++14"; case CPPVersion::CPP17: return "/std=c++17"; } } } assert(false); return ""; } const char* getObjectFileExtension(Compiler compiler) { switch (compiler) { case Compiler::GCC: return ".o"; case Compiler::MSVC: return ".obj"; default: return nullptr; } } Ninja::Ninja() { } void Ninja::addGlobalIncludeDirs(const string &globalIncludeDirs) { customGlobalIncludeDirs = globalIncludeDirs; } void Ninja::addSourceFile(const char *filepath) { string filePathStr = filepath ? filepath : ""; if(filepath && !containsSourceFile(filePathStr)) { sourceFiles.emplace_back(filePathStr); printf("Adding source file: %s\n", filepath); } } void Ninja::addTestSourceDir(const char *dir) { string dirStr = dir ? dir : ""; if(dir && !containsTestSourceDir(dirStr)) { testSourceDirs.emplace_back(dirStr); printf("Adding test source directory: %s\n", dir); } } void Ninja::addDependency(const std::string &binaryFile) { if(!containsDependency(binaryFile)) binaryDependencies.emplace_back(binaryFile); } void Ninja::addSubProject(Ninja *subProject, SibsConfig *config, sibs::FileString &&buildPath) { subProjects.emplace_back(NinjaSubProject{ subProject, config, move(buildPath) }); } const std::vector& Ninja::getSourceFiles() const { return sourceFiles; } bool Ninja::containsSourceFile(const string &filepath) const { for(const string &sourceFile : sourceFiles) { if(sourceFile == filepath) return true; } return false; } bool Ninja::containsTestSourceDir(const string &dir) const { for(const string &testSourceDir : testSourceDirs) { if(testSourceDir == dir) return true; } return false; } bool Ninja::containsDependency(const string &dependency) const { for(const string &binaryDependency : binaryDependencies) { if(binaryDependency == dependency) return true; } return false; } // TODO: First check if pkg-config is installed. If it's not, only check dependencies that exists in the dependencies sub directory. // If pkg-config is installed and dependency is not installed, check in dependencies sub directory. Result Ninja::getLinkerFlags(const SibsConfig &config, LinkerFlagCallbackFunc staticLinkerFlagCallbackFunc, LinkerFlagCallbackFunc dynamicLinkerFlagCallback, GlobalIncludeDirCallbackFunc globalIncludeDirCallback, CflagsCallbackFunc cflagsCallbackFunc) const { const vector &packageListDependencies = config.getPackageListDependencies(); Result globalLibDirResult = getHomeDir(); if (!globalLibDirResult) return Result::Err(globalLibDirResult); FileString globalLibDir = globalLibDirResult.unwrap(); globalLibDir += TINYDIR_STRING("/.cache/sibs/lib"); Result createGlobalLibDirResult = createDirectoryRecursive(globalLibDir.c_str()); if(createGlobalLibDirResult.isErr()) return createGlobalLibDirResult; vector globalLibDependencies; #if OS_FAMILY == OS_FAMILY_POSIX vector pkgConfigDependencies; for(PackageListDependency *dependency : packageListDependencies) { Result pkgConfigDependencyValidation = PkgConfig::validatePkgConfigPackageVersionExists(dependency); if(pkgConfigDependencyValidation.isOk()) { pkgConfigDependencies.push_back(dependency); } else { globalLibDependencies.push_back(dependency); } } Result pkgConfigFlagsResult = PkgConfig::getDynamicLibsFlags(pkgConfigDependencies); if (!pkgConfigFlagsResult) { printf("%s, using global lib...\n", pkgConfigFlagsResult.getErrMsg().c_str()); globalLibDependencies.reserve(globalLibDependencies.size() + pkgConfigDependencies.size()); for (PackageListDependency *pkgConfigDependency : pkgConfigDependencies) { globalLibDependencies.push_back(pkgConfigDependency); } pkgConfigDependencies.clear(); } else { const PkgConfigFlags &pkgConfigFlag = pkgConfigFlagsResult.unwrap(); if (!pkgConfigFlag.linkerFlags.empty()) dynamicLinkerFlagCallback(pkgConfigFlag.linkerFlags); if(!pkgConfigFlag.cflags.empty()) cflagsCallbackFunc(pkgConfigFlag.cflags); } #else for (const Dependency &dependency : dependencies) { globalLibDependencies.push_back(dependency); } #endif return GlobalLib::getLibs(globalLibDependencies, config, globalLibDir, staticLinkerFlagCallbackFunc, dynamicLinkerFlagCallback, globalIncludeDirCallback); } enum class SourceFileLanguage { C, CPP }; Result Ninja::buildSubProjects(LinkerFlagCallbackFunc staticLinkerFlagCallbackFunc, LinkerFlagCallbackFunc dynamicLinkerFlagCallback, GlobalIncludeDirCallbackFunc globalIncludeDirCallback) { for(auto &subProject : subProjects) { if(subProject.config->getPackageType() == PackageType::EXECUTABLE) { string errMsg = "The sub project "; errMsg += toUtf8(subProject.buildPath); errMsg += " is an executable. Only libraries can be sub projects"; return Result::Err(errMsg); } Result buildResult = subProject.subProject->build(*subProject.config, subProject.buildPath.c_str(), staticLinkerFlagCallbackFunc, dynamicLinkerFlagCallback, globalIncludeDirCallback); if(!buildResult) return buildResult; } return Result::Ok(true); } Result Ninja::build(const SibsConfig &config, const _tinydir_char_t *savePath, LinkerFlagCallbackFunc staticLinkerFlagCallbackFunc, LinkerFlagCallbackFunc dynamicLinkerFlagCallback, GlobalIncludeDirCallbackFunc globalIncludeDirCallback) { if (!sourceFiles.empty()) { Result createBuildDirResult = createDirectoryRecursive(savePath); if (createBuildDirResult.isErr()) return createBuildDirResult; } LibraryType libraryType = getNinjaLibraryType(config.getPackageType()); string savePathUtf8 = toUtf8(savePath); string projectPathUtf8 = toUtf8(config.getProjectPath()); FileString ninjaBuildFilePath = savePath; ninjaBuildFilePath += TINYDIR_STRING("/build.ninja"); string result; result.reserve(2048); result += "globalIncDir = "; for(const auto &includeDir : config.getIncludeDirs()) { string includeDirRelative = "../../"; includeDirRelative += includeDir; result += " "; result += getIncludeOptionFlag(config.getCompiler(), includeDirRelative); } auto parentGlobalIncludeDirCallback = globalIncludeDirCallback; for (const string &globalIncludeDir : config.getGlobalIncludeDirs()) { string globalIncludeDirFull = projectPathUtf8; globalIncludeDirFull += "/"; globalIncludeDirFull += globalIncludeDir; if(parentGlobalIncludeDirCallback) parentGlobalIncludeDirCallback(globalIncludeDirFull); } string dependencyExportIncludeDirs = customGlobalIncludeDirs; globalIncludeDirCallback = [&parentGlobalIncludeDirCallback, &globalIncludeDirCallback, &dependencyExportIncludeDirs, &config](const string &globalIncludeDir) { dependencyExportIncludeDirs += " "; dependencyExportIncludeDirs += getIncludeOptionFlag(config.getCompiler(), globalIncludeDir); if (parentGlobalIncludeDirCallback) parentGlobalIncludeDirCallback(globalIncludeDir); }; #if OS_TYPE == OS_TYPE_LINUX // TODO: Allow configuring default linking flags. Maybe have `package.useThreads = false` to disable this flag string allLinkerFlags = "-pthread"; #else string allLinkerFlags = ""; #endif // TODO: Somehow check loading order, because it has to be correct to work.. Or does it for dynamic libraries? // Anyways it's required for static libraries for (const string &binaryDependency : binaryDependencies) { allLinkerFlags += " "; allLinkerFlags += binaryDependency; } string staticLinkerFlags; auto parentProjStaticLinkerFlagCallbackFunc = staticLinkerFlagCallbackFunc; if (!staticLinkerFlagCallbackFunc || libraryType == LibraryType::DYNAMIC) { staticLinkerFlagCallbackFunc = [&staticLinkerFlags](const string &linkerFlag) { staticLinkerFlags += " "; staticLinkerFlags += linkerFlag; }; } string dynamicLinkerFlags; auto parentProjDynamicLinkerFlagCallbackFunc = dynamicLinkerFlagCallback; if(!dynamicLinkerFlagCallback || libraryType != LibraryType::STATIC) { dynamicLinkerFlagCallback = [&dynamicLinkerFlags](const string &linkerFlag) { dynamicLinkerFlags += " "; dynamicLinkerFlags += linkerFlag; }; } string cflags; auto cflagsCallbackFunc = [&cflags](const string &dependencyCflags) { cflags += " "; cflags += dependencyCflags; }; Result buildSubProjectResult = buildSubProjects(staticLinkerFlagCallbackFunc, dynamicLinkerFlagCallback, globalIncludeDirCallback); if(!buildSubProjectResult) return buildSubProjectResult; Result linkerFlags = getLinkerFlags(config, staticLinkerFlagCallbackFunc, dynamicLinkerFlagCallback, globalIncludeDirCallback, cflagsCallbackFunc); if (linkerFlags.isErr()) return Result::Err(linkerFlags.getErrMsg()); allLinkerFlags += staticLinkerFlags + dynamicLinkerFlags; result += dependencyExportIncludeDirs; result += "\n\n"; string defines; for(const auto &definePair : config.getDefines()) { defines += " "; defines += getDefineFlag(config.getCompiler(), definePair.first, definePair.second); } switch (SYSTEM_PLATFORM) { case PLATFORM_WIN64: defines += " /DWIN64 /D_WIN64"; case PLATFORM_WIN32: defines += " /DWIN32 /D_WIN32"; break; } #if OS_TYPE == OS_TYPE_WINDOWS switch(libraryType) { case LibraryType::EXECUTABLE: defines += " _CONSOLE"; break; case LibraryType::STATIC: defines += " _LIB"; break; } #endif string buildJob; switch(libraryType) { case LibraryType::EXECUTABLE: { switch (config.getCompiler()) { case Compiler::GCC: { result += "rule cpp_COMPILER\n"; result += " depfile = $out.d\n"; result += " command = ccache c++ $ARGS -c $in -o $out\n\n"; result += "rule BUILD_EXEC\n"; result += " depfile = $out.d\n"; result += " command = ccache c++ $ARGS -o $out $in $LINK_ARGS $aliasing\n\n"; result += "rule c_COMPILER\n"; result += " depfile = $out.d\n"; result += " command = ccache cc $ARGS -c $in -o $out\n\n"; break; } case Compiler::MSVC: { result += "rule cpp_COMPILER\n"; result += " command = cl.exe $ARGS /c $in /Fo$out\n\n"; result += "rule BUILD_EXEC\n"; result += " command = cl.exe $ARGS $in /Fe$out $LINK_ARGS\n\n"; result += "rule c_COMPILER\n"; result += " command = cl.exe $ARGS /c $in /Fo$out\n\n"; break; } } buildJob = "BUILD_EXEC"; break; } case LibraryType::STATIC: { switch (config.getCompiler()) { case Compiler::GCC: { result += "rule cpp_COMPILER\n"; result += " depfile = $out.d\n"; result += " command = ccache c++ $ARGS -c -fPIC $in -o $out\n\n"; result += "rule BUILD_STATIC\n"; result += " command = ar rcs lib"; result += config.getPackageName(); result += ".a"; result += " $in\n\n"; result += "rule c_COMPILER\n"; result += " depfile = $out.d\n"; result += " command = ccache cc $ARGS -c -fPIC $in -o $out\n\n"; break; } case Compiler::MSVC: { result += "rule cpp_COMPILER\n"; result += " command = cl.exe $ARGS /c $in /Fo$out\n\n"; result += "rule BUILD_STATIC\n"; result += " command = lib.exe /OUT:$out $in\n\n"; result += "rule c_COMPILER\n"; result += " command = cl.exe $ARGS /c $in /Fo$out\n\n"; break; } } buildJob = "BUILD_STATIC"; break; } case LibraryType::DYNAMIC: { switch (config.getCompiler()) { case Compiler::GCC: { result += "rule cpp_COMPILER\n"; result += " depfile = $out.d\n"; result += " command = ccache c++ $ARGS -c -fPIC $in -o $out\n\n"; // --whole-archive result += "rule BUILD_DYNAMIC\n"; result += " depfile = $out.d\n"; result += " command = ccache c++ $in -shared -o $out $LINK_ARGS $aliasing\n\n"; result += "rule c_COMPILER\n"; result += " depfile = $out.d\n"; result += " command = ccache cc $ARGS -c -fPIC $in -o $out\n\n"; break; } case Compiler::MSVC: { result += "rule cpp_COMPILER\n"; result += " command = cl.exe $ARGS /c $in /Fo$out\n\n"; //result += "rule BUILD_DYNAMIC\n"; //result += " command = cl.exe /LD $in /Fe$out $LINK_ARGS\n\n"; result += "rule BUILD_DYNAMIC\n"; result += " command = lib.exe /OUT:$out $in\n\n"; result += "rule c_COMPILER\n"; result += " command = cl.exe $ARGS /c $in /Fo$out\n\n"; break; } } buildJob = "BUILD_DYNAMIC"; break; } default: assert(false); return Result::Err("Unexpected error"); } // TODO: Add equivalent functionality for msvc. Currently msvc always builds as debug build string optimizationFlags; switch (config.getOptimizationLevel()) { case OPT_LEV_DEBUG: { switch (config.getCompiler()) { case Compiler::GCC: { optimizationFlags = "'-Og'"; break; } case Compiler::MSVC: { optimizationFlags = "/Od"; break; } } break; } case OPT_LEV_RELEASE: { switch (config.getCompiler()) { case Compiler::GCC: { optimizationFlags = "'-O3' '-DNDEBUG'"; break; } case Compiler::MSVC: { optimizationFlags = "/Ox /DNDEBUG"; break; } } break; } } vector objectNames; objectNames.reserve(sourceFiles.size()); for(const string &sourceFile : sourceFiles) { SourceFileLanguage sourceFileLanguage = SourceFileLanguage::CPP; if(endsWith(sourceFile, ".c")) sourceFileLanguage = SourceFileLanguage::C; const char *buildTarget; switch(sourceFileLanguage) { case SourceFileLanguage::C: buildTarget = "c_COMPILER"; break; case SourceFileLanguage::CPP: buildTarget = "cpp_COMPILER"; break; } //string sourceFileEncoded = sourceFile; //replace(sourceFileEncoded, '/', '@'); string objectName = config.getPackageName() + "@exe/" + sourceFile; objectName += getObjectFileExtension(config.getCompiler()); result += "build "; result += objectName; result += ": "; result += buildTarget; result += " ../../"; result += sourceFile; result += "\n"; result += " ARGS = $globalIncDir "; if(!defines.empty()) result += defines; result += " "; switch(sourceFileLanguage) { case SourceFileLanguage::C: result += getLanguageVersionFlag(config.getCompiler(), config.getCversion()); break; case SourceFileLanguage::CPP: result += getLanguageVersionFlag(config.getCompiler(), config.getCppVersion()); break; } switch (config.getCompiler()) { case Compiler::GCC: { // -Werror // TODO: Find equivalent -MMD -MP for other compilers than gcc. MMD is used to create "dependency files" -> if headers are modified then source files will be recompiled // when compiling next time... result += " -fpie -MMD -MP '-I" + config.getPackageName() + "@exe' " + cflags + " '-I..' -Wall -Wextra -Werror=return-type -fdiagnostics-show-option '-fdiagnostics-color=always' '-pipe' '-D_FILE_OFFSET_BITS=64' '-Winvalid-pch' -fstack-protector " + optimizationFlags; if(sourceFileLanguage == SourceFileLanguage::CPP) { result += " -fexceptions -Wnon-virtual-dtor"; } switch (config.getOptimizationLevel()) { case OPT_LEV_DEBUG: result += " -g3 -D_FORITY_SOURCE=2 -D_GLIBCXX_ASSERTIONS -fasynchronous-unwind-tables "; break; case OPT_LEV_RELEASE: result += " -g0"; break; } break; } case Compiler::MSVC: { result += " "; result += optimizationFlags; result += " /EHsc /W3 "; result += cflags; switch (config.getOptimizationLevel()) { case OPT_LEV_DEBUG: result += " /MTd"; break; case OPT_LEV_RELEASE: result += " /MT"; break; } switch (SYSTEM_PLATFORM) { case PLATFORM_WIN32: result += " /MACHINE:X86"; break; case PLATFORM_WIN64: result += " /MACHINE:X64"; break; } break; } } result += "\n\n"; objectNames.emplace_back(objectName); } string projectGeneratedBinaryFlags; if (!sourceFiles.empty()) { string projectGeneratedBinary = "\""; projectGeneratedBinary += savePathUtf8; projectGeneratedBinary += "/"; switch (libraryType) { case LibraryType::EXECUTABLE: { result += "build "; result += config.getPackageName(); result += ": " + buildJob + " "; result += join(objectNames, " "); result += "\n"; switch (config.getCompiler()) { case Compiler::GCC: { // TODO: Add flag to disable -ldl (dlopen, dlclose...) result += " LINK_ARGS = '-Wl,--no-undefined,--as-needed' -ldl "; break; } case Compiler::MSVC: { // TODO: Do not link all of these. Find a way to only link the ones that are needed result += " LINK_ARGS = Ws2_32.lib Wldap32.lib Crypt32.lib Advapi32.lib Gdi32.lib User32.lib Userenv.lib OpenGL32.lib GlU32.lib Shell32.lib "; break; } } if (!allLinkerFlags.empty()) { result += allLinkerFlags; } result += "\n\n"; projectGeneratedBinary += config.getPackageName(); #if OS_FAMILY == OS_FAMILY_WINDOWS projectGeneratedBinary += ".exe"; #endif break; } case LibraryType::STATIC: { switch (config.getCompiler()) { case Compiler::GCC: { result += "build lib"; result += config.getPackageName(); result += ".a: " + buildJob + " "; result += join(objectNames, " "); result += "\n\n"; projectGeneratedBinary += "lib" + config.getPackageName() + ".a"; break; } case Compiler::MSVC: { result += "build "; result += config.getPackageName(); result += ".lib: " + buildJob + " "; result += join(objectNames, " "); result += "\n\n"; projectGeneratedBinary += config.getPackageName() + ".lib"; break; } } projectGeneratedBinary += "\""; if(parentProjStaticLinkerFlagCallbackFunc) parentProjStaticLinkerFlagCallbackFunc(projectGeneratedBinary); break; } case LibraryType::DYNAMIC: { switch (config.getCompiler()) { case Compiler::GCC: { result += "build lib"; result += config.getPackageName(); result += ".so: " + buildJob + " "; result += join(objectNames, " "); result += "\n"; result += " LINK_ARGS = '-Wl,--no-undefined,--as-needed' -ldl "; projectGeneratedBinary += "lib" + config.getPackageName() + ".so"; break; } case Compiler::MSVC: { result += "build "; result += config.getPackageName(); result += ".lib: " + buildJob + " "; result += join(objectNames, " "); result += "\n"; // TODO: Do not link all of these. Find a way to only link the ones that are needed result += " LINK_ARGS = Ws2_32.lib Wldap32.lib Crypt32.lib Advapi32.lib Gdi32.lib User32.lib Userenv.lib OpenGL32.lib GlU32.lib Shell32.lib "; projectGeneratedBinary += config.getPackageName() + ".lib"; break; } } if (!allLinkerFlags.empty()) { result += allLinkerFlags; //result += " '-Wl,--no-whole-archive'"; } result += "\n\n"; projectGeneratedBinary += "\""; if(parentProjDynamicLinkerFlagCallbackFunc) parentProjDynamicLinkerFlagCallbackFunc(projectGeneratedBinary); break; } default: assert(false); return Result::Err("Unexpected error"); } projectGeneratedBinaryFlags = allLinkerFlags + " " + projectGeneratedBinary; Result fileOverwriteResult = sibs::fileOverwrite(ninjaBuildFilePath.c_str(), sibs::StringView(result.data(), result.size())); if (fileOverwriteResult.isErr()) return fileOverwriteResult; nprintf(TINYDIR_STRING("Created ninja build file: %s\n"), ninjaBuildFilePath.c_str()); Result buildResult = compile(savePath); if (!buildResult) return buildResult; if(config.isMainProject()) { buildResult = buildCompilationDatabase(savePath); if(!buildResult) return buildResult; } } // TODO: If tests are being run (sibs test) and root project is an executable, do not run compile (above code) as executable. // Sibs test will compile root project as dynamic library so you end up compiling the project twice, first as an executable and then as a dynamic library. // Even if the root project has been built before and there is cached object, it will take a few seconds to run compile Result buildTestResult = buildTests(projectGeneratedBinaryFlags, config, savePath, dependencyExportIncludeDirs); if(!buildTestResult) return buildTestResult; return Result::Ok(true); } Result Ninja::buildTests(const std::string &projectGeneratedBinaryFlags, const SibsConfig &config, const _tinydir_char_t *savePath, const string &parentDependencyExportIncludeDirs) { if(testSourceDirs.empty() || !config.shouldBuildTests()) return Result::Ok(true); assert(getNinjaLibraryType(config.getPackageType()) != LibraryType::EXECUTABLE); string parentProjectPathUtf8 = toUtf8(config.getProjectPath()); string parentExportIncludeDirs = parentDependencyExportIncludeDirs; for (const string &parentGlobalIncludeDir : config.getGlobalIncludeDirs()) { string parentExportIncludeDir = parentProjectPathUtf8; parentExportIncludeDir += "/"; parentExportIncludeDir += parentGlobalIncludeDir; parentExportIncludeDirs += " "; parentExportIncludeDirs += getIncludeOptionFlag(config.getCompiler(), parentExportIncludeDir); } for(const string &testSourceDir : testSourceDirs) { #if OS_FAMILY == OS_FAMILY_POSIX FileString testSourceDirNative = testSourceDir; FileString projectConfFilePath = testSourceDir; #else FileString testSourceDirNative = utf8To16(testSourceDir); FileString projectConfFilePath = testSourceDirNative; #endif projectConfFilePath += TINYDIR_STRING("/project.conf"); FileType projectConfFileType = getFileType(projectConfFilePath.c_str()); SibsTestConfig sibsTestConfig(config.getCompiler(), testSourceDirNative, config.getOptimizationLevel()); if(projectConfFileType == FileType::REGULAR) { Result result = Config::readFromFile(projectConfFilePath.c_str(), sibsTestConfig); if(!result) return result; } backend::Ninja ninja; ninja.addGlobalIncludeDirs(parentExportIncludeDirs); if(!projectGeneratedBinaryFlags.empty()) ninja.addDependency(projectGeneratedBinaryFlags); // TODO: Use same source file finder as in main.cpp walkDirFilesRecursive(testSourceDirNative.c_str(), [&ninja, &sibsTestConfig](tinydir_file *file) { if (backend::BackendUtils::isSourceFile(file)) { string filePathUtf8 = toUtf8(file->path + sibsTestConfig.getProjectPath().size() + 1); ninja.addSourceFile(filePathUtf8.c_str()); } else { //printf("Ignoring non-source file: %s\n", file->path + projectPath.size()); } }); if(!ninja.getSourceFiles().empty()) { FileString buildPath = testSourceDirNative; switch(sibsTestConfig.getOptimizationLevel()) { case OPT_LEV_DEBUG: buildPath += TINYDIR_STRING("/sibs-build/debug"); break; case OPT_LEV_RELEASE: buildPath += TINYDIR_STRING("/sibs-build/release"); break; } Result buildFileResult = ninja.build(sibsTestConfig, buildPath.c_str()); if (!buildFileResult) return buildFileResult; Result buildResult = ninja.compile(buildPath.c_str()); if (!buildResult) return buildResult; // Convenient to have project setup to tests as well if(config.isMainProject()) { buildResult = buildCompilationDatabase(buildPath.c_str()); if(!buildResult) return buildResult; } FileString testExecutableName = buildPath; testExecutableName += TINYDIR_STRING("/"); testExecutableName += toFileString(sibsTestConfig.getPackageName()); Result runTestResult = exec(testExecutableName.c_str(), true); if(!runTestResult) return Result::Err(runTestResult); if(runTestResult.unwrap().exitCode != 0) return Result::Err("Tests failed", runTestResult.unwrap().exitCode); } } return Result::Ok(true); } Result Ninja::compile(const _tinydir_char_t *buildFilePath) { // TODO: Verify `script` is installed. We need `script` to get colored output #if OS_FAMILY == OS_FAMILY_POSIX FileString command = TINYDIR_STRING("script -eqc 'ninja -C \""); command += buildFilePath; command += TINYDIR_STRING("\"' /dev/null"); #else FileString command = TINYDIR_STRING("ninja -C \""); command += buildFilePath; command += TINYDIR_STRING("\""); #endif Result execResult = exec(command.c_str(), true); if(execResult.isOk()) { if(execResult.unwrap().exitCode == 0) { //printf("%s\n", execResult.unwrap().execStdout.c_str()); return Result::Ok(true); } else return Result::Err(""); } else return Result::Err(""); } Result Ninja::buildCompilationDatabase(const _tinydir_char_t *buildFilePath) { FileString command = TINYDIR_STRING("ninja -C \""); command += buildFilePath; command += TINYDIR_STRING("\" -t compdb c_COMPILER cpp_COMPILER BUILD_EXEC BUILD_STATIC BUILD_DYNAMIC > \""); command += buildFilePath; command += TINYDIR_STRING("/compile_commands.json\""); Result execResult = exec(command.c_str(), false); if(execResult.isOk()) { if(execResult.unwrap().exitCode == 0) { //printf("%s\n", execResult.unwrap().execStdout.c_str()); return Result::Ok(true); } else { string errMsg = "Failed to build compilation database, reason: "; errMsg += execResult.unwrap().execStdout; return Result::Err(errMsg); } } else { string errMsg = "Failed to build compilation database, reason: "; errMsg += execResult.getErrMsg(); return Result::Err(errMsg); } } }