#include "../include/Conf.hpp" #include "../include/types.hpp" #include "../include/VersionParser.hpp" #include "../external/utf8/unchecked.h" #include using namespace std; using u8string = utf8::unchecked::iterator; #if OS_FAMILY == OS_FAMILY_POSIX #define ferr std::cerr #else #define ferr std::wcerr #endif namespace sibs { static const string EMPTY_STRING = ""; class UnexpectedTokenException : public std::runtime_error { public: UnexpectedTokenException(const string &errMsg) : runtime_error(errMsg) { } }; enum class Token { NONE, END_OF_FILE, IDENTIFIER, OPEN_BRACKET, CLOSING_BRACKET, OPEN_BRACE, CLOSING_BRACE, EQUALS, STRING, COMMA }; const char *getTokenName(Token token) { switch(token) { case Token::NONE: return "NONE"; case Token::END_OF_FILE: return ""; case Token::IDENTIFIER: return "identifier"; case Token::OPEN_BRACKET: return "["; case Token::CLOSING_BRACKET: return "]"; case Token::OPEN_BRACE: return "{"; case Token::CLOSING_BRACE: return "}"; case Token::EQUALS: return "="; case Token::STRING: return "string"; case Token::COMMA: return ","; default: return "Unknown"; } } class Tokenizer { public: Tokenizer(const char *_code) : code((char*)_code) { } Token nextToken() { u32 c = *code; while(isWhitespace(c)) { ++code; c = *code; } if(isIdentifierChar(c)) { char *startOfIdentifier = code.base(); ++code; c = *code; char prevChar = '\0'; while(isIdentifierChar(c)) { if(c == '.' && prevChar == '.') throw UnexpectedTokenException("Identifier can't have two dots in a row"); prevChar = c; ++code; c = *code; } char *endOfIdentifier = code.base(); identifier = StringView(startOfIdentifier, endOfIdentifier - startOfIdentifier); return Token::IDENTIFIER; } else if(c == '[') { ++code; return Token::OPEN_BRACKET; } else if(c == ']') { ++code; return Token::CLOSING_BRACKET; } else if(c == '{') { ++code; return Token::OPEN_BRACE; } else if(c == '}') { ++code; return Token::CLOSING_BRACE; } else if(c == '=') { ++code; return Token::EQUALS; } else if(c == '"') { bool escapeQuote = false; ++code; char *startOfStr = code.base(); while(true) { c = *code; if(c == '"' && !escapeQuote) break; else if(c == '\\') escapeQuote = !escapeQuote; else if(c == '\0') throw UnexpectedTokenException("Reached end of file before string end"); else escapeQuote = false; ++code; } str = StringView(startOfStr, code.base() - startOfStr); ++code; return Token::STRING; } else if (c == ',') { ++code; return Token::COMMA; } else if(c == '#') { ++code; while(true) { c = *code; if(c == '\n') return nextToken(); else if(c == '\0') return Token::END_OF_FILE; ++code; } } else if(c == '\0') { return Token::END_OF_FILE; } else { string errMsg = "Unexpected token: "; errMsg += (char)c; throw UnexpectedTokenException(errMsg); } } StringView getIdentifier() const { return identifier; } StringView getString() const { return str; } private: bool isWhitespace(u32 c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; } bool isAlpha(u32 c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); } bool isDigit(u32 c) { return c >= '0' && c <= '9'; } bool isIdentifierChar(u32 c) { return isAlpha(c) || isDigit(c) || c == '_' || c == '-' || c == '.' || c == '+'; } private: u8string code; union { StringView identifier; StringView str; }; }; class Parser { public: static Result parse(const char *code, ConfigCallback &callback) { try { Parser parser(code, &callback); parser.parse(); return Result::Ok(true); } catch (const UnexpectedTokenException &e) { return Result::Err(e.what()); } catch (const ParserException &e) { return Result::Err(e.what()); } } private: Parser(const char *code, ConfigCallback *_callback) : tokenizer(code), callback(_callback), objectDefined(false) { } void parse() { while(true) { Token token = tokenizer.nextToken(); switch(token) { case Token::IDENTIFIER: { parseConfigField(); break; } case Token::OPEN_BRACKET: { parseConfigObject(); break; } case Token::END_OF_FILE: { callback->finished(); return; } default: { string errMsg = "Expected identifier or object, got: "; errMsg += getTokenName(token); throw ParserException(errMsg); } } } } void parseConfigField() { StringView fieldName = tokenizer.getIdentifier(); if (!objectDefined) { string errMsg = "An object has to be the first element defined in a config file"; throw ParserException(errMsg); } Token token = tokenizer.nextToken(); if(token == Token::EQUALS) { parseConfigFieldRhs(fieldName); } else { string errMsg = "Expected '=' after identifier, got: "; errMsg += getTokenName(token); throw ParserException(errMsg); } } void parseConfigFieldRhs(const StringView &fieldName) { Token token = tokenizer.nextToken(); if(token == Token::STRING) { callback->processField(fieldName, tokenizer.getString()); } else if(token == Token::OPEN_BRACKET) { parseConfigFieldRhsList(fieldName); } else if(token == Token::OPEN_BRACE) { parseConfigFieldRhsObject(fieldName); } else { string errMsg = "Expected string on right-hand side of field '"; errMsg += string(fieldName.data, fieldName.size); errMsg += "', got: "; errMsg += getTokenName(token); throw ParserException(errMsg); } } void parseConfigFieldRhsList(const StringView &fieldName) { vector values; Token token = tokenizer.nextToken(); if (token == Token::CLOSING_BRACKET) { callback->processField(fieldName, values); return; } while (true) { if (token == Token::STRING) values.push_back(tokenizer.getString()); else { string errMsg = "Expected list to contain string, got: "; errMsg += getTokenName(token); throw ParserException(errMsg); } token = tokenizer.nextToken(); if (token == Token::COMMA) { token = tokenizer.nextToken(); continue; } else if (token == Token::CLOSING_BRACKET) { break; } else { string errMsg = "Expected list value to be followed by ']' or ',', got: "; errMsg += getTokenName(token); throw ParserException(errMsg); } } callback->processField(fieldName, values); } void parseConfigFieldRhsObject(const StringView &fieldName) { unordered_map fields; Token token = tokenizer.nextToken(); if (token == Token::CLOSING_BRACE) { callback->processField(fieldName, fields); return; } while (true) { if (token == Token::IDENTIFIER) { StringView objectKey = tokenizer.getIdentifier(); token = tokenizer.nextToken(); if(token == Token::EQUALS) { token = tokenizer.nextToken(); if (token == Token::STRING) fields[string(objectKey.data, objectKey.size)] = tokenizer.getString(); else { string errMsg = "Expected object field value to be a string, got: "; errMsg += getTokenName(token); throw ParserException(errMsg); } } else { string errMsg = "Expected object key to be followed by '=', got: "; errMsg += getTokenName(token); throw ParserException(errMsg); } } else { string errMsg = "Expected object to contain key, got: "; errMsg += getTokenName(token); throw ParserException(errMsg); } token = tokenizer.nextToken(); if (token == Token::COMMA) { token = tokenizer.nextToken(); continue; } else if (token == Token::CLOSING_BRACE) { break; } else { string errMsg = "Expected object field to be followed by '}' or ',', got: "; errMsg += getTokenName(token); throw ParserException(errMsg); } } callback->processField(fieldName, fields); } void parseConfigObject() { Token token = tokenizer.nextToken(); if(token == Token::IDENTIFIER) { StringView objectName = tokenizer.getIdentifier(); token = tokenizer.nextToken(); if(token == Token::CLOSING_BRACKET) { objectDefined = true; callback->processObject(objectName); } else { string errMsg = "Expected ']' after identifier to close object definition, got: "; errMsg += getTokenName(token); throw ParserException(errMsg); } } else { string errMsg = "Expected identifier after '[', got: "; errMsg += getTokenName(token); throw ParserException(errMsg); } } private: Tokenizer tokenizer; ConfigCallback *callback; bool objectDefined; }; static void replaceAll(std::string &input, const std::string &oldStr, const std::string &newStr) { size_t index = 0; while(true) { index = input.find(oldStr, index); if(index == std::string::npos) break; input.replace(index, oldStr.size(), newStr); index += newStr.size(); } } Result Config::readFromFile(const _tinydir_char_t *filepath, SibsConfig &config) { Result fileContentResult = getFileContent(filepath); if(fileContentResult.isErr()) return Result::Err(fileContentResult.getErrMsg()); const char *code = fileContentResult.unwrap().data; if(!utf8::is_valid(code, code + fileContentResult.unwrap().size)) return Result::Err("File is not in valid utf8 format"); if(fileContentResult.unwrap().size >= 3 && utf8::is_bom(code)) code += 3; // Do not free file content (fileContentResult) on purpose, since we are using the data and sibs is short lived Result parseResult = Parser::parse(code, config); if(!parseResult) { string errMsg = "Failed while parsing project.conf for project "; errMsg += config.isTest() ? "tests" : config.getPackageName(); errMsg += ", reason: " + parseResult.getErrMsg(); return Result::Err(errMsg); } if(!config.isTest()) { if(config.getPackageName().empty()) { string errMsg = "The project "; errMsg += config.getPackageName(); errMsg += " is missing required field package.name is project.conf"; return Result::Err(errMsg); } if(config.versionStr.empty()) { string errMsg = "The project "; errMsg += config.getPackageName(); errMsg += " is missing required field package.version is project.conf"; return Result::Err(errMsg); } if (!containsPlatform(config.getPlatforms(), config.platform)) { string errMsg = "The project "; errMsg += config.getPackageName(); errMsg += " does not support your target platform ("; errMsg += asString(config.platform); errMsg += ")"; return Result::Err(errMsg); } FileString testsDir = config.getProjectPath() + TINYDIR_STRING("/tests"); if(getFileType(testsDir.c_str()) == FileType::DIRECTORY) { Result testRealPathResult = getRealPath(testsDir.c_str()); if(testRealPathResult) config.setTestPath(testRealPathResult.unwrap()); else fprintf(stderr, "Warning: Project contains tests directory but we got an error while retrieving the full path to it\n"); } if(config.isMainProject() && (config.packaging || config.bundling) && config.getPackageType() != PackageType::EXECUTABLE) return Result::Err("Packaging is only supported for projects that are of type executable"); } std::string outDir = std::string("sibs-build/") + asString(config.platform) + "/"; if(config.packaging) { outDir += "package"; } else { switch(config.getOptimizationLevel()) { case OPT_LEV_DEBUG: outDir += "debug"; break; case OPT_LEV_RELEASE: outDir += "release"; break; } } for(std::string &includeDir : config.includeDirs) { replaceAll(includeDir, "$out", outDir); } for(std::string &exposeIncludeDir : config.exposeIncludeDirs) { replaceAll(exposeIncludeDir, "$out", outDir); } for(std::string &ignoreDir : config.ignoreDirs) { replaceAll(ignoreDir, "$out", outDir); } return parseResult; } void readSibsConfig(const FileString &projectPath, const FileString &projectConfFilePath, SibsConfig &sibsConfig, FileString &buildPath) { Result result = Config::readFromFile(projectConfFilePath.c_str(), sibsConfig); if(!result) { ferr << "Failed to read config: " << toFileString(result.getErrMsg()) << endl; exit(6); } std::string outDir = std::string("sibs-build/") + asString(sibsConfig.platform) + "/"; if(sibsConfig.packaging) { outDir += "package"; } else { switch(sibsConfig.getOptimizationLevel()) { case OPT_LEV_DEBUG: outDir += "debug"; break; case OPT_LEV_RELEASE: outDir += "release"; break; } } buildPath = projectPath + TINYDIR_STRING("/") + toFileString(outDir); } const char* asString(OptimizationLevel optLevel) { switch(optLevel) { case OPT_LEV_NONE: return "none"; case OPT_LEV_DEBUG: return "debug"; case OPT_LEV_RELEASE: return "release"; default: return nullptr; } } bool isValidCIdentifier(const StringView &identifier) { if(identifier.size == 0) return false; char c = identifier.data[0]; if(isalpha(c) || c == '_') { for(int i = 1; i < identifier.size; ++i) { c = identifier.data[i]; if(!isalnum(c) && c != '_') return false; } } return true; } bool directoryToIgnore(const FileString &dir, const vector &ignoreDirList) { string dirUtf8 = toUtf8(dir); for(const string &ignoreDir : ignoreDirList) { if(pathEquals(dirUtf8, ignoreDir)) return true; } return false; } bool isProjectNameValid(const string &projectName) { if(projectName.empty()) return false; for(int i = 0; i < projectName.size(); ++i) { char c = projectName[i]; if(!isalpha(c) && !isdigit(c) && c != '-' && c != '_' && c != '.') return false; } return true; } SibsConfig::~SibsConfig() { // TODO: Fix this shit.. why does this cause segfault? /* for(PackageListDependency *dependency : packageListDependencies) { //delete dependency; } for(GitDependency *dependency : gitDependencies) { //delete dependency; } */ } bool SibsConfig::isDefined(const std::string &name) const { return defines.find(name) != defines.end(); } bool SibsConfig::define(const std::string &name, const std::string &value) { if(isDefined(name)) return false; else { defines[name] = value; return true; } } const std::unordered_map& SibsConfig::getDefines() const { return defines; } const string& SibsConfig::getDefinedValue(const string &name) const { auto it = defines.find(name); if(it != defines.end()) return it->second; return EMPTY_STRING; } void getLibFiles(const string &libPath, vector &outputFiles) { FileString nativePath = toFileString(libPath); FileType fileType = getFileType(nativePath.c_str()); switch (fileType) { case FileType::FILE_NOT_FOUND: { string errMsg = "Library path not found: "; errMsg += libPath; throw ParserException(errMsg); } case FileType::REGULAR: { string errMsg = "Expected library path "; errMsg += libPath; errMsg += " to be a directory, was a regular file"; throw ParserException(errMsg); } } walkDirFiles(nativePath.c_str(), [&outputFiles](tinydir_file *file) { if(_tinydir_strcmp(file->extension, CONFIG_STATIC_LIB_FILE_EXTENSION) == 0) outputFiles.push_back(toUtf8(file->path)); return true; }); } static string combineSupportedPlatformsAsString() { string result; int i = 0; int size = PLATFORM_BY_NAME.size(); for(const auto &it : PLATFORM_BY_NAME) { if(i > 0 && i == size - 1) result += " or "; else if(i > 0) result += ", "; result += string(it.first.data, it.first.size); ++i; } return result; } static bool isVersionStringValid(const string &version) { for(char c : version) { bool isValidChar = (c == '.' || (c >= '0' && c <= '9')); if(!isValidChar) return false; } return true; } void SibsConfig::processObject(StringView name) { currentObject = name; if(currentObject.equals("cmake") || currentObject.equals("cmake.static") || currentObject.equals("cmake.dynamic")) useCmake = true; //printf("Process object: %.*s\n", name.size, name.data); } void SibsConfig::processField(StringView name, const ConfigValue &value) { /* printf("Process field: %.*s, value: ", name.size, name.data); if(value.isSingle()) { printf("\"%.*s\"", value.asSingle().size, value.asSingle().data); } else { printf("["); int i = 0; for(auto listElement : value.asList()) { if(i > 0) printf(", "); printf("\"%.*s\"", listElement.size, listElement.data); ++i; } printf("]"); } printf("\n"); */ if(currentObject.equals("package")) { if(name.equals("name")) { if (value.isSingle()) packageName = string(value.asSingle().data, value.asSingle().size); else throw ParserException("Expected package.name to be a single value, was a list"); validatePackageName(); } else if(name.equals("version")) { if (value.isSingle()) versionStr = string(value.asSingle().data, value.asSingle().size); else throw ParserException("Expected package.version to be a single value, was a list"); int versionSize = 0; Result versionResult = parsePackageVersion(value.asSingle(), &versionSize); if(!versionResult) throw ParserException("package.version is in invalid format, error: " + versionResult.getErrMsg()); if(versionSize != (int)versionStr.size()) throw ParserException("package.version is in invalid format, expected to only contain numbers and dots"); version = versionResult.unwrap(); } else if(name.equals("authors")) { // TODO: Use authors for something? } else if(name.equals("type")) { if (value.isSingle()) { const StringView &packageTypeStr = value.asSingle(); if(packageTypeStr.equals("executable")) packageType = PackageType::EXECUTABLE; else if(packageTypeStr.equals("static")) packageType = PackageType::STATIC; else if(packageTypeStr.equals("dynamic")) packageType = PackageType::DYNAMIC; else if(packageTypeStr.equals("library")) packageType = PackageType::LIBRARY; else { string errMsg = "Expected package.type to be either 'executable', 'static', 'dynamic' or 'library', was: "; errMsg += string(packageTypeStr.data, packageTypeStr.size); throw ParserException(errMsg); } } else throw ParserException("Expected package.type to be a single value, was a list"); } else if(name.equals("tests")) { if (value.isSingle()) { if(value.asSingle().equals("tests")) { fprintf(stderr, "Warning: package.tests is deprecated, a subdirectory called tests is now automatically chosen as the tests directory\n"); } else { throw ParserException("package.tests is a deprecated field and can only be defined as \"tests\" if it's defined"); } } else throw ParserException("Expected package.tests to be a single value, was a list"); } else if(name.equals("include_dirs")) { if(value.isList()) { // TODO: Checking for duplicate declaration should be done in the config parser if(!includeDirs.empty()) throw ParserException("Found duplicate declaration of package.include_dirs"); for(const StringView &includeDir : value.asList()) { includeDirs.emplace_back(string(includeDir.data, includeDir.size)); } fprintf(stderr, "Warning: package.include_dirs is deprecated, please move include_dirs under config\n"); } else throw ParserException("Expected package.include_dirs to be a list, was a single value"); } else if (name.equals("platforms")) { if (value.isList()) { // TODO: Checking for duplicate declaration should be done in the config parser if (!platforms.empty()) throw ParserException("Found duplicate declaration of package.platforms"); for (const StringView &platform : value.asList()) { Platform platformType = getPlatformByName(platform); if (platformType != PLATFORM_INVALID) { platforms.push_back(platformType); } else { string errMsg = "package.platforms contains invalid platform \""; errMsg += string(platform.data, platform.size); errMsg += "\". Expected platform to be one of: " + combineSupportedPlatformsAsString(); throw ParserException(errMsg); } } } else throw ParserException("Expected package.platforms to be a list, was a single value"); } else if(name.equals("ignore_dirs")) { if (value.isList()) { string projectPathUtf8 = toUtf8(projectPath); // TODO: Checking for duplicate declaration should be done in the config parser if (!ignoreDirs.empty()) throw ParserException("Found duplicate declaration of package.ignore_dirs"); for (const StringView &ignoreDir : value.asList()) { string ignoreDirFull = projectPathUtf8; ignoreDirFull += "/"; ignoreDirFull += string(ignoreDir.data, ignoreDir.size); ignoreDirs.emplace_back(ignoreDirFull); } fprintf(stderr, "Warning: package.ignore_dirs is deprecated, please move ignore_dirs under config\n"); } else throw ParserException("Expected package.ignore_dirs to be a list, was a single value"); } else failInvalidFieldUnderObject(name); } else if(currentObject.size >= 6 && strncmp(currentObject.data, "config", 6) == 0) { if(currentObject.size == 6) // [config] { parseConfig(name, value); } else parsePlatformConfig(name, value); } else if(currentObject.equals("dependencies")) { parseDependencies(name, value); } else if(currentObject.equals("define")) { if(value.isSingle()) { if(!isValidCIdentifier(name)) { string errMsg = "Definition \""; errMsg.append(name.data, name.size); errMsg += "\" is not in a valid format. The first character have to match [a-zA-Z_] and the next characters have to match [a-zA-Z0-9_]"; throw ParserException(errMsg); } defines[string(name.data, name.size)] = string(value.asSingle().data, value.asSingle().size); } else throw ParserException("Expected field under define to be a single value, was a list"); } else if(currentObject.equals("define.static")) { // TODO: Do same for cmake args and other objects where you have static and dynamic. // Makes it easier to handle config (no need for switch for different libraryTypes) validatePackageTypeDefined(); if(value.isSingle()) { if(packageType == PackageType::STATIC) { if(!isValidCIdentifier(name)) { string errMsg = "Definition \""; errMsg.append(name.data, name.size); errMsg += "\" is not in a valid format. The first character have to match [a-zA-Z_] and the next characters have to match [a-zA-Z0-9_]"; throw ParserException(errMsg); } defines[string(name.data, name.size)] = string(value.asSingle().data, value.asSingle().size); } } else throw ParserException("Expected field under define.static to be a single value, was a list"); } else if(currentObject.equals("define.dynamic")) { validatePackageTypeDefined(); if(value.isSingle()) { // TODO: Remove `LIBRARY` from PackageType and if building a project where type is `library`, // then convert it to dynamic. If a dependency has type `library`, then convert to dynamic // unless build option includes to build dependencies as static libraries if(packageType == PackageType::DYNAMIC || packageType == PackageType::LIBRARY) { if(!isValidCIdentifier(name)) { string errMsg = "Definition \""; errMsg.append(name.data, name.size); errMsg += "\" is not in a valid format. The first character have to match [a-zA-Z_] and the next characters have to match [a-zA-Z0-9_]"; throw ParserException(errMsg); } defines[string(name.data, name.size)] = string(value.asSingle().data, value.asSingle().size); } } else throw ParserException("Expected field under define.dynamic to be a single value, was a list"); } else if(currentObject.equals("cmake")) { parseCmake(name, value, cmakeDirGlobal, cmakeArgsGlobal); } else if(currentObject.equals("cmake.static")) { parseCmake(name, value, cmakeDirStatic, cmakeArgsStatic); } else if(currentObject.equals("cmake.dynamic")) { parseCmake(name, value, cmakeDirDynamic, cmakeArgsDynamic); } else if(currentObject.equals("lang.c")) { parseCLang(name, value); } else if(currentObject.equals("lang.cpp")) { parseCppLang(name, value); } else { string errMsg = "Invalid config object \""; errMsg += string(currentObject.data, currentObject.size); errMsg += "\""; throw ParserException(errMsg); } } void SibsConfig::parseConfig(const StringView &name, const ConfigValue &value) { if(name.equals("expose_include_dirs")) { if (value.isList()) { for (const StringView &includeDir : value.asList()) { exposeIncludeDirs.emplace_back(string(includeDir.data, includeDir.size)); } } else { string errMsg = "Expected "; errMsg += string(currentObject.data, currentObject.size); errMsg += " to be a list, was a single value"; throw ParserException(errMsg); } } else if(name.equals("include_dirs")) { if(value.isList()) { for(const StringView &includeDir : value.asList()) { includeDirs.emplace_back(string(includeDir.data, includeDir.size)); } } else throw ParserException("Expected " + string(currentObject.data, currentObject.size) + ".include_dirs to be a list, was a single value"); } else if(name.equals("ignore_dirs")) { if (value.isList()) { string projectPathUtf8 = toUtf8(projectPath); for (const StringView &ignoreDir : value.asList()) { string ignoreDirFull = projectPathUtf8; ignoreDirFull += "/"; ignoreDirFull += string(ignoreDir.data, ignoreDir.size); ignoreDirs.emplace_back(move(ignoreDirFull)); } } else throw ParserException("Expected " + string(currentObject.data, currentObject.size) + ".ignore_dirs to be a list, was a single value"); } else if(name.equals("error_on_warning")) { if (value.isSingle()) { StringView value_str = value.asSingle(); bool value_bool = false; if(value_str.equals("true")) value_bool = true; else if(value_str.equals("false")) value_bool = false; else throw ParserException("Expected " + string(currentObject.data, currentObject.size) + ".error_on_warning to be either true or false"); errorOnWarning = value_bool; } else throw ParserException("Expected " + string(currentObject.data, currentObject.size) + ".error_on_warning to be a single value, was a list"); } else failInvalidFieldUnderObject(name); } void SibsConfig::parseDependencies(const StringView &name, const ConfigValue &value) { if(value.isSingle()) { VersionParser versionParser; Result dependencyVersionResult = versionParser.parse(value.asSingle().data, value.asSingle().size); if(!dependencyVersionResult) throw ParserException("Dependency " + string(name.data, name.size) + " version is in invalid format, error: " + dependencyVersionResult.getErrMsg()); PackageListDependency *dependency = new PackageListDependency(); dependency->name = string(name.data, name.size); dependency->version = dependencyVersionResult.unwrap(); packageListDependencies.emplace_back(dependency); } else if(value.isObject()) { enum class DepType { NONE, GIT }; DepType depType = DepType::NONE; for(auto it : value.asObject()) { DepType fieldDepType = DepType::NONE; if(it.first == "git" || it.first == "branch" || it.first == "revision") fieldDepType = DepType::GIT; if(fieldDepType == DepType::NONE) { string errMsg = "Invalid dependency object field \""; errMsg += it.first; errMsg += "\""; throw ParserException(errMsg); } if(depType != DepType::NONE && fieldDepType != depType) { switch(depType) { case DepType::GIT: { string errMsg = "Invalid dependency object field \""; errMsg += it.first; errMsg += "\" is invalid for a git dependency"; throw ParserException(errMsg); } default: throw ParserException("Invalid dependency object"); } } depType = fieldDepType; } switch(depType) { case DepType::GIT: { auto gitIt = value.asObject().find("git"); if(gitIt == value.asObject().end()) { throw ParserException("Required field \"git\" is missing from git dependency. Expected an url to location of git repository"); } auto branchIt = value.asObject().find("branch"); string branch; if(branchIt == value.asObject().end()) branch = "master"; else branch = string(branchIt->second.data, branchIt->second.size); auto revisionIt = value.asObject().find("revision"); string revision; if(revisionIt == value.asObject().end()) revision = "HEAD"; else revision = string(revisionIt->second.data, revisionIt->second.size); GitDependency *dependency = new GitDependency(); dependency->name = string(name.data, name.size); dependency->url = string(gitIt->second.data, gitIt->second.size); dependency->branch = branch; dependency->revision = revision; gitDependencies.emplace_back(dependency); break; } default: throw ParserException("Invalid dependency object"); } } else throw ParserException("Expected field under dependencies to be a single value or an object, was a list"); } void SibsConfig::parseCLang(const StringView &fieldName, const ConfigValue &fieldValue) { if(fieldName.equals("version")) { // TODO: Support several versions (a list of versions where the latest possible version is used)? if(fieldValue.isSingle()) { const StringView &cVersionStr = fieldValue.asSingle(); if(cVersionStr.equals("c89") || cVersionStr.equals("ansi")) { cVersion = CVersion::C89; } else if(cVersionStr.equals("c99")) { cVersion = CVersion::C99; } else if(cVersionStr.equals("c11")) { cVersion = CVersion::C11; } else { string errMsg = "Expected lang.c.version to be ansi, c89, c99 or c11, was "; errMsg += string(cVersionStr.data, cVersionStr.size); throw ParserException(errMsg); } } else throw ParserException("Expected lang.c.version to be a single value, was a list"); } else failInvalidFieldUnderObject(fieldName); } void SibsConfig::parseCppLang(const StringView &fieldName, const ConfigValue &fieldValue) { if(fieldName.equals("version")) { // TODO: Support several versions (a list of versions where the latest possible version is used)? if(fieldValue.isSingle()) { const StringView &cppVersionStr = fieldValue.asSingle(); if(cppVersionStr.equals("c++11")) { cppVersion = CPPVersion::CPP11; } else if(cppVersionStr.equals("c++14")) { cppVersion = CPPVersion::CPP14; } else if(cppVersionStr.equals("c++17")) { cppVersion = CPPVersion::CPP17; } else { string errMsg = "Expected lang.cpp.version to be c++11, c++14 or c++17, was "; errMsg += string(cppVersionStr.data, cppVersionStr.size); throw ParserException(errMsg); } } else throw ParserException("Expected lang.cpp.version to be a single value, was a list"); } else failInvalidFieldUnderObject(fieldName); } static bool isObjectIdentifierSymbol(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-'; } static StringView getNextIdentifierInObject(const StringView &objectName) { size_t identifierStart = -1; for(size_t i = 0; i < objectName.size; ++i) { char c = objectName[i]; if(c == '.') { if(identifierStart == -1) identifierStart = i + 1; else return { objectName.data + identifierStart, i - identifierStart }; } } if(identifierStart == -1) return { objectName.data + objectName.size, 0 }; else return { objectName.data + identifierStart, objectName.size - identifierStart }; } void SibsConfig::parsePlatformConfig(const StringView &fieldName, const ConfigValue &fieldValue) { StringView platformName = getNextIdentifierInObject(currentObject); Platform platform = getPlatformByName(platformName); if(platform == PLATFORM_INVALID) { string errMsg = "Invalid config object \""; errMsg += string(currentObject.data, currentObject.size); errMsg += "\", invalid platform: "; errMsg += string(platformName.data, platformName.size); throw ParserException(errMsg); } if(!isBaseForPlatform(platform, SYSTEM_PLATFORM)) return; const char *start = platformName.data + platformName.size; const char *currentObjEnd = currentObject.data + currentObject.size; if(start < currentObjEnd) { size_t size = currentObjEnd - start; StringView platformConfigType = { start, size }; if(size == 13 && strncmp(platformConfigType.data, ".static.debug", 13) == 0) { parsePlatformConfigStaticDebug(fieldName, fieldValue); } else if(size == 15 && strncmp(platformConfigType.data, ".static.release", 15) == 0) { parsePlatformConfigStaticRelease(fieldName, fieldValue); } else { string errMsg = "Invalid config object \""; errMsg += string(currentObject.data, currentObject.size); errMsg += "\""; throw ParserException(errMsg); } } else parseConfig(fieldName, fieldValue); } string SibsConfig::parsePlatformConfigStatic(const StringView &fieldName, const ConfigValue &fieldValue) { // TODO: Verify the library is actually a static library if (fieldName.equals("lib")) { if (fieldValue.isSingle()) { string staticLibPath = toUtf8(projectPath); staticLibPath += "/"; staticLibPath += string(fieldValue.asSingle().data, fieldValue.asSingle().size); return staticLibPath; } else { string errMsg = "Expected "; errMsg += string(currentObject.data, currentObject.size); errMsg += " to be a single value, was a list"; throw ParserException(errMsg); } } else failInvalidFieldUnderObject(fieldName); return ""; } void SibsConfig::parsePlatformConfigStaticDebug(const StringView &fieldName, const ConfigValue &fieldValue) { getLibFiles(parsePlatformConfigStatic(fieldName, fieldValue), debugStaticLibs); } void SibsConfig::parsePlatformConfigStaticRelease(const StringView &fieldName, const ConfigValue &fieldValue) { getLibFiles(parsePlatformConfigStatic(fieldName, fieldValue), releaseStaticLibs); } void SibsConfig::parseCmake(const StringView &fieldName, const ConfigValue &fieldValue, FileString &cmakeDir, FileString &cmakeArgs) { if(fieldName.equals("dir")) { if(fieldValue.isSingle()) { cmakeDir = projectPath; cmakeDir += TINYDIR_STRING("/"); cmakeDir += toFileString(fieldValue.asSingle()); // No need to validate if CMakeLists.txt exists here, cmake will tell us if the file doesn't exist } else { string errMsg = "Expected "; errMsg.append(currentObject.data, currentObject.size); errMsg += "."; errMsg.append(fieldName.data, fieldName.size); errMsg += " to be a single value, was a list"; throw ParserException(errMsg); } } else if(fieldName.equals("args")) { if(fieldValue.isList()) { for(const StringView &arg : fieldValue.asList()) { bool prependSpace = !cmakeArgs.empty(); cmakeArgs.reserve(cmakeArgs.size() + 4 + (prependSpace ? 1 : 0) + arg.size); if(prependSpace) cmakeArgs += TINYDIR_STRING(" "); cmakeArgs += TINYDIR_STRING("\"-D"); cmakeArgs += toFileString(arg); cmakeArgs += TINYDIR_STRING("\""); } } else { string errMsg = "Expected "; errMsg.append(currentObject.data, currentObject.size); errMsg += "."; errMsg.append(fieldName.data, fieldName.size); errMsg += " to be a list, was a single value"; throw ParserException(errMsg); } } else failInvalidFieldUnderObject(fieldName); } void SibsConfig::finished() { if((int)packageType == -1) throw ParserException("Missing required config package.type. Expected to be one either 'executable', 'static', 'dynamic' or 'library'"); finishedProcessing = true; if(platforms.empty()) throw ParserException("Missing required config package.platforms. If the package supports all platforms, add:\nplatforms = [\"any\"]\nto project.conf under [package]"); if(useCmake) { switch(packageType) { case PackageType::EXECUTABLE: { if(getCmakeDir().empty()) throw ParserException("Missing required config cmake.dir"); break; } case PackageType::STATIC: { if(getCmakeDirStatic().empty()) throw ParserException("Missing required config cmake.static"); break; } case PackageType::DYNAMIC: case PackageType::LIBRARY: { if(getCmakeDirDynamic().empty()) throw ParserException("Missing required config cmake.dynamic"); break; } } } } void SibsConfig::validatePackageName() const { if(!isProjectNameValid(packageName)) { string errMsg = "Invalid package name: "; errMsg += packageName; errMsg += ". Package name can only contain alphanumerical characters, dash (-) or underscore (_)"; throw ParserException(errMsg); } } void SibsConfig::failInvalidFieldUnderObject(const StringView &fieldName) const { string errMsg = "Invalid field \""; errMsg += string(fieldName.data, fieldName.size); errMsg += "\" under object \""; errMsg += string(currentObject.data, currentObject.size); errMsg += "\""; throw ParserException(errMsg); } void SibsConfig::validatePackageTypeDefined() const { if((int)packageType == -1) throw ParserException("package.type type has not been defined yet. Expected to be either 'executable', 'static', 'dynamic' or 'library'"); } void SibsTestConfig::processObject(StringView name) { currentObject = name; } void SibsTestConfig::processField(StringView name, const ConfigValue &value) { if(currentObject.equals("dependencies")) { parseDependencies(name, value); } 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; } }