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& recipes) -> std::vector { std::vector 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& 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& recipes, const std::vector& 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(c)) ? static_cast(std::toupper( static_cast(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& 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& recipes, const std::vector& 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& 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& 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& recipes, const std::vector& 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 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& 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::vector 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