From ff7e4b8f23e5079996a94c617dc40aea95a80f1e Mon Sep 17 00:00:00 2001 From: Duncan Ogilvie Date: Mon, 15 Apr 2024 02:12:02 +0200 Subject: [PATCH 1/2] Allow arbitrary CMake expressions as conditions --- docs/cmake-toml.md | 20 +++++++----- include/project_parser.hpp | 1 + src/cmake_generator.cpp | 28 +++++++++++----- src/project_parser.cpp | 64 ++++++++++++++++++++++++++++++------- tests/conditions/cmake.toml | 4 +++ 5 files changed, 90 insertions(+), 27 deletions(-) diff --git a/docs/cmake-toml.md b/docs/cmake-toml.md index 5967302..5f719bf 100644 --- a/docs/cmake-toml.md +++ b/docs/cmake-toml.md @@ -67,15 +67,17 @@ _Note_: It is generally discouraged to disable the `C` language, unless you are ## Conditions -You can specify your own conditions and use them in any `condition` field: +You can specify your own named conditions and use them in any `condition` field: ```toml [conditions] -arch64 = "CMAKE_SIZEOF_VOID_P EQUAL 8" -arch32 = "CMAKE_SIZEOF_VOID_P EQUAL 4" +ptr64 = "CMAKE_SIZEOF_VOID_P EQUAL 8" +ptr32 = "CMAKE_SIZEOF_VOID_P EQUAL 4" ``` -This will make the `arch64` and `arch32` conditions available with their respective CMake expressions. +This will make the `ptr64` and `ptr32` conditions available with their respective CMake expressions. + +**Note**: condition names can only contain lower-case alphanumeric characters (`[0-9a-z]`) and dashes (`-`). You can also prefix most keys with `condition.` to represent a conditional: @@ -83,9 +85,11 @@ You can also prefix most keys with `condition.` to represent a conditional: [target] type = "executable" sources = ["src/main.cpp"] -windows.sources = ["src/windows_specific.cpp"] +ptr64.sources = ["src/ptr64_only.cpp"] ``` +Instead of a named condition you can also specify a [CMake expression](https://cmake.org/cmake/help/latest/command/if.html#condition-syntax) directly. + ### Predefined conditions The following conditions are predefined (you can override them if you desire): @@ -131,7 +135,7 @@ MYPROJECT_SPECIAL_OPTION = { value = true, help = "Docstring for this option." } MYPROJECT_BUILD_EXAMPLES = "root" ``` -Options correspond to [CMake cache variables](https://cmake.org/cmake/help/book/mastering-cmake/chapter/CMake%20Cache.html) that can be used to customize your project at configure-time. You can configure with `cmake -DMYPROJECT_BUILD_TESTS=ON` to enable the option. Every option automatically gets a corresponding [condition](#conditions). +Options correspond to [CMake cache variables](https://cmake.org/cmake/help/book/mastering-cmake/chapter/CMake%20Cache.html) that can be used to customize your project at configure-time. You can configure with `cmake -DMYPROJECT_BUILD_TESTS=ON` to enable the option. Every option automatically gets a corresponding [condition](#conditions). Additionally, a normalized condition is created based on the `[project].name` (i.e. `MYPROJECT_BUILD_TESTS` becomes `build-tests`). The special value `root` can be used to set the option to `true` if the project is compiled as the root project (it will be `false` if someone is including your project via `[fetch-content]` or `[subdir]`). @@ -149,8 +153,8 @@ Variables emit a [`set`](https://cmake.org/cmake/help/latest/command/set.html) a ```toml [vcpkg] -version = "2021.05.12" -url = "https://github.com/microsoft/vcpkg/archive/refs/tags/2021.05.12.tar.gz" +version = "2024.03.25" +url = "https://github.com/microsoft/vcpkg/archive/refs/tags/2024.03.25.tar.gz" packages = ["fmt", "zlib"] ``` diff --git a/include/project_parser.hpp b/include/project_parser.hpp index 6ffcc4f..c49eaaa 100644 --- a/include/project_parser.hpp +++ b/include/project_parser.hpp @@ -208,6 +208,7 @@ struct Project { Project(const Project *parent, const std::string &path, bool build); const Project *root() const; bool cmake_minimum_version(int major, int minor) const; + static bool is_condition_name(const std::string &name); }; bool is_root_path(const std::string &path); diff --git a/src/cmake_generator.cpp b/src/cmake_generator.cpp index ae81f9c..e42c4df 100644 --- a/src/cmake_generator.cpp +++ b/src/cmake_generator.cpp @@ -514,15 +514,13 @@ struct Generator { if (!value.empty()) { for (const auto &itr : value) { const auto &condition = itr.first; - if (!condition.empty()) { - cmd("if", condition)(RawArg(project.conditions.at(condition))); - } + auto endif = if_condition(condition); if (!itr.second.empty()) { fn(condition, itr.second); } - if (!condition.empty()) { + if (endif) { cmd("endif")().endl(); } else if (!itr.second.empty()) { endl(); @@ -538,6 +536,23 @@ struct Generator { void conditional_cmake(const parser::Condition &cmake) { handle_condition(cmake, [this](const std::string &, const std::string &cmake) { inject_cmake(cmake); }); } + + bool if_condition(const std::string &condition) { + if (condition.empty()) { + return false; + } + auto found = project.conditions.find(condition); + if (found == project.conditions.end()) { + if (cmkr::parser::Project::is_condition_name(condition)) { + // NOTE: this should have been caught by the parser already + throw std::runtime_error("Condition '" + condition + "' is not defined"); + } + cmd("if", "NOTE: unnamed condition")(RawArg(condition)); + } else { + cmd("if", condition)(RawArg(found->second)); + } + return true; + } }; struct ConditionScope { @@ -545,10 +560,7 @@ struct ConditionScope { bool endif = false; ConditionScope(Generator &gen, const std::string &condition) : gen(gen) { - if (!condition.empty()) { - gen.cmd("if", condition)(RawArg(gen.project.conditions.at(condition))); - endif = true; - } + endif = gen.if_condition(condition); } ConditionScope(const ConditionScope &) = delete; diff --git a/src/project_parser.cpp b/src/project_parser.cpp index 050aec6..4f14515 100644 --- a/src/project_parser.cpp +++ b/src/project_parser.cpp @@ -140,7 +140,7 @@ class TomlChecker { for (const auto &itr : m_v.as_table()) { const auto &ky = itr.first; if (m_conditionVisited.contains(ky)) { - if (!conditions.contains(ky)) { + if (!conditions.contains(ky) && Project::is_condition_name(ky)) { throw_key_error("Unknown condition '" + ky + "'", ky, itr.second); } @@ -160,7 +160,7 @@ class TomlChecker { throw_key_error("Unknown key '" + ky + "'", ky, itr.second); } else if (ky == "condition") { std::string condition = itr.second.as_string(); - if (!conditions.contains(condition)) { + if (!conditions.contains(condition) && Project::is_condition_name(condition)) { throw_key_error("Unknown condition '" + condition + "'", condition, itr.second); } } @@ -282,6 +282,9 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p if (checker.contains("conditions")) { auto conds = toml::find(toml, "conditions"); for (const auto &cond : conds) { + if (!is_condition_name(cond.first)) { + throw_key_error("Invalid condition name '" + cond.first + "'", cond.first, toml::find(toml::find(toml, "conditions"), cond.first)); + } conditions[cond.first] = cond.second; } } @@ -372,6 +375,24 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p } if (checker.contains("options")) { + auto normalize = [](const std::string &name) { + std::string normalized; + for (char ch : name) { + if (ch == '_') { + normalized += '-'; + } else if (ch >= 'A' && ch <= 'Z') { + normalized += std::tolower(ch); + } else if (ch == '-' || (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z')) { + normalized += ch; + } else { + // Ignore all other characters + } + } + return normalized; + }; + auto nproject_prefix = normalize(project_name); + nproject_prefix += '-'; + using opts_map = tsl::ordered_map; const auto &opts = toml::find(toml, "options"); for (const auto &itr : opts) { @@ -409,7 +430,18 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p 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); + + // Add an implicit condition for the option + auto ncondition = normalize(o.name); + if (ncondition.find(nproject_prefix) == 0) { + ncondition = ncondition.substr(nproject_prefix.size()); + } + if (!ncondition.empty()) { + if (conditions.contains(ncondition)) { + print_key_warning("Option '" + o.name + "' would create a condition '" + ncondition + "' that already exists", o.name, value); + } + conditions.emplace(ncondition, o.name); + } } } @@ -639,28 +671,28 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p Condition msvc_runtime; t.optional("msvc-runtime", msvc_runtime); - for (const auto &condItr : msvc_runtime) { - switch (parse_msvcRuntimeType(condItr.second)) { + for (const auto &cond_itr : msvc_runtime) { + switch (parse_msvcRuntimeType(cond_itr.second)) { case msvc_dynamic: - target.properties[condItr.first]["MSVC_RUNTIME_LIBRARY"] = "MultiThreaded$<$:Debug>DLL"; + target.properties[cond_itr.first]["MSVC_RUNTIME_LIBRARY"] = "MultiThreaded$<$:Debug>DLL"; break; case msvc_static: - target.properties[condItr.first]["MSVC_RUNTIME_LIBRARY"] = "MultiThreaded$<$:Debug>"; + target.properties[cond_itr.first]["MSVC_RUNTIME_LIBRARY"] = "MultiThreaded$<$:Debug>"; break; default: { - std::string error = "Unknown runtime '" + condItr.second + "'\n"; + std::string error = "Unknown runtime '" + cond_itr.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()) { + if (cond_itr.first.empty()) { report = &t.find("msvc-runtime"); } else { - report = &t.find(condItr.first).as_table().find("msvc-runtime").value(); + report = &t.find(cond_itr.first).as_table().find("msvc-runtime").value(); } - throw_key_error(error, condItr.second, *report); + throw_key_error(error, cond_itr.second, *report); } } } @@ -833,6 +865,16 @@ bool Project::cmake_minimum_version(int major, int minor) const { return std::tie(root_major, root_minor) >= std::tie(major, minor); } +bool Project::is_condition_name(const std::string &name) { + auto is_named_condition = true; + for (auto ch : name) { + if (!(ch == '-' || (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z'))) { + return false; + } + } + return true; +} + bool is_root_path(const std::string &path) { const auto toml_path = fs::path(path) / "cmake.toml"; if (!fs::exists(toml_path)) { diff --git a/tests/conditions/cmake.toml b/tests/conditions/cmake.toml index 2ceb54c..0216029 100644 --- a/tests/conditions/cmake.toml +++ b/tests/conditions/cmake.toml @@ -2,6 +2,9 @@ name = "conditions" cmake-after = "set(CUSTOM ON)" +[options] +CONDITIONS_BUILD_TESTS = false + [conditions] custom = "CUSTOM" @@ -15,6 +18,7 @@ macos.cmake-after = "message(STATUS macos-after)" linux.cmake-after = "message(STATUS linux-after)" unix.cmake-after = "message(STATUS unix-after)" custom.cmake-after = "message(STATUS custom-after)" +build-tests.cmake-after = "message(STATUS build-tests)" [target.example.properties] AUTOMOC = false From 53820c9f65abcabaffab58d1f0c581cfa0febf53 Mon Sep 17 00:00:00 2001 From: Duncan Ogilvie Date: Mon, 15 Apr 2024 03:01:51 +0200 Subject: [PATCH 2/2] Support $ in unnamed conditions --- docs/cmake-toml.md | 2 +- src/cmake_generator.cpp | 47 ++++++++++++++++++++++++++++++++++++- tests/conditions/cmake.toml | 3 ++- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/docs/cmake-toml.md b/docs/cmake-toml.md index 5f719bf..1c32d4f 100644 --- a/docs/cmake-toml.md +++ b/docs/cmake-toml.md @@ -88,7 +88,7 @@ sources = ["src/main.cpp"] ptr64.sources = ["src/ptr64_only.cpp"] ``` -Instead of a named condition you can also specify a [CMake expression](https://cmake.org/cmake/help/latest/command/if.html#condition-syntax) directly. +Instead of a named condition you can also specify a [CMake expression](https://cmake.org/cmake/help/latest/command/if.html#condition-syntax) in quotes. Instances of `$` are replaced with the corresponding condition. For example: `"CONDITIONS_BUILD_TESTS AND $"` becomes `CONDITIONS_BUILD_TESTS AND (CMAKE_SYSTEM_NAME MATCHES "Linux")` in the final `CMakeLists.txt` file. ### Predefined conditions diff --git a/src/cmake_generator.cpp b/src/cmake_generator.cpp index e42c4df..f3ed4b0 100644 --- a/src/cmake_generator.cpp +++ b/src/cmake_generator.cpp @@ -547,12 +547,57 @@ struct Generator { // NOTE: this should have been caught by the parser already throw std::runtime_error("Condition '" + condition + "' is not defined"); } - cmd("if", "NOTE: unnamed condition")(RawArg(condition)); + cmd("if", "NOTE: unnamed condition")(RawArg(cmake_condition(condition))); } else { cmd("if", condition)(RawArg(found->second)); } return true; } + + private: + std::string cmake_condition(const std::string &condition) { + // HACK: this replaces '$' with the value of the 'name' condition. We can safely + // reuse the generator expression syntax, because it is not valid in CMake conditions. + // TODO: properly handle quoted arguments (using a simple state machine): + // https://cmake.org/cmake/help/latest/manual/cmake-language.7.html#quoted-argument + std::string result = ""; + bool in_replacement = false; + std::string temp; + for (size_t i = 0; i < condition.length(); i++) { + if (in_replacement) { + if (condition[i] == '>') { + in_replacement = false; + if (temp.empty()) { + throw std::runtime_error("Empty replacement in condition '" + condition + "'"); + } + auto found = project.conditions.find(temp); + if (found == project.conditions.end()) { + throw std::runtime_error("Unknown condition '" + temp + "' in replacement"); + } + auto has_space = found->second.find(' ') != std::string::npos; + if (has_space) { + result += '('; + } + result += found->second; + if (has_space) { + result += ')'; + } + temp.clear(); + } else { + temp += condition[i]; + } + } else if (condition[i] == '$' && i + 1 < condition.length() && condition[i + 1] == '<') { + i++; + in_replacement = true; + } else { + result += condition[i]; + } + } + if (!temp.empty()) { + throw std::runtime_error("Unterminated replacement in condition '" + condition + "'"); + } + return result; + } }; struct ConditionScope { diff --git a/tests/conditions/cmake.toml b/tests/conditions/cmake.toml index 0216029..3913d5f 100644 --- a/tests/conditions/cmake.toml +++ b/tests/conditions/cmake.toml @@ -3,7 +3,7 @@ name = "conditions" cmake-after = "set(CUSTOM ON)" [options] -CONDITIONS_BUILD_TESTS = false +CONDITIONS_BUILD_TESTS = "root" [conditions] custom = "CUSTOM" @@ -19,6 +19,7 @@ linux.cmake-after = "message(STATUS linux-after)" unix.cmake-after = "message(STATUS unix-after)" custom.cmake-after = "message(STATUS custom-after)" build-tests.cmake-after = "message(STATUS build-tests)" +"CONDITIONS_BUILD_TESTS AND $".cmake-after = "message(STATUS linux-tests)" [target.example.properties] AUTOMOC = false