mirror of
https://github.com/build-cpp/cmkr
synced 2026-06-08 13:22:55 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2e376080a | |||
| 634908c651 | |||
| 33911f3f1b | |||
| e650073bef | |||
| 96e44f7771 | |||
| cb9ad5cdb3 | |||
| c785b540d3 | |||
| 275b73eefa | |||
| ea49a3acb7 | |||
| 257e14c869 | |||
| 7508816fe7 | |||
| 45678bc5bc | |||
| b869e5646f |
Generated
+1
-1
@@ -22,7 +22,7 @@ project(cmkr
|
||||
LANGUAGES
|
||||
CXX
|
||||
VERSION
|
||||
0.2.19
|
||||
0.2.22
|
||||
DESCRIPTION
|
||||
"CMakeLists generator from TOML"
|
||||
)
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ cmkr-include = false
|
||||
|
||||
[project]
|
||||
name = "cmkr"
|
||||
version = "0.2.19"
|
||||
version = "0.2.22"
|
||||
description = "CMakeLists generator from TOML"
|
||||
languages = ["CXX"]
|
||||
include-after = [
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ include_guard()
|
||||
|
||||
# Change these defaults to point to your infrastructure if desired
|
||||
set(CMKR_REPO "https://github.com/build-cpp/cmkr" CACHE STRING "cmkr git repository" FORCE)
|
||||
set(CMKR_TAG "v0.2.19" CACHE STRING "cmkr git tag (this needs to be available forever)" FORCE)
|
||||
set(CMKR_TAG "v0.2.22" CACHE STRING "cmkr git tag (this needs to be available forever)" FORCE)
|
||||
set(CMKR_COMMIT_HASH "" CACHE STRING "cmkr git commit hash (optional)" FORCE)
|
||||
|
||||
# To bootstrap/generate a cmkr project: cmake -P cmkr.cmake
|
||||
|
||||
+27
-1
@@ -100,9 +100,12 @@ include-after = ["cmake/after-subdir.cmake"]
|
||||
[options]
|
||||
MYPROJECT_BUILD_TESTS = false
|
||||
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 options 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).
|
||||
|
||||
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]`).
|
||||
|
||||
## Variables
|
||||
|
||||
@@ -209,6 +212,29 @@ CXX_STANDARD_REQUIRED = true
|
||||
FOLDER = "MyFolder"
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
||||
To avoid repeating yourself you can create your own target type and use it in your targets:
|
||||
|
||||
```toml
|
||||
[template.example]
|
||||
condition = "MYPROJECT_BUILD_EXAMPLES"
|
||||
type = "executable"
|
||||
link-libraries = ["myproject::mylib"]
|
||||
add-function = ""
|
||||
pass-sources = false
|
||||
|
||||
# Properties from the template are merged with the ones here
|
||||
[target.myexample]
|
||||
type = "example"
|
||||
sources = ["src/myexample.cpp"]
|
||||
```
|
||||
|
||||
The properties declared on a `template` are the same as the ones you use for targets. The only exceptions are:
|
||||
|
||||
- `add-function`: Specifies a custom add function. Projects like [pybind11](https://pybind11.readthedocs.io/en/stable/cmake/index.html#new-findpython-mode) have their own `add_xxx` function, which you can specify here.
|
||||
- `pass-sources`: Pass sources directly to the add function instead of using `target_sources`.
|
||||
|
||||
## Tests and installation (unfinished)
|
||||
|
||||
**Note**: The `[[test]]` and `[[install]]` are unfinished features and will likely change in a future release.
|
||||
|
||||
@@ -25,7 +25,7 @@ struct Variable {
|
||||
struct Option {
|
||||
std::string name;
|
||||
std::string help;
|
||||
bool value = false;
|
||||
mpark::variant<bool, std::string> value;
|
||||
};
|
||||
|
||||
struct Package {
|
||||
|
||||
+114
-10
@@ -119,6 +119,19 @@ static void create_file(const fs::path &path, const std::string &contents) {
|
||||
ofs << contents;
|
||||
}
|
||||
|
||||
static std::string read_file(const fs::path &path) {
|
||||
std::ifstream ifs(path, std::ios::binary);
|
||||
if (!ifs) {
|
||||
throw std::runtime_error("Failed to read " + path.string());
|
||||
}
|
||||
std::string contents;
|
||||
ifs.seekg(0, std::ios::end);
|
||||
contents.resize(ifs.tellg());
|
||||
ifs.seekg(0, std::ios::beg);
|
||||
ifs.read(&contents[0], contents.size());
|
||||
return contents;
|
||||
}
|
||||
|
||||
// CMake target name rules: https://cmake.org/cmake/help/latest/policy/CMP0037.html [A-Za-z0-9_.+\-]
|
||||
// TOML bare keys: non-empty strings composed only of [A-Za-z0-9_-]
|
||||
// We replace all non-TOML bare key characters with _
|
||||
@@ -135,18 +148,82 @@ static std::string escape_project_name(const std::string &name) {
|
||||
return escaped;
|
||||
}
|
||||
|
||||
static void generate_gitfile(const char *gitfile, const std::vector<std::string> &desired_lines) {
|
||||
// Reference: https://github.com/github/linguist/blob/master/docs/overrides.md#summary
|
||||
auto lines = desired_lines;
|
||||
auto generate = [&lines](const char *newline) {
|
||||
std::string generated;
|
||||
generated += "# cmkr";
|
||||
generated += newline;
|
||||
for (const auto &line : lines) {
|
||||
generated += line;
|
||||
generated += newline;
|
||||
}
|
||||
return generated;
|
||||
};
|
||||
if (!fs::exists(gitfile)) {
|
||||
create_file(gitfile, generate("\n"));
|
||||
} else {
|
||||
auto contents = read_file(gitfile);
|
||||
std::string line;
|
||||
auto cr = 0, lf = 0;
|
||||
auto flush_line = [&line, &lines]() {
|
||||
auto itr = std::find(lines.begin(), lines.end(), line);
|
||||
if (itr != lines.end()) {
|
||||
lines.erase(itr);
|
||||
}
|
||||
line.clear();
|
||||
};
|
||||
for (size_t i = 0; i < contents.length(); i++) {
|
||||
if (contents[i] == '\r') {
|
||||
cr++;
|
||||
continue;
|
||||
}
|
||||
if (contents[i] == '\n') {
|
||||
lf++;
|
||||
flush_line();
|
||||
} else {
|
||||
line += contents[i];
|
||||
}
|
||||
}
|
||||
if (!line.empty()) {
|
||||
flush_line();
|
||||
}
|
||||
|
||||
if (!lines.empty()) {
|
||||
// Append the cmkr .gitattributes using the detected newline
|
||||
auto newline = cr == lf ? "\r\n" : "\n";
|
||||
if (!contents.empty() && contents.back() != '\n') {
|
||||
contents += newline;
|
||||
}
|
||||
contents += newline;
|
||||
contents += generate(newline);
|
||||
create_file(gitfile, contents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void generate_project(const std::string &type) {
|
||||
const auto name = escape_project_name(fs::current_path().stem().string());
|
||||
if (fs::exists(fs::current_path() / "cmake.toml")) {
|
||||
throw std::runtime_error("Cannot initialize a project when cmake.toml already exists!");
|
||||
}
|
||||
|
||||
// Check if the folder is empty before creating any files
|
||||
auto is_empty = fs::is_empty(fs::current_path());
|
||||
|
||||
// Automatically generate .gitattributes to not skew the statistics of the repo
|
||||
generate_gitfile(".gitattributes", {"/**/CMakeLists.txt linguist-generated", "/**/cmkr.cmake linguist-vendored"});
|
||||
|
||||
// Generate .gitignore with reasonable defaults for CMake
|
||||
generate_gitfile(".gitignore", {"build*/", "cmake-build*/", ".idea/", ".vscode/"});
|
||||
|
||||
tsl::ordered_map<std::string, std::string> variables = {
|
||||
{"@name", name},
|
||||
{"@type", type},
|
||||
};
|
||||
|
||||
if (!fs::is_empty(fs::current_path())) {
|
||||
if (!is_empty) {
|
||||
// Make a backup of an existing CMakeLists.txt if it exists
|
||||
std::error_code ec;
|
||||
fs::rename("CMakeLists.txt", "CMakeLists.txt.bak", ec);
|
||||
@@ -543,7 +620,7 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
|
||||
for (auto const &lang : project.project_languages) {
|
||||
if (known_languages.find(lang) == known_languages.end()) {
|
||||
if (project.project_allow_unknown_languages) {
|
||||
printf("Unknown language '%s' specified\n", lang.c_str());
|
||||
printf("[warning] Unknown language '%s' specified\n", lang.c_str());
|
||||
} else {
|
||||
throw std::runtime_error("Unknown language '" + lang + "' specified");
|
||||
}
|
||||
@@ -656,7 +733,13 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
|
||||
if (!project.options.empty()) {
|
||||
comment("Options");
|
||||
for (const auto &opt : project.options) {
|
||||
cmd("option")(opt.name, RawArg(Command::quote(opt.help)), opt.value ? "ON" : "OFF");
|
||||
std::string default_val;
|
||||
if (opt.value.index() == 0) {
|
||||
default_val = mpark::get<0>(opt.value) ? "ON" : "OFF";
|
||||
} else {
|
||||
default_val = mpark::get<1>(opt.value);
|
||||
}
|
||||
cmd("option")(opt.name, RawArg(Command::quote(opt.help)), default_val);
|
||||
}
|
||||
endl();
|
||||
}
|
||||
@@ -892,7 +975,7 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
|
||||
}
|
||||
}
|
||||
|
||||
auto contains_language_source = [&project_extensions](const std::vector<std::string>& sources) {
|
||||
auto contains_language_source = [&project_extensions](const std::vector<std::string> &sources) {
|
||||
for (const auto &source : sources) {
|
||||
auto extension = fs::path(source).extension().string();
|
||||
if (project_extensions.count(extension) > 0) {
|
||||
@@ -906,6 +989,9 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
|
||||
auto project_root = project.root();
|
||||
for (size_t i = 0; i < project.targets.size(); i++) {
|
||||
const auto &target = project.targets[i];
|
||||
|
||||
auto throw_target_error = [&target](const std::string &message) { throw std::runtime_error("[target." + target.name + "] " + message); };
|
||||
|
||||
const parser::Template *tmplate = nullptr;
|
||||
std::unique_ptr<ConditionScope> tmplate_cs{};
|
||||
|
||||
@@ -1009,7 +1095,7 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
|
||||
auto sources = expand_cmake_paths(condition_sources, path, is_root_project);
|
||||
if (sources.empty()) {
|
||||
auto source_key = condition.empty() ? "sources" : (condition + ".sources");
|
||||
throw std::runtime_error(target.name + " " + source_key + " wildcard found 0 files");
|
||||
throw_target_error(source_key + " wildcard found 0 files");
|
||||
}
|
||||
|
||||
// Make sure there are source files for the languages used by the project
|
||||
@@ -1020,7 +1106,25 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
|
||||
case parser::target_static:
|
||||
case parser::target_object:
|
||||
if (!contains_language_source(sources)) {
|
||||
throw std::runtime_error("There were no source files linked within the target " + target.name);
|
||||
std::string extensions;
|
||||
for (const auto &language : project_extensions) {
|
||||
if (!extensions.empty()) {
|
||||
extensions += " ";
|
||||
}
|
||||
extensions += language;
|
||||
}
|
||||
throw_target_error("No sources found with valid extensions (" + extensions + ")");
|
||||
}
|
||||
|
||||
// Make sure relative source files exist
|
||||
for (const auto &source : sources) {
|
||||
auto var_index = source.find("${");
|
||||
if (var_index != std::string::npos)
|
||||
continue;
|
||||
const auto &source_path = fs::path(path) / source;
|
||||
if (!fs::exists(source_path)) {
|
||||
throw_target_error("Source file does not exist " + source);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -1030,7 +1134,7 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
|
||||
if (sources_with_set) {
|
||||
// This is a sanity check to make sure the unconditional sources are first
|
||||
if (!condition.empty()) {
|
||||
throw std::runtime_error("Unreachable code, make sure unconditional sources are first");
|
||||
throw_target_error("Unreachable code, make sure unconditional sources are first");
|
||||
}
|
||||
cmd("set")(sources_var, sources);
|
||||
sources_with_set = false;
|
||||
@@ -1043,7 +1147,7 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
|
||||
|
||||
if (tmplate != nullptr) {
|
||||
if (target_type != parser::target_template) {
|
||||
throw std::runtime_error("Unreachable code, unexpected target type for template");
|
||||
throw_target_error("Unreachable code, unexpected target type for template");
|
||||
}
|
||||
target_type = tmplate->outline.type;
|
||||
}
|
||||
@@ -1091,7 +1195,7 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
|
||||
target_scope = "PUBLIC";
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("Unimplemented enum value");
|
||||
throw_target_error("Unimplemented enum value");
|
||||
}
|
||||
|
||||
// Handle custom add commands from templates.
|
||||
@@ -1172,7 +1276,7 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
|
||||
for (const auto &propItr : properties) {
|
||||
if (propItr.first == "MSVC_RUNTIME_LIBRARY") {
|
||||
if (project_root->project_msvc_runtime == parser::msvc_last) {
|
||||
throw std::runtime_error("You cannot set [target].msvc-runtime without setting the root [project].msvc-runtime");
|
||||
throw_target_error("You cannot set msvc-runtime without setting the root [project].msvc-runtime");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-2
@@ -377,10 +377,33 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p
|
||||
const auto &value = itr.second;
|
||||
if (value.is_boolean()) {
|
||||
o.value = value.as_boolean();
|
||||
} else {
|
||||
} 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);
|
||||
option.optional("value", o.value);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user