#include #include #include #include #include "../include/FileUtil.hpp" #include "../include/Conf.hpp" #include "../include/Exec.hpp" #include "../include/CmakeModule.hpp" #include "../backend/ninja/Ninja.hpp" using namespace std; using namespace sibs; using namespace std::chrono; // TODO: Fail if multiple versions of the same dependency is used // as linking will fail because of multiple definitions of the same thing // TODO: Detect circular dependencies // TODO: Prevent infinite recursion in source file searching when there are symlinks. // 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?) // TODO: Remove install.sh when sibs supports installation of packages (so it can install itself) // TODO: Move package tests, include_dirs and ignore_dirs under config in project.conf. // Package object should only contain metadata for the package, which is used for finding dependencies // and when building a list of installed packages / available packages. // TODO: Allow different platforms to have different dependencies. // This can be done by specifying dependencies under [dependencies.platform] instead of [dependencies], // for example for win32: [dependencies.win32] // TODO: When `package` command is added, the target executable and shared library dependencies should be put into // an archive to make the executable distributable (especially on windows). A GUI installer could then extract the archive. // On Linux we can use https://github.com/DEC05EBA/glibc_version_header to make the executable work on many distros // without compiling project from source on the end-users machine. // TODO: Add optional dependencies [optional_dependencies]. Optional dependencies should also support platform specific dependencies [dependencies.cmake]. // Might need to make it possible to define variables if a dependency exists (or doesn't exist) because the code might have // preprocessor like: USE_LIBSODIUM or NO_LIBSODIUM. // TODO: Set c++ standard to c++11 and c standard to c98 - for consistency across different compilers and compiler version. // It should be possible to override language version in project.conf // TODO: If file extension is common c extension (.c, ...) then remove cast checking (for example using malloc without casting result to result type) // TODO: When building a sibs project, add a hardlink in ~/.sibs/lib to it. This allows us to have dependency to a project that we can modify // without having to commit & push to remote // TODO: Check compiler flags generated by cmake and visual studio in debug and release mode and use the same ones when building sibs project. // There are certain compiler flags we do not currently have, for example _ITERATOR_DEBUG_LEVEL in debug mode which enables runtime checks. // You should be able to specify runtime checks as an option to `sibs build` and project specific config in .conf file. // Also add stack protection option. If it's enabled, shouldn't sibs prefer to compile dependencies from source? // and should this force static compilation so dependencies can also be built with protection and if dependencies dont exist // as static library/source, then fail build? // TODO: Add support for common package managers (in distros). If package with the dependency version exists in package manager, install and use it instead // TODO: Make dependency/project names case insensitive. This means we can't use pkgconfig // TODO: Fail build if dependency requires newer language version than dependant package. // To make it work properly, should language version be required in project.conf? // TODO: Remove duplicate compiler options (include flags, linker flags etc...) to improve compilation speed. // The compiler ignores duplicate symbols but it's faster to just remove duplicate options because we only have // to compare strings. Duplicate options can happen if for example a project has two dependencies and both dependencies // have dependency on the same package (would be common for example with boost libraries or libraries that dpepend on boost) // TODO: Create symlink to dependencies, for example if we have dependency on xxhash which has xxhash.h in its root directory, // then you should be able to include it from dependant project by typing #include "xxhash/xxhash.h" // that means we create a symlink with the dependency name to the dependency version directory. // This will make it easier to prevent clashes in header file names and it's easier to see from the include statement // what exactly we are including #if OS_FAMILY == OS_FAMILY_POSIX #define ferr std::cerr #else #define ferr std::wcerr #endif void usage() { printf("Usage: sibs COMMAND\n\n"); printf("Simple Build System for Native Languages\n\n"); printf("Commands:\n"); printf(" build\t\tBuild a project that contains a project.conf file\n"); printf(" new\t\tCreate a new project\n"); printf(" test\t\tBuild and run tests for a sibs project\n"); exit(1); } void usageBuild() { printf("Usage: sibs build [project_path] [--debug|--release]\n\n"); printf("Build a sibs project\n\n"); printf("Options:\n"); printf(" project_path\t\t The directory containing a project.conf file - Optional (default: current working directory)\n"); printf(" --debug|--release\t\tOptimization level to build project and dependencies with (if not a system package) - Optional (default: --debug)\n"); printf("Examples:\n"); printf(" sibs build\n"); printf(" sibs build dirA/dirB\n"); printf(" sibs build --release\n"); printf(" sibs build dirA --release\n"); exit(1); } void usageNew() { printf("Usage: sibs new <--exec|--static|--dynamic>\n\n"); printf("Create a new sibs project\n\n"); printf("Options:\n"); printf(" project_name\t\tThe name of the project you want to create\n"); printf(" --exec\t\t\tProject compiles to an executable\n"); printf(" --static\t\t\tProject compiles to a static library\n"); printf(" --dynamic\t\t\tProject compiles to a dynamic library\n"); printf("Examples:\n"); printf(" sibs new hello_world --exec\n"); exit(1); } void usageTest() { printf("Usage: sibs test [project_path]\n\n"); printf("Build and run tests for a sibs project\n\n"); printf("Options:\n"); printf(" project_path\t\t The directory containing a project.conf file - Optional (default: current working directory)\n"); printf("Examples:\n"); printf(" sibs test\n"); printf(" sibs test dirA/dirB\n"); exit(1); } void validateDirectoryPath(const _tinydir_char_t *projectPath) { FileType projectPathFileType = getFileType(projectPath); if(projectPathFileType == FileType::FILE_NOT_FOUND) { string errMsg = "Invalid project path: "; errMsg += toUtf8(projectPath); perror(errMsg.c_str()); exit(2); } else if(projectPathFileType == FileType::REGULAR) { ferr <<"Expected project path (" << projectPath << ") to be a directory, was a file" << endl; exit(3); } } void validateFilePath(const _tinydir_char_t *projectConfPath) { FileType projectConfFileType = getFileType(projectConfPath); if(projectConfFileType == FileType::FILE_NOT_FOUND) { string errMsg = "Invalid project.conf path: "; errMsg += toUtf8(projectConfPath); perror(errMsg.c_str()); exit(4); } else if(projectConfFileType == FileType::DIRECTORY) { ferr << "Expected project path (" << projectConfPath << ") to be a file, was a directory" << endl; exit(5); } } const _tinydir_char_t *sourceFileExtensions[] = { TINYDIR_STRING("c"), TINYDIR_STRING("cc"), TINYDIR_STRING("cpp"), TINYDIR_STRING("cxx") }; bool isSourceFile(tinydir_file *file) { if(!file->is_reg) return false; for(const _tinydir_char_t *sourceFileExtension : sourceFileExtensions) { if(_tinydir_strcmp(sourceFileExtension, file->extension) == 0) return true; } return false; } bool isPathSubPathOf(const FileString &path, const FileString &subPathOf) { return _tinydir_strncmp(path.c_str(), subPathOf.c_str(), subPathOf.size()) == 0; } int buildProject(const FileString &projectPath, const FileString &projectConfFilePath, const SibsConfig &sibsConfig) { Result result = Config::readFromFile(projectConfFilePath.c_str(), sibsConfig); if(result.isErr()) { ferr << "Failed to read config: " << toFileString(result.getErrMsg()) << endl; exit(6); } if(sibsConfig.getPackageName().empty()) { ferr << "project.conf is missing required field package.name" << endl; exit(10); } if (!containsPlatform(sibsConfig.getPlatforms(), SYSTEM_PLATFORM)) { string errMsg = "The project "; errMsg += sibsConfig.getPackageName(); errMsg += " does not support your platform ("; errMsg += asString(SYSTEM_PLATFORM); errMsg += ")"; cerr << errMsg << endl; exit(11); } FileString buildPath = projectPath + TINYDIR_STRING("/sibs-build/"); switch(sibsConfig.getOptimizationLevel()) { case OPT_LEV_DEBUG: buildPath += TINYDIR_STRING("debug"); break; case OPT_LEV_RELEASE: buildPath += TINYDIR_STRING("release"); break; } auto startTime = high_resolution_clock::now(); if(sibsConfig.shouldUseCmake()) { auto dummyCallback = [](const string&){}; CmakeModule cmakeModule; Result cmakeCompileResult = cmakeModule.compile(sibsConfig, buildPath, dummyCallback, dummyCallback, dummyCallback); if(!cmakeCompileResult) { ferr << "Failed to compile using cmake: " << toFileString(cmakeCompileResult.getErrMsg()) << endl; exit(7); } } else { backend::Ninja ninja; FileWalkCallbackFunc collectSourceFiles = [&ninja, &sibsConfig, &collectSourceFiles](tinydir_file *file) { FileString pathNative = file->path; #if OS_FAMILY == OS_FAMILY_WINDOWS replaceChar(pathNative, L'/', L'\\'); #endif if(file->is_reg) { if (isSourceFile(file)) { string filePathUtf8 = toUtf8(pathNative.c_str() + sibsConfig.getProjectPath().size()); ninja.addSourceFile(filePathUtf8.c_str()); } else { //printf("Ignoring non-source file: %s\n", file->path + projectPath.size()); } } else { if (!sibsConfig.getTestPath().empty() && isPathSubPathOf(pathNative.c_str(), sibsConfig.getTestPath())) { string filePathUtf8 = toUtf8(pathNative.c_str()); ninja.addTestSourceDir(filePathUtf8.c_str()); } else if(!directoryToIgnore(pathNative, sibsConfig.getIgnoreDirs())) walkDir(file->path, collectSourceFiles); } }; walkDir(projectPath.c_str(), collectSourceFiles); if(sibsConfig.shouldBuildTests() && sibsConfig.getTestPath().empty()) { printf("Project is missing package.tests config. No tests to build\n"); exit(0); } Result buildFileResult = ninja.build(sibsConfig, buildPath.c_str()); if(buildFileResult.isErr()) { ferr << "Failed to build ninja file: " << toFileString(buildFileResult.getErrMsg()) << endl; exit(7); } } auto elapsedTime = duration_cast>(high_resolution_clock::now() - startTime); printf("Finished in %fs\n", elapsedTime.count()); return 0; } int buildProject(int argc, const _tinydir_char_t **argv) { if(argc > 2) usageBuild(); OptimizationLevel optimizationLevel = OPT_LEV_NONE; FileString projectPath; for(int i = 0; i < argc; ++i) { const _tinydir_char_t *arg = argv[i]; if(_tinydir_strcmp(arg, TINYDIR_STRING("--debug")) == 0) { if(optimizationLevel != OPT_LEV_NONE) { ferr << "Error: Optimization level defined more than once. First defined as " << asString(optimizationLevel) << " then as debug" << endl; usageBuild(); } optimizationLevel = OPT_LEV_DEBUG; } else if(_tinydir_strcmp(arg, TINYDIR_STRING("--release")) == 0) { if(optimizationLevel != OPT_LEV_NONE) { ferr << "Error: Optimization level defined more than once. First defined as " << asString(optimizationLevel) << " then as release" << endl; usageBuild(); } optimizationLevel = OPT_LEV_RELEASE; } else { if(!projectPath.empty()) { ferr << "Error: Project path was defined more than once. First defined as " << projectPath << " then as " << arg << endl; usageBuild(); } projectPath = arg; } } if(optimizationLevel == OPT_LEV_NONE) optimizationLevel = OPT_LEV_DEBUG; // TODO: If projectPath is not defined and working directory does not contain project.conf, then search every parent directory until one is found if(projectPath.empty()) projectPath = TINYDIR_STRING("."); validateDirectoryPath(projectPath.c_str()); if(projectPath.back() != '/') projectPath += TINYDIR_STRING("/"); Result projectRealPathResult = getRealPath(projectPath.c_str()); if(!projectRealPathResult) { ferr << "Failed to get real path for: '" << projectPath.c_str() << "': " << toFileString(projectRealPathResult.getErrMsg()) << endl; exit(40); } projectPath = projectRealPathResult.unwrap(); FileString projectConfFilePath = projectPath; projectConfFilePath += TINYDIR_STRING("/project.conf"); validateFilePath(projectConfFilePath.c_str()); // TODO: Detect compiler to use at runtime. Should also be configurable // by passing argument to `sibs build` #if OS_FAMILY == OS_FAMILY_POSIX Compiler compiler = Compiler::GCC; #else Compiler compiler = Compiler::MSVC; #endif SibsConfig sibsConfig(compiler, projectPath, optimizationLevel, false); return buildProject(projectPath, projectConfFilePath, sibsConfig); } int testProject(int argc, const _tinydir_char_t **argv) { if(argc > 1) usageTest(); FileString projectPath; if(argc == 1) projectPath = argv[0]; // TODO: If projectPath is not defined and working directory does not contain project.conf, then search every parent directory until one is found if(projectPath.empty()) projectPath = TINYDIR_STRING("."); validateDirectoryPath(projectPath.c_str()); if(projectPath.back() != '/') projectPath += TINYDIR_STRING("/"); Result projectRealPathResult = getRealPath(projectPath.c_str()); if(!projectRealPathResult) { ferr << "Failed to get real path for: '" << projectPath.c_str() << "': " << toFileString(projectRealPathResult.getErrMsg()) << endl; exit(40); } projectPath = projectRealPathResult.unwrap(); FileString projectConfFilePath = projectPath; projectConfFilePath += TINYDIR_STRING("/project.conf"); validateFilePath(projectConfFilePath.c_str()); // TODO: Detect compiler to use at runtime. Should also be configurable // by passing argument to `sibs build` #if OS_FAMILY == OS_FAMILY_POSIX Compiler compiler = Compiler::GCC; #else Compiler compiler = Compiler::MSVC; #endif SibsConfig sibsConfig(compiler, projectPath, OPT_LEV_DEBUG, true); return buildProject(projectPath, projectConfFilePath, sibsConfig); } void newProjectCreateMainDir(const FileString &projectPath) { Result createProjectDirResult = createDirectoryRecursive(projectPath.c_str()); if(createProjectDirResult.isErr()) { ferr << "Failed to create project main directory: " << toFileString(createProjectDirResult.getErrMsg()) << endl; exit(20); } } void createProjectSubDir(const FileString &dir) { Result createProjectDirResult = createDirectoryRecursive(dir.c_str()); if(createProjectDirResult.isErr()) { ferr << "Failed to create directory in project: " << toFileString(createProjectDirResult.getErrMsg()) << endl; exit(20); } } void createProjectFile(const FileString &projectFilePath, const string &fileContent) { Result fileOverwriteResult = fileOverwrite(projectFilePath.c_str(), fileContent.c_str()); if(fileOverwriteResult.isErr()) { ferr << "Failed to create project file: " << toFileString(fileOverwriteResult.getErrMsg()) << endl; exit(20); } } void newProjectCreateConf(const string &projectName, const string &projectType, const FileString &projectPath) { string projectConfStr = "[package]\n"; projectConfStr += "name = \"" + projectName + "\"\n"; projectConfStr += "type = \"" + projectType + "\"\n"; projectConfStr += "version = \"0.1.0\"\n"; projectConfStr += "platforms = [\""; projectConfStr += asString(SYSTEM_PLATFORM); projectConfStr += "\"]\n"; projectConfStr += "\n"; projectConfStr += "[dependencies]\n"; FileString projectConfPath = projectPath; projectConfPath += TINYDIR_STRING("/project.conf"); Result fileOverwriteResult = fileOverwrite(projectConfPath.c_str(), projectConfStr.c_str()); if(fileOverwriteResult.isErr()) { ferr << "Failed to create project.conf: " << toFileString(fileOverwriteResult.getErrMsg()) << endl; exit(20); } } // This can be replaced with createDirectory and fileOverwrite, but it's not important // so there is no reason to do it (right now) Result gitInitProject(const FileString &projectPath) { FileString cmd = TINYDIR_STRING("git init \""); cmd += projectPath; cmd += TINYDIR_STRING("\""); return exec(cmd.c_str()); } int newProject(int argc, const _tinydir_char_t **argv) { if(argc != 2) { ferr << "Expected 'new' command to be followed by two arguments - project name and type of project (--exec, --static or --dynamic)" << endl << endl; usageNew(); } Result cwdResult = getCwd(); if(cwdResult.isErr()) { ferr << "Failed to get current working directory: " << toFileString(cwdResult.getErrMsg()) << endl; exit(20); } string projectName = toUtf8(argv[0]); if(!isProjectNameValid(projectName)) { ferr << "Project name can only contain alphanumerical characters, dash (-) or underscore (_)" << endl; exit(20); } FileString projectPath = cwdResult.unwrap(); projectPath += TINYDIR_STRING("/"); projectPath += toFileString(projectName); bool projectPathExists = getFileType(projectPath.c_str()) != FileType::FILE_NOT_FOUND; if(projectPathExists) { ferr << "Unable to create a new project at path '" << projectPath << "'. A file or directory already exists in the same location" << endl; exit(20); } const _tinydir_char_t *projectType = argv[1]; string projectTypeConf; if(_tinydir_strcmp(projectType, TINYDIR_STRING("--exec")) == 0) projectTypeConf = "executable"; else if(_tinydir_strcmp(projectType, TINYDIR_STRING("--static")) == 0) projectTypeConf = "static"; else if(_tinydir_strcmp(projectType, TINYDIR_STRING("--dynamic")) == 0) projectTypeConf = "dynamic"; else { ferr << "Expected project type to be either --exec, --static or --dynamic; was: " << projectType << endl << endl; usageNew(); } newProjectCreateMainDir(projectPath); newProjectCreateConf(projectName, projectTypeConf, projectPath); createProjectSubDir(projectPath + TINYDIR_STRING("/src")); createProjectSubDir(projectPath + TINYDIR_STRING("/include")); createProjectSubDir(projectPath + TINYDIR_STRING("/tests")); createProjectFile(projectPath + TINYDIR_STRING("/src/main.cpp"), "#include \n\nint main(int argc, char **argv)\n{\n printf(\"hello, world!\\n\");\n return 0;\n}\n"); // We are ignoring git init result on purpose. If it fails, just ignore it; not important gitInitProject(projectPath); return 0; } #if OS_FAMILY == OS_FAMILY_POSIX int main(int argc, const _tinydir_char_t **argv) #else int wmain(int argc, const _tinydir_char_t **argv) #endif { unordered_map param; unordered_set flags; for(int i = 1; i < argc; ++i) { const _tinydir_char_t *arg = argv[i]; int subCommandArgCount = argc - i - 1; const _tinydir_char_t **subCommandArgPtr = argv + i + 1; if(_tinydir_strcmp(arg, TINYDIR_STRING("build")) == 0) { return buildProject(subCommandArgCount, subCommandArgPtr); } else if(_tinydir_strcmp(arg, TINYDIR_STRING("new")) == 0) { return newProject(subCommandArgCount, subCommandArgPtr); } else if(_tinydir_strcmp(arg, TINYDIR_STRING("test")) == 0) { return testProject(subCommandArgCount, subCommandArgPtr); } else { ferr << "Expected command to be either 'build' or 'new', was: " << arg << endl << endl; usage(); } } usage(); return 0; }