353 lines
12 KiB
C++
353 lines
12 KiB
C++
module cargoxx.codegen;
|
|
|
|
import std;
|
|
import cargoxx.manifest;
|
|
import cargoxx.layout;
|
|
import cargoxx.linkdb;
|
|
import cargoxx.lockfile;
|
|
|
|
namespace cargoxx::codegen {
|
|
|
|
namespace {
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
auto edition_to_int(manifest::Edition e) -> int {
|
|
switch (e) {
|
|
case manifest::Edition::Cpp20:
|
|
return 20;
|
|
case manifest::Edition::Cpp23:
|
|
return 23;
|
|
case manifest::Edition::Cpp26:
|
|
return 26;
|
|
}
|
|
return 23;
|
|
}
|
|
|
|
auto rel_to_build(const fs::path& p, const fs::path& project_root) -> std::string {
|
|
return (fs::path{".."} / p.lexically_relative(project_root)).generic_string();
|
|
}
|
|
|
|
auto collect_dep_targets(const std::vector<linkdb::Recipe>& recipes)
|
|
-> std::vector<std::string> {
|
|
std::vector<std::string> out;
|
|
for (const auto& r : recipes) {
|
|
for (const auto& t : r.targets) {
|
|
out.push_back(t);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
auto link_block(std::string_view target_name, std::string_view visibility,
|
|
bool include_lib, std::string_view package_name,
|
|
const std::vector<std::string>& dep_targets) -> std::string {
|
|
if (!include_lib && dep_targets.empty()) {
|
|
return "";
|
|
}
|
|
std::string out = std::format("target_link_libraries({} {}\n", target_name, visibility);
|
|
if (include_lib) {
|
|
out += std::format(" {}\n", package_name);
|
|
}
|
|
for (const auto& t : dep_targets) {
|
|
out += std::format(" {}\n", t);
|
|
}
|
|
out += ")\n";
|
|
return out;
|
|
}
|
|
|
|
auto emit_header(const manifest::Manifest& m) -> std::string {
|
|
return std::format(
|
|
"cmake_minimum_required(VERSION 3.30)\n"
|
|
"\n"
|
|
"set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 1)\n"
|
|
"set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD \"d0edc3af-4c50-42ea-a356-e2862fe7a444\")\n"
|
|
"set(CMAKE_CXX_MODULE_STD ON)\n"
|
|
"\n"
|
|
"project({} LANGUAGES CXX)\n"
|
|
"\n"
|
|
"# Generated by cargoxx — do not edit.\n"
|
|
"# Source of truth: ../Cargoxx.toml\n"
|
|
"\n"
|
|
"set(CMAKE_CXX_STANDARD {})\n"
|
|
"set(CMAKE_CXX_STANDARD_REQUIRED ON)\n"
|
|
"set(CMAKE_CXX_EXTENSIONS ON)\n"
|
|
"set(CMAKE_CXX_SCAN_FOR_MODULES ON)\n"
|
|
"set(CMAKE_EXPORT_COMPILE_COMMANDS ON)\n"
|
|
"\n"
|
|
"add_compile_options(-Wall -Wextra -Wpedantic -Wconversion)\n",
|
|
m.package.name, edition_to_int(m.package.edition));
|
|
}
|
|
|
|
auto emit_find_packages(const std::vector<linkdb::Recipe>& recipes,
|
|
const std::vector<linkdb::Recipe>& dev_recipes)
|
|
-> std::string {
|
|
if (recipes.empty() && dev_recipes.empty()) {
|
|
return {};
|
|
}
|
|
std::string out = "\n# ----- dependencies -----\n";
|
|
|
|
bool pkgconfig_emitted = false;
|
|
auto emit_one = [&](const linkdb::Recipe& r) {
|
|
if (r.pkg_config_module && !r.pkg_config_module->empty()) {
|
|
if (!pkgconfig_emitted) {
|
|
out += "find_package(PkgConfig REQUIRED)\n";
|
|
pkgconfig_emitted = true;
|
|
}
|
|
std::string upper;
|
|
upper.reserve(r.pkg_config_module->size());
|
|
for (char c : *r.pkg_config_module) {
|
|
upper += std::isalnum(static_cast<unsigned char>(c))
|
|
? static_cast<char>(std::toupper(
|
|
static_cast<unsigned char>(c)))
|
|
: '_';
|
|
}
|
|
out += std::format("pkg_check_modules({} REQUIRED IMPORTED_TARGET {})\n",
|
|
upper, *r.pkg_config_module);
|
|
} else {
|
|
out += std::format("find_package({})\n", r.find_package);
|
|
}
|
|
};
|
|
for (const auto& r : recipes) {
|
|
emit_one(r);
|
|
}
|
|
for (const auto& r : dev_recipes) {
|
|
emit_one(r);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
auto recipe_is_catch2(const linkdb::Recipe& r) -> bool {
|
|
return r.find_package.starts_with("Catch2 ");
|
|
}
|
|
|
|
auto any_recipe_is_catch2(const std::vector<linkdb::Recipe>& dev_recipes) -> bool {
|
|
return std::ranges::any_of(dev_recipes, recipe_is_catch2);
|
|
}
|
|
|
|
auto emit_library(const layout::Target& lib, const std::string& package_name,
|
|
const std::vector<linkdb::Recipe>& recipes,
|
|
const std::vector<std::string>& include_dirs,
|
|
const fs::path& project_root) -> std::string {
|
|
std::string out = "\n# ----- library target -----\n";
|
|
out += std::format("add_library({} STATIC)\n", package_name);
|
|
out += std::format("target_sources({}\n", package_name);
|
|
out += " PUBLIC\n";
|
|
out += " FILE_SET CXX_MODULES BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/.. FILES\n";
|
|
for (const auto& m : lib.module_units) {
|
|
out += std::format(" {}\n", rel_to_build(m, project_root));
|
|
}
|
|
if (!lib.additional_sources.empty()) {
|
|
out += " PRIVATE\n";
|
|
for (const auto& s : lib.additional_sources) {
|
|
out += std::format(" {}\n", rel_to_build(s, project_root));
|
|
}
|
|
}
|
|
out += ")\n";
|
|
if (!include_dirs.empty()) {
|
|
out += std::format("target_include_directories({} SYSTEM PRIVATE", package_name);
|
|
for (const auto& d : include_dirs) {
|
|
out += std::format(" ../{}", d);
|
|
}
|
|
out += ")\n";
|
|
}
|
|
out += link_block(package_name, "PUBLIC", false, package_name,
|
|
collect_dep_targets(recipes));
|
|
return out;
|
|
}
|
|
|
|
auto emit_primary_binary(const layout::Target& bin, const std::string& package_name,
|
|
const std::vector<linkdb::Recipe>& recipes, bool has_lib,
|
|
const fs::path& project_root) -> std::string {
|
|
std::string out = "\n# ----- binary target -----\n";
|
|
out += std::format("add_executable({}_bin {})\n", package_name,
|
|
rel_to_build(bin.entry, project_root));
|
|
out += std::format("set_target_properties({}_bin PROPERTIES OUTPUT_NAME {})\n",
|
|
package_name, package_name);
|
|
out += link_block(std::format("{}_bin", package_name), "PRIVATE", has_lib, package_name,
|
|
collect_dep_targets(recipes));
|
|
return out;
|
|
}
|
|
|
|
auto emit_extra_binary(const layout::Target& bin, const std::string& package_name,
|
|
const std::vector<linkdb::Recipe>& recipes, bool has_lib,
|
|
const fs::path& project_root) -> std::string {
|
|
std::string out = std::format("\n# ----- binary target: {} -----\n", bin.name);
|
|
out += std::format("add_executable({} {})\n", bin.name,
|
|
rel_to_build(bin.entry, project_root));
|
|
out += link_block(bin.name, "PRIVATE", has_lib, package_name,
|
|
collect_dep_targets(recipes));
|
|
return out;
|
|
}
|
|
|
|
auto emit_test(const layout::Target& t, const std::string& package_name,
|
|
const std::vector<linkdb::Recipe>& recipes,
|
|
const std::vector<linkdb::Recipe>& dev_recipes, bool has_lib,
|
|
bool use_catch_discover, const fs::path& project_root) -> std::string {
|
|
auto target = std::format("test_{}", t.name);
|
|
std::string out = std::format("add_executable({} {})\n", target,
|
|
rel_to_build(t.entry, project_root));
|
|
std::vector<std::string> link_targets = collect_dep_targets(recipes);
|
|
for (const auto& dt : collect_dep_targets(dev_recipes)) {
|
|
link_targets.push_back(dt);
|
|
}
|
|
out += link_block(target, "PRIVATE", has_lib, package_name, link_targets);
|
|
if (use_catch_discover) {
|
|
out += std::format("catch_discover_tests({})\n", target);
|
|
} else {
|
|
out += std::format("add_test(NAME {} COMMAND {})\n", t.name, target);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
auto emit_example(const layout::Target& e, const std::string& package_name,
|
|
const std::vector<linkdb::Recipe>& recipes, bool has_lib,
|
|
const fs::path& project_root) -> std::string {
|
|
auto target = std::format("example_{}", e.name);
|
|
std::string out = std::format("add_executable({} {})\n", target,
|
|
rel_to_build(e.entry, project_root));
|
|
out += link_block(target, "PRIVATE", has_lib, package_name,
|
|
collect_dep_targets(recipes));
|
|
return out;
|
|
}
|
|
|
|
auto find_primary_bin(const layout::DiscoveredLayout& layout) -> const layout::Target* {
|
|
for (const auto& b : layout.binaries) {
|
|
if (b.entry.filename() == "main.cpp" &&
|
|
b.entry.parent_path().filename() == "src") {
|
|
return &b;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
auto collect_all_target_names(const layout::DiscoveredLayout& layout,
|
|
const std::string& package_name) -> std::vector<std::string> {
|
|
std::vector<std::string> out;
|
|
if (layout.library) {
|
|
out.push_back(package_name);
|
|
}
|
|
if (find_primary_bin(layout) != nullptr) {
|
|
out.push_back(std::format("{}_bin", package_name));
|
|
}
|
|
for (const auto& b : layout.binaries) {
|
|
if (b.entry.filename() == "main.cpp" &&
|
|
b.entry.parent_path().filename() == "src") {
|
|
continue;
|
|
}
|
|
out.push_back(b.name);
|
|
}
|
|
for (const auto& t : layout.tests) {
|
|
out.push_back(std::format("test_{}", t.name));
|
|
}
|
|
for (const auto& e : layout.examples) {
|
|
out.push_back(std::format("example_{}", e.name));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
auto emit_build_flags(const manifest::BuildSettings& build,
|
|
const layout::DiscoveredLayout& layout,
|
|
const std::string& package_name) -> std::string {
|
|
if (!build.warnings_as_errors && build.sanitizers.empty()) {
|
|
return {};
|
|
}
|
|
auto targets = collect_all_target_names(layout, package_name);
|
|
if (targets.empty()) {
|
|
return {};
|
|
}
|
|
|
|
std::string targets_list;
|
|
for (std::size_t i = 0; i < targets.size(); ++i) {
|
|
if (i > 0) {
|
|
targets_list += ' ';
|
|
}
|
|
targets_list += targets[i];
|
|
}
|
|
|
|
std::string out = "\n# ----- build flags from [build] section -----\n";
|
|
|
|
if (build.warnings_as_errors) {
|
|
out += std::format("foreach(target_name IN ITEMS {})\n", targets_list);
|
|
out += " target_compile_options(${target_name} PRIVATE -Wall -Wextra -Wpedantic "
|
|
"-Werror)\n";
|
|
out += "endforeach()\n";
|
|
}
|
|
|
|
if (!build.sanitizers.empty()) {
|
|
std::string san_list;
|
|
for (std::size_t i = 0; i < build.sanitizers.size(); ++i) {
|
|
if (i > 0) {
|
|
san_list += ',';
|
|
}
|
|
san_list += build.sanitizers[i];
|
|
}
|
|
out += std::format("foreach(target_name IN ITEMS {})\n", targets_list);
|
|
out += std::format(" target_compile_options(${{target_name}} PRIVATE "
|
|
"-fsanitize={})\n",
|
|
san_list);
|
|
out += std::format(" target_link_options(${{target_name}} PRIVATE -fsanitize={})\n",
|
|
san_list);
|
|
out += "endforeach()\n";
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
auto cmake_lists(const GenerateInputs& in) -> std::string {
|
|
const auto& pkg_name = in.manifest.package.name;
|
|
const bool has_lib = in.layout.library.has_value();
|
|
const bool use_catch_discover = any_recipe_is_catch2(in.dev_recipes);
|
|
|
|
std::string out;
|
|
out += emit_header(in.manifest);
|
|
out += emit_find_packages(in.recipes, in.dev_recipes);
|
|
|
|
if (in.layout.library) {
|
|
out += emit_library(*in.layout.library, pkg_name, in.recipes,
|
|
in.manifest.build.include_dirs, in.project_root);
|
|
}
|
|
|
|
const auto* primary = find_primary_bin(in.layout);
|
|
if (primary) {
|
|
out += emit_primary_binary(*primary, pkg_name, in.recipes, has_lib, in.project_root);
|
|
}
|
|
|
|
bool has_extra_bin = false;
|
|
for (const auto& b : in.layout.binaries) {
|
|
if (&b == primary) {
|
|
continue;
|
|
}
|
|
if (!has_extra_bin) {
|
|
has_extra_bin = true;
|
|
}
|
|
out += emit_extra_binary(b, pkg_name, in.recipes, has_lib, in.project_root);
|
|
}
|
|
|
|
if (!in.layout.tests.empty()) {
|
|
out += "\n# ----- tests -----\nenable_testing()\n";
|
|
if (use_catch_discover) {
|
|
out += "include(Catch)\n";
|
|
}
|
|
for (const auto& t : in.layout.tests) {
|
|
out += emit_test(t, pkg_name, in.recipes, in.dev_recipes, has_lib,
|
|
use_catch_discover, in.project_root);
|
|
}
|
|
}
|
|
|
|
if (!in.layout.examples.empty()) {
|
|
out += "\n# ----- examples -----\n";
|
|
for (const auto& e : in.layout.examples) {
|
|
out += emit_example(e, pkg_name, in.recipes, has_lib, in.project_root);
|
|
}
|
|
}
|
|
|
|
out += emit_build_flags(in.manifest.build, in.layout, pkg_name);
|
|
|
|
return out;
|
|
}
|
|
|
|
} // namespace cargoxx::codegen
|