#include #include #include #include #include #include "../include/FileUtil.hpp" #include "../include/Conf.hpp" #include "../include/Exec.hpp" #include "../include/CmakeModule.hpp" #include "../backend/BackendUtils.hpp" #include "../backend/ninja/Ninja.hpp" using namespace std; using namespace sibs; using namespace std::chrono; // TODO: Use XDG (XDG_CACHE_HOME) for cache directory // 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: 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: 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: Implement link-time-optimization which should be used if building with a certain optimization level (sibs build) // TODO (bug): Fix issue where running sibs-build in a project that uses cmake wont build and run tests // TODO: Add 'license' in project.conf. License must be commonly known license or 'custom'. // This is to include lgpl dependencies as dynamic libraries and give error if you are using gpl dependencies in a non gpl project. // If the license type is custom, then the ways the library can be used should be defined in such a way that dependent projects can determinate // if the dependency can be used without breaking compatibility between licenses. // License notice has to be in a file called LICENSE or COPYING in the root directory of the project and these files will be combined automatically // for all dependencies when releasing your software, as including license with software is often required. // TODO: Dynamically link libgit2 (sibs dependency) since it's under lgpl license. Optionally implement git clone/pull with MIT license. // TODO: Auto export all symbols under windows (https://stackoverflow.com/questions/225432/export-all-symbols-when-creating-a-dll) // TODO: Fix issue where when you have a dependency on a cmake project with dynamic library, the dynamic library wont be found at runtime for whatever reason // TODO: Add program command for generating compile_commands.json without compiling code, without using Ninja // TODO: Make Process::exec safe to use. Currently you pass an argument and it's run as a command, but the string can be escaped to perform malicious acts. // Process::exec should be modified to take a list of arguments to execute command with. // TODO: Verify paths (test path, ignore dirs, include dirs, expose include dir, sibs test --file dir) are sub directory of the project #if OS_FAMILY == OS_FAMILY_POSIX #define fout std::cout #define ferr std::cerr #else #define fout std::wcout #define ferr std::wcerr #endif static string SIBS_GITIGNORE_HEADER = "# Compiled sibs files"; static string SIBS_GITIGNORE_FILES = "sibs-build/\n" "compile_commands.json\n" "tests/sibs-build/\n" "tests/compile_commands.json\n"; static 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(" init\t\tInitialize project in an existing directory\n"); printf(" test\t\tBuild and run tests for a sibs project\n"); exit(1); } static void usageBuild() { printf("Usage: sibs build [project_path] [--debug|--release] [--sanitize]\n\n"); printf("Build a sibs project\n\n"); printf("Options:\n"); printf(" project_path\t\tThe directory containing a project.conf file - Optional (default: current directory)\n"); printf(" --debug|--release\t\tOptimization level to build project and dependencies with (if not a system package) - Optional (default: --debug)\n"); printf(" --sanitize\t\tAdd runtime address/undefined behavior sanitization. Program can be up to 3 times slower and use 10 times as much RAM. Ignored if compiler doesn't support sanitization - Optional (default: disabled)\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"); printf(" sibs build --sanitize\n"); exit(1); } static void usageNew() { printf("Usage: sibs new <--exec|--static|--dynamic> [--lang c|c++|zig]\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(" --lang\t\t\tProject template language - Optional (default: c++)\n"); printf("Examples:\n"); printf(" sibs new hello_world --exec\n"); exit(1); } static void usageTest() { printf("Usage: sibs test [project_path] [--no-sanitize] [--file filepath...|--all-files]\n\n"); printf("Build and run tests for a sibs project\n\n"); printf("Options:\n"); printf(" project_path\t\tThe directory containing a project.conf file - Optional (default: current directory)\n"); printf(" --no-sanitize\t\tDisable runtime address/undefined behavior - Optional (default: enabled), Only applicable for C and C++\n"); printf(" --file\t\t\tSpecify file to test, path to test file should be defined after this. Can be defined multiple times to test multiple files - Optional (default: not used), Only applicable for Zig\n"); printf(" --all-files\t\t\tTest all files - Optional (default: not used), Only applicable for Zig\n"); printf("Examples:\n"); printf(" sibs test\n"); printf(" sibs test dirA/dirB\n"); exit(1); } static void usageInit() { printf("Usage: sibs init [project_path] <--exec|--static|--dynamic> [--lang c|c++|zig]\n\n"); printf("Create sibs project structure in an existing directory\n\n"); printf("Options:\n"); printf(" project_path\t\tThe directory where you want to initialize sibs project - Optional (default: current directory)\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(" --lang\t\t\tProject template language - Optional (default: c++)\n"); printf("Examples:\n"); printf(" sibs init . --exec\n"); printf(" sibs init dirA/dirB --dynamic"); exit(1); } static 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); } } static 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); } } static bool isPathSubPathOf(const FileString &path, const FileString &subPathOf) { return _tinydir_strncmp(path.c_str(), subPathOf.c_str(), subPathOf.size()) == 0; } #if OS_FAMILY == OS_FAMILY_WINDOWS static char* join(const vector &strs, const char separator) { vector lengths; lengths.reserve(strs.size()); int totalLength = strs.size() - 1; for (const char *str : strs) { int length = strlen(str); totalLength += length; lengths.push_back(length); } char *result = new char[totalLength + 1]; result[totalLength] = '\0'; int offset = 0; for (int i = 0; i < strs.size(); ++i) { if (i > 0) { result[offset] = separator; ++offset; } memcpy(result + offset, strs[i], lengths[i]); offset += lengths[i]; } return result; } struct MicrosoftBuildTool { // 0 if version not found int version; // empty if not found char binPath[_TINYDIR_PATH_MAX]; // empty if not found char vsLibPath[_TINYDIR_PATH_MAX]; // empty if not found char umLibPath[_TINYDIR_PATH_MAX]; // empty if not found char ucrtLibPath[_TINYDIR_PATH_MAX]; // empty if not found char vsIncludePath[_TINYDIR_PATH_MAX]; // empty if not found char umIncludePath[_TINYDIR_PATH_MAX]; // empty if not found char ucrtIncludePath[_TINYDIR_PATH_MAX]; bool found() { return version != 0; } }; static MicrosoftBuildTool locateLatestMicrosoftBuildTool() { MicrosoftBuildTool result = { 0 }; Result execResult = exec(TINYDIR_STRING("locate_windows_sdk x64")); if (execResult && execResult.unwrap().exitCode == 0) { auto &str = execResult.unwrap().execStdout; sscanf(execResult.unwrap().execStdout.c_str(), "%d %[^\r\n] %[^\r\n] %[^\r\n] %[^\r\n] %[^\r\n] %[^\r\n] %[^\r\n]", &result.version, result.binPath, result.vsLibPath, result.umLibPath, result.ucrtLibPath, result.vsIncludePath, result.umIncludePath, result.ucrtIncludePath); } return result; } // We do not free allocated data here because they needs to live as long as they're used as env (in _putenv) static void appendMicrosoftBuildToolToPathEnv() { MicrosoftBuildTool msBuildTool = locateLatestMicrosoftBuildTool(); if (msBuildTool.found()) { fprintf(stderr, "Located microsoft build tools at %s\n", msBuildTool.binPath); if (const char *pathEnv = getenv("PATH")) { if (_putenv_s("PATH", join({ pathEnv, msBuildTool.binPath }, ';')) != 0) fprintf(stderr, "Warning: Failed to add microsoft build tools to PATH env\n"); } if (_putenv_s("INCLUDE", join({ msBuildTool.vsIncludePath, msBuildTool.umIncludePath, msBuildTool.ucrtIncludePath }, ';')) != 0) fprintf(stderr, "Warning: Failed to add microsoft build libraries to INCLUDE env\n"); if (_putenv_s("LIB", join({ msBuildTool.vsLibPath, msBuildTool.umLibPath, msBuildTool.ucrtLibPath }, ';')) != 0) fprintf(stderr, "Warning: Failed to add microsoft build libraries to LIB env\n"); } } #endif static void appendBuildToolToPathEnv() { #if OS_FAMILY == OS_FAMILY_WINDOWS // TODO: We shouldn't do this if user wants to compile with clang/mingw? appendMicrosoftBuildToolToPathEnv(); #endif } static int buildProject(const FileString &projectPath, const FileString &projectConfFilePath, SibsConfig &sibsConfig) { FileString buildPath; readSibsConfig(projectPath, projectConfFilePath, sibsConfig, buildPath); // Test project has the main project as dependency, and therefore the main project can't be built as an executable if(sibsConfig.shouldBuildTests()) { // HACK: We can build a package that is defined as executable and contains main function by redefining `main` as something else. // TODO: Do not allow defining `main` in project.conf or as program argument to sibs. // It's ok if `define` fails. It could fail if `main` has already been replaced by other tests somehow. sibsConfig.define("main", "sibs_lib_ignore_main"); sibsConfig.define("wmain", "sibs_lib_ignore_wmain"); sibsConfig.define("WinMain", "sibs_lib_ignore_WinMain"); sibsConfig.setPackageType(PackageType::DYNAMIC); } auto startTime = high_resolution_clock::now(); if(sibsConfig.shouldUseCmake()) { auto dummyCallback = [](const string&){}; // TODO: Add test and sub projects 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; // TODO: Do same for cmake switch (sibsConfig.getOptimizationLevel()) { case OPT_LEV_DEBUG: { // TODO: Check if this dependency is static or dynamic and decide which lib path to use from that for(const string &staticLib : sibsConfig.getDebugStaticLibs()) { string staticLibCmd = "\""; staticLibCmd += staticLib; staticLibCmd += "\""; ninja.addDependency(staticLibCmd); } break; } case OPT_LEV_RELEASE: { // TODO: Check if this dependency is static or dynamic and decide which lib path to use from that for (const string &staticLib : sibsConfig.getReleaseStaticLibs()) { string staticLibCmd = "\""; staticLibCmd += staticLib; staticLibCmd += "\""; ninja.addDependency(staticLibCmd); } break; } } if(sibsConfig.shouldBuildTests() && sibsConfig.getTestPath().empty() && !sibsConfig.zigTestAllFiles && sibsConfig.zigTestFiles.empty()) { printf("Project is missing tests subdirectory. No tests to build\n"); exit(50); } backend::BackendUtils::collectSourceFiles(projectPath.c_str(), &ninja, sibsConfig); sibsConfig.setMainProject(true); 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; } static int buildProject(int argc, const _tinydir_char_t **argv) { if(argc > 3) usageBuild(); OptimizationLevel optimizationLevel = OPT_LEV_NONE; FileString projectPath; bool sanitize = false; 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(_tinydir_strcmp(arg, TINYDIR_STRING("--sanitize")) == 0) { sanitize = true; } else if(_tinydir_strncmp(arg, TINYDIR_STRING("--"), 2) == 0) { ferr << "Error: Invalid argument " << arg << endl; usageBuild(); } 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); sibsConfig.showWarnings = true; sibsConfig.setSanitize(sanitize); return buildProject(projectPath, projectConfFilePath, sibsConfig); } static int testProject(int argc, const _tinydir_char_t **argv) { if(argc > 2) usageTest(); FileString projectPath; vector filesToTest; bool testAllFiles = false; bool sanitize = true; for(int i = 0; i < argc; ++i) { const _tinydir_char_t *arg = argv[i]; if(_tinydir_strcmp(arg, TINYDIR_STRING("--no-sanitize")) == 0) { sanitize = false; } else if(_tinydir_strcmp(arg, TINYDIR_STRING("--file")) == 0) { if(i == argc - 1) { ferr << "Error: Expected path to file to test after --file " << endl; usageTest(); } ++i; arg = argv[i]; filesToTest.push_back(arg); if(testAllFiles) { ferr << "Error: --file can't be used together with --all-files " << endl; usageTest(); } } else if(_tinydir_strcmp(arg, TINYDIR_STRING("--all-files")) == 0) { if(testAllFiles) { ferr << "Error: --all-files defined twice " << endl; usageTest(); } testAllFiles = true; if(!filesToTest.empty()) { ferr << "Error: --all-files can't be used together with --file " << endl; usageTest(); } } else if(_tinydir_strncmp(arg, TINYDIR_STRING("--"), 2) == 0) { ferr << "Error: Invalid argument " << arg << endl; usageTest(); } else { if(!projectPath.empty()) { ferr << "Error: Project path was defined more than once. First defined as " << projectPath << " then as " << arg << endl; usageTest(); } projectPath = arg; } } // 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(); for(const FileString &testFile : filesToTest) { if(testFile.empty()) { ferr << "Error: Test filepath can't be empty" << endl; exit(20); } FileType fileType = getFileType(testFile.c_str()); switch(fileType) { case FileType::FILE_NOT_FOUND: { ferr << "Error: Test file not found: " << testFile << endl; exit(20); break; } case FileType::DIRECTORY: { ferr << "Error: Test file " << testFile << " is a directory, expected to be a file" << endl; exit(20); break; } case FileType::REGULAR: { // TODO: This can be optimized, there is no need to create a copy to check file extension FileString fileExtension = backend::BackendUtils::getFileExtension(testFile); sibs::Language fileLanguage = backend::BackendUtils::getFileLanguage(fileExtension.c_str()); if(fileLanguage != sibs::Language::ZIG) { ferr << "Error: file specific testing can only be done on zig files. " << testFile << " is not a zig file" << endl; exit(42); } break; } } } 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); sibsConfig.showWarnings = true; sibsConfig.setSanitize(sanitize); sibsConfig.zigTestFiles = move(filesToTest); sibsConfig.zigTestAllFiles = testAllFiles; return buildProject(projectPath, projectConfFilePath, sibsConfig); } // Returns nullptr if @charToFind is not found static const _tinydir_char_t* findLastOf(const _tinydir_char_t *str, const int strSize, const char charToFind) { for(int i = strSize; i >= 0; --i) { if(str[i] == charToFind) return str + i; } return nullptr; } static Result 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 = [\"any\"]\n\n"; projectConfStr += "[dependencies]\n"; FileString projectConfPath = projectPath; projectConfPath += TINYDIR_STRING("/project.conf"); return fileWrite(projectConfPath.c_str(), projectConfStr.c_str()); } static Result createDirectoryRecursive(const FileString &dir) { return createDirectoryRecursive(dir.c_str()); } static 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); } } // This can be replaced with createDirectory and fileOverwrite, but it's not important // so there is no reason to do it (right now) static Result gitInitProject(const FileString &projectPath) { FileString cmd = TINYDIR_STRING("git init \""); cmd += projectPath; cmd += TINYDIR_STRING("\""); return exec(cmd.c_str()); } static bool gitIgnoreContainsSibs(const FileString &gitIgnoreFilePath) { Result fileContentResult = getFileContent(gitIgnoreFilePath.c_str()); if(!fileContentResult) return false; StringView fileContent = fileContentResult.unwrap(); const char *fileContentEnd = fileContent.data + fileContent.size; auto it = std::search(fileContent.data, fileContentEnd, SIBS_GITIGNORE_HEADER.begin(), SIBS_GITIGNORE_HEADER.end()); bool containsSibs = it != fileContentEnd; free((void*)fileContent.data); return containsSibs; } static void gitIgnoreAppendSibs(const FileString &gitIgnoreFilePath) { Result fileContentResult = getFileContent(gitIgnoreFilePath.c_str()); string fileContentNew; if(fileContentResult) { StringView fileContent = fileContentResult.unwrap(); fileContentNew.append(fileContent.data, fileContent.data + fileContent.size); fileContentNew += "\n\n"; free((void*)fileContent.data); } fileContentNew += SIBS_GITIGNORE_HEADER; fileContentNew += "\n"; fileContentNew += SIBS_GITIGNORE_FILES; Result result = fileOverwrite(gitIgnoreFilePath.c_str(), { fileContentNew.data(), fileContentNew.size() }); if(!result) ferr << "Failed to add sibs to .gitignore, reason: " << toFileString(result.getErrMsg()) << endl; } static int initProject(int argc, const _tinydir_char_t **argv) { FileString projectPath; const _tinydir_char_t *projectType = nullptr; const _tinydir_char_t *lang = nullptr; for(int i = 0; i < argc; ++i) { const _tinydir_char_t *arg = argv[i]; if(_tinydir_strcmp(arg, TINYDIR_STRING("--exec")) == 0) { if(projectType) { ferr << "Error: Project type was defined more than once. First as " << projectType << " then as " << arg << endl; usageInit(); } projectType = arg; } else if(_tinydir_strcmp(arg, TINYDIR_STRING("--static")) == 0) { if(projectType) { ferr << "Error: Project type was defined more than once. First as " << projectType << " then as " << arg << endl; usageInit(); } projectType = arg; } else if(_tinydir_strcmp(arg, TINYDIR_STRING("--dynamic")) == 0) { if(projectType) { ferr << "Error: Project type was defined more than once. First as " << projectType << " then as " << arg << endl; usageInit(); } projectType = arg; } else if(_tinydir_strcmp(arg, TINYDIR_STRING("--lang")) == 0) { if(i == argc - 1) { ferr << "Error: Expected language argument after --lang" << endl; usageInit(); } ++i; arg = argv[i]; if(lang) { ferr << "Error: Project language was defined more than once. First as " << lang << " then as " << arg << endl; usageInit(); } lang = arg; if(_tinydir_strcmp(lang, TINYDIR_STRING("c")) != 0 && _tinydir_strcmp(lang, TINYDIR_STRING("c++")) != 0 && _tinydir_strcmp(lang, TINYDIR_STRING("zig")) != 0) { ferr << "Expected project language to be either c, c++ or zig; was: " << lang << endl << endl; usageInit(); } } else if(_tinydir_strncmp(arg, TINYDIR_STRING("--"), 2) == 0) { ferr << "Error: Invalid argument " << arg << endl; usageInit(); } else { if(!projectPath.empty()) { ferr << "Error: Project path was defined more than once. First defined as " << projectPath << " then as " << arg << endl; usageInit(); } projectPath = arg; } } if(!projectType) { ferr << "Error: Project type not defined, expected to be either --exec, --static or --dynamic" << endl; usageInit(); } if(!lang) lang = TINYDIR_STRING("c++"); 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; usageInit(); } // 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()) { ferr << "Error: Project path not defined" << endl; usageInit(); } 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(); FileType projectFileType = getFileType(projectPath.c_str()); if(projectFileType == FileType::FILE_NOT_FOUND) { ferr << "Directory not found: '" << projectPath << "', unable to initialize project" << endl; exit(20); } else if(projectFileType == FileType::REGULAR) { ferr << "Expected project path : '" << projectPath << "' to be a directory, was a file. Unable to initialize project" << endl; exit(21); } const _tinydir_char_t *projectNameForwardSlash = findLastOf(projectPath.c_str(), projectPath.size(), '/'); const _tinydir_char_t *projectNameBackwardSlash = findLastOf(projectPath.c_str(), projectPath.size(), '\\'); string projectName; if(projectNameForwardSlash && projectNameBackwardSlash) { if(projectNameForwardSlash > projectNameBackwardSlash) projectName = toUtf8(projectNameForwardSlash + 1); else projectName = toUtf8(projectNameBackwardSlash + 1); } else if(!projectNameForwardSlash && projectNameBackwardSlash) projectName = toUtf8(projectNameBackwardSlash + 1); else if(!projectNameBackwardSlash && projectNameForwardSlash) projectName = toUtf8(projectNameForwardSlash + 1); else projectName = toUtf8(projectPath); if(!isProjectNameValid(projectName)) { ferr << "Project name can only contain alphanumerical characters, dash (-) or underscore (_) and has to be longer than 0 characters" << endl; exit(20); } auto createProjectConfResult = newProjectCreateConf(projectName, projectTypeConf, projectPath); if(!createProjectConfResult) { ferr << "A project already exists in the directory " << projectPath << ". Error: failed to create project.conf, reason: " << toFileString(createProjectConfResult.getErrMsg()) << endl; exit(20); } createDirectoryRecursive(projectPath + TINYDIR_STRING("/src")); if(_tinydir_strcmp(lang, TINYDIR_STRING("c")) == 0 || _tinydir_strcmp(lang, TINYDIR_STRING("c++")) == 0) { createDirectoryRecursive(projectPath + TINYDIR_STRING("/tests")); createDirectoryRecursive(projectPath + TINYDIR_STRING("/include")); FileString mainFileName; if(_tinydir_strcmp(lang, TINYDIR_STRING("c")) == 0) mainFileName = TINYDIR_STRING("main.c"); else mainFileName = TINYDIR_STRING("main.cpp"); if(projectTypeConf == "executable") { auto mainFilePath = projectPath + TINYDIR_STRING("/src/") + mainFileName; Result fileOverwriteResult = fileWrite(mainFilePath.c_str(), "#include \n\nint main(int argc, char **argv)\n{\n printf(\"hello, world!\\n\");\n return 0;\n}\n"); if(!fileOverwriteResult) fout << "Warning: Failed to create project file: " << toFileString(fileOverwriteResult.getErrMsg()) << endl; } auto testFilePath = projectPath + TINYDIR_STRING("/tests/") + mainFileName; Result fileOverwriteResult = fileWrite(testFilePath.c_str(), "#include \n\nint main(int argc, char **argv)\n{\n printf(\"hello, world!\\n\");\n return 0;\n}\n"); if(!fileOverwriteResult) fout << "Warning: Failed to create project file: " << toFileString(fileOverwriteResult.getErrMsg()) << endl; } else if(_tinydir_strcmp(lang, TINYDIR_STRING("zig")) == 0 && projectTypeConf == "executable") { auto mainFilePath = projectPath + TINYDIR_STRING("/src/main.zig"); Result fileOverwriteResult = fileWrite(mainFilePath.c_str(), "const warn = @import(\"std\").debug.warn;\n\npub fn main() void\n{\n warn(\"Hello, world!\\n\");\n}\n"); if(!fileOverwriteResult) fout << "Warning: Failed to create project file: " << toFileString(fileOverwriteResult.getErrMsg()) << endl; } auto gitProjDir = projectPath + TINYDIR_STRING("/.git"); if(getFileType(gitProjDir.c_str()) == FileType::FILE_NOT_FOUND) gitInitProject(projectPath); auto gitIgnoreFilePath = projectPath + TINYDIR_STRING("/.gitignore"); if(!gitIgnoreContainsSibs(gitIgnoreFilePath)) gitIgnoreAppendSibs(gitIgnoreFilePath); return 0; } static 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); } } static void checkFailCreateSubDir(Result createSubDirResult) { if(!createSubDirResult) { ferr << "Failed to create directory in project: " << toFileString(createSubDirResult.getErrMsg()) << endl; exit(20); } } static int newProject(int argc, const _tinydir_char_t **argv) { string projectName; const _tinydir_char_t *projectType = nullptr; const _tinydir_char_t *lang = nullptr; for(int i = 0; i < argc; ++i) { const _tinydir_char_t *arg = argv[i]; if(_tinydir_strcmp(arg, TINYDIR_STRING("--exec")) == 0) { if(projectType) { ferr << "Error: Project type was defined more than once. First as " << projectType << " then as " << arg << endl; usageNew(); } projectType = arg; } else if(_tinydir_strcmp(arg, TINYDIR_STRING("--static")) == 0) { if(projectType) { ferr << "Error: Project type was defined more than once. First as " << projectType << " then as " << arg << endl; usageNew(); } projectType = arg; } else if(_tinydir_strcmp(arg, TINYDIR_STRING("--dynamic")) == 0) { if(projectType) { ferr << "Error: Project type was defined more than once. First as " << projectType << " then as " << arg << endl; usageNew(); } projectType = arg; } else if(_tinydir_strcmp(arg, TINYDIR_STRING("--lang")) == 0) { if(i == argc - 1) { ferr << "Error: Expected language argument after --lang" << endl; usageNew(); } ++i; arg = argv[i]; if(lang) { ferr << "Error: Project language was defined more than once. First as " << lang << " then as " << arg << endl; usageNew(); } lang = arg; if(_tinydir_strcmp(lang, TINYDIR_STRING("c")) != 0 && _tinydir_strcmp(lang, TINYDIR_STRING("c++")) != 0 && _tinydir_strcmp(lang, TINYDIR_STRING("zig")) != 0) { ferr << "Expected project language to be either c, c++ or zig; was: " << lang << endl << endl; usageNew(); } } else if(_tinydir_strncmp(arg, TINYDIR_STRING("--"), 2) == 0) { ferr << "Error: Invalid argument " << arg << endl; usageNew(); } else { if(!projectName.empty()) { ferr << "Error: Project name was defined more than once. First defined as " << toFileString(projectName) << " then as " << arg << endl; usageNew(); } projectName = toUtf8(arg); } } if(!projectType) { ferr << "Error: Project type not defined, expected to be either --exec, --static or --dynamic" << endl; usageNew(); } if(!lang) lang = TINYDIR_STRING("c++"); 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(); } Result cwdResult = getCwd(); if(cwdResult.isErr()) { ferr << "Failed to get current working directory: " << toFileString(cwdResult.getErrMsg()) << endl; exit(20); } 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); } newProjectCreateMainDir(projectPath); auto createProjectConfResult = newProjectCreateConf(projectName, projectTypeConf, projectPath); if(!createProjectConfResult) { ferr << "Failed to create project.conf: " << toFileString(createProjectConfResult.getErrMsg()) << endl; exit(20); } createDirectoryRecursive(projectPath + TINYDIR_STRING("/src")); if(_tinydir_strcmp(lang, TINYDIR_STRING("c")) == 0 || _tinydir_strcmp(lang, TINYDIR_STRING("c++")) == 0) { createDirectoryRecursive(projectPath + TINYDIR_STRING("/tests")); createDirectoryRecursive(projectPath + TINYDIR_STRING("/include")); FileString mainFileName; if(_tinydir_strcmp(lang, TINYDIR_STRING("c")) == 0) mainFileName = TINYDIR_STRING("main.c"); else mainFileName = TINYDIR_STRING("main.cpp"); if(projectTypeConf == "executable") { auto mainFilePath = projectPath + TINYDIR_STRING("/src/") + mainFileName; Result fileOverwriteResult = fileWrite(mainFilePath.c_str(), "#include \n\nint main(int argc, char **argv)\n{\n printf(\"hello, world!\\n\");\n return 0;\n}\n"); if(!fileOverwriteResult) { ferr << "Failed to create project file: " << toFileString(fileOverwriteResult.getErrMsg()) << endl; exit(20); } } auto testFilePath = projectPath + TINYDIR_STRING("/tests/") + mainFileName; Result fileOverwriteResult = fileWrite(testFilePath.c_str(), "#include \n\nint main(int argc, char **argv)\n{\n printf(\"hello, world!\\n\");\n return 0;\n}\n"); if(!fileOverwriteResult) { ferr << "Failed to create project file: " << toFileString(fileOverwriteResult.getErrMsg()) << endl; exit(20); } } else if(_tinydir_strcmp(lang, TINYDIR_STRING("zig")) == 0 && projectTypeConf == "executable") { auto mainFilePath = projectPath + TINYDIR_STRING("/src/main.zig"); Result fileOverwriteResult = fileWrite(mainFilePath.c_str(), "const warn = @import(\"std\").debug.warn;\n\npub fn main() void\n{\n warn(\"Hello, world!\\n\");\n}\n"); if(!fileOverwriteResult) { ferr << " Failed to create project file: " << toFileString(fileOverwriteResult.getErrMsg()) << endl; exit(20); } } // We are ignoring git init result on purpose. If it fails, just ignore it; not important gitInitProject(projectPath); auto gitIgnoreFilePath = projectPath + TINYDIR_STRING("/.gitignore"); gitIgnoreAppendSibs(gitIgnoreFilePath); 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) { appendBuildToolToPathEnv(); 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) { appendBuildToolToPathEnv(); return testProject(subCommandArgCount, subCommandArgPtr); } else if(_tinydir_strcmp(arg, TINYDIR_STRING("init")) == 0) { return initProject(subCommandArgCount, subCommandArgPtr); } else { ferr << "Expected command to be either 'build', 'new' or 'test', was: " << arg << endl << endl; usage(); } } usage(); return 0; }