Skip to content

Commit ff7e4b8

Browse files
committed
Allow arbitrary CMake expressions as conditions
1 parent 1072cb4 commit ff7e4b8

File tree

5 files changed

+90
-27
lines changed

5 files changed

+90
-27
lines changed

docs/cmake-toml.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,25 +67,29 @@ _Note_: It is generally discouraged to disable the `C` language, unless you are
6767

6868
## Conditions
6969

70-
You can specify your own conditions and use them in any `condition` field:
70+
You can specify your own named conditions and use them in any `condition` field:
7171

7272
```toml
7373
[conditions]
74-
arch64 = "CMAKE_SIZEOF_VOID_P EQUAL 8"
75-
arch32 = "CMAKE_SIZEOF_VOID_P EQUAL 4"
74+
ptr64 = "CMAKE_SIZEOF_VOID_P EQUAL 8"
75+
ptr32 = "CMAKE_SIZEOF_VOID_P EQUAL 4"
7676
```
7777

78-
This will make the `arch64` and `arch32` conditions available with their respective CMake expressions.
78+
This will make the `ptr64` and `ptr32` conditions available with their respective CMake expressions.
79+
80+
**Note**: condition names can only contain lower-case alphanumeric characters (`[0-9a-z]`) and dashes (`-`).
7981

8082
You can also prefix most keys with `condition.` to represent a conditional:
8183

8284
```toml
8385
[target]
8486
type = "executable"
8587
sources = ["src/main.cpp"]
86-
windows.sources = ["src/windows_specific.cpp"]
88+
ptr64.sources = ["src/ptr64_only.cpp"]
8789
```
8890

91+
Instead of a named condition you can also specify a [CMake expression](https://cmake.org/cmake/help/latest/command/if.html#condition-syntax) directly.
92+
8993
### Predefined conditions
9094

9195
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." }
131135
MYPROJECT_BUILD_EXAMPLES = "root"
132136
```
133137

134-
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).
138+
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`).
135139

136140
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]`).
137141

@@ -149,8 +153,8 @@ Variables emit a [`set`](https://cmake.org/cmake/help/latest/command/set.html) a
149153

150154
```toml
151155
[vcpkg]
152-
version = "2021.05.12"
153-
url = "https://github.com/microsoft/vcpkg/archive/refs/tags/2021.05.12.tar.gz"
156+
version = "2024.03.25"
157+
url = "https://github.com/microsoft/vcpkg/archive/refs/tags/2024.03.25.tar.gz"
154158
packages = ["fmt", "zlib"]
155159
```
156160

include/project_parser.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ struct Project {
208208
Project(const Project *parent, const std::string &path, bool build);
209209
const Project *root() const;
210210
bool cmake_minimum_version(int major, int minor) const;
211+
static bool is_condition_name(const std::string &name);
211212
};
212213

213214
bool is_root_path(const std::string &path);

src/cmake_generator.cpp

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -514,15 +514,13 @@ struct Generator {
514514
if (!value.empty()) {
515515
for (const auto &itr : value) {
516516
const auto &condition = itr.first;
517-
if (!condition.empty()) {
518-
cmd("if", condition)(RawArg(project.conditions.at(condition)));
519-
}
517+
auto endif = if_condition(condition);
520518

521519
if (!itr.second.empty()) {
522520
fn(condition, itr.second);
523521
}
524522

525-
if (!condition.empty()) {
523+
if (endif) {
526524
cmd("endif")().endl();
527525
} else if (!itr.second.empty()) {
528526
endl();
@@ -538,17 +536,31 @@ struct Generator {
538536
void conditional_cmake(const parser::Condition<std::string> &cmake) {
539537
handle_condition(cmake, [this](const std::string &, const std::string &cmake) { inject_cmake(cmake); });
540538
}
539+
540+
bool if_condition(const std::string &condition) {
541+
if (condition.empty()) {
542+
return false;
543+
}
544+
auto found = project.conditions.find(condition);
545+
if (found == project.conditions.end()) {
546+
if (cmkr::parser::Project::is_condition_name(condition)) {
547+
// NOTE: this should have been caught by the parser already
548+
throw std::runtime_error("Condition '" + condition + "' is not defined");
549+
}
550+
cmd("if", "NOTE: unnamed condition")(RawArg(condition));
551+
} else {
552+
cmd("if", condition)(RawArg(found->second));
553+
}
554+
return true;
555+
}
541556
};
542557

543558
struct ConditionScope {
544559
Generator &gen;
545560
bool endif = false;
546561

547562
ConditionScope(Generator &gen, const std::string &condition) : gen(gen) {
548-
if (!condition.empty()) {
549-
gen.cmd("if", condition)(RawArg(gen.project.conditions.at(condition)));
550-
endif = true;
551-
}
563+
endif = gen.if_condition(condition);
552564
}
553565

554566
ConditionScope(const ConditionScope &) = delete;

src/project_parser.cpp

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ class TomlChecker {
140140
for (const auto &itr : m_v.as_table()) {
141141
const auto &ky = itr.first;
142142
if (m_conditionVisited.contains(ky)) {
143-
if (!conditions.contains(ky)) {
143+
if (!conditions.contains(ky) && Project::is_condition_name(ky)) {
144144
throw_key_error("Unknown condition '" + ky + "'", ky, itr.second);
145145
}
146146

@@ -160,7 +160,7 @@ class TomlChecker {
160160
throw_key_error("Unknown key '" + ky + "'", ky, itr.second);
161161
} else if (ky == "condition") {
162162
std::string condition = itr.second.as_string();
163-
if (!conditions.contains(condition)) {
163+
if (!conditions.contains(condition) && Project::is_condition_name(condition)) {
164164
throw_key_error("Unknown condition '" + condition + "'", condition, itr.second);
165165
}
166166
}
@@ -282,6 +282,9 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p
282282
if (checker.contains("conditions")) {
283283
auto conds = toml::find<decltype(conditions)>(toml, "conditions");
284284
for (const auto &cond : conds) {
285+
if (!is_condition_name(cond.first)) {
286+
throw_key_error("Invalid condition name '" + cond.first + "'", cond.first, toml::find(toml::find(toml, "conditions"), cond.first));
287+
}
285288
conditions[cond.first] = cond.second;
286289
}
287290
}
@@ -372,6 +375,24 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p
372375
}
373376

374377
if (checker.contains("options")) {
378+
auto normalize = [](const std::string &name) {
379+
std::string normalized;
380+
for (char ch : name) {
381+
if (ch == '_') {
382+
normalized += '-';
383+
} else if (ch >= 'A' && ch <= 'Z') {
384+
normalized += std::tolower(ch);
385+
} else if (ch == '-' || (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z')) {
386+
normalized += ch;
387+
} else {
388+
// Ignore all other characters
389+
}
390+
}
391+
return normalized;
392+
};
393+
auto nproject_prefix = normalize(project_name);
394+
nproject_prefix += '-';
395+
375396
using opts_map = tsl::ordered_map<std::string, TomlBasicValue>;
376397
const auto &opts = toml::find<opts_map>(toml, "options");
377398
for (const auto &itr : opts) {
@@ -409,7 +430,18 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p
409430
throw_key_error(toml::concat_to_string("Unsupported value type: ", itr.second.type()), itr.first, itr.second);
410431
}
411432
options.push_back(o);
412-
conditions.emplace(o.name, o.name);
433+
434+
// Add an implicit condition for the option
435+
auto ncondition = normalize(o.name);
436+
if (ncondition.find(nproject_prefix) == 0) {
437+
ncondition = ncondition.substr(nproject_prefix.size());
438+
}
439+
if (!ncondition.empty()) {
440+
if (conditions.contains(ncondition)) {
441+
print_key_warning("Option '" + o.name + "' would create a condition '" + ncondition + "' that already exists", o.name, value);
442+
}
443+
conditions.emplace(ncondition, o.name);
444+
}
413445
}
414446
}
415447

@@ -639,28 +671,28 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p
639671

640672
Condition<std::string> msvc_runtime;
641673
t.optional("msvc-runtime", msvc_runtime);
642-
for (const auto &condItr : msvc_runtime) {
643-
switch (parse_msvcRuntimeType(condItr.second)) {
674+
for (const auto &cond_itr : msvc_runtime) {
675+
switch (parse_msvcRuntimeType(cond_itr.second)) {
644676
case msvc_dynamic:
645-
target.properties[condItr.first]["MSVC_RUNTIME_LIBRARY"] = "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL";
677+
target.properties[cond_itr.first]["MSVC_RUNTIME_LIBRARY"] = "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL";
646678
break;
647679
case msvc_static:
648-
target.properties[condItr.first]["MSVC_RUNTIME_LIBRARY"] = "MultiThreaded$<$<CONFIG:Debug>:Debug>";
680+
target.properties[cond_itr.first]["MSVC_RUNTIME_LIBRARY"] = "MultiThreaded$<$<CONFIG:Debug>:Debug>";
649681
break;
650682
default: {
651-
std::string error = "Unknown runtime '" + condItr.second + "'\n";
683+
std::string error = "Unknown runtime '" + cond_itr.second + "'\n";
652684
error += "Available types:\n";
653685
for (std::string type_name : msvcRuntimeTypeNames) {
654686
error += " - " + type_name + "\n";
655687
}
656688
error.pop_back(); // Remove last newline
657689
const TomlBasicValue *report;
658-
if (condItr.first.empty()) {
690+
if (cond_itr.first.empty()) {
659691
report = &t.find("msvc-runtime");
660692
} else {
661-
report = &t.find(condItr.first).as_table().find("msvc-runtime").value();
693+
report = &t.find(cond_itr.first).as_table().find("msvc-runtime").value();
662694
}
663-
throw_key_error(error, condItr.second, *report);
695+
throw_key_error(error, cond_itr.second, *report);
664696
}
665697
}
666698
}
@@ -833,6 +865,16 @@ bool Project::cmake_minimum_version(int major, int minor) const {
833865
return std::tie(root_major, root_minor) >= std::tie(major, minor);
834866
}
835867

868+
bool Project::is_condition_name(const std::string &name) {
869+
auto is_named_condition = true;
870+
for (auto ch : name) {
871+
if (!(ch == '-' || (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z'))) {
872+
return false;
873+
}
874+
}
875+
return true;
876+
}
877+
836878
bool is_root_path(const std::string &path) {
837879
const auto toml_path = fs::path(path) / "cmake.toml";
838880
if (!fs::exists(toml_path)) {

tests/conditions/cmake.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
name = "conditions"
33
cmake-after = "set(CUSTOM ON)"
44

5+
[options]
6+
CONDITIONS_BUILD_TESTS = false
7+
58
[conditions]
69
custom = "CUSTOM"
710

@@ -15,6 +18,7 @@ macos.cmake-after = "message(STATUS macos-after)"
1518
linux.cmake-after = "message(STATUS linux-after)"
1619
unix.cmake-after = "message(STATUS unix-after)"
1720
custom.cmake-after = "message(STATUS custom-after)"
21+
build-tests.cmake-after = "message(STATUS build-tests)"
1822

1923
[target.example.properties]
2024
AUTOMOC = false

0 commit comments

Comments
 (0)