diff --git a/docs/cmake-toml.md b/docs/cmake-toml.md index cdeb4dd..7573b51 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 @@ -246,6 +257,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 = false # incompatible with job-pool when true +command-expand-lists = true cmake-before = """ message(STATUS "CMake injected before the target") @@ -263,6 +292,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 = ["generated.cpp"] # relative paths are interpreted from ${CMAKE_CURRENT_BINARY_DIR} +command = [ + "${CMAKE_COMMAND}", + "-DOUTPUT=${CMAKE_CURRENT_BINARY_DIR}/generated.cpp", + "-P", + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/generate.cmake", +] +depends = ["cmake/generate.cmake"] +byproducts = ["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" # incompatible with uses-terminal when uses-terminal = true +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. 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"`. In both `add_custom_command(OUTPUT ...)` and `add_custom_target(...)`, `job-pool` cannot be combined with `uses-terminal = true`. When `append = true` is used with the `OUTPUT` form, at least one `command`/`commands` entry or `depends` must also be provided. + A table mapping the cmkr features to the relevant CMake construct and the relevant documentation pages: | cmkr | CMake construct | Description | @@ -279,6 +353,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 +419,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..862a825 --- /dev/null +++ b/docs/examples/custom-command.md @@ -0,0 +1,141 @@ +--- +# 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 + +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. +# 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 = ["generated/generated.cpp"] +byproducts = ["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 form: This post-build command runs after 'custom-command' is built. +[[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 + +# ----------------------------------------------------------------------------- +# 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 +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 + +# ----------------------------------------------------------------------------- +# 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 + +# A target-level all = false should override the template's all = true. +# If this target is incorrectly included in the default build, the build fails. +[target.custom-codegen-template-disabled] +type = "generated-custom-target" +all = false +command = ["${CMAKE_COMMAND}", "-E", "false"] +comment = "This target must not run as part of the default build" +``` + + + +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/docs/examples/generator-executable.md b/docs/examples/generator-executable.md new file mode 100644 index 0000000..17de60a --- /dev/null +++ b/docs/examples/generator-executable.md @@ -0,0 +1,74 @@ +--- +# 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"] + +# Create the generated directory before the generator runs. +cmake-before = """ +file(MAKE_DIRECTORY "${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/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 43d40db..49f60de 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; @@ -154,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 ab697cb..40fab8c 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 @@ -1123,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") + ")"; @@ -1205,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 @@ -1244,6 +1269,103 @@ 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_target.has_all && custom.has_all) { + custom_target.has_all = true; + 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()); + + if (resolved_target_type == parser::target_custom && custom_target.has_job_pool && custom_target.has_uses_terminal && + custom_target.uses_terminal) { + throw_target_error("job-pool cannot be used with uses-terminal"); + } + + auto has_cmake_path_reference = [](const std::string &value) { + return value.find("${") != std::string::npos || value.find("$ENV{") != std::string::npos || + value.find("$CACHE{") != std::string::npos; + }; + + auto normalize_generated_output_source = [&has_cmake_path_reference](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{") || + has_cmake_path_reference(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; @@ -1262,8 +1384,21 @@ 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(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"); } @@ -1297,7 +1432,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: @@ -1316,8 +1451,7 @@ void generate_cmake(const char *path, const parser::Project *parent_project) { // Make sure relative source files exist for (const auto &source : sources) { - auto var_index = source.find("${"); - if (var_index != std::string::npos) + if (has_cmake_path_reference(source)) continue; const auto &source_path = fs::path(path) / source; if (!fs::exists(source_path)) { @@ -1341,18 +1475,12 @@ 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; std::string target_scope; + bool is_custom_target = false; switch (target_type) { case parser::target_executable: @@ -1381,9 +1509,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 +1522,157 @@ 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); + }; + 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 +1686,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 (has_custom_target_depends) { + append_keyword(args, "DEPENDS"); + append_value(args, "${" + custom_target_depends_var + "}"); + } + 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 +1738,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 +1781,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..1157187 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,105 @@ 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; +} + +static TargetType resolve_target_type(const Target &target, const std::vector