#include #include #include #include "../include/FileUtil.hpp" #include "../include/Conf.hpp" #include "../include/Exec.hpp" #include "../backend/ninja/Ninja.hpp" using namespace std; using namespace sibs; // 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?) #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"); 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 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(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); 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); } //string projectSrcPath = projectPath + "/src"; //validateDirectoryPath(projectSrcPath.c_str()); PackageType packageType = sibsConfig.getPackageType(); backend::Ninja::LibraryType libraryType; switch(packageType) { case PackageType::EXECUTABLE: libraryType = backend::Ninja::LibraryType::EXECUTABLE; break; case PackageType::STATIC: libraryType = backend::Ninja::LibraryType::STATIC; break; case PackageType::DYNAMIC: case PackageType::LIBRARY: libraryType = backend::Ninja::LibraryType::DYNAMIC; break; } backend::Ninja ninja(libraryType); FileWalkCallbackFunc collectSourceFiles = [&ninja, &sibsConfig, &collectSourceFiles](tinydir_file *file) { if(file->is_reg) { if (isSourceFile(file)) { string filePathUtf8 = toUtf8(file->path + sibsConfig.getProjectPath().size()); ninja.addSourceFile(filePathUtf8.c_str()); } else { //printf("Ignoring non-source file: %s\n", file->path + projectPath.size()); } } else { // 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())) { string filePathUtf8 = toUtf8(file->path); ninja.addTestSourceDir(filePathUtf8.c_str()); } else walkDir(file->path, collectSourceFiles); } }; walkDir(projectPath.c_str(), collectSourceFiles); 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; } Result buildFileResult = ninja.build(sibsConfig, buildPath.c_str()); if(buildFileResult.isErr()) { ferr << "Failed to build ninja file: " << toFileString(buildFileResult.getErrMsg()) << endl; exit(7); } return 0; } 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\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]); 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")); createProjectFile(projectPath + TINYDIR_STRING("/src/main.cpp"), "#include \n\nint main()\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 { ferr << "Expected command to be either 'build' or 'new', was: " << arg << endl << endl; usage(); } } usage(); return 0; }