#include "project_parser.hpp" #include "fs.hpp" #include #include #include namespace cmkr { namespace parser { const char *targetTypeNames[target_last] = {"executable", "library", "shared", "static", "interface", "custom", "object", "template"}; static TargetType parse_targetType(const std::string &name) { for (int i = 0; i < target_last; i++) { if (name == targetTypeNames[i]) { return static_cast(i); } } return target_last; } const char *msvcRuntimeTypeNames[msvc_last] = {"dynamic", "static"}; static MsvcRuntimeType parse_msvcRuntimeType(const std::string &name) { for (int i = 0; i < msvc_last; i++) { if (name == msvcRuntimeTypeNames[i]) { return static_cast(i); } } return msvc_last; } using TomlBasicValue = toml::basic_value; static std::string format_key_message(const std::string &message, const toml::key &ky, const TomlBasicValue &value) { auto loc = value.location(); auto line_number_str = std::to_string(loc.line()); auto line_width = line_number_str.length(); const auto &line_str = loc.line_str(); std::ostringstream oss; oss << message << "\n"; oss << " --> " << loc.file_name() << ':' << loc.line() << '\n'; oss << std::string(line_width + 2, ' ') << "|\n"; oss << ' ' << line_number_str << " | " << line_str << '\n'; oss << std::string(line_width + 2, ' ') << '|'; auto key_start = line_str.substr(0, loc.column() - 1).rfind(ky); if (key_start == std::string::npos) { key_start = line_str.find(ky); } if (key_start != std::string::npos) { oss << std::string(key_start + 1, ' ') << std::string(ky.length(), '~'); } return oss.str(); } static void throw_key_error(const std::string &error, const toml::key &ky, const TomlBasicValue &value) { throw std::runtime_error(format_key_message("[error] " + error, ky, value)); } static void print_key_warning(const std::string &message, const toml::key &ky, const TomlBasicValue &value) { puts(format_key_message("[warning] " + message, ky, value).c_str()); } class TomlChecker { const TomlBasicValue &m_v; tsl::ordered_set m_visited; tsl::ordered_set m_conditionVisited; public: TomlChecker(const TomlBasicValue &v, const toml::key &ky) : m_v(toml::find(v, ky)) { } explicit TomlChecker(const TomlBasicValue &v) : m_v(v) { } TomlChecker(const TomlChecker &) = delete; TomlChecker(TomlChecker &&) = delete; template void optional(const toml::key &ky, Condition &destination) { // TODO: this algorithm in O(n) over the amount of keys, kinda bad const auto &table = m_v.as_table(); for (const auto &itr : table) { const auto &key = itr.first; const auto &value = itr.second; if (value.is_table()) { if (value.contains(ky)) { destination[key] = toml::find(value, ky); } } else if (key == ky) { destination[""] = toml::find(m_v, ky); } } // Handle visiting logic for (const auto &itr : destination) { if (!itr.first.empty()) { m_conditionVisited.emplace(itr.first); } } visit(ky); } template void optional(const toml::key &ky, T &destination) { // TODO: this currently doesn't allow you to get an optional map if (m_v.contains(ky)) { destination = toml::find(m_v, ky); } visit(ky); } template void required(const toml::key &ky, T &destination) { destination = toml::find(m_v, ky); visit(ky); } bool contains(const toml::key &ky) { visit(ky); return m_v.contains(ky); } const TomlBasicValue &find(const toml::key &ky) { visit(ky); return toml::find(m_v, ky); } void visit(const toml::key &ky) { m_visited.emplace(ky); } bool visisted(const toml::key &ky) const { return m_visited.contains(ky); } void check(const tsl::ordered_map &conditions) const { for (const auto &itr : m_v.as_table()) { const auto &ky = itr.first; if (m_conditionVisited.contains(ky)) { if (!conditions.contains(ky)) { throw_key_error("Unknown condition '" + ky + "'", ky, itr.second); } for (const auto &jtr : itr.second.as_table()) { if (!m_visited.contains(jtr.first)) { throw_key_error("Unknown key '" + jtr.first + "'", jtr.first, jtr.second); } } } else if (!m_visited.contains(ky)) { if (itr.second.is_table()) { for (const auto &jtr : itr.second.as_table()) { if (!m_visited.contains(jtr.first)) { throw_key_error("Unknown key '" + jtr.first + "'", jtr.first, jtr.second); } } } throw_key_error("Unknown key '" + ky + "'", ky, itr.second); } else if (ky == "condition") { std::string condition = itr.second.as_string(); if (!conditions.contains(condition)) { throw_key_error("Unknown condition '" + condition + "'", condition, itr.second); } } } } }; class TomlCheckerRoot { const TomlBasicValue &m_root; std::deque m_checkers; tsl::ordered_set m_visisted; bool m_checked = false; public: explicit TomlCheckerRoot(const TomlBasicValue &root) : m_root(root) { } TomlCheckerRoot(const TomlCheckerRoot &) = delete; TomlCheckerRoot(TomlCheckerRoot &&) = delete; bool contains(const toml::key &ky) { m_visisted.emplace(ky); return m_root.contains(ky); } TomlChecker &create(const TomlBasicValue &v) { m_checkers.emplace_back(v); return m_checkers.back(); } TomlChecker &create(const TomlBasicValue &v, const toml::key &ky) { m_checkers.emplace_back(v, ky); return m_checkers.back(); } void check(const tsl::ordered_map &conditions, bool check_root) { if (check_root) { for (const auto &itr : m_root.as_table()) { if (!m_visisted.contains(itr.first)) { throw_key_error("Unknown key '" + itr.first + "'", itr.first, itr.second); } } } for (const auto &checker : m_checkers) { checker.check(conditions); } } }; Project::Project(const Project *parent, const std::string &path, bool build) : parent(parent) { const auto toml_path = fs::path(path) / "cmake.toml"; if (!fs::exists(toml_path)) { throw std::runtime_error("File not found '" + toml_path.string() + "'"); } const auto toml = toml::parse(toml_path.string()); if (toml.size() == 0) { throw std::runtime_error("Empty TOML '" + toml_path.string() + "'"); } TomlCheckerRoot checker(toml); if (checker.contains("cmake")) { auto &cmake = checker.create(toml, "cmake"); cmake.required("version", cmake_version); if (cmake.contains("bin-dir")) { throw_key_error("bin-dir has been renamed to build-dir", "bin-dir", cmake.find("bin-dir")); } cmake.optional("build-dir", build_dir); cmake.optional("generator", generator); cmake.optional("config", config); cmake.optional("arguments", gen_args); cmake.optional("allow-in-tree", allow_in_tree); if (cmake.contains("cmkr-include")) { const auto &cmkr_include_kv = cmake.find("cmkr-include"); if (cmkr_include_kv.is_string()) { cmkr_include = cmkr_include_kv.as_string(); } else { // Allow disabling this feature with cmkr-include = false cmkr_include = ""; } } cmake.optional("cpp-flags", cppflags); cmake.optional("c-flags", cflags); cmake.optional("link-flags", linkflags); } // Skip the rest of the parsing when building if (build) { checker.check(conditions, false); return; } // Reasonable default conditions (you can override these if you desire) if (parent == nullptr) { conditions["windows"] = R"cmake(WIN32)cmake"; conditions["macos"] = R"cmake(CMAKE_SYSTEM_NAME MATCHES "Darwin")cmake"; conditions["unix"] = R"cmake(UNIX)cmake"; conditions["bsd"] = R"cmake(CMAKE_SYSTEM_NAME MATCHES "BSD")cmake"; conditions["linux"] = conditions["lunix"] = R"cmake(CMAKE_SYSTEM_NAME MATCHES "Linux")cmake"; conditions["gcc"] = R"cmake(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID STREQUAL "GNU")cmake"; conditions["msvc"] = R"cmake(MSVC)cmake"; conditions["clang"] = R"cmake((CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT CMAKE_CXX_COMPILER_FRONTEND_VARIANT MATCHES "^MSVC$") OR (CMAKE_C_COMPILER_ID MATCHES "Clang" AND NOT CMAKE_C_COMPILER_FRONTEND_VARIANT MATCHES "^MSVC$"))cmake"; conditions["clang-cl"] = R"cmake((CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT MATCHES "^MSVC$") OR (CMAKE_C_COMPILER_ID MATCHES "Clang" AND CMAKE_C_COMPILER_FRONTEND_VARIANT MATCHES "^MSVC$"))cmake"; conditions["clang-any"] = R"cmake(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_C_COMPILER_ID MATCHES "Clang")cmake"; conditions["root"] = R"cmake(CMKR_ROOT_PROJECT)cmake"; conditions["x64"] = R"cmake(CMAKE_SIZEOF_VOID_P EQUAL 8)cmake"; conditions["x32"] = R"cmake(CMAKE_SIZEOF_VOID_P EQUAL 4)cmake"; } else { conditions = parent->conditions; templates = parent->templates; } if (checker.contains("conditions")) { auto conds = toml::find(toml, "conditions"); for (const auto &cond : conds) { conditions[cond.first] = cond.second; } } if (checker.contains("project")) { auto &project = checker.create(toml, "project"); project.required("name", project_name); project.optional("version", project_version); project.optional("description", project_description); project.optional("languages", project_languages); project.optional("allow-unknown-languages", project_allow_unknown_languages); project.optional("cmake-before", cmake_before); project.optional("cmake-after", cmake_after); project.optional("include-before", include_before); project.optional("include-after", include_after); project.optional("subdirs", project_subdirs); std::string msvc_runtime; project.optional("msvc-runtime", msvc_runtime); if (!msvc_runtime.empty()) { project_msvc_runtime = parse_msvcRuntimeType(msvc_runtime); if (project_msvc_runtime == msvc_last) { std::string error = "Unknown runtime '" + msvc_runtime + "'\n"; error += "Available types:\n"; for (std::string type_name : msvcRuntimeTypeNames) { error += " - " + type_name + "\n"; } error.pop_back(); // Remove last newline throw_key_error(error, msvc_runtime, project.find("msvc-runtime")); } } } if (checker.contains("subdir")) { const auto &subs = toml::find(toml, "subdir").as_table(); for (const auto &itr : subs) { Subdir subdir; subdir.name = itr.first; auto &sub = checker.create(itr.second); sub.optional("condition", subdir.condition); sub.optional("cmake-before", subdir.cmake_before); sub.optional("cmake-after", subdir.cmake_after); sub.optional("include-before", subdir.include_before); sub.optional("include-after", subdir.include_after); subdirs.push_back(subdir); } } if (checker.contains("variables")) { using set_map = tsl::ordered_map; auto vars = toml::find(toml, "variables"); if (checker.contains("settings")) { print_key_warning("[settings] has been renamed to [variables]", "settings", toml.at("settings")); const auto &sets = toml::find(toml, "settings"); for (const auto &itr : sets) { if (!vars.insert(itr).second) { throw_key_error("Key '" + itr.first + "' shadows existing variable", itr.first, itr.second); } } } for (const auto &itr : vars) { Variable s; s.name = itr.first; const auto &value = itr.second; if (value.is_boolean()) { s.value = value.as_boolean(); } else if (value.is_string()) { s.value = value.as_string(); } else { auto &setting = checker.create(value); setting.optional("help", s.help); if (setting.contains("value")) { const auto &v = setting.find("value"); if (v.is_boolean()) { s.value = v.as_boolean(); } else { s.value = v.as_string(); } } setting.optional("cache", s.cache); setting.optional("force", s.force); } variables.push_back(s); } } if (checker.contains("options")) { using opts_map = tsl::ordered_map; const auto &opts = toml::find(toml, "options"); for (const auto &itr : opts) { Option o; o.name = itr.first; const auto &value = itr.second; if (value.is_boolean()) { o.value = value.as_boolean(); } else if (value.is_string()) { auto str = std::string(value.as_string()); if (str == "root") { o.value = std::string("${CMKR_ROOT_PROJECT}"); } else { throw_key_error("Unsupported option value '" + str + "'", str, value); } } else if (value.is_table()) { auto &option = checker.create(value); option.optional("help", o.help); if (option.contains("value")) { const auto &ovalue = option.find("value"); if (ovalue.is_boolean()) { o.value = ovalue.as_boolean(); } else if (ovalue.is_string()) { auto str = std::string(ovalue.as_string()); if (str == "root") { o.value = std::string("${CMKR_ROOT_PROJECT}"); } else { throw_key_error("Unsupported option value '" + str + "'", str, value); } } else { throw_key_error(toml::concat_to_string("Unsupported value type: ", ovalue.type()), "value", value); } } } else { throw_key_error(toml::concat_to_string("Unsupported value type: ", itr.second.type()), itr.first, itr.second); } options.push_back(o); conditions.emplace(o.name, o.name); } } if (checker.contains("find-package")) { using pkg_map = tsl::ordered_map; const auto &pkgs = toml::find(toml, "find-package"); for (const auto &itr : pkgs) { Package p; p.name = itr.first; const auto &value = itr.second; if (itr.second.is_string()) { p.version = itr.second.as_string(); } else { auto &pkg = checker.create(value); pkg.optional("condition", p.condition); pkg.optional("version", p.version); pkg.optional("required", p.required); pkg.optional("config", p.config); pkg.optional("components", p.components); } packages.push_back(p); } } if (checker.contains("fetch-content")) { const auto &fc = toml::find(toml, "fetch-content").as_table(); for (const auto &itr : fc) { Content content; content.name = itr.first; auto &c = checker.create(itr.second); c.optional("condition", content.condition); c.optional("cmake-before", content.cmake_before); c.optional("cmake-after", content.cmake_after); c.optional("include-before", content.include_before); c.optional("include-after", content.include_after); c.optional("system", content.system); // Check if the minimum version requirement is satisfied (CMake 3.25) if (c.contains("system") && !this->cmake_minimum_version(3, 25)) { throw_key_error("The system argument is only supported on CMake version 3.25 and above.\nSet the CMake version in cmake.toml:\n" "[cmake]\n" "version = \"3.25\"\n", "system", ""); } for (const auto &argItr : itr.second.as_table()) { std::string value; if (argItr.second.is_array()) { for (const auto &list_val : argItr.second.as_array()) { if (!value.empty()) { value += ';'; } value += list_val.as_string(); } } else if (argItr.second.is_boolean()) { value = argItr.second.as_boolean() ? "ON" : "OFF"; } else { value = argItr.second.as_string(); } auto is_cmake_arg = [](const std::string &s) { for (auto c : s) { if (!(std::isdigit(c) || std::isupper(c) || c == '_')) { return false; } } return true; }; // https://cmake.org/cmake/help/latest/command/string.html#supported-hash-algorithms tsl::ordered_set hash_algorithms = { "md5", "sha1", "sha224", "sha256", "sha384", "sha512", "sha3_224", "sha3_256", "sha3_384", "sha3_512", }; auto key = argItr.first; if (key == "git") { key = "GIT_REPOSITORY"; } else if (key == "tag") { key = "GIT_TAG"; } else if (key == "shallow") { key = "GIT_SHALLOW"; } else if (key == "svn") { key = "SVN_REPOSITORY"; } else if (key == "rev") { key = "SVN_REVISION"; } else if (key == "url") { key = "URL"; } else if (hash_algorithms.contains(key)) { std::string algo; for (auto c : key) { algo.push_back(std::toupper(c)); } key = "URL_HASH"; value = algo + "=" + value; } else if (key == "hash") { key = "URL_HASH"; } else if (is_cmake_arg(key)) { // allow passthrough of ExternalProject options } else if (!c.visisted(key)) { throw_key_error("Unknown key '" + argItr.first + "'", argItr.first, argItr.second); } // Make sure not to emit keys like "condition" in the FetchContent call if (!c.visisted(key)) { content.arguments.emplace(key, value); } c.visit(argItr.first); } contents.emplace_back(std::move(content)); } } if (checker.contains("bin")) { throw_key_error("[[bin]] has been renamed to [target.]", "", toml.at("bin")); } auto parse_target = [&](const std::string &name, TomlChecker &t, bool isTemplate) { Target target; target.name = name; t.required("type", target.type_name); target.type = parse_targetType(target.type_name); // Users cannot set this target type if (target.type == target_template) { target.type = target_last; } if (!isTemplate && target.type == target_last) { for (const auto &tmplate : templates) { if (target.type_name == tmplate.outline.name) { target.type = target_template; break; } } } if (target.type == target_last) { std::string error = "Unknown target type '" + target.type_name + "'\n"; error += "Available types:\n"; for (std::string type_name : targetTypeNames) { if (type_name != "template") { error += " - " + type_name + "\n"; } } if (!isTemplate && !templates.empty()) { error += "Available templates:\n"; for (const auto &tmplate : templates) { error += " - " + tmplate.outline.name + "\n"; } } error.pop_back(); // Remove last newline throw_key_error(error, target.type_name, t.find("type")); } t.optional("sources", target.sources); // Merge the headers into the sources ConditionVector headers; t.optional("headers", headers); for (const auto &itr : headers) { auto &dest = target.sources[itr.first]; for (const auto &jtr : itr.second) { dest.push_back(jtr); } } t.optional("compile-definitions", target.compile_definitions); t.optional("private-compile-definitions", target.private_compile_definitions); t.optional("compile-features", target.compile_features); t.optional("private-compile-features", target.private_compile_features); t.optional("compile-options", target.compile_options); t.optional("private-compile-options", target.private_compile_options); t.optional("include-directories", target.include_directories); t.optional("private-include-directories", target.private_include_directories); t.optional("link-directories", target.link_directories); t.optional("private-link-directories", target.private_link_directories); t.optional("link-libraries", target.link_libraries); t.optional("private-link-libraries", target.private_link_libraries); t.optional("link-options", target.link_options); t.optional("private-link-options", target.private_link_options); t.optional("precompile-headers", target.precompile_headers); t.optional("private-precompile-headers", target.private_precompile_headers); Condition msvc_runtime; t.optional("msvc-runtime", msvc_runtime); for (const auto &condItr : msvc_runtime) { switch (parse_msvcRuntimeType(condItr.second)) { case msvc_dynamic: target.properties[condItr.first]["MSVC_RUNTIME_LIBRARY"] = "MultiThreaded$<$:Debug>DLL"; break; case msvc_static: target.properties[condItr.first]["MSVC_RUNTIME_LIBRARY"] = "MultiThreaded$<$:Debug>"; break; default: { std::string error = "Unknown runtime '" + condItr.second + "'\n"; error += "Available types:\n"; for (std::string type_name : msvcRuntimeTypeNames) { error += " - " + type_name + "\n"; } error.pop_back(); // Remove last newline const TomlBasicValue *report; if (condItr.first.empty()) { report = &t.find("msvc-runtime"); } else { report = &t.find(condItr.first).as_table().find("msvc-runtime").value(); } throw_key_error(error, condItr.second, *report); } } } t.optional("condition", target.condition); t.optional("alias", target.alias); if (t.contains("properties")) { auto store_property = [&target](const toml::key &k, const TomlBasicValue &v, const std::string &condition) { if (v.is_array()) { std::string property_list; for (const auto &list_val : v.as_array()) { if (!property_list.empty()) { property_list += ';'; } property_list += list_val.as_string(); } target.properties[condition][k] = property_list; } else if (v.is_boolean()) { target.properties[condition][k] = v.as_boolean() ? "ON" : "OFF"; } else { target.properties[condition][k] = v.as_string(); } }; const auto &props = t.find("properties").as_table(); for (const auto &propKv : props) { const auto &k = propKv.first; const auto &v = propKv.second; if (v.is_table()) { for (const auto &condKv : v.as_table()) { store_property(condKv.first, condKv.second, k); } } else { store_property(k, v, ""); } } } t.optional("cmake-before", target.cmake_before); t.optional("cmake-after", target.cmake_after); t.optional("include-before", target.include_before); t.optional("include-after", target.include_after); return target; }; if (checker.contains("template")) { const auto &ts = toml::find(toml, "template").as_table(); for (const auto &itr : ts) { auto &t = checker.create(itr.second); const auto &name = itr.first; for (const auto &type_name : targetTypeNames) { if (name == type_name) { throw_key_error("Reserved template name '" + name + "'", name, itr.second); } } for (const auto &tmplate : templates) { if (name == tmplate.outline.name) { throw_key_error("Template '" + name + "' already defined", name, itr.second); } } Template tmplate; tmplate.outline = parse_target(name, t, true); t.optional("add-function", tmplate.add_function); t.optional("pass-sources-to-add-function", tmplate.pass_sources_to_add_function); t.optional("pass-sources", tmplate.pass_sources_to_add_function); templates.push_back(tmplate); } } if (checker.contains("target")) { const auto &ts = toml::find(toml, "target").as_table(); for (const auto &itr : ts) { auto &t = checker.create(itr.second); targets.push_back(parse_target(itr.first, t, false)); } } if (checker.contains("test")) { const auto &ts = toml::find(toml, "test").as_array(); for (const auto &value : ts) { auto &t = checker.create(value); Test test; t.required("name", test.name); t.optional("condition", test.condition); t.optional("configurations", test.configurations); t.optional("working-directory", test.working_directory); t.required("command", test.command); t.optional("arguments", test.arguments); tests.push_back(test); } } if (checker.contains("install")) { const auto &is = toml::find(toml, "install").as_array(); for (const auto &value : is) { auto &i = checker.create(value); Install inst; i.optional("condition", inst.condition); i.optional("targets", inst.targets); i.optional("files", inst.files); i.optional("dirs", inst.dirs); i.optional("configs", inst.configs); i.required("destination", inst.destination); i.optional("component", inst.component); i.optional("optional", inst.optional); installs.push_back(inst); } } if (checker.contains("vcpkg")) { auto &v = checker.create(toml, "vcpkg"); v.optional("url", vcpkg.url); v.optional("version", vcpkg.version); for (const auto &p : v.find("packages").as_array()) { Vcpkg::Package package; const auto &package_str = p.as_string().str; const auto open_bracket = package_str.find('['); const auto close_bracket = package_str.find(']', open_bracket); if (open_bracket == std::string::npos && close_bracket == std::string::npos) { package.name = package_str; } else if (close_bracket != std::string::npos) { package.name = package_str.substr(0, open_bracket); const auto features = package_str.substr(open_bracket + 1, close_bracket - open_bracket - 1); std::istringstream feature_stream{features}; std::string feature; while (std::getline(feature_stream, feature, ',')) { package.features.emplace_back(feature); } } else { throw_key_error("Invalid package name '" + package_str + "'", "packages", p); } vcpkg.packages.emplace_back(std::move(package)); } } checker.check(conditions, true); } const Project *Project::root() const { auto root = this; while (root->parent != nullptr) root = root->parent; return root; } bool Project::cmake_minimum_version(int major, int minor) const { // NOTE: this code is like pulling teeth, sorry auto root_version = root()->cmake_version; auto range_index = root_version.find("..."); if (range_index != std::string::npos) { root_version.resize(range_index); } auto period_index = root_version.find('.'); auto root_major = atoi(root_version.substr(0, period_index).c_str()); int root_minor = 0; if (period_index != std::string::npos) { auto end_index = root_version.find('.', period_index + 1); root_minor = atoi(root_version.substr(period_index + 1, end_index).c_str()); } return std::tie(root_major, root_minor) >= std::tie(major, minor); } bool is_root_path(const std::string &path) { const auto toml_path = fs::path(path) / "cmake.toml"; if (!fs::exists(toml_path)) { return false; } const auto toml = toml::parse(toml_path.string()); return toml.contains("project"); } } // namespace parser } // namespace cmkr