From 6cb1009c0ef3b4c6e4c07c38ec6b1303f6e3e96b Mon Sep 17 00:00:00 2001 From: Duncan Ogilvie Date: Thu, 12 Feb 2026 03:41:54 +0100 Subject: [PATCH 1/8] Add support for custom commands --- docs/cmake-toml.md | 68 +++- docs/examples/custom-command.md | 66 ++++ include/project_parser.hpp | 101 ++++++ src/cmake_generator.cpp | 282 ++++++++++++++-- src/project_parser.cpp | 313 ++++++++++++++++++ tests/CMakeLists.txt | 10 + tests/cmake.toml | 6 + tests/custom-command/cmake.toml | 47 +++ .../cmake/generate_source.cmake | 13 + tests/custom-command/src/main.cpp | 7 + 10 files changed, 893 insertions(+), 20 deletions(-) create mode 100644 docs/examples/custom-command.md create mode 100644 tests/custom-command/cmake.toml create mode 100644 tests/custom-command/cmake/generate_source.cmake create mode 100644 tests/custom-command/src/main.cpp diff --git a/docs/cmake-toml.md b/docs/cmake-toml.md index cdeb4dd..5ad64a2 100644 --- a/docs/cmake-toml.md +++ b/docs/cmake-toml.md @@ -246,6 +246,24 @@ link-options = [""] # linker flags private-link-options = [""] precompile-headers = [""] # precompiled headers private-precompile-headers = [""] +dependencies = [""] # add_dependencies(target ...) + +# Keys below are for type = "custom" (add_custom_target) +all = false +command = ["${CMAKE_COMMAND}", "-E", "echo", "Hello"] # one command line +commands = [ # multiple command lines + ["${CMAKE_COMMAND}", "-E", "echo", "First"], + ["${CMAKE_COMMAND}", "-E", "echo", "Second"], +] +depends = ["generated.txt"] # add_custom_target(... DEPENDS ...) +byproducts = ["generated.txt"] +working-directory = "${CMAKE_CURRENT_BINARY_DIR}" +comment = "Run custom target" +job-pool = "pool" +job-server-aware = true +verbatim = true +uses-terminal = true +command-expand-lists = true cmake-before = """ message(STATUS "CMake injected before the target") @@ -263,6 +281,51 @@ CXX_STANDARD_REQUIRED = true FOLDER = "MyFolder" ``` +### Custom commands + +Use `[[target..custom-command]]` to map directly to [`add_custom_command`](https://cmake.org/cmake/help/latest/command/add_custom_command.html). A custom command can use either the `OUTPUT` form or the `TARGET` form: + +```toml +# OUTPUT form (add_custom_command(OUTPUT ...)) +[[target.mytarget.custom-command]] +condition = "mycondition" +outputs = ["${CMAKE_CURRENT_BINARY_DIR}/generated.cpp"] +command = [ + "${CMAKE_COMMAND}", + "-DOUTPUT=${CMAKE_CURRENT_BINARY_DIR}/generated.cpp", + "-P", + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/generate.cmake", +] +depends = ["cmake/generate.cmake"] +byproducts = ["${CMAKE_CURRENT_BINARY_DIR}/generated.hpp"] +main-dependency = "cmake/generate.cmake" +implicit-depends = [["CXX", "src/input.idl"]] +working-directory = "${CMAKE_CURRENT_BINARY_DIR}" +comment = "Generate source" +depfile = "${CMAKE_CURRENT_BINARY_DIR}/generated.d" +job-pool = "pool" +job-server-aware = true +verbatim = true +append = false +uses-terminal = false +codegen = false +command-expand-lists = false +depends-explicit-only = false + +# TARGET form (add_custom_command(TARGET ...)) +[[target.mytarget.custom-command]] +build-event = "post-build" # pre-build, pre-link, post-build +command = ["${CMAKE_COMMAND}", "-E", "touch", "${CMAKE_CURRENT_BINARY_DIR}/post-build.stamp"] +byproducts = ["${CMAKE_CURRENT_BINARY_DIR}/post-build.stamp"] +working-directory = "${CMAKE_CURRENT_BINARY_DIR}" +comment = "Post-build step" +verbatim = true +command-expand-lists = false +uses-terminal = false +``` + +For each custom command entry, exactly one of `outputs` or `build-event` is required. + A table mapping the cmkr features to the relevant CMake construct and the relevant documentation pages: | cmkr | CMake construct | Description | @@ -279,6 +342,9 @@ A table mapping the cmkr features to the relevant CMake construct and the releva | `link-libraries` | [`target_link_libraries`](https://cmake.org/cmake/help/latest/command/target_link_libraries.html) | Adds library dependencies. Use `::mylib` to make sure a target exists. | | `link-options` | [`target_link_options`](https://cmake.org/cmake/help/latest/command/target_link_options.html) | Adds linker flags. | | `precompile-headers` | [`target_precompile_headers`](https://cmake.org/cmake/help/latest/command/target_precompile_headers.html) | Specifies precompiled headers. | +| `dependencies` | [`add_dependencies`](https://cmake.org/cmake/help/latest/command/add_dependencies.html) | Adds target-level dependencies. | +| `type = "custom"` + custom target keys | [`add_custom_target`](https://cmake.org/cmake/help/latest/command/add_custom_target.html) | Creates a custom target with command/dependency options. | +| `[[target..custom-command]]` | [`add_custom_command`](https://cmake.org/cmake/help/latest/command/add_custom_command.html) | Supports both `OUTPUT` and `TARGET` forms. | | `properties` | [`set_target_properties`](https://cmake.org/cmake/help/latest/command/set_target_properties.html) | See [properties on targets](https://cmake.org/cmake/help/latest/manual/cmake-properties.7.html#properties-on-targets) for more information. | The default [visibility](/basics) is as follows: @@ -342,4 +408,4 @@ files = ["content/my.png"] dirs = ["include"] configs = ["Release", "Debug"] optional = false -``` \ No newline at end of file +``` diff --git a/docs/examples/custom-command.md b/docs/examples/custom-command.md new file mode 100644 index 0000000..7a4413b --- /dev/null +++ b/docs/examples/custom-command.md @@ -0,0 +1,66 @@ +--- +# Automatically generated from tests/custom-command/cmake.toml - DO NOT EDIT +layout: default +title: Tests add_custom_command and add_custom_target support +permalink: /examples/custom-command +parent: Examples +nav_order: 12 +--- + +# Tests add_custom_command and add_custom_target support + + + +```toml +[project] +name = "custom-command" +description = "Tests add_custom_command and add_custom_target support" + +[target.custom-command] +type = "executable" +sources = ["src/main.cpp"] +include-directories = ["${CMAKE_CURRENT_BINARY_DIR}/generated"] + +[[target.custom-command.custom-command]] +outputs = ["${CMAKE_CURRENT_BINARY_DIR}/generated/generated.cpp"] +byproducts = ["${CMAKE_CURRENT_BINARY_DIR}/generated/generated.hpp"] +depends = ["cmake/generate_source.cmake"] +command = [ + "${CMAKE_COMMAND}", + "-DOUTPUT_CPP=${CMAKE_CURRENT_BINARY_DIR}/generated/generated.cpp", + "-DOUTPUT_HPP=${CMAKE_CURRENT_BINARY_DIR}/generated/generated.hpp", + "-P", + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/generate_source.cmake", +] +comment = "Generate source files" +verbatim = true + +[[target.custom-command.custom-command]] +build-event = "post-build" +command = [ + "${CMAKE_COMMAND}", + "-E", + "touch", + "${CMAKE_CURRENT_BINARY_DIR}/custom-command-post-build.stamp", +] +byproducts = ["${CMAKE_CURRENT_BINARY_DIR}/custom-command-post-build.stamp"] +comment = "Create a post-build stamp file" +verbatim = true + +[target.custom-codegen] +type = "custom" +all = true +command = [ + "${CMAKE_COMMAND}", + "-E", + "touch", + "${CMAKE_CURRENT_BINARY_DIR}/custom-target.stamp", +] +byproducts = ["${CMAKE_CURRENT_BINARY_DIR}/custom-target.stamp"] +comment = "Run custom target" +verbatim = true +``` + + + +This page was automatically generated from [tests/custom-command/cmake.toml](https://github.com/build-cpp/cmkr/tree/main/tests/custom-command/cmake.toml). diff --git a/include/project_parser.hpp b/include/project_parser.hpp index 43d40db..79e493f 100644 --- a/include/project_parser.hpp +++ b/include/project_parser.hpp @@ -74,6 +74,104 @@ enum TargetType { extern const char *targetTypeNames[target_last]; struct Target { + struct CustomTarget { + bool has_all = false; + bool all = false; + + bool has_command = false; + bool has_commands = false; + std::vector> commands; + std::vector depends; + std::vector byproducts; + + bool has_working_directory = false; + std::string working_directory; + + bool has_comment = false; + std::string comment; + + bool has_job_pool = false; + std::string job_pool; + + bool has_job_server_aware = false; + bool job_server_aware = false; + + bool has_verbatim = false; + bool verbatim = false; + + bool has_uses_terminal = false; + bool uses_terminal = false; + + bool has_command_expand_lists = false; + bool command_expand_lists = false; + + bool empty() const { + return !has_all && !has_command && !has_commands && commands.empty() && depends.empty() && byproducts.empty() && !has_working_directory && + !has_comment && !has_job_pool && !has_job_server_aware && !has_verbatim && !has_uses_terminal && !has_command_expand_lists; + } + }; + + struct CustomCommand { + struct ImplicitDependency { + std::string language; + std::string file; + }; + + std::string condition; + std::vector> commands; + + std::vector outputs; + std::string build_event; + + bool has_append = false; + bool append = false; + + bool has_main_dependency = false; + std::string main_dependency; + + std::vector depends; + std::vector byproducts; + std::vector implicit_depends; + + bool has_working_directory = false; + std::string working_directory; + + bool has_comment = false; + std::string comment; + + bool has_depfile = false; + std::string depfile; + + bool has_job_pool = false; + std::string job_pool; + + bool has_job_server_aware = false; + bool job_server_aware = false; + + bool has_verbatim = false; + bool verbatim = false; + + bool has_uses_terminal = false; + bool uses_terminal = false; + + bool has_codegen = false; + bool codegen = false; + + bool has_command_expand_lists = false; + bool command_expand_lists = false; + + bool has_depends_explicit_only = false; + bool depends_explicit_only = false; + + bool is_output_form() const { + return !outputs.empty(); + } + + bool is_target_form() const { + return !build_event.empty(); + } + }; + std::string name; TargetType type = target_last; std::string type_name; @@ -107,6 +205,9 @@ struct Target { ConditionVector dependencies; + CustomTarget custom_target; + std::vector custom_commands; + std::string condition; std::string alias; Condition> properties; diff --git a/src/cmake_generator.cpp b/src/cmake_generator.cpp index ab697cb..f97368c 100644 --- a/src/cmake_generator.cpp +++ b/src/cmake_generator.cpp @@ -4,6 +4,7 @@ #include "fs.hpp" #include "project_parser.hpp" +#include #include #include #include @@ -1244,6 +1245,68 @@ void generate_cmake(const char *path, const parser::Project *parent_project) { gen.conditional_includes(target.include_before); gen.conditional_cmake(target.cmake_before); + auto merge_unique_vector = [](std::vector &destination, const std::vector &source) { + for (const auto &item : source) { + if (std::find(destination.begin(), destination.end(), item) == destination.end()) { + destination.push_back(item); + } + } + }; + + auto custom_target = target.custom_target; + if (tmplate != nullptr) { + const auto &custom = tmplate->outline.custom_target; + if (custom.has_all) { + custom_target.has_all = true; + custom_target.all = custom_target.all || custom.all; + } + if (custom.has_command) { + custom_target.has_command = true; + } + if (custom.has_commands) { + custom_target.has_commands = true; + } + if (!custom.commands.empty()) { + custom_target.commands.insert(custom_target.commands.begin(), custom.commands.begin(), custom.commands.end()); + } + merge_unique_vector(custom_target.depends, custom.depends); + merge_unique_vector(custom_target.byproducts, custom.byproducts); + if (!custom_target.has_working_directory && custom.has_working_directory) { + custom_target.has_working_directory = true; + custom_target.working_directory = custom.working_directory; + } + if (!custom_target.has_comment && custom.has_comment) { + custom_target.has_comment = true; + custom_target.comment = custom.comment; + } + if (!custom_target.has_job_pool && custom.has_job_pool) { + custom_target.has_job_pool = true; + custom_target.job_pool = custom.job_pool; + } + if (!custom_target.has_job_server_aware && custom.has_job_server_aware) { + custom_target.has_job_server_aware = true; + custom_target.job_server_aware = custom.job_server_aware; + } + if (!custom_target.has_verbatim && custom.has_verbatim) { + custom_target.has_verbatim = true; + custom_target.verbatim = custom.verbatim; + } + if (!custom_target.has_uses_terminal && custom.has_uses_terminal) { + custom_target.has_uses_terminal = true; + custom_target.uses_terminal = custom.uses_terminal; + } + if (!custom_target.has_command_expand_lists && custom.has_command_expand_lists) { + custom_target.has_command_expand_lists = true; + custom_target.command_expand_lists = custom.command_expand_lists; + } + } + + std::vector custom_commands; + if (tmplate != nullptr) { + custom_commands.insert(custom_commands.end(), tmplate->outline.custom_commands.begin(), tmplate->outline.custom_commands.end()); + } + custom_commands.insert(custom_commands.end(), target.custom_commands.begin(), target.custom_commands.end()); + // Merge the sources from the template and the target. The sources // without condition need to be processed first parser::Condition> msources; @@ -1262,6 +1325,16 @@ void generate_cmake(const char *path, const parser::Project *parent_project) { } merge_sources(target.sources); + for (const auto &custom_command : custom_commands) { + if (!custom_command.is_output_form()) { + continue; + } + auto &condition_sources = msources[custom_command.condition]; + for (const auto &output : custom_command.outputs) { + condition_sources.insert(output); + } + } + // Improve IDE support if (target.type != parser::target_interface) { msources[""].insert("cmake.toml"); @@ -1353,6 +1426,7 @@ void generate_cmake(const char *path, const parser::Project *parent_project) { std::string add_command; std::string target_type_string; std::string target_scope; + bool is_custom_target = false; switch (target_type) { case parser::target_executable: @@ -1381,9 +1455,7 @@ void generate_cmake(const char *path, const parser::Project *parent_project) { target_scope = "INTERFACE"; break; case parser::target_custom: - // TODO: add proper support, this is hacky - add_command = "add_custom_target"; - target_type_string = "SOURCES"; + is_custom_target = true; target_scope = "PUBLIC"; break; case parser::target_object: @@ -1396,6 +1468,124 @@ void generate_cmake(const char *path, const parser::Project *parent_project) { throw_target_error("Unimplemented enum value"); } + auto append_keyword = [](std::vector &args, const std::string &keyword) { + args.emplace_back(keyword); + }; + auto append_value = [](std::vector &args, const std::string &value) { + args.emplace_back(Command::quote(value)); + }; + auto append_values = [&](std::vector &args, const std::vector &values) { + for (const auto &value : values) { + append_value(args, value); + } + }; + auto append_commands = [&](std::vector &args, const std::vector> &commands) { + for (const auto &command_args : commands) { + append_keyword(args, "COMMAND"); + append_values(args, command_args); + } + }; + + auto emit_custom_command = [&](const parser::Target::CustomCommand &custom_command) { + std::vector args; + if (custom_command.is_output_form()) { + append_keyword(args, "OUTPUT"); + append_values(args, custom_command.outputs); + append_commands(args, custom_command.commands); + if (custom_command.has_main_dependency) { + append_keyword(args, "MAIN_DEPENDENCY"); + append_value(args, custom_command.main_dependency); + } + if (!custom_command.depends.empty()) { + append_keyword(args, "DEPENDS"); + append_values(args, custom_command.depends); + } + if (!custom_command.byproducts.empty()) { + append_keyword(args, "BYPRODUCTS"); + append_values(args, custom_command.byproducts); + } + if (!custom_command.implicit_depends.empty()) { + append_keyword(args, "IMPLICIT_DEPENDS"); + for (const auto &implicit_dependency : custom_command.implicit_depends) { + append_value(args, implicit_dependency.language); + append_value(args, implicit_dependency.file); + } + } + if (custom_command.has_working_directory) { + append_keyword(args, "WORKING_DIRECTORY"); + append_value(args, custom_command.working_directory); + } + if (custom_command.has_comment) { + append_keyword(args, "COMMENT"); + append_value(args, custom_command.comment); + } + if (custom_command.has_depfile) { + append_keyword(args, "DEPFILE"); + append_value(args, custom_command.depfile); + } + if (custom_command.has_job_pool) { + append_keyword(args, "JOB_POOL"); + append_value(args, custom_command.job_pool); + } + if (custom_command.has_job_server_aware) { + append_keyword(args, "JOB_SERVER_AWARE"); + append_value(args, custom_command.job_server_aware ? "ON" : "OFF"); + } + if (custom_command.has_verbatim && custom_command.verbatim) { + append_keyword(args, "VERBATIM"); + } + if (custom_command.has_append && custom_command.append) { + append_keyword(args, "APPEND"); + } + if (custom_command.has_uses_terminal && custom_command.uses_terminal) { + append_keyword(args, "USES_TERMINAL"); + } + if (custom_command.has_codegen && custom_command.codegen) { + append_keyword(args, "CODEGEN"); + } + if (custom_command.has_command_expand_lists && custom_command.command_expand_lists) { + append_keyword(args, "COMMAND_EXPAND_LISTS"); + } + if (custom_command.has_depends_explicit_only && custom_command.depends_explicit_only) { + append_keyword(args, "DEPENDS_EXPLICIT_ONLY"); + } + } else { + append_keyword(args, "TARGET"); + append_value(args, target.name); + append_keyword(args, custom_command.build_event); + append_commands(args, custom_command.commands); + if (!custom_command.byproducts.empty()) { + append_keyword(args, "BYPRODUCTS"); + append_values(args, custom_command.byproducts); + } + if (custom_command.has_working_directory) { + append_keyword(args, "WORKING_DIRECTORY"); + append_value(args, custom_command.working_directory); + } + if (custom_command.has_comment) { + append_keyword(args, "COMMENT"); + append_value(args, custom_command.comment); + } + if (custom_command.has_verbatim && custom_command.verbatim) { + append_keyword(args, "VERBATIM"); + } + if (custom_command.has_command_expand_lists && custom_command.command_expand_lists) { + append_keyword(args, "COMMAND_EXPAND_LISTS"); + } + if (custom_command.has_uses_terminal && custom_command.uses_terminal) { + append_keyword(args, "USES_TERMINAL"); + } + } + cmd("add_custom_command")(args).endl(); + }; + + for (const auto &custom_command : custom_commands) { + if (custom_command.is_output_form()) { + ConditionScope custom_scope(gen, custom_command.condition); + emit_custom_command(custom_command); + } + } + // Handle custom add commands from templates. if (tmplate != nullptr && !tmplate->add_function.empty()) { add_command = tmplate->add_function; @@ -1409,6 +1599,51 @@ void generate_cmake(const char *path, const parser::Project *parent_project) { "${" + sources_var + "}"); } } + } else if (is_custom_target) { + std::vector args; + append_value(args, target.name); + if (custom_target.has_all && custom_target.all) { + append_keyword(args, "ALL"); + } + append_commands(args, custom_target.commands); + if (!custom_target.depends.empty()) { + append_keyword(args, "DEPENDS"); + append_values(args, custom_target.depends); + } + if (!custom_target.byproducts.empty()) { + append_keyword(args, "BYPRODUCTS"); + append_values(args, custom_target.byproducts); + } + if (custom_target.has_working_directory) { + append_keyword(args, "WORKING_DIRECTORY"); + append_value(args, custom_target.working_directory); + } + if (custom_target.has_comment) { + append_keyword(args, "COMMENT"); + append_value(args, custom_target.comment); + } + if (custom_target.has_job_pool) { + append_keyword(args, "JOB_POOL"); + append_value(args, custom_target.job_pool); + } + if (custom_target.has_job_server_aware) { + append_keyword(args, "JOB_SERVER_AWARE"); + append_value(args, custom_target.job_server_aware ? "ON" : "OFF"); + } + if (custom_target.has_verbatim && custom_target.verbatim) { + append_keyword(args, "VERBATIM"); + } + if (custom_target.has_uses_terminal && custom_target.uses_terminal) { + append_keyword(args, "USES_TERMINAL"); + } + if (custom_target.has_command_expand_lists && custom_target.command_expand_lists) { + append_keyword(args, "COMMAND_EXPAND_LISTS"); + } + if (has_sources) { + append_keyword(args, "SOURCES"); + append_value(args, "${" + sources_var + "}"); + } + cmd("add_custom_target")(args).endl(); } else { cmd(add_command)(target.name, target_type_string).endl(); if (has_sources) { @@ -1416,6 +1651,13 @@ void generate_cmake(const char *path, const parser::Project *parent_project) { } } + for (const auto &custom_command : custom_commands) { + if (custom_command.is_target_form()) { + ConditionScope custom_scope(gen, custom_command.condition); + emit_custom_command(custom_command); + } + } + // TODO: support sources from other directories if (has_sources) { cmd("source_group")("TREE", "${CMAKE_CURRENT_SOURCE_DIR}", "FILES", "${" + sources_var + "}").endl(); @@ -1452,29 +1694,31 @@ void generate_cmake(const char *path, const parser::Project *parent_project) { }; auto gen_target_cmds = [&](const parser::Target &t) { - target_cmd("target_compile_definitions", t.compile_definitions, target_scope); - target_cmd("target_compile_definitions", t.private_compile_definitions, "PRIVATE"); + if (!is_custom_target) { + target_cmd("target_compile_definitions", t.compile_definitions, target_scope); + target_cmd("target_compile_definitions", t.private_compile_definitions, "PRIVATE"); - target_cmd("target_compile_features", t.compile_features, target_scope); - target_cmd("target_compile_features", t.private_compile_features, "PRIVATE"); + target_cmd("target_compile_features", t.compile_features, target_scope); + target_cmd("target_compile_features", t.private_compile_features, "PRIVATE"); - target_cmd("target_compile_options", t.compile_options, target_scope); - target_cmd("target_compile_options", t.private_compile_options, "PRIVATE"); + target_cmd("target_compile_options", t.compile_options, target_scope); + target_cmd("target_compile_options", t.private_compile_options, "PRIVATE"); - target_cmd("target_include_directories", t.include_directories, target_scope); - target_cmd("target_include_directories", t.private_include_directories, "PRIVATE"); + target_cmd("target_include_directories", t.include_directories, target_scope); + target_cmd("target_include_directories", t.private_include_directories, "PRIVATE"); - target_cmd("target_link_directories", t.link_directories, target_scope); - target_cmd("target_link_directories", t.private_link_directories, "PRIVATE"); + target_cmd("target_link_directories", t.link_directories, target_scope); + target_cmd("target_link_directories", t.private_link_directories, "PRIVATE"); - link_cmd("target_link_libraries", t.link_libraries, target_scope); - link_cmd("target_link_libraries", t.private_link_libraries, "PRIVATE"); + link_cmd("target_link_libraries", t.link_libraries, target_scope); + link_cmd("target_link_libraries", t.private_link_libraries, "PRIVATE"); - target_cmd("target_link_options", t.link_options, target_scope); - target_cmd("target_link_options", t.private_link_options, "PRIVATE"); + target_cmd("target_link_options", t.link_options, target_scope); + target_cmd("target_link_options", t.private_link_options, "PRIVATE"); - target_cmd("target_precompile_headers", t.precompile_headers, target_scope); - target_cmd("target_precompile_headers", t.private_precompile_headers, "PRIVATE"); + target_cmd("target_precompile_headers", t.precompile_headers, target_scope); + target_cmd("target_precompile_headers", t.private_precompile_headers, "PRIVATE"); + } link_cmd("add_dependencies", t.dependencies, ""); }; diff --git a/src/project_parser.cpp b/src/project_parser.cpp index 45acb4e..42d188b 100644 --- a/src/project_parser.cpp +++ b/src/project_parser.cpp @@ -1,6 +1,7 @@ #include "project_parser.hpp" #include "fs.hpp" +#include #include #include #include @@ -209,6 +210,93 @@ class TomlCheckerRoot { } }; +static std::vector parse_string_array(const TomlBasicValue &value, const toml::key &key) { + if (!value.is_array()) { + throw_key_error("Expected an array", key, value); + } + std::vector values; + for (const auto &entry : value.as_array()) { + if (!entry.is_string()) { + throw_key_error("Expected an array of strings", key, entry); + } + values.push_back(entry.as_string()); + } + return values; +} + +static std::vector> parse_command_arguments(const TomlBasicValue &value, const toml::key &key) { + if (!value.is_array()) { + throw_key_error("Expected an array", key, value); + } + const auto &array = value.as_array(); + if (array.empty()) { + return {}; + } + + bool all_strings = true; + bool all_arrays = true; + for (const auto &entry : array) { + if (!entry.is_string()) { + all_strings = false; + } + if (!entry.is_array()) { + all_arrays = false; + } + } + + std::vector> commands; + if (all_strings) { + commands.push_back(parse_string_array(value, key)); + if (commands[0].empty()) { + throw_key_error("Command arrays cannot be empty", key, value); + } + return commands; + } + + if (!all_arrays) { + throw_key_error("Expected an array of command arrays", key, value); + } + + for (const auto &entry : array) { + auto command = parse_string_array(entry, key); + if (command.empty()) { + throw_key_error("Command arrays cannot be empty", key, entry); + } + commands.push_back(std::move(command)); + } + + return commands; +} + +static std::vector parse_implicit_dependencies(const TomlBasicValue &value, const toml::key &key) { + if (!value.is_array()) { + throw_key_error("Expected an array", key, value); + } + std::vector implicit_dependencies; + for (const auto &entry : value.as_array()) { + auto values = parse_string_array(entry, key); + if (values.size() != 2) { + throw_key_error("Each implicit dependency must have exactly two strings: [language, file]", key, entry); + } + Target::CustomCommand::ImplicitDependency implicit_dependency; + implicit_dependency.language = std::move(values[0]); + implicit_dependency.file = std::move(values[1]); + implicit_dependencies.push_back(std::move(implicit_dependency)); + } + return implicit_dependencies; +} + +static std::string normalize_build_event(std::string build_event) { + for (auto &ch : build_event) { + if (ch == '-') { + ch = '_'; + } else { + ch = static_cast(std::toupper(static_cast(ch))); + } + } + return build_event; +} + 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)) { @@ -698,6 +786,231 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p t.optional("dependencies", target.dependencies); + if (t.contains("all")) { + target.custom_target.has_all = true; + target.custom_target.all = t.find("all").as_boolean(); + } + + if (t.contains("command")) { + target.custom_target.has_command = true; + auto commands = parse_command_arguments(t.find("command"), "command"); + for (auto &command : commands) { + target.custom_target.commands.push_back(std::move(command)); + } + } + + if (t.contains("commands")) { + target.custom_target.has_commands = true; + auto commands = parse_command_arguments(t.find("commands"), "commands"); + for (auto &command : commands) { + target.custom_target.commands.push_back(std::move(command)); + } + } + + t.optional("depends", target.custom_target.depends); + t.optional("byproducts", target.custom_target.byproducts); + + if (t.contains("working-directory")) { + target.custom_target.has_working_directory = true; + target.custom_target.working_directory = t.find("working-directory").as_string(); + } + + if (t.contains("comment")) { + target.custom_target.has_comment = true; + target.custom_target.comment = t.find("comment").as_string(); + } + + if (t.contains("job-pool")) { + target.custom_target.has_job_pool = true; + target.custom_target.job_pool = t.find("job-pool").as_string(); + } + + if (t.contains("job-server-aware")) { + target.custom_target.has_job_server_aware = true; + target.custom_target.job_server_aware = t.find("job-server-aware").as_boolean(); + } + + if (t.contains("verbatim")) { + target.custom_target.has_verbatim = true; + target.custom_target.verbatim = t.find("verbatim").as_boolean(); + } + + if (t.contains("uses-terminal")) { + target.custom_target.has_uses_terminal = true; + target.custom_target.uses_terminal = t.find("uses-terminal").as_boolean(); + } + + if (t.contains("command-expand-lists")) { + target.custom_target.has_command_expand_lists = true; + target.custom_target.command_expand_lists = t.find("command-expand-lists").as_boolean(); + } + + if (t.contains("custom-command")) { + const auto &custom_commands = t.find("custom-command"); + if (!custom_commands.is_array()) { + throw_key_error("Expected an array of tables", "custom-command", custom_commands); + } + for (const auto &custom_command_value : custom_commands.as_array()) { + if (!custom_command_value.is_table()) { + throw_key_error("Expected an array of tables", "custom-command", custom_command_value); + } + + auto &custom = checker.create(custom_command_value); + Target::CustomCommand custom_command; + + custom.optional("condition", custom_command.condition); + + if (custom.contains("command")) { + auto commands = parse_command_arguments(custom.find("command"), "command"); + for (auto &command : commands) { + custom_command.commands.push_back(std::move(command)); + } + } + + if (custom.contains("commands")) { + auto commands = parse_command_arguments(custom.find("commands"), "commands"); + for (auto &command : commands) { + custom_command.commands.push_back(std::move(command)); + } + } + + custom.optional("outputs", custom_command.outputs); + + if (custom.contains("build-event")) { + custom_command.build_event = normalize_build_event(custom.find("build-event").as_string()); + } + + if (custom.contains("append")) { + custom_command.has_append = true; + custom_command.append = custom.find("append").as_boolean(); + } + + if (custom.contains("main-dependency")) { + custom_command.has_main_dependency = true; + custom_command.main_dependency = custom.find("main-dependency").as_string(); + } + + custom.optional("depends", custom_command.depends); + custom.optional("byproducts", custom_command.byproducts); + + if (custom.contains("implicit-depends")) { + custom_command.implicit_depends = parse_implicit_dependencies(custom.find("implicit-depends"), "implicit-depends"); + } + + if (custom.contains("working-directory")) { + custom_command.has_working_directory = true; + custom_command.working_directory = custom.find("working-directory").as_string(); + } + + if (custom.contains("comment")) { + custom_command.has_comment = true; + custom_command.comment = custom.find("comment").as_string(); + } + + if (custom.contains("depfile")) { + custom_command.has_depfile = true; + custom_command.depfile = custom.find("depfile").as_string(); + } + + if (custom.contains("job-pool")) { + custom_command.has_job_pool = true; + custom_command.job_pool = custom.find("job-pool").as_string(); + } + + if (custom.contains("job-server-aware")) { + custom_command.has_job_server_aware = true; + custom_command.job_server_aware = custom.find("job-server-aware").as_boolean(); + } + + if (custom.contains("verbatim")) { + custom_command.has_verbatim = true; + custom_command.verbatim = custom.find("verbatim").as_boolean(); + } + + if (custom.contains("uses-terminal")) { + custom_command.has_uses_terminal = true; + custom_command.uses_terminal = custom.find("uses-terminal").as_boolean(); + } + + if (custom.contains("codegen")) { + custom_command.has_codegen = true; + custom_command.codegen = custom.find("codegen").as_boolean(); + } + + if (custom.contains("command-expand-lists")) { + custom_command.has_command_expand_lists = true; + custom_command.command_expand_lists = custom.find("command-expand-lists").as_boolean(); + } + + if (custom.contains("depends-explicit-only")) { + custom_command.has_depends_explicit_only = true; + custom_command.depends_explicit_only = custom.find("depends-explicit-only").as_boolean(); + } + + if (custom_command.is_output_form() == custom_command.is_target_form()) { + throw_key_error("Specify exactly one of outputs or build-event", "custom-command", custom_command_value); + } + + if (custom_command.is_target_form()) { + if (custom_command.build_event != "PRE_BUILD" && custom_command.build_event != "PRE_LINK" && + custom_command.build_event != "POST_BUILD") { + throw_key_error("build-event must be one of: pre-build, pre-link, post-build", "build-event", custom.find("build-event")); + } + if (custom_command.has_append || custom_command.has_main_dependency || !custom_command.depends.empty() || + !custom_command.implicit_depends.empty() || custom_command.has_depfile || custom_command.has_job_pool || + custom_command.has_job_server_aware || custom_command.has_codegen || custom_command.has_depends_explicit_only) { + throw_key_error("Unsupported option for TARGET custom commands", "custom-command", custom_command_value); + } + } else { + if (custom_command.has_depfile && !custom_command.implicit_depends.empty()) { + throw_key_error("depfile cannot be used with implicit-depends", "depfile", custom.find("depfile")); + } + if (custom_command.append && + (custom_command.has_depfile || custom_command.has_job_pool || custom_command.has_job_server_aware || + custom_command.has_codegen || custom_command.has_command_expand_lists || custom_command.has_uses_terminal || + custom_command.has_verbatim || custom_command.has_depends_explicit_only || !custom_command.byproducts.empty())) { + throw_key_error("append cannot be used with depfile, job-pool, job-server-aware, codegen, command-expand-lists, " + "uses-terminal, verbatim, depends-explicit-only, or byproducts", + "append", custom.find("append")); + } + } + + if (custom_command.commands.empty() && !(custom_command.is_output_form() && custom_command.append)) { + throw_key_error("At least one command is required", "custom-command", custom_command_value); + } + + target.custom_commands.push_back(std::move(custom_command)); + } + } + + if (target.type != target_custom && !target.custom_target.empty()) { + const char *custom_key = "all"; + if (target.custom_target.has_command) { + custom_key = "command"; + } else if (target.custom_target.has_commands) { + custom_key = "commands"; + } else if (!target.custom_target.depends.empty()) { + custom_key = "depends"; + } else if (!target.custom_target.byproducts.empty()) { + custom_key = "byproducts"; + } else if (target.custom_target.has_working_directory) { + custom_key = "working-directory"; + } else if (target.custom_target.has_comment) { + custom_key = "comment"; + } else if (target.custom_target.has_job_pool) { + custom_key = "job-pool"; + } else if (target.custom_target.has_job_server_aware) { + custom_key = "job-server-aware"; + } else if (target.custom_target.has_verbatim) { + custom_key = "verbatim"; + } else if (target.custom_target.has_uses_terminal) { + custom_key = "uses-terminal"; + } else if (target.custom_target.has_command_expand_lists) { + custom_key = "command-expand-lists"; + } + throw_key_error("Custom target options can only be used with type = \"custom\"", custom_key, t.find(custom_key)); + } + Condition msvc_runtime; t.optional("msvc-runtime", msvc_runtime); for (const auto &cond_itr : msvc_runtime) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a4422a5..43ad11f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -131,3 +131,13 @@ add_test( "$" build ) + +add_test( + NAME + custom-command + WORKING_DIRECTORY + "${CMAKE_CURRENT_LIST_DIR}/custom-command" + COMMAND + "$" + build +) diff --git a/tests/cmake.toml b/tests/cmake.toml index 177fedd..f994df2 100644 --- a/tests/cmake.toml +++ b/tests/cmake.toml @@ -71,3 +71,9 @@ name = "objective-c" working-directory = "objective-c" command = "$" arguments = ["build"] + +[[test]] +name = "custom-command" +working-directory = "custom-command" +command = "$" +arguments = ["build"] diff --git a/tests/custom-command/cmake.toml b/tests/custom-command/cmake.toml new file mode 100644 index 0000000..ff2cf43 --- /dev/null +++ b/tests/custom-command/cmake.toml @@ -0,0 +1,47 @@ +[project] +name = "custom-command" +description = "Tests add_custom_command and add_custom_target support" + +[target.custom-command] +type = "executable" +sources = ["src/main.cpp"] +include-directories = ["${CMAKE_CURRENT_BINARY_DIR}/generated"] + +[[target.custom-command.custom-command]] +outputs = ["${CMAKE_CURRENT_BINARY_DIR}/generated/generated.cpp"] +byproducts = ["${CMAKE_CURRENT_BINARY_DIR}/generated/generated.hpp"] +depends = ["cmake/generate_source.cmake"] +command = [ + "${CMAKE_COMMAND}", + "-DOUTPUT_CPP=${CMAKE_CURRENT_BINARY_DIR}/generated/generated.cpp", + "-DOUTPUT_HPP=${CMAKE_CURRENT_BINARY_DIR}/generated/generated.hpp", + "-P", + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/generate_source.cmake", +] +comment = "Generate source files" +verbatim = true + +[[target.custom-command.custom-command]] +build-event = "post-build" +command = [ + "${CMAKE_COMMAND}", + "-E", + "touch", + "${CMAKE_CURRENT_BINARY_DIR}/custom-command-post-build.stamp", +] +byproducts = ["${CMAKE_CURRENT_BINARY_DIR}/custom-command-post-build.stamp"] +comment = "Create a post-build stamp file" +verbatim = true + +[target.custom-codegen] +type = "custom" +all = true +command = [ + "${CMAKE_COMMAND}", + "-E", + "touch", + "${CMAKE_CURRENT_BINARY_DIR}/custom-target.stamp", +] +byproducts = ["${CMAKE_CURRENT_BINARY_DIR}/custom-target.stamp"] +comment = "Run custom target" +verbatim = true diff --git a/tests/custom-command/cmake/generate_source.cmake b/tests/custom-command/cmake/generate_source.cmake new file mode 100644 index 0000000..1141a4e --- /dev/null +++ b/tests/custom-command/cmake/generate_source.cmake @@ -0,0 +1,13 @@ +if(NOT DEFINED OUTPUT_CPP) + message(FATAL_ERROR "OUTPUT_CPP is required") +endif() + +if(NOT DEFINED OUTPUT_HPP) + message(FATAL_ERROR "OUTPUT_HPP is required") +endif() + +get_filename_component(_output_dir "${OUTPUT_CPP}" DIRECTORY) +file(MAKE_DIRECTORY "${_output_dir}") + +file(WRITE "${OUTPUT_HPP}" "#pragma once\n#define GENERATED_VALUE 42\n") +file(WRITE "${OUTPUT_CPP}" "#include \"generated.hpp\"\nint generated_value() { return GENERATED_VALUE; }\n") diff --git a/tests/custom-command/src/main.cpp b/tests/custom-command/src/main.cpp new file mode 100644 index 0000000..ac86bc8 --- /dev/null +++ b/tests/custom-command/src/main.cpp @@ -0,0 +1,7 @@ +#include "generated.hpp" + +int generated_value(); + +int main() { + return generated_value() == GENERATED_VALUE ? 0 : 1; +} From 8650f531749d7684ab4efeb3d4eb70bce6fefe9d Mon Sep 17 00:00:00 2001 From: Duncan Ogilvie Date: Fri, 13 Feb 2026 04:54:02 +0100 Subject: [PATCH 2/8] Simple post build test --- tests/custom-command/cmake.toml | 34 ++++++++++++++++++++++++++++++ tests/custom-command/src/hello.cpp | 6 ++++++ 2 files changed, 40 insertions(+) create mode 100644 tests/custom-command/src/hello.cpp diff --git a/tests/custom-command/cmake.toml b/tests/custom-command/cmake.toml index ff2cf43..f23fb68 100644 --- a/tests/custom-command/cmake.toml +++ b/tests/custom-command/cmake.toml @@ -1,12 +1,39 @@ +# This test demonstrates add_custom_command and add_custom_target support in cmkr. +# +# There are two forms of add_custom_command in CMake: +# 1. Output form: generates files that can be consumed by other targets +# 2. Target form: runs commands at build time for a specific target (pre-build, pre-link, post-build) + [project] name = "custom-command" description = "Tests add_custom_command and add_custom_target support" +# ----------------------------------------------------------------------------- +# Simple executable demonstrating post-build events +# ----------------------------------------------------------------------------- + +[target.hello] +type = "executable" +sources = ["src/hello.cpp"] + +# This post-build command runs after the 'hello' executable is built. +# build-event can be: "pre-build", "pre-link", or "post-build" +[[target.hello.custom-command]] +build-event = "post-build" +command = ["${CMAKE_COMMAND}", "-E", "echo", "Built executable: $"] +comment = "Print the built executable name" + +# ----------------------------------------------------------------------------- +# Executable with code generation (output form custom command) +# ----------------------------------------------------------------------------- + [target.custom-command] type = "executable" sources = ["src/main.cpp"] include-directories = ["${CMAKE_CURRENT_BINARY_DIR}/generated"] +# Output form: This custom command generates source files before building. +# The outputs are automatically added as sources to the target. [[target.custom-command.custom-command]] outputs = ["${CMAKE_CURRENT_BINARY_DIR}/generated/generated.cpp"] byproducts = ["${CMAKE_CURRENT_BINARY_DIR}/generated/generated.hpp"] @@ -21,6 +48,7 @@ command = [ comment = "Generate source files" verbatim = true +# Target form: This post-build command runs after 'custom-command' is built. [[target.custom-command.custom-command]] build-event = "post-build" command = [ @@ -33,6 +61,12 @@ byproducts = ["${CMAKE_CURRENT_BINARY_DIR}/custom-command-post-build.stamp"] comment = "Create a post-build stamp file" verbatim = true +# ----------------------------------------------------------------------------- +# Custom target (add_custom_target) +# ----------------------------------------------------------------------------- + +# A custom target runs commands independently of any executable/library. +# Setting 'all = true' makes it run as part of the default build. [target.custom-codegen] type = "custom" all = true diff --git a/tests/custom-command/src/hello.cpp b/tests/custom-command/src/hello.cpp new file mode 100644 index 0000000..54e47ec --- /dev/null +++ b/tests/custom-command/src/hello.cpp @@ -0,0 +1,6 @@ +#include + +int main() { + std::cout << "Hello, world!" << std::endl; + return 0; +} From 9f80ce4429358de3c8b2383e75cd7d76098bb928 Mon Sep 17 00:00:00 2001 From: Duncan Ogilvie Date: Fri, 13 Feb 2026 23:58:59 +0100 Subject: [PATCH 3/8] Generator executable example --- docs/examples/custom-command.md | 35 +++++++++- docs/examples/generator-executable.md | 69 +++++++++++++++++++ tests/CMakeLists.txt | 10 +++ tests/cmake.toml | 6 ++ tests/generator-executable/cmake.toml | 49 +++++++++++++ .../src/generate_numbers.cpp | 49 +++++++++++++ tests/generator-executable/src/main.cpp | 26 +++++++ 7 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 docs/examples/generator-executable.md create mode 100644 tests/generator-executable/cmake.toml create mode 100644 tests/generator-executable/src/generate_numbers.cpp create mode 100644 tests/generator-executable/src/main.cpp diff --git a/docs/examples/custom-command.md b/docs/examples/custom-command.md index 7a4413b..4a4b1ac 100644 --- a/docs/examples/custom-command.md +++ b/docs/examples/custom-command.md @@ -9,18 +9,44 @@ nav_order: 12 # Tests add_custom_command and add_custom_target support - +This test demonstrates add_custom_command and add_custom_target support in cmkr. ```toml +# +# There are two forms of add_custom_command in CMake: +# 1. Output form: generates files that can be consumed by other targets +# 2. Target form: runs commands at build time for a specific target (pre-build, pre-link, post-build) + [project] name = "custom-command" description = "Tests add_custom_command and add_custom_target support" +# ----------------------------------------------------------------------------- +# Simple executable demonstrating post-build events +# ----------------------------------------------------------------------------- + +[target.hello] +type = "executable" +sources = ["src/hello.cpp"] + +# This post-build command runs after the 'hello' executable is built. +# build-event can be: "pre-build", "pre-link", or "post-build" +[[target.hello.custom-command]] +build-event = "post-build" +command = ["${CMAKE_COMMAND}", "-E", "echo", "Built executable: $"] +comment = "Print the built executable name" + +# ----------------------------------------------------------------------------- +# Executable with code generation (output form custom command) +# ----------------------------------------------------------------------------- + [target.custom-command] type = "executable" sources = ["src/main.cpp"] include-directories = ["${CMAKE_CURRENT_BINARY_DIR}/generated"] +# Output form: This custom command generates source files before building. +# The outputs are automatically added as sources to the target. [[target.custom-command.custom-command]] outputs = ["${CMAKE_CURRENT_BINARY_DIR}/generated/generated.cpp"] byproducts = ["${CMAKE_CURRENT_BINARY_DIR}/generated/generated.hpp"] @@ -35,6 +61,7 @@ command = [ comment = "Generate source files" verbatim = true +# Target form: This post-build command runs after 'custom-command' is built. [[target.custom-command.custom-command]] build-event = "post-build" command = [ @@ -47,6 +74,12 @@ byproducts = ["${CMAKE_CURRENT_BINARY_DIR}/custom-command-post-build.stamp"] comment = "Create a post-build stamp file" verbatim = true +# ----------------------------------------------------------------------------- +# Custom target (add_custom_target) +# ----------------------------------------------------------------------------- + +# A custom target runs commands independently of any executable/library. +# Setting 'all = true' makes it run as part of the default build. [target.custom-codegen] type = "custom" all = true diff --git a/docs/examples/generator-executable.md b/docs/examples/generator-executable.md new file mode 100644 index 0000000..8da3c86 --- /dev/null +++ b/docs/examples/generator-executable.md @@ -0,0 +1,69 @@ +--- +# Automatically generated from tests/generator-executable/cmake.toml - DO NOT EDIT +layout: default +title: Tests using an executable to generate sources for another target +permalink: /examples/generator-executable +parent: Examples +nav_order: 13 +--- + +# Tests using an executable to generate sources for another target + +This test demonstrates a common pattern: using an executable to generate sources + +for another target. The generator runs at build time and produces code that is + +consumed by the main executable. + +```toml +# +# CMake handles the dependency automatically: the generator executable is built +# first, then it runs to produce the generated sources, and finally the main +# executable is built using those sources. + +[project] +name = "generator-executable" +description = "Tests using an executable to generate sources for another target" + +# ----------------------------------------------------------------------------- +# Generator executable: produces code at build time +# ----------------------------------------------------------------------------- + +[target.generate_numbers] +type = "executable" +sources = ["src/generate_numbers.cpp"] + +# Post-build: confirm the generator was built +[[target.generate_numbers.custom-command]] +build-event = "post-build" +command = ["${CMAKE_COMMAND}", "-E", "echo", "Generator built successfully: $"] +comment = "Confirm generator executable was built" + +# ----------------------------------------------------------------------------- +# Main executable: uses generated sources +# ----------------------------------------------------------------------------- + +[target.main] +type = "executable" +sources = ["src/main.cpp"] +include-directories = ["${CMAKE_CURRENT_BINARY_DIR}/generated"] + +# Output form custom command: runs the generate_numbers executable to generate sources. +# The outputs are automatically added as sources to this target. +# The DEPENDS on "generate_numbers" ensures the generator is built before this runs. +[[target.main.custom-command]] +outputs = ["${CMAKE_CURRENT_BINARY_DIR}/generated/numbers.cpp"] +depends = ["generate_numbers"] +command = ["$", "${CMAKE_CURRENT_BINARY_DIR}/generated/numbers.cpp"] +comment = "Run generate_numbers to generate numbers.cpp" + +# Post-build: run the executable to verify it works +[[target.main.custom-command]] +build-event = "post-build" +command = ["$"] +comment = "Run the main executable to verify generated code works" +``` + + + +This page was automatically generated from [tests/generator-executable/cmake.toml](https://github.com/build-cpp/cmkr/tree/main/tests/generator-executable/cmake.toml). diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 43ad11f..c04a938 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -141,3 +141,13 @@ add_test( "$" build ) + +add_test( + NAME + generator-executable + WORKING_DIRECTORY + "${CMAKE_CURRENT_LIST_DIR}/generator-executable" + COMMAND + "$" + build +) diff --git a/tests/cmake.toml b/tests/cmake.toml index f994df2..3997904 100644 --- a/tests/cmake.toml +++ b/tests/cmake.toml @@ -77,3 +77,9 @@ name = "custom-command" working-directory = "custom-command" command = "$" arguments = ["build"] + +[[test]] +name = "generator-executable" +working-directory = "generator-executable" +command = "$" +arguments = ["build"] diff --git a/tests/generator-executable/cmake.toml b/tests/generator-executable/cmake.toml new file mode 100644 index 0000000..0cb90d3 --- /dev/null +++ b/tests/generator-executable/cmake.toml @@ -0,0 +1,49 @@ +# This test demonstrates a common pattern: using an executable to generate sources +# for another target. The generator runs at build time and produces code that is +# consumed by the main executable. +# +# CMake handles the dependency automatically: the generator executable is built +# first, then it runs to produce the generated sources, and finally the main +# executable is built using those sources. + +[project] +name = "generator-executable" +description = "Tests using an executable to generate sources for another target" + +# ----------------------------------------------------------------------------- +# Generator executable: produces code at build time +# ----------------------------------------------------------------------------- + +[target.generate_numbers] +type = "executable" +sources = ["src/generate_numbers.cpp"] + +# Post-build: confirm the generator was built +[[target.generate_numbers.custom-command]] +build-event = "post-build" +command = ["${CMAKE_COMMAND}", "-E", "echo", "Generator built successfully: $"] +comment = "Confirm generator executable was built" + +# ----------------------------------------------------------------------------- +# Main executable: uses generated sources +# ----------------------------------------------------------------------------- + +[target.main] +type = "executable" +sources = ["src/main.cpp"] +include-directories = ["${CMAKE_CURRENT_BINARY_DIR}/generated"] + +# Output form custom command: runs the generate_numbers executable to generate sources. +# The outputs are automatically added as sources to this target. +# The DEPENDS on "generate_numbers" ensures the generator is built before this runs. +[[target.main.custom-command]] +outputs = ["${CMAKE_CURRENT_BINARY_DIR}/generated/numbers.cpp"] +depends = ["generate_numbers"] +command = ["$", "${CMAKE_CURRENT_BINARY_DIR}/generated/numbers.cpp"] +comment = "Run generate_numbers to generate numbers.cpp" + +# Post-build: run the executable to verify it works +[[target.main.custom-command]] +build-event = "post-build" +command = ["$"] +comment = "Run the main executable to verify generated code works" diff --git a/tests/generator-executable/src/generate_numbers.cpp b/tests/generator-executable/src/generate_numbers.cpp new file mode 100644 index 0000000..981777a --- /dev/null +++ b/tests/generator-executable/src/generate_numbers.cpp @@ -0,0 +1,49 @@ +// Simple code generator that produces a C++ source file with a function +// that returns a vector of numbers. + +#include +#include +#include + +int main(int argc, char *argv[]) { + if (argc != 2) { + std::cerr << "Usage: " << argv[0] << " " << std::endl; + return 1; + } + + std::string output_path = argv[1]; + + // Create output directory if needed + auto last_slash = output_path.find_last_of("/\\"); + if (last_slash != std::string::npos) { + // Note: In a real generator, you'd create the directory + // CMake creates it for us when using add_custom_command + } + + std::ofstream out(output_path); + if (!out) { + std::cerr << "Failed to open: " << output_path << std::endl; + return 1; + } + + out << "// Auto-generated by codegen - DO NOT EDIT\n"; + out << "#include \n"; + out << "#include \n"; + out << "\n"; + out << "std::vector get_numbers() {\n"; + out << " return {"; + for (int i = 1; i <= 10; ++i) { + if (i > 1) + out << ", "; + out << i * i; // 1, 4, 9, 16, 25, 36, 49, 64, 81, 100 + } + out << "};\n"; + out << "}\n"; + out << "\n"; + out << "std::size_t get_numbers_count() {\n"; + out << " return 10;\n"; + out << "}\n"; + + std::cout << "Generated: " << output_path << std::endl; + return 0; +} diff --git a/tests/generator-executable/src/main.cpp b/tests/generator-executable/src/main.cpp new file mode 100644 index 0000000..0b5fae9 --- /dev/null +++ b/tests/generator-executable/src/main.cpp @@ -0,0 +1,26 @@ +// Main executable that uses the generated code + +#include +#include + +// Functions generated by codegen executable +std::vector get_numbers(); +std::size_t get_numbers_count(); + +int main() { + std::cout << "Numbers from generated code:" << std::endl; + + auto numbers = get_numbers(); + for (std::size_t i = 0; i < numbers.size(); ++i) { + std::cout << " numbers[" << i << "] = " << numbers[i] << std::endl; + } + + // Verify the count + if (numbers.size() == get_numbers_count()) { + std::cout << "Success: Generated " << numbers.size() << " numbers!" << std::endl; + return 0; + } else { + std::cerr << "Error: Count mismatch!" << std::endl; + return 1; + } +} From 411eb03db45969bc26ece95c919014113245cd92 Mon Sep 17 00:00:00 2001 From: Duncan Ogilvie Date: Sun, 12 Apr 2026 15:59:41 +0200 Subject: [PATCH 4/8] Protobuf example --- docs/cmake-toml.md | 11 ++ docs/examples/protobuf.md | 99 +++++++++++++++++ include/project_parser.hpp | 5 + src/cmake_generator.cpp | 16 +++ src/project_parser.cpp | 30 +++++ tests/CMakeLists.txt | 10 ++ tests/cmake.toml | 6 + tests/protobuf/.gitignore | 2 + tests/protobuf/cmake.toml | 81 ++++++++++++++ tests/protobuf/proto/addressbook.proto | 31 ++++++ tests/protobuf/src/main.cpp | 146 +++++++++++++++++++++++++ 11 files changed, 437 insertions(+) create mode 100644 docs/examples/protobuf.md create mode 100644 tests/protobuf/.gitignore create mode 100644 tests/protobuf/cmake.toml create mode 100644 tests/protobuf/proto/addressbook.proto create mode 100644 tests/protobuf/src/main.cpp diff --git a/docs/cmake-toml.md b/docs/cmake-toml.md index 5ad64a2..b803e36 100644 --- a/docs/cmake-toml.md +++ b/docs/cmake-toml.md @@ -199,6 +199,8 @@ tag = "v0.1" shallow = false # shallow clone (--depth 1) system = false subdir = "" # folder containing CMakeLists.txt +# Set cache variables before FetchContent_MakeAvailable +options = { BUILD_TESTS = false, BUILD_EXAMPLES = false } # Include a CMake project from a URL [fetch-content.urlcontent] @@ -213,10 +215,19 @@ sha1 = "502a4e25b8b209889c99c7fa0732102682c2e4ff" condition = "mycondition" svn = "https://svn-host.com/url" rev = "svn_rev" + +# For many options, you can also use a separate table: +# [fetch-content.bigproject.options] +# BUILD_TESTS = false +# BUILD_EXAMPLES = false +# SOME_STRING_OPTION = "value" +# SOME_LIST_OPTION = ["item1", "item2"] # becomes "item1;item2" ``` Table keys that match CMake variable names (`[A-Z_]+`) will be passed to the [`FetchContent_Declare`](https://cmake.org/cmake/help/latest/module/FetchContent.html#command:fetchcontent_declare) command. +The `options` field allows setting CACHE variables before `FetchContent_MakeAvailable` is called. This is useful for configuring options on the fetched dependency (e.g., disabling tests). Boolean values become `ON`/`OFF`, arrays become semicolon-separated strings. + ## Targets ```toml diff --git a/docs/examples/protobuf.md b/docs/examples/protobuf.md new file mode 100644 index 0000000..ef50708 --- /dev/null +++ b/docs/examples/protobuf.md @@ -0,0 +1,99 @@ +--- +# Automatically generated from tests/protobuf/cmake.toml - DO NOT EDIT +layout: default +title: Demonstrates protobuf integration with cmkr custom commands +permalink: /examples/protobuf +parent: Examples +nav_order: 14 +--- + +# Demonstrates protobuf integration with cmkr custom commands + +This test demonstrates using protobuf with cmkr for code generation. + +```toml +# +# The example: +# 1. Fetches protobuf from GitHub using FetchContent +# 2. Uses a custom command to generate C++ sources from a .proto file +# 3. Links an executable against the generated sources and protobuf library +# 4. Serializes and deserializes a simple message +# +# Note: protobuf v21.x is used because newer versions (v22+) require abseil-cpp +# as a dependency, which significantly complicates the build. + +[cmake] +version = "3.18...3.31" + +[project] +name = "protobuf-example" +description = "Demonstrates protobuf integration with cmkr custom commands" + +# ----------------------------------------------------------------------------- +# Fetch protobuf from GitHub +# Using v21.12 (last version before abseil-cpp became required) +# ----------------------------------------------------------------------------- + +[fetch-content.protobuf] +git = "https://github.com/protocolbuffers/protobuf" +tag = "v21.12" +options = { + protobuf_BUILD_TESTS = false, + protobuf_BUILD_EXAMPLES = false, + protobuf_BUILD_LIBPROTOC = false, + protobuf_BUILD_SHARED_LIBS = false, + protobuf_MSVC_STATIC_RUNTIME = false, + SKIP_INSTALL_ALL = true, + protobuf_WITH_ZLIB = false, +} + +# ----------------------------------------------------------------------------- +# Main executable using protobuf +# ----------------------------------------------------------------------------- + +[target.protobuf_example] +type = "executable" +sources = [ + "src/main.cpp", +] +include-directories = [ + "${CMAKE_CURRENT_BINARY_DIR}/generated", +] +link-libraries = ["protobuf::libprotobuf"] +compile-features = ["cxx_std_17"] + +# Create the generated directory before running protoc +cmake-before = """ +file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/generated") +""" + +# Custom command to generate C++ sources from .proto file +# This uses protoc from the fetched protobuf repository +[[target.protobuf_example.custom-command]] +outputs = [ + "${CMAKE_CURRENT_BINARY_DIR}/generated/addressbook.pb.h", + "${CMAKE_CURRENT_BINARY_DIR}/generated/addressbook.pb.cc", +] +depends = [ + "${CMAKE_CURRENT_SOURCE_DIR}/proto/addressbook.proto", + "protobuf::protoc", # Ensure protoc is built first +] +command = [ + "$", + "--cpp_out=${CMAKE_CURRENT_BINARY_DIR}/generated", + "-I${CMAKE_CURRENT_SOURCE_DIR}/proto", + "${CMAKE_CURRENT_SOURCE_DIR}/proto/addressbook.proto", +] +comment = "Generate C++ sources from addressbook.proto" +verbatim = true + +# Post-build: run the executable to verify it works +[[target.protobuf_example.custom-command]] +build-event = "post-build" +command = ["$"] +comment = "Run protobuf_example to verify serialization/deserialization works" +``` + + + +This page was automatically generated from [tests/protobuf/cmake.toml](https://github.com/build-cpp/cmkr/tree/main/tests/protobuf/cmake.toml). diff --git a/include/project_parser.hpp b/include/project_parser.hpp index 79e493f..49f60de 100644 --- a/include/project_parser.hpp +++ b/include/project_parser.hpp @@ -255,10 +255,15 @@ struct Subdir { ConditionVector include_after; }; +struct ContentOption { + mpark::variant value; +}; + struct Content { std::string name; std::string condition; tsl::ordered_map arguments; + tsl::ordered_map options; Condition cmake_before; Condition cmake_after; diff --git a/src/cmake_generator.cpp b/src/cmake_generator.cpp index f97368c..134da37 100644 --- a/src/cmake_generator.cpp +++ b/src/cmake_generator.cpp @@ -1124,6 +1124,22 @@ void generate_cmake(const char *path, const parser::Project *parent_project) { gen.conditional_includes(content.include_before); gen.conditional_cmake(content.cmake_before); + // Set cache variables before FetchContent + if (!content.options.empty()) { + for (const auto &opt : content.options) { + std::string value; + std::string type; + if (opt.second.value.index() == 0) { + value = mpark::get<0>(opt.second.value) ? "ON" : "OFF"; + type = "BOOL"; + } else { + value = mpark::get<1>(opt.second.value); + type = "STRING"; + } + cmd("set")(opt.first, value, "CACHE", type, RawArg("\"\""), "FORCE"); + } + } + std::string version_info; if (content.arguments.contains("GIT_TAG")) { version_info = " (" + content.arguments.at("GIT_TAG") + ")"; diff --git a/src/project_parser.cpp b/src/project_parser.cpp index 42d188b..752d925 100644 --- a/src/project_parser.cpp +++ b/src/project_parser.cpp @@ -593,7 +593,37 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p "system", ""); } + // Parse options table - these are CACHE variables set before FetchContent + if (c.contains("options")) { + c.visit("options"); + const auto &opts = toml::find(itr.second, "options").as_table(); + for (const auto &optItr : opts) { + ContentOption opt; + if (optItr.second.is_boolean()) { + opt.value = optItr.second.as_boolean(); + } else if (optItr.second.is_array()) { + // Arrays become semicolon-separated lists + std::string list_value; + for (const auto &list_val : optItr.second.as_array()) { + if (!list_value.empty()) { + list_value += ';'; + } + list_value += list_val.as_string(); + } + opt.value = list_value; + } else { + opt.value = optItr.second.as_string(); + } + content.options.emplace(optItr.first, opt); + } + } + for (const auto &argItr : itr.second.as_table()) { + // Skip keys that were already handled (like "options") + if (c.visisted(argItr.first)) { + continue; + } + std::string value; if (argItr.second.is_array()) { for (const auto &list_val : argItr.second.as_array()) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c04a938..15f03a8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -151,3 +151,13 @@ add_test( "$" build ) + +add_test( + NAME + protobuf + WORKING_DIRECTORY + "${CMAKE_CURRENT_LIST_DIR}/protobuf" + COMMAND + "$" + build +) diff --git a/tests/cmake.toml b/tests/cmake.toml index 3997904..13bcf77 100644 --- a/tests/cmake.toml +++ b/tests/cmake.toml @@ -83,3 +83,9 @@ name = "generator-executable" working-directory = "generator-executable" command = "$" arguments = ["build"] + +[[test]] +name = "protobuf" +working-directory = "protobuf" +command = "$" +arguments = ["build"] diff --git a/tests/protobuf/.gitignore b/tests/protobuf/.gitignore new file mode 100644 index 0000000..962a955 --- /dev/null +++ b/tests/protobuf/.gitignore @@ -0,0 +1,2 @@ +build/ +*.bin diff --git a/tests/protobuf/cmake.toml b/tests/protobuf/cmake.toml new file mode 100644 index 0000000..4d287a6 --- /dev/null +++ b/tests/protobuf/cmake.toml @@ -0,0 +1,81 @@ +# This test demonstrates using protobuf with cmkr for code generation. +# +# The example: +# 1. Fetches protobuf from GitHub using FetchContent +# 2. Uses a custom command to generate C++ sources from a .proto file +# 3. Links an executable against the generated sources and protobuf library +# 4. Serializes and deserializes a simple message +# +# Note: protobuf v21.x is used because newer versions (v22+) require abseil-cpp +# as a dependency, which significantly complicates the build. + +[cmake] +version = "3.18...3.31" + +[project] +name = "protobuf-example" +description = "Demonstrates protobuf integration with cmkr custom commands" + +# ----------------------------------------------------------------------------- +# Fetch protobuf from GitHub +# Using v21.12 (last version before abseil-cpp became required) +# ----------------------------------------------------------------------------- + +[fetch-content.protobuf] +git = "https://github.com/protocolbuffers/protobuf" +tag = "v21.12" +options = { + protobuf_BUILD_TESTS = false, + protobuf_BUILD_EXAMPLES = false, + protobuf_BUILD_LIBPROTOC = false, + protobuf_BUILD_SHARED_LIBS = false, + protobuf_MSVC_STATIC_RUNTIME = false, + SKIP_INSTALL_ALL = true, + protobuf_WITH_ZLIB = false, +} + +# ----------------------------------------------------------------------------- +# Main executable using protobuf +# ----------------------------------------------------------------------------- + +[target.protobuf_example] +type = "executable" +sources = [ + "src/main.cpp", +] +include-directories = [ + "${CMAKE_CURRENT_BINARY_DIR}/generated", +] +link-libraries = ["protobuf::libprotobuf"] +compile-features = ["cxx_std_17"] + +# Create the generated directory before running protoc +cmake-before = """ +file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/generated") +""" + +# Custom command to generate C++ sources from .proto file +# This uses protoc from the fetched protobuf repository +[[target.protobuf_example.custom-command]] +outputs = [ + "${CMAKE_CURRENT_BINARY_DIR}/generated/addressbook.pb.h", + "${CMAKE_CURRENT_BINARY_DIR}/generated/addressbook.pb.cc", +] +depends = [ + "${CMAKE_CURRENT_SOURCE_DIR}/proto/addressbook.proto", + "protobuf::protoc", # Ensure protoc is built first +] +command = [ + "$", + "--cpp_out=${CMAKE_CURRENT_BINARY_DIR}/generated", + "-I${CMAKE_CURRENT_SOURCE_DIR}/proto", + "${CMAKE_CURRENT_SOURCE_DIR}/proto/addressbook.proto", +] +comment = "Generate C++ sources from addressbook.proto" +verbatim = true + +# Post-build: run the executable to verify it works +[[target.protobuf_example.custom-command]] +build-event = "post-build" +command = ["$"] +comment = "Run protobuf_example to verify serialization/deserialization works" diff --git a/tests/protobuf/proto/addressbook.proto b/tests/protobuf/proto/addressbook.proto new file mode 100644 index 0000000..a166c67 --- /dev/null +++ b/tests/protobuf/proto/addressbook.proto @@ -0,0 +1,31 @@ +// A simple address book example demonstrating protobuf serialization. +// Based on the official protobuf tutorial example. + +syntax = "proto3"; + +package tutorial; + +// A person with basic contact information +message Person { + string name = 1; + int32 id = 2; + string email = 3; + + enum PhoneType { + MOBILE = 0; + HOME = 1; + WORK = 2; + } + + message PhoneNumber { + string number = 1; + PhoneType type = 2; + } + + repeated PhoneNumber phones = 4; +} + +// An address book containing multiple people +message AddressBook { + repeated Person people = 1; +} diff --git a/tests/protobuf/src/main.cpp b/tests/protobuf/src/main.cpp new file mode 100644 index 0000000..f7b4aa8 --- /dev/null +++ b/tests/protobuf/src/main.cpp @@ -0,0 +1,146 @@ +// Protobuf serialization/deserialization example +// This demonstrates using protobuf with cmkr for code generation + +#include +#include +#include +#include "addressbook.pb.h" + +// Creates a sample address book with some people +void create_address_book(tutorial::AddressBook &book) { + // Add first person + tutorial::Person *person1 = book.add_people(); + person1->set_name("Alice Smith"); + person1->set_id(1); + person1->set_email("alice@example.com"); + + // Add phone numbers for Alice + tutorial::Person::PhoneNumber *phone1 = person1->add_phones(); + phone1->set_number("555-1234"); + phone1->set_type(tutorial::Person::HOME); + + tutorial::Person::PhoneNumber *phone2 = person1->add_phones(); + phone2->set_number("555-5678"); + phone2->set_type(tutorial::Person::WORK); + + // Add second person + tutorial::Person *person2 = book.add_people(); + person2->set_name("Bob Johnson"); + person2->set_id(2); + person2->set_email("bob@example.com"); + + tutorial::Person::PhoneNumber *phone3 = person2->add_phones(); + phone3->set_number("555-9999"); + phone3->set_type(tutorial::Person::MOBILE); +} + +// Prints the contents of an address book +void print_address_book(const tutorial::AddressBook &book) { + std::cout << "Address Book (" << book.people_size() << " people):\n"; + std::cout << "==========================================\n\n"; + + for (int i = 0; i < book.people_size(); ++i) { + const tutorial::Person &person = book.people(i); + + std::cout << "Person ID: " << person.id() << "\n"; + std::cout << " Name: " << person.name() << "\n"; + std::cout << " Email: " << person.email() << "\n"; + + for (int j = 0; j < person.phones_size(); ++j) { + const tutorial::Person::PhoneNumber &phone = person.phones(j); + std::cout << " Phone #" << (j + 1) << ": " << phone.number() << " ("; + + switch (phone.type()) { + case tutorial::Person::MOBILE: + std::cout << "mobile"; + break; + case tutorial::Person::HOME: + std::cout << "home"; + break; + case tutorial::Person::WORK: + std::cout << "work"; + break; + default: + std::cout << "unknown"; + } + std::cout << ")\n"; + } + std::cout << "\n"; + } +} + +int main() { + // Verify that the version of the library that we linked against is + // compatible with the version of the headers we compiled against. + GOOGLE_PROTOBUF_VERIFY_VERSION; + + std::cout << "Protobuf cmkr Example\n"; + std::cout << "=====================\n\n"; + + // Create an address book + tutorial::AddressBook original_book; + create_address_book(original_book); + + std::cout << "Original Address Book:\n"; + print_address_book(original_book); + + // Serialize to string + std::string serialized; + if (!original_book.SerializeToString(&serialized)) { + std::cerr << "Error: Failed to serialize address book\n"; + return 1; + } + std::cout << "Serialized size: " << serialized.size() << " bytes\n\n"; + + // Deserialize into a new object + tutorial::AddressBook deserialized_book; + if (!deserialized_book.ParseFromString(serialized)) { + std::cerr << "Error: Failed to deserialize address book\n"; + return 1; + } + + std::cout << "Deserialized Address Book:\n"; + print_address_book(deserialized_book); + + // Write to file + const char *filename = "addressbook.bin"; + { + std::ofstream output(filename, std::ios::binary); + if (!original_book.SerializeToOstream(&output)) { + std::cerr << "Error: Failed to write to file\n"; + return 1; + } + } + std::cout << "Written to file: " << filename << "\n"; + + // Read back from file + tutorial::AddressBook file_book; + { + std::ifstream input(filename, std::ios::binary); + if (!file_book.ParseFromIstream(&input)) { + std::cerr << "Error: Failed to read from file\n"; + return 1; + } + } + std::cout << "Read back from file successfully!\n\n"; + + // Verify all three versions are identical + bool all_match = true; + if (original_book.SerializeAsString() != deserialized_book.SerializeAsString()) { + std::cerr << "Error: Original and deserialized don't match!\n"; + all_match = false; + } + if (original_book.SerializeAsString() != file_book.SerializeAsString()) { + std::cerr << "Error: Original and file don't match!\n"; + all_match = false; + } + + if (all_match) { + std::cout << "SUCCESS: All serialization methods produced identical results!\n"; + } + + // Clean up any global objects allocated by protobuf + google::protobuf::ShutdownProtobufLibrary(); + + return all_match ? 0 : 1; +} From 6d41c92e37a62db32cc544182713e09ececb6f0a Mon Sep 17 00:00:00 2001 From: Duncan Ogilvie Date: Tue, 14 Apr 2026 12:48:37 +0200 Subject: [PATCH 5/8] Fix custom command review issues --- docs/cmake-toml.md | 6 +- docs/examples/custom-command.md | 38 +++++++- src/cmake_generator.cpp | 88 ++++++++++++++++--- src/project_parser.cpp | 23 ++++- tests/custom-command/cmake.toml | 38 +++++++- .../cmake/verify_file_exists.cmake | 9 ++ 6 files changed, 181 insertions(+), 21 deletions(-) create mode 100644 tests/custom-command/cmake/verify_file_exists.cmake diff --git a/docs/cmake-toml.md b/docs/cmake-toml.md index b803e36..a0b9168 100644 --- a/docs/cmake-toml.md +++ b/docs/cmake-toml.md @@ -300,7 +300,7 @@ Use `[[target..custom-command]]` to map directly to [`add_custom_command`] # OUTPUT form (add_custom_command(OUTPUT ...)) [[target.mytarget.custom-command]] condition = "mycondition" -outputs = ["${CMAKE_CURRENT_BINARY_DIR}/generated.cpp"] +outputs = ["generated.cpp"] # relative paths are interpreted from ${CMAKE_CURRENT_BINARY_DIR} command = [ "${CMAKE_COMMAND}", "-DOUTPUT=${CMAKE_CURRENT_BINARY_DIR}/generated.cpp", @@ -308,7 +308,7 @@ command = [ "${CMAKE_CURRENT_SOURCE_DIR}/cmake/generate.cmake", ] depends = ["cmake/generate.cmake"] -byproducts = ["${CMAKE_CURRENT_BINARY_DIR}/generated.hpp"] +byproducts = ["generated.hpp"] main-dependency = "cmake/generate.cmake" implicit-depends = [["CXX", "src/input.idl"]] working-directory = "${CMAKE_CURRENT_BINARY_DIR}" @@ -335,7 +335,7 @@ command-expand-lists = false uses-terminal = false ``` -For each custom command entry, exactly one of `outputs` or `build-event` is required. +For each custom command entry, exactly one of `outputs` or `build-event` is required. Relative `outputs`/`byproducts` paths follow CMake and are interpreted from the current binary directory. `build-event` is only valid for non-`interface` targets, and `pre-link` is not supported for `type = "custom"`. A table mapping the cmkr features to the relevant CMake construct and the relevant documentation pages: diff --git a/docs/examples/custom-command.md b/docs/examples/custom-command.md index 4a4b1ac..eb328a8 100644 --- a/docs/examples/custom-command.md +++ b/docs/examples/custom-command.md @@ -46,10 +46,11 @@ sources = ["src/main.cpp"] include-directories = ["${CMAKE_CURRENT_BINARY_DIR}/generated"] # Output form: This custom command generates source files before building. +# Relative outputs follow CMake and are interpreted from the binary directory. # The outputs are automatically added as sources to the target. [[target.custom-command.custom-command]] -outputs = ["${CMAKE_CURRENT_BINARY_DIR}/generated/generated.cpp"] -byproducts = ["${CMAKE_CURRENT_BINARY_DIR}/generated/generated.hpp"] +outputs = ["generated/generated.cpp"] +byproducts = ["generated/generated.hpp"] depends = ["cmake/generate_source.cmake"] command = [ "${CMAKE_COMMAND}", @@ -92,6 +93,39 @@ command = [ byproducts = ["${CMAKE_CURRENT_BINARY_DIR}/custom-target.stamp"] comment = "Run custom target" verbatim = true + +# ----------------------------------------------------------------------------- +# Template-based custom target +# ----------------------------------------------------------------------------- + +# Custom target options should also work when the target type comes from a template. +[template.generated-custom-target] +type = "custom" +all = true +verbatim = true + +[target.custom-codegen-template] +type = "generated-custom-target" +command = [ + "${CMAKE_COMMAND}", + "-DINPUT=${CMAKE_CURRENT_BINARY_DIR}/template-custom-target.stamp", + "-P", + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/verify_file_exists.cmake", +] +comment = "Verify the generated stamp exists" + +# The output-form custom command should automatically become a dependency of the +# custom target, so the verification command sees the stamp file. +[[target.custom-codegen-template.custom-command]] +outputs = ["template-custom-target.stamp"] +command = [ + "${CMAKE_COMMAND}", + "-E", + "touch", + "${CMAKE_CURRENT_BINARY_DIR}/template-custom-target.stamp", +] +comment = "Create a stamp file for the template-based custom target" +verbatim = true ``` diff --git a/src/cmake_generator.cpp b/src/cmake_generator.cpp index 134da37..60916d2 100644 --- a/src/cmake_generator.cpp +++ b/src/cmake_generator.cpp @@ -1222,6 +1222,14 @@ void generate_cmake(const char *path, const parser::Project *parent_project) { } } + auto resolved_target_type = target.type; + if (tmplate != nullptr) { + if (resolved_target_type != parser::target_template) { + throw_target_error("Unreachable code, unexpected target type for template"); + } + resolved_target_type = tmplate->outline.type; + } + ConditionScope cs(gen, target.condition); // Detect if there is cmake included before/after the target @@ -1323,6 +1331,31 @@ void generate_cmake(const char *path, const parser::Project *parent_project) { } custom_commands.insert(custom_commands.end(), target.custom_commands.begin(), target.custom_commands.end()); + auto normalize_generated_output_source = [](const std::string &output) { + auto starts_with = [](const std::string &value, const std::string &prefix) { + return value.rfind(prefix, 0) == 0; + }; + if (fs::path(output).is_absolute()) { + return output; + } + if (starts_with(output, "${CMAKE_CURRENT_BINARY_DIR}") || starts_with(output, "${CMAKE_BINARY_DIR}") || + starts_with(output, "${PROJECT_BINARY_DIR}") || starts_with(output, "${CMAKE_CURRENT_SOURCE_DIR}") || + starts_with(output, "${CMAKE_SOURCE_DIR}") || starts_with(output, "${PROJECT_SOURCE_DIR}") || + starts_with(output, "${CMAKE_CURRENT_LIST_DIR}") || starts_with(output, "$ENV{") || starts_with(output, "$CACHE{") || + starts_with(output, "${")) { + return output; + } + return "${CMAKE_CURRENT_BINARY_DIR}/" + output; + }; + + parser::Condition> mcustom_target_depends; + if (resolved_target_type == parser::target_custom) { + auto &unconditional_depends = mcustom_target_depends[""]; + for (const auto &dep : custom_target.depends) { + unconditional_depends.insert(dep); + } + } + // Merge the sources from the template and the target. The sources // without condition need to be processed first parser::Condition> msources; @@ -1347,12 +1380,15 @@ void generate_cmake(const char *path, const parser::Project *parent_project) { } auto &condition_sources = msources[custom_command.condition]; for (const auto &output : custom_command.outputs) { - condition_sources.insert(output); + condition_sources.insert(normalize_generated_output_source(output)); + if (resolved_target_type == parser::target_custom) { + mcustom_target_depends[custom_command.condition].insert(output); + } } } // Improve IDE support - if (target.type != parser::target_interface) { + if (resolved_target_type != parser::target_interface) { msources[""].insert("cmake.toml"); } @@ -1386,7 +1422,7 @@ void generate_cmake(const char *path, const parser::Project *parent_project) { } // Make sure there are source files for the languages used by the project - switch (target.type) { + switch (resolved_target_type) { case parser::target_executable: case parser::target_library: case parser::target_shared: @@ -1430,14 +1466,7 @@ void generate_cmake(const char *path, const parser::Project *parent_project) { } }); - auto target_type = target.type; - - if (tmplate != nullptr) { - if (target_type != parser::target_template) { - throw_target_error("Unreachable code, unexpected target type for template"); - } - target_type = tmplate->outline.type; - } + auto target_type = resolved_target_type; std::string add_command; std::string target_type_string; @@ -1484,6 +1513,39 @@ void generate_cmake(const char *path, const parser::Project *parent_project) { throw_target_error("Unimplemented enum value"); } + auto has_custom_target_depends = false; + auto custom_target_depends_var = target.name + "_DEPENDS"; + auto custom_target_depends_with_set = true; + if (is_custom_target) { + for (const auto &itr : mcustom_target_depends) { + if (!itr.second.empty()) { + has_custom_target_depends = true; + break; + } + } + if (has_custom_target_depends && mcustom_target_depends[""].empty()) { + custom_target_depends_with_set = false; + cmd("set")(custom_target_depends_var, RawArg("\"\"")).endl(); + } + gen.handle_condition(mcustom_target_depends, [&](const std::string &condition, const tsl::ordered_set &depend_set) { + std::vector depends; + depends.reserve(depend_set.size()); + for (const auto &depend : depend_set) { + depends.push_back(depend); + } + + if (custom_target_depends_with_set) { + if (!condition.empty()) { + throw_target_error("Unreachable code, make sure unconditional depends are first"); + } + cmd("set")(custom_target_depends_var, depends); + custom_target_depends_with_set = false; + } else { + cmd("list")("APPEND", custom_target_depends_var, depends); + } + }); + } + auto append_keyword = [](std::vector &args, const std::string &keyword) { args.emplace_back(keyword); }; @@ -1622,9 +1684,9 @@ void generate_cmake(const char *path, const parser::Project *parent_project) { append_keyword(args, "ALL"); } append_commands(args, custom_target.commands); - if (!custom_target.depends.empty()) { + if (has_custom_target_depends) { append_keyword(args, "DEPENDS"); - append_values(args, custom_target.depends); + append_value(args, "${" + custom_target_depends_var + "}"); } if (!custom_target.byproducts.empty()) { append_keyword(args, "BYPRODUCTS"); diff --git a/src/project_parser.cpp b/src/project_parser.cpp index 752d925..f42061b 100644 --- a/src/project_parser.cpp +++ b/src/project_parser.cpp @@ -297,6 +297,18 @@ static std::string normalize_build_event(std::string build_event) { return build_event; } +static TargetType resolve_target_type(const Target &target, const std::vector