From 94caff5f66cacdd21e5a93cd3de9150a22eeaa3a Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 16 Dec 2017 04:21:33 +0100 Subject: Add support for sub project (unit tests) --- backend/ninja/Ninja.cpp | 177 +++++++++++++++++++++++++++++++++++++++--------- backend/ninja/Ninja.hpp | 12 +++- include/Conf.hpp | 51 +++++++++++--- include/FileUtil.hpp | 1 + src/Conf.cpp | 53 +++++++++++++++ src/FileUtil.cpp | 21 +++++- src/GlobalLib.cpp | 38 ++++++++--- src/main.cpp | 37 +++++++--- 8 files changed, 329 insertions(+), 61 deletions(-) diff --git a/backend/ninja/Ninja.cpp b/backend/ninja/Ninja.cpp index d8345a4..6a6eafd 100644 --- a/backend/ninja/Ninja.cpp +++ b/backend/ninja/Ninja.cpp @@ -47,8 +47,28 @@ namespace backend void Ninja::addSourceFile(const char *filepath) { - if(filepath && !containsSourceFile(filepath)) - sourceFiles.emplace_back(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); } const std::vector& Ninja::getSourceFiles() const @@ -56,7 +76,7 @@ namespace backend return sourceFiles; } - bool Ninja::containsSourceFile(const char *filepath) const + bool Ninja::containsSourceFile(const string &filepath) const { for(const string &sourceFile : sourceFiles) { @@ -66,6 +86,26 @@ namespace backend 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; + } + Result validatePkgConfigPackageVersionExists(const Dependency &dependency) { Result dependencyValidationResult = PkgConfig::validatePackageExists(dependency.name); @@ -156,7 +196,7 @@ namespace backend return Result::Ok(true); } - Result Ninja::createBuildFile(const std::string &packageName, const vector &dependencies, const char *savePath, LinkerFlagCallbackFunc staticLinkerFlagCallbackFunc, LinkerFlagCallbackFunc dynamicLinkerFlagCallback) + Result Ninja::createBuildFile(const SibsConfig &config, const char *savePath, LinkerFlagCallbackFunc staticLinkerFlagCallbackFunc, LinkerFlagCallbackFunc dynamicLinkerFlagCallback) { // TODO: Do not quit here if no source files are provided. The source-less project could have dependencies if(sourceFiles.empty()) @@ -199,7 +239,7 @@ namespace backend result += "rule cpp_BUILD_STATIC\n"; result += " command = ar rcs lib"; - result += packageName; + result += config.getPackageName(); result += ".a"; result += " $in\n\n"; buildJob = "cpp_BUILD_STATIC"; @@ -225,25 +265,28 @@ namespace backend objectNames.reserve(sourceFiles.size()); for(const string &sourceFile : sourceFiles) { - // TODO: Handle tests differently. - // Maybe test directory should have project file and sub directories with project files - // should be their own project? - if(_tinydir_strncmp(sourceFile.c_str(), "tests", 5) == 0) - continue; - //string sourceFileEncoded = sourceFile; //replace(sourceFileEncoded, '/', '@'); - string objectName = packageName + "@exe/" + sourceFile + ".o"; + string objectName = config.getPackageName() + "@exe/" + sourceFile + ".o"; result += "build "; result += objectName; result += ": cpp_COMPILER ../../"; result += sourceFile; result += "\n"; - result += " ARGS = $globalLibDir '-I" + packageName + "@exe' '-I.' '-I..' '-fdiagnostics-color=always' '-pipe' '-D_FILE_OFFSET_BITS=64' '-Wall' '-Winvalid-pch' '-Wnon-virtual-dtor' '-O0' '-g'\n\n"; + result += " ARGS = $globalLibDir '-I" + config.getPackageName() + "@exe' '-I.' '-I..' '-fdiagnostics-color=always' '-pipe' '-D_FILE_OFFSET_BITS=64' '-Wall' '-Winvalid-pch' '-Wnon-virtual-dtor' '-O0' '-g'\n\n"; objectNames.emplace_back(objectName); } string allLinkerFlags; + + // 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 (especially on Windows) + for(const string &binaryDependency : binaryDependencies) + { + allLinkerFlags += " "; + allLinkerFlags += binaryDependency; + } + if(!staticLinkerFlagCallbackFunc || libraryType == LibraryType::DYNAMIC) { staticLinkerFlagCallbackFunc = [&allLinkerFlags](const string &linkerFlag) @@ -260,16 +303,20 @@ namespace backend allLinkerFlags += linkerFlag; }; + Result linkerFlags = getLinkerFlags(config.getDependencies(), staticLinkerFlagCallbackFunc, dynamicLinkerFlagCallback); + if(linkerFlags.isErr()) + return Result::Err(linkerFlags.getErrMsg()); + + string projectGeneratedBinary = allLinkerFlags; + projectGeneratedBinary += " '"; + projectGeneratedBinary += savePath; + projectGeneratedBinary += "/"; switch(libraryType) { case LibraryType::EXECUTABLE: { - Result linkerFlags = getLinkerFlags(dependencies, staticLinkerFlagCallbackFunc, dynamicLinkerFlagCallback); - if(linkerFlags.isErr()) - return Result::Err(linkerFlags.getErrMsg()); - result += "build "; - result += packageName; + result += config.getPackageName(); result += ": " + buildJob + " "; result += join(objectNames, " "); result += "\n"; @@ -279,29 +326,23 @@ namespace backend result += allLinkerFlags; } result += "\n\n"; + projectGeneratedBinary += config.getPackageName(); break; } case LibraryType::STATIC: { - Result linkerFlags = getLinkerFlags(dependencies, staticLinkerFlagCallbackFunc, dynamicLinkerFlagCallback); - if(linkerFlags.isErr()) - return linkerFlags; - result += "build "; - result += packageName; + result += config.getPackageName(); result += ": " + buildJob + " "; result += join(objectNames, " "); result += "\n\n"; + projectGeneratedBinary += config.getPackageName() + ".a"; break; } case LibraryType::DYNAMIC: { - Result linkerFlags = getLinkerFlags(dependencies, staticLinkerFlagCallbackFunc, dynamicLinkerFlagCallback); - if(linkerFlags.isErr()) - return Result::Err(linkerFlags.getErrMsg()); - result += "build lib"; - result += packageName; + result += config.getPackageName(); result += ".so: " + buildJob + " "; result += join(objectNames, " "); result += "\n"; @@ -312,18 +353,93 @@ namespace backend //result += " '-Wl,--no-whole-archive'"; } result += "\n\n"; + projectGeneratedBinary += "lib" + config.getPackageName() + ".so"; break; } default: assert(false); return Result::Err("Unexpected error"); } + projectGeneratedBinary += "'"; Result fileOverwriteResult = sibs::fileOverwrite(ninjaBuildFilePath.c_str(), sibs::StringView(result.data(), result.size())); if(fileOverwriteResult.isErr()) return fileOverwriteResult; printf("Created ninja build file: %s\n", ninjaBuildFilePath.c_str()); + + Result buildTestResult = buildTests(projectGeneratedBinary); + if(!buildTestResult) + return buildTestResult; + + return Result::Ok(true); + } + + const char *sourceFileExtensions[] = { "c", "cc", "cpp", "cxx" }; + bool isSourceFile(tinydir_file *file) + { + if(!file->is_reg) + return false; + + for(const char *sourceFileExtension : sourceFileExtensions) + { + if(_tinydir_strcmp(sourceFileExtension, file->extension) == 0) + return true; + } + + return false; + } + + Result Ninja::buildTests(const std::string &projectGeneratedBinary) + { + if(testSourceDirs.empty()) + return Result::Ok(true); + + // TODO: Include executable as dependency??? or compile project as dynamic library even if it's not a library + if(libraryType == LibraryType::EXECUTABLE) + return Result::Err("Unit tests are currently only supported in projects that compile to static/dynamic library"); + + for(const string &testSourceDir : testSourceDirs) + { + string projectConfFilePath = testSourceDir; + projectConfFilePath += "/project.conf"; + + FileType projectConfFileType = getFileType(projectConfFilePath.c_str()); + SibsTestConfig sibsTestConfig(testSourceDir); + if(projectConfFileType == FileType::REGULAR) + { + Result result = Config::readFromFile(projectConfFilePath.c_str(), sibsTestConfig); + if(!result) + return result; + } + + backend::Ninja ninja; + ninja.addDependency(projectGeneratedBinary); + walkDirFilesRecursive(testSourceDir.c_str(), [&ninja, &sibsTestConfig](tinydir_file *file) + { + if (isSourceFile(file)) + { + ninja.addSourceFile(file->path + sibsTestConfig.getProjectPath().size() + 1); + } + else + { + //printf("Ignoring non-source file: %s\n", file->path + projectPath.size()); + } + }); + + if(!ninja.getSourceFiles().empty()) + { + string debugBuildPath = testSourceDir + "/sibs-build/debug"; + Result buildFileResult = ninja.createBuildFile(sibsTestConfig, debugBuildPath.c_str()); + if (!buildFileResult) + return buildFileResult; + + Result buildResult = ninja.build(debugBuildPath.c_str()); + if (!buildResult) + return buildResult; + } + } + return Result::Ok(true); } @@ -332,14 +448,11 @@ namespace backend string command = "ninja -C '"; command += buildFilePath; command += "'"; - Result execResult = exec(command.c_str()); + Result execResult = exec(command.c_str(), true); if(execResult.isOk()) { if(execResult.unwrap().exitCode == 0) - { - printf("%s", execResult.unwrap().execStdout.c_str()); return Result::Ok(true); - } else return Result::Err(execResult.unwrap().execStdout); } diff --git a/backend/ninja/Ninja.hpp b/backend/ninja/Ninja.hpp index a0239d2..7013f78 100644 --- a/backend/ninja/Ninja.hpp +++ b/backend/ninja/Ninja.hpp @@ -4,6 +4,7 @@ #include "../../include/Dependency.hpp" #include "../../include/Result.hpp" #include "../../include/Linker.hpp" +#include "../../include/Conf.hpp" #include #include #include @@ -24,14 +25,21 @@ namespace backend Ninja(LibraryType libraryType = LibraryType::EXECUTABLE); void addSourceFile(const char *filepath); + void addTestSourceDir(const char *dir); + void addDependency(const std::string &binaryFile); const std::vector& getSourceFiles() const; - sibs::Result createBuildFile(const std::string &packageName, const std::vector &dependencies, const char *savePath, sibs::LinkerFlagCallbackFunc staticLinkerFlagCallbackFunc = nullptr, sibs::LinkerFlagCallbackFunc dynamicLinkerFlagCallback = nullptr); + sibs::Result createBuildFile(const sibs::SibsConfig &config, const char *savePath, sibs::LinkerFlagCallbackFunc staticLinkerFlagCallbackFunc = nullptr, sibs::LinkerFlagCallbackFunc dynamicLinkerFlagCallback = nullptr); sibs::Result build(const char *buildFilePath); private: - bool containsSourceFile(const char *filepath) const; + sibs::Result buildTests(const std::string &projectGeneratedBinary); + bool containsSourceFile(const std::string &filepath) const; + bool containsTestSourceDir(const std::string &dir) const; + bool containsDependency(const std::string &dependency) const; sibs::Result getLinkerFlags(const std::vector &dependencies, sibs::LinkerFlagCallbackFunc staticLinkerFlagCallbackFunc, sibs::LinkerFlagCallbackFunc dynamicLinkerFlagCallback) const; private: std::vector sourceFiles; + std::vector testSourceDirs; + std::vector binaryDependencies; LibraryType libraryType; }; } diff --git a/include/Conf.hpp b/include/Conf.hpp index c792075..8da352e 100644 --- a/include/Conf.hpp +++ b/include/Conf.hpp @@ -70,6 +70,8 @@ namespace sibs class ConfigCallback { friend class Parser; + public: + virtual ~ConfigCallback(){} protected: virtual void processObject(StringView name) = 0; virtual void processField(StringView name, const ConfigValue &value) = 0; @@ -85,35 +87,68 @@ namespace sibs class SibsConfig : public ConfigCallback { public: - SibsConfig() : finishedProcessing(false), packageType((PackageType)-1) {} + SibsConfig(const std::string &_projectPath) : projectPath(_projectPath), finishedProcessing(false), packageType((PackageType)-1) {} + virtual ~SibsConfig(){} - const std::string& getPackageName() const + virtual const std::string& getPackageName() const { assert(finishedProcessing); return packageName; } - PackageType getPackageType() const + virtual PackageType getPackageType() const { assert(finishedProcessing); return packageType; } - const std::vector& getDependencies() const + virtual const std::string& getTestPath() const + { + return testPath; + } + + virtual const std::vector& getDependencies() const { return dependencies; } + + virtual const std::string& getProjectPath() const + { + return projectPath; + } + protected: + virtual void processObject(StringView name) override; + virtual void processField(StringView name, const ConfigValue &value) override; + virtual void finished() override; protected: - void processObject(StringView name) override; - void processField(StringView name, const ConfigValue &value) override; - void finished() override; - private: StringView currentObject; + std::string projectPath; std::string packageName; + std::string testPath; PackageType packageType; std::vector dependencies; bool finishedProcessing; }; + + class SibsTestConfig : public SibsConfig + { + public: + SibsTestConfig(const std::string &_projectPath) : SibsConfig(_projectPath) + { + packageName = "test"; + } + + virtual ~SibsTestConfig(){} + + PackageType getPackageType() const override + { + return PackageType::EXECUTABLE; + } + protected: + void processObject(StringView name) override; + void processField(StringView name, const ConfigValue &value) override; + void finished() override; + }; } #endif //SIBS_CONF_HPP diff --git a/include/FileUtil.hpp b/include/FileUtil.hpp index 5b91aad..5d1594a 100644 --- a/include/FileUtil.hpp +++ b/include/FileUtil.hpp @@ -27,6 +27,7 @@ namespace sibs Result getCwd(); // Note: Will not delete created directories if this operation fails for some reason Result createDirectoryRecursive(const char *path); + Result getRealPath(const char *path); } #endif //SIBS_FILEUTIL_HPP diff --git a/src/Conf.cpp b/src/Conf.cpp index 1f439cd..b1e05bb 100644 --- a/src/Conf.cpp +++ b/src/Conf.cpp @@ -394,6 +394,25 @@ namespace sibs else throw ParserException("Expected package.type to be a single value, was a list"); } + else if(name.equals("tests")) + { + if (value.isSingle()) + { + testPath = projectPath; + testPath += "/"; + testPath += string(value.asSingle().data, value.asSingle().size); + Result testRealPathResult = getRealPath(testPath.c_str()); + if(!testRealPathResult) + { + string errMsg = "Failed to resolve package.tests path: "; + errMsg += testRealPathResult.getErrMsg(); + throw ParserException(errMsg); + } + testPath = testRealPathResult.unwrap(); + } + else + throw ParserException("Expected package.tests to be a single value, was a list"); + } } else if(currentObject.equals("dependencies")) { @@ -416,4 +435,38 @@ namespace sibs throw ParserException("Missing required config package.type. Expected to be one either 'executable', 'static', 'dynamic' or 'library'"); finishedProcessing = true; } + + void SibsTestConfig::processObject(StringView name) + { + currentObject = name; + } + + void SibsTestConfig::processField(StringView name, const ConfigValue &value) + { + if(currentObject.equals("dependencies")) + { + if(value.isSingle()) + { + // TODO: Validate version is number in correct format + Dependency dependency; + dependency.name = string(name.data, name.size); + dependency.version = string(value.asSingle().data, value.asSingle().size); + dependencies.emplace_back(dependency); + } + else + throw ParserException("Expected field under dependencies to be a single value, was a list"); + } + else + { + string errMsg = "project.conf: Expected category to be 'dependencies', was: '"; + errMsg += string(currentObject.data, currentObject.size); + errMsg += "'"; + throw ParserException(errMsg); + } + } + + void SibsTestConfig::finished() + { + finishedProcessing = true; + } } \ No newline at end of file diff --git a/src/FileUtil.cpp b/src/FileUtil.cpp index 899a1af..7248537 100644 --- a/src/FileUtil.cpp +++ b/src/FileUtil.cpp @@ -206,7 +206,26 @@ namespace sibs return Result::Ok(true); } + + Result getRealPath(const char *path) + { + // TODO: Verify NULL can be passed as 'resolved' argument with different compilers and operating systems (clang, freebsd etc) + char *resolved = realpath(path, nullptr); + if(!resolved) + { + int error = errno; + string errMsg = "Failed to get real path for \""; + errMsg += path; + errMsg += "\": "; + errMsg += strerror(error); + return Result::Err(errMsg, error); + } + + string result = resolved; + free(resolved); + return Result::Ok(resolved); + } #else -#error "TODO: Implement createDirectoryRecursive on windows" +#error "TODO: Implement createDirectoryRecursive and getRealPath on windows" #endif } \ No newline at end of file diff --git a/src/GlobalLib.cpp b/src/GlobalLib.cpp index a07e23d..c38e22f 100644 --- a/src/GlobalLib.cpp +++ b/src/GlobalLib.cpp @@ -55,6 +55,11 @@ namespace sibs return false; } + bool isPathSubPathOf(const char *path, const string &subPathOf) + { + return _tinydir_strncmp(path, subPathOf.c_str(), subPathOf.size()) == 0; + } + Result GlobalLib::getLibsLinkerFlags(const string &globalLibRootDir, const string &name, const string &version, LinkerFlagCallbackFunc staticLinkerFlagCallbackFunc, LinkerFlagCallbackFunc dynamicLinkerFlagCallbackFunc) { Result packageExistsResult = validatePackageExists(globalLibRootDir, name); @@ -105,7 +110,7 @@ namespace sibs } } - SibsConfig sibsConfig; + SibsConfig sibsConfig(packageDir); Result result = Config::readFromFile(projectConfFilePath.c_str(), sibsConfig); if(result.isErr()) return Result::Err(result.getErrMsg()); @@ -137,18 +142,33 @@ namespace sibs } backend::Ninja ninja(libraryType); - walkDirFilesRecursive(packageDir.c_str(), [&ninja, &packageDir](tinydir_file *file) + FileWalkCallbackFunc collectSourceFiles = [&ninja, &sibsConfig, &collectSourceFiles](tinydir_file *file) { - if (isSourceFile(file)) + if(file->is_reg) { - printf("Adding source file: %s\n", file->path + packageDir.size() + 1); - ninja.addSourceFile(file->path + packageDir.size() + 1); - } else + if (isSourceFile(file)) + { + ninja.addSourceFile(file->path + sibsConfig.getProjectPath().size() + 1); + } + else + { + //printf("Ignoring non-source file: %s\n", file->path + projectPath.size()); + } + } + else { - //printf("Ignoring non-source file: %s\n", file->path + packageDir.size() + 1); + // TODO: If compiling without "test" option, do not add test source dir to ninja. Ninja logic will then not build tests... + // OR I believe there is no reason to run tests in dependencies. The main projects tests should cover that? + // But you might want to know exactly which dependency is causing issue and which part of it... + if(!sibsConfig.getTestPath().empty() && isPathSubPathOf(file->path, sibsConfig.getTestPath())) + ninja.addTestSourceDir(file->path); + else + walkDir(file->path, collectSourceFiles); } - }); + }; + walkDir(packageDir.c_str(), collectSourceFiles); + // TODO: Dont do this. Unit tests wont be built. Need to call the below ninja.createBuildFile if(ninja.getSourceFiles().empty()) { return Result::Ok("No source files in dependency (header only library?)"); @@ -183,7 +203,7 @@ namespace sibs if(createBuildDirResult.isErr()) return Result::Err(createBuildDirResult); - Result buildFileResult = ninja.createBuildFile(sibsConfig.getPackageName(), sibsConfig.getDependencies(), debugBuildPath.c_str(), staticLinkerFlagCallbackFunc, dynamicLinkerFlagCallbackFunc); + Result buildFileResult = ninja.createBuildFile(sibsConfig, debugBuildPath.c_str(), staticLinkerFlagCallbackFunc, dynamicLinkerFlagCallbackFunc); if (buildFileResult.isErr()) return Result::Err(buildFileResult.getErrMsg()); diff --git a/src/main.cpp b/src/main.cpp index 9d6c88e..c1ce277 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -18,6 +18,8 @@ using namespace sibs; // Either do not follow the symlinks or use a hash map with every searched directory // to only go inside a directory once +// TODO: Places that use PATH_MAX should be modified. A path CAN be longer than PATH_MAX... (does this include replacing tinydir.h?) + void usage() { printf("Usage: sibs COMMAND\n\n"); @@ -91,6 +93,11 @@ bool isSourceFile(tinydir_file *file) return false; } +bool isPathSubPathOf(const char *path, const string &subPathOf) +{ + return _tinydir_strncmp(path, subPathOf.c_str(), subPathOf.size()) == 0; +} + int buildProject(int argc, const char **argv) { if(argc > 1) @@ -113,7 +120,7 @@ int buildProject(int argc, const char **argv) projectConfFilePath += "/project.conf"; validateFilePath(projectConfFilePath.c_str()); - SibsConfig sibsConfig; + SibsConfig sibsConfig(projectPath); Result result = Config::readFromFile(projectConfFilePath.c_str(), sibsConfig); if(result.isErr()) { @@ -147,20 +154,32 @@ int buildProject(int argc, const char **argv) } backend::Ninja ninja(libraryType); - walkDirFilesRecursive(projectPath.c_str(), [&ninja, &projectPath](tinydir_file *file) + FileWalkCallbackFunc collectSourceFiles = [&ninja, &sibsConfig, &collectSourceFiles](tinydir_file *file) { - if (isSourceFile(file)) + if(file->is_reg) { - printf("Adding source file: %s\n", file->path + projectPath.size()); - ninja.addSourceFile(file->path + projectPath.size()); - } else + if (isSourceFile(file)) + { + ninja.addSourceFile(file->path + sibsConfig.getProjectPath().size()); + } + else + { + //printf("Ignoring non-source file: %s\n", file->path + projectPath.size()); + } + } + else { - //printf("Ignoring non-source file: %s\n", file->path + projectPath.size()); + // TODO: If compiling without "test" option, do not add test source dir to ninja. Ninja logic will then not build tests + if(!sibsConfig.getTestPath().empty() && isPathSubPathOf(file->path, sibsConfig.getTestPath())) + ninja.addTestSourceDir(file->path); + else + walkDir(file->path, collectSourceFiles); } - }); + }; + walkDir(projectPath.c_str(), collectSourceFiles); string debugBuildPath = projectPath + "/sibs-build/debug"; - Result buildFileResult = ninja.createBuildFile(sibsConfig.getPackageName(), sibsConfig.getDependencies(), debugBuildPath.c_str()); + Result buildFileResult = ninja.createBuildFile(sibsConfig, debugBuildPath.c_str()); if(buildFileResult.isErr()) { cerr << "Failed to build ninja file: " << buildFileResult.getErrMsg() << endl; -- cgit v1.2.3