Compare commits

..

8 Commits

Author SHA1 Message Date
Duncan Ogilvie 39b4522bc5 Validate APPEND custom commands more strictly 2026-04-14 20:09:11 +02:00
Duncan Ogilvie da4ec26df0 Fix latest custom target review issues 2026-04-14 15:32:07 +02:00
Duncan Ogilvie 31f77ab328 Address latest custom command review feedback 2026-04-14 15:11:31 +02:00
Duncan Ogilvie 6d41c92e37 Fix custom command review issues 2026-04-14 15:07:23 +02:00
Duncan Ogilvie 411eb03db4 Protobuf example 2026-04-14 15:07:16 +02:00
Duncan Ogilvie 9f80ce4429 Generator executable example 2026-04-14 15:06:54 +02:00
Duncan Ogilvie 8650f53174 Simple post build test 2026-04-14 15:06:53 +02:00
Duncan Ogilvie 6cb1009c0e Add support for custom commands 2026-04-14 15:06:53 +02:00
22 changed files with 1827 additions and 33 deletions
-1
View File
@@ -10,7 +10,6 @@ if(CMAKE_SCRIPT_MODE_FILE)
set(CMAKE_BINARY_DIR "${CMAKE_BINARY_DIR}/build") set(CMAKE_BINARY_DIR "${CMAKE_BINARY_DIR}/build")
set(CMAKE_CURRENT_BINARY_DIR "${CMAKE_BINARY_DIR}") set(CMAKE_CURRENT_BINARY_DIR "${CMAKE_BINARY_DIR}")
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}") file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}")
file(WRITE "${CMAKE_BINARY_DIR}/.gitignore" "# This file is autogenerated by cmkr.\n*\n")
endif() endif()
# Set these from the command line to customize for development/debugging purposes # Set these from the command line to customize for development/debugging purposes
+78 -1
View File
@@ -199,6 +199,8 @@ tag = "v0.1"
shallow = false # shallow clone (--depth 1) shallow = false # shallow clone (--depth 1)
system = false system = false
subdir = "" # folder containing CMakeLists.txt 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 # Include a CMake project from a URL
[fetch-content.urlcontent] [fetch-content.urlcontent]
@@ -213,10 +215,19 @@ sha1 = "502a4e25b8b209889c99c7fa0732102682c2e4ff"
condition = "mycondition" condition = "mycondition"
svn = "https://svn-host.com/url" svn = "https://svn-host.com/url"
rev = "svn_rev" 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. 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 ## Targets
```toml ```toml
@@ -246,6 +257,24 @@ link-options = [""] # linker flags
private-link-options = [""] private-link-options = [""]
precompile-headers = [""] # precompiled headers precompile-headers = [""] # precompiled headers
private-precompile-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 = """ cmake-before = """
message(STATUS "CMake injected before the target") message(STATUS "CMake injected before the target")
@@ -263,6 +292,51 @@ CXX_STANDARD_REQUIRED = true
FOLDER = "MyFolder" FOLDER = "MyFolder"
``` ```
### Custom commands
Use `[[target.<name>.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: A table mapping the cmkr features to the relevant CMake construct and the relevant documentation pages:
| cmkr | CMake construct | Description | | 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-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. | | `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. | | `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.<name>.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. | | `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: The default [visibility](/basics) is as follows:
@@ -342,4 +419,4 @@ files = ["content/my.png"]
dirs = ["include"] dirs = ["include"]
configs = ["Release", "Debug"] configs = ["Release", "Debug"]
optional = false optional = false
``` ```
+141
View File
@@ -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: $<TARGET_FILE_NAME:hello>"]
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"
```
<sup><sub>This page was automatically generated from [tests/custom-command/cmake.toml](https://github.com/build-cpp/cmkr/tree/main/tests/custom-command/cmake.toml).</sub></sup>
+74
View File
@@ -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: $<TARGET_FILE_NAME:generate_numbers>"]
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 = ["$<TARGET_FILE:generate_numbers>", "${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 = ["$<TARGET_FILE:main>"]
comment = "Run the main executable to verify generated code works"
```
<sup><sub>This page was automatically generated from [tests/generator-executable/cmake.toml](https://github.com/build-cpp/cmkr/tree/main/tests/generator-executable/cmake.toml).</sub></sup>
+99
View File
@@ -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 = [
"$<TARGET_FILE:protobuf::protoc>",
"--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 = ["$<TARGET_FILE:protobuf_example>"]
comment = "Run protobuf_example to verify serialization/deserialization works"
```
<sup><sub>This page was automatically generated from [tests/protobuf/cmake.toml](https://github.com/build-cpp/cmkr/tree/main/tests/protobuf/cmake.toml).</sub></sup>
+106
View File
@@ -74,6 +74,104 @@ enum TargetType {
extern const char *targetTypeNames[target_last]; extern const char *targetTypeNames[target_last];
struct Target { struct Target {
struct CustomTarget {
bool has_all = false;
bool all = false;
bool has_command = false;
bool has_commands = false;
std::vector<std::vector<std::string>> commands;
std::vector<std::string> depends;
std::vector<std::string> 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<std::vector<std::string>> commands;
std::vector<std::string> outputs;
std::string build_event;
bool has_append = false;
bool append = false;
bool has_main_dependency = false;
std::string main_dependency;
std::vector<std::string> depends;
std::vector<std::string> byproducts;
std::vector<ImplicitDependency> 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; std::string name;
TargetType type = target_last; TargetType type = target_last;
std::string type_name; std::string type_name;
@@ -107,6 +205,9 @@ struct Target {
ConditionVector dependencies; ConditionVector dependencies;
CustomTarget custom_target;
std::vector<CustomCommand> custom_commands;
std::string condition; std::string condition;
std::string alias; std::string alias;
Condition<tsl::ordered_map<std::string, std::string>> properties; Condition<tsl::ordered_map<std::string, std::string>> properties;
@@ -154,10 +255,15 @@ struct Subdir {
ConditionVector include_after; ConditionVector include_after;
}; };
struct ContentOption {
mpark::variant<bool, std::string> value;
};
struct Content { struct Content {
std::string name; std::string name;
std::string condition; std::string condition;
tsl::ordered_map<std::string, std::string> arguments; tsl::ordered_map<std::string, std::string> arguments;
tsl::ordered_map<std::string, ContentOption> options;
Condition<std::string> cmake_before; Condition<std::string> cmake_before;
Condition<std::string> cmake_after; Condition<std::string> cmake_after;
+362 -31
View File
@@ -4,6 +4,7 @@
#include "fs.hpp" #include "fs.hpp"
#include "project_parser.hpp" #include "project_parser.hpp"
#include <algorithm>
#include <cstdio> #include <cstdio>
#include <memory> #include <memory>
#include <sstream> #include <sstream>
@@ -1123,6 +1124,22 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
gen.conditional_includes(content.include_before); gen.conditional_includes(content.include_before);
gen.conditional_cmake(content.cmake_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; std::string version_info;
if (content.arguments.contains("GIT_TAG")) { if (content.arguments.contains("GIT_TAG")) {
version_info = " (" + content.arguments.at("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); ConditionScope cs(gen, target.condition);
// Detect if there is cmake included before/after the target // 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_includes(target.include_before);
gen.conditional_cmake(target.cmake_before); gen.conditional_cmake(target.cmake_before);
auto merge_unique_vector = [](std::vector<std::string> &destination, const std::vector<std::string> &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<parser::Target::CustomCommand> 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<tsl::ordered_set<std::string>> 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 // Merge the sources from the template and the target. The sources
// without condition need to be processed first // without condition need to be processed first
parser::Condition<tsl::ordered_set<std::string>> msources; parser::Condition<tsl::ordered_set<std::string>> msources;
@@ -1262,8 +1384,21 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
} }
merge_sources(target.sources); 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 // Improve IDE support
if (target.type != parser::target_interface) { if (resolved_target_type != parser::target_interface) {
msources[""].insert("cmake.toml"); 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 // 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_executable:
case parser::target_library: case parser::target_library:
case parser::target_shared: 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 // Make sure relative source files exist
for (const auto &source : sources) { for (const auto &source : sources) {
auto var_index = source.find("${"); if (has_cmake_path_reference(source))
if (var_index != std::string::npos)
continue; continue;
const auto &source_path = fs::path(path) / source; const auto &source_path = fs::path(path) / source;
if (!fs::exists(source_path)) { 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; auto target_type = resolved_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;
}
std::string add_command; std::string add_command;
std::string target_type_string; std::string target_type_string;
std::string target_scope; std::string target_scope;
bool is_custom_target = false;
switch (target_type) { switch (target_type) {
case parser::target_executable: case parser::target_executable:
@@ -1381,9 +1509,7 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
target_scope = "INTERFACE"; target_scope = "INTERFACE";
break; break;
case parser::target_custom: case parser::target_custom:
// TODO: add proper support, this is hacky is_custom_target = true;
add_command = "add_custom_target";
target_type_string = "SOURCES";
target_scope = "PUBLIC"; target_scope = "PUBLIC";
break; break;
case parser::target_object: 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"); 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<std::string> &depend_set) {
std::vector<std::string> 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<RawArg> &args, const std::string &keyword) {
args.emplace_back(keyword);
};
auto append_value = [](std::vector<RawArg> &args, const std::string &value) {
args.emplace_back(Command::quote(value));
};
auto append_values = [&](std::vector<RawArg> &args, const std::vector<std::string> &values) {
for (const auto &value : values) {
append_value(args, value);
}
};
auto append_commands = [&](std::vector<RawArg> &args, const std::vector<std::vector<std::string>> &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<RawArg> 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. // Handle custom add commands from templates.
if (tmplate != nullptr && !tmplate->add_function.empty()) { if (tmplate != nullptr && !tmplate->add_function.empty()) {
add_command = tmplate->add_function; add_command = tmplate->add_function;
@@ -1409,6 +1686,51 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
"${" + sources_var + "}"); "${" + sources_var + "}");
} }
} }
} else if (is_custom_target) {
std::vector<RawArg> 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 { } else {
cmd(add_command)(target.name, target_type_string).endl(); cmd(add_command)(target.name, target_type_string).endl();
if (has_sources) { 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 // TODO: support sources from other directories
if (has_sources) { if (has_sources) {
cmd("source_group")("TREE", "${CMAKE_CURRENT_SOURCE_DIR}", "FILES", "${" + sources_var + "}").endl(); 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) { auto gen_target_cmds = [&](const parser::Target &t) {
target_cmd("target_compile_definitions", t.compile_definitions, target_scope); if (!is_custom_target) {
target_cmd("target_compile_definitions", t.private_compile_definitions, "PRIVATE"); 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.compile_features, target_scope);
target_cmd("target_compile_features", t.private_compile_features, "PRIVATE"); 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.compile_options, target_scope);
target_cmd("target_compile_options", t.private_compile_options, "PRIVATE"); 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.include_directories, target_scope);
target_cmd("target_include_directories", t.private_include_directories, "PRIVATE"); 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.link_directories, target_scope);
target_cmd("target_link_directories", t.private_link_directories, "PRIVATE"); 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.link_libraries, target_scope);
link_cmd("target_link_libraries", t.private_link_libraries, "PRIVATE"); 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.link_options, target_scope);
target_cmd("target_link_options", t.private_link_options, "PRIVATE"); 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.precompile_headers, target_scope);
target_cmd("target_precompile_headers", t.private_precompile_headers, "PRIVATE"); target_cmd("target_precompile_headers", t.private_precompile_headers, "PRIVATE");
}
link_cmd("add_dependencies", t.dependencies, ""); link_cmd("add_dependencies", t.dependencies, "");
}; };
+377
View File
@@ -1,6 +1,7 @@
#include "project_parser.hpp" #include "project_parser.hpp"
#include "fs.hpp" #include "fs.hpp"
#include <cctype>
#include <deque> #include <deque>
#include <stdexcept> #include <stdexcept>
#include <toml.hpp> #include <toml.hpp>
@@ -209,6 +210,105 @@ class TomlCheckerRoot {
} }
}; };
static std::vector<std::string> parse_string_array(const TomlBasicValue &value, const toml::key &key) {
if (!value.is_array()) {
throw_key_error("Expected an array", key, value);
}
std::vector<std::string> 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<std::vector<std::string>> 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<std::vector<std::string>> 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<Target::CustomCommand::ImplicitDependency> parse_implicit_dependencies(const TomlBasicValue &value, const toml::key &key) {
if (!value.is_array()) {
throw_key_error("Expected an array", key, value);
}
std::vector<Target::CustomCommand::ImplicitDependency> 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<char>(std::toupper(static_cast<unsigned char>(ch)));
}
}
return build_event;
}
static TargetType resolve_target_type(const Target &target, const std::vector<Template> &templates) {
if (target.type != target_template) {
return target.type;
}
for (const auto &tmplate : templates) {
if (target.type_name == tmplate.outline.name) {
return tmplate.outline.type;
}
}
return target.type;
}
Project::Project(const Project *parent, const std::string &path, bool build) : parent(parent) { Project::Project(const Project *parent, const std::string &path, bool build) : parent(parent) {
const auto toml_path = fs::path(path) / "cmake.toml"; const auto toml_path = fs::path(path) / "cmake.toml";
if (!fs::exists(toml_path)) { if (!fs::exists(toml_path)) {
@@ -505,7 +605,37 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p
"system", ""); "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()) { 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; std::string value;
if (argItr.second.is_array()) { if (argItr.second.is_array()) {
for (const auto &list_val : argItr.second.as_array()) { for (const auto &list_val : argItr.second.as_array()) {
@@ -698,6 +828,253 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p
t.optional("dependencies", target.dependencies); t.optional("dependencies", target.dependencies);
const auto resolved_target_type = resolve_target_type(target, templates);
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 (resolved_target_type == target_interface) {
throw_key_error("build-event cannot be used with type = \"interface\"", "build-event", custom.find("build-event"));
}
if (resolved_target_type == target_custom && custom_command.build_event == "PRE_LINK") {
throw_key_error("build-event = \"pre-link\" cannot be used with type = \"custom\"", "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.has_job_pool && custom_command.has_uses_terminal && custom_command.uses_terminal) {
throw_key_error("job-pool cannot be used with uses-terminal", "job-pool", custom.find("job-pool"));
}
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.is_output_form() && custom_command.append && custom_command.commands.empty() && custom_command.depends.empty()) {
throw_key_error("append requires at least one command or depends", "append", custom.find("append"));
}
if (custom_command.commands.empty() &&
!(custom_command.is_output_form() && custom_command.append && !custom_command.depends.empty())) {
throw_key_error("At least one command is required", "custom-command", custom_command_value);
}
target.custom_commands.push_back(std::move(custom_command));
}
}
if (resolved_target_type == target_custom && target.custom_target.has_job_pool && target.custom_target.has_uses_terminal &&
target.custom_target.uses_terminal) {
throw_key_error("job-pool cannot be used with uses-terminal", "job-pool", t.find("job-pool"));
}
if (resolved_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<std::string> msvc_runtime; Condition<std::string> msvc_runtime;
t.optional("msvc-runtime", msvc_runtime); t.optional("msvc-runtime", msvc_runtime);
for (const auto &cond_itr : msvc_runtime) { for (const auto &cond_itr : msvc_runtime) {
+30
View File
@@ -131,3 +131,33 @@ add_test(
"$<TARGET_FILE:cmkr>" "$<TARGET_FILE:cmkr>"
build build
) )
add_test(
NAME
custom-command
WORKING_DIRECTORY
"${CMAKE_CURRENT_LIST_DIR}/custom-command"
COMMAND
"$<TARGET_FILE:cmkr>"
build
)
add_test(
NAME
generator-executable
WORKING_DIRECTORY
"${CMAKE_CURRENT_LIST_DIR}/generator-executable"
COMMAND
"$<TARGET_FILE:cmkr>"
build
)
add_test(
NAME
protobuf
WORKING_DIRECTORY
"${CMAKE_CURRENT_LIST_DIR}/protobuf"
COMMAND
"$<TARGET_FILE:cmkr>"
build
)
+18
View File
@@ -71,3 +71,21 @@ name = "objective-c"
working-directory = "objective-c" working-directory = "objective-c"
command = "$<TARGET_FILE:cmkr>" command = "$<TARGET_FILE:cmkr>"
arguments = ["build"] arguments = ["build"]
[[test]]
name = "custom-command"
working-directory = "custom-command"
command = "$<TARGET_FILE:cmkr>"
arguments = ["build"]
[[test]]
name = "generator-executable"
working-directory = "generator-executable"
command = "$<TARGET_FILE:cmkr>"
arguments = ["build"]
[[test]]
name = "protobuf"
working-directory = "protobuf"
command = "$<TARGET_FILE:cmkr>"
arguments = ["build"]
+123
View File
@@ -0,0 +1,123 @@
# 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: $<TARGET_FILE_NAME:hello>"]
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"
@@ -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")
@@ -0,0 +1,9 @@
if(NOT DEFINED INPUT)
message(FATAL_ERROR "INPUT is required")
endif()
if(NOT EXISTS "${INPUT}")
message(FATAL_ERROR "Expected file not found: ${INPUT}")
endif()
message(STATUS "Verified file exists: ${INPUT}")
+6
View File
@@ -0,0 +1,6 @@
#include <iostream>
int main() {
std::cout << "Hello, world!" << std::endl;
return 0;
}
+7
View File
@@ -0,0 +1,7 @@
#include "generated.hpp"
int generated_value();
int main() {
return generated_value() == GENERATED_VALUE ? 0 : 1;
}
+54
View File
@@ -0,0 +1,54 @@
# 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: $<TARGET_FILE_NAME:generate_numbers>"]
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 = ["$<TARGET_FILE:generate_numbers>", "${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 = ["$<TARGET_FILE:main>"]
comment = "Run the main executable to verify generated code works"
@@ -0,0 +1,44 @@
// Simple code generator that produces a C++ source file with a function
// that returns a vector of numbers.
#include <fstream>
#include <iostream>
#include <string>
int main(int argc, char *argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " <output_file>" << std::endl;
return 1;
}
std::string output_path = argv[1];
// The example ensures the output directory exists before invoking the generator.
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 <vector>\n";
out << "#include <cstddef>\n";
out << "\n";
out << "std::vector<int> 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;
}
+26
View File
@@ -0,0 +1,26 @@
// Main executable that uses the generated code
#include <iostream>
#include <vector>
// Functions generated by codegen executable
std::vector<int> 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;
}
}
+2
View File
@@ -0,0 +1,2 @@
build/
*.bin
+81
View File
@@ -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 = [
"$<TARGET_FILE:protobuf::protoc>",
"--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 = ["$<TARGET_FILE:protobuf_example>"]
comment = "Run protobuf_example to verify serialization/deserialization works"
+31
View File
@@ -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;
}
+146
View File
@@ -0,0 +1,146 @@
// Protobuf serialization/deserialization example
// This demonstrates using protobuf with cmkr for code generation
#include <iostream>
#include <fstream>
#include <string>
#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;
}