diff --git a/MODULE.bazel b/MODULE.bazel index a0b8b1e..cdae5f1 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -22,6 +22,7 @@ bazel_dep(name = "boost.url", version = "1.83.0.bzl.2") bazel_dep(name = "yaml-cpp", version = "0.8.0") bazel_dep(name = "libarchive", version = "3.7.4.bcr.2") +bazel_dep(name = "googletest", version = "1.15.2", dev_dependency = True) bazel_dep(name = "toolchains_llvm", version = "1.0.0", dev_dependency = True) bazel_dep(name = "hedron_compile_commands", dev_dependency = True) diff --git a/ecsact/cli/commands/build/build_recipe.cc b/ecsact/cli/commands/build/build_recipe.cc index a803783..63e9512 100644 --- a/ecsact/cli/commands/build/build_recipe.cc +++ b/ecsact/cli/commands/build/build_recipe.cc @@ -242,8 +242,10 @@ static auto parse_sources( // outdir = src["outdir"].as(); } - if(!relative_to_cwd && !src_path.is_absolute()) { - src_path = recipe_path.parent_path() / src_path; + if(relative_to_cwd && !src_path.is_absolute()) { + src_path = (fs::current_path() / src_path).lexically_normal(); + } else { + src_path = src_path.lexically_normal(); } result.emplace_back(source_path{ @@ -252,10 +254,7 @@ static auto parse_sources( // }); } } else if(src.IsScalar()) { - auto path = fs::path{src.as()}; - if(!path.is_absolute()) { - path = recipe_path.parent_path() / path; - } + auto path = fs::path{src.as()}.lexically_normal(); result.emplace_back(source_path{ .path = path, .outdir = ".", @@ -266,76 +265,90 @@ static auto parse_sources( // return result; } -auto ecsact::build_recipe::from_yaml_file( // - std::filesystem::path p +auto ecsact::build_recipe::build_recipe_from_yaml_node( // + YAML::Node doc, + fs::path p ) -> create_result { - auto file = std::ifstream{p}; - try { - auto doc = YAML::LoadFile(p.string()); - - if(!doc.IsMap()) { - return build_recipe_parse_error::expected_map_top_level; - } + if(!doc.IsMap()) { + return ecsact::build_recipe_parse_error::expected_map_top_level; + } - auto exports = parse_exports(doc["exports"]); - if(auto err = get_if_error(exports)) { - return *err; - } + auto exports = parse_exports(doc["exports"]); + if(auto err = get_if_error(exports)) { + return *err; + } - auto imports = parse_imports(doc["imports"]); - if(auto err = get_if_error(imports)) { - return *err; - } + auto imports = parse_imports(doc["imports"]); + if(auto err = get_if_error(imports)) { + return *err; + } - auto sources = parse_sources(p, doc["sources"]); - if(auto err = get_if_error(sources)) { - return *err; - } + auto sources = parse_sources(p, doc["sources"]); + if(auto err = get_if_error(sources)) { + return *err; + } - auto system_libs = parse_system_libs(doc["system_libs"]); - if(auto err = get_if_error(system_libs)) { - return *err; - } + auto system_libs = parse_system_libs(doc["system_libs"]); + if(auto err = get_if_error(system_libs)) { + return *err; + } - auto recipe = build_recipe{}; - if(doc["name"]) { - recipe._name = doc["name"].as(); - } - if(p.has_parent_path()) { - recipe._base_directory = p.parent_path().generic_string(); - } - recipe._exports = get_value(exports); - recipe._imports = get_value(imports); - recipe._sources = get_value(sources); - recipe._system_libs = get_value(system_libs); + auto recipe = ecsact::build_recipe{}; + if(doc["name"]) { + recipe._name = doc["name"].as(); + } + if(p.has_parent_path()) { + recipe._base_directory = p.parent_path().generic_string(); + } + recipe._exports = get_value(exports); + recipe._imports = get_value(imports); + recipe._sources = get_value(sources); + recipe._system_libs = get_value(system_libs); - if(recipe._exports.empty()) { - return build_recipe_parse_error::missing_exports; - } + if(recipe._exports.empty()) { + return build_recipe_parse_error::missing_exports; + } - auto import_modules = - ecsact::cli::detail::get_ecsact_modules(recipe._imports); - auto export_modules = - ecsact::cli::detail::get_ecsact_modules(recipe._exports); + auto import_modules = + ecsact::cli::detail::get_ecsact_modules(recipe._imports); + auto export_modules = + ecsact::cli::detail::get_ecsact_modules(recipe._exports); - if(!import_modules.unknown_module_methods.empty()) { - return build_recipe_parse_error::unknown_import_method; - } + if(!import_modules.unknown_module_methods.empty()) { + return build_recipe_parse_error::unknown_import_method; + } - if(!export_modules.unknown_module_methods.empty()) { - return build_recipe_parse_error::unknown_export_method; - } + if(!export_modules.unknown_module_methods.empty()) { + return build_recipe_parse_error::unknown_export_method; + } - for(auto&& [imp_mod, _] : import_modules.module_methods) { - for(auto&& [exp_mod, _] : export_modules.module_methods) { - if(imp_mod == exp_mod) { - return build_recipe_parse_error:: - conflicting_import_export_method_modules; - } + for(auto&& [imp_mod, _] : import_modules.module_methods) { + for(auto&& [exp_mod, _] : export_modules.module_methods) { + if(imp_mod == exp_mod) { + return build_recipe_parse_error:: + conflicting_import_export_method_modules; } } + } - return recipe; + return recipe; +} + +auto ecsact::build_recipe::from_yaml_string( // + const std::string& str, + fs::path p +) -> create_result { + auto doc = YAML::Load(str); + return build_recipe_from_yaml_node(doc, p); +} + +auto ecsact::build_recipe::from_yaml_file( // + std::filesystem::path p +) -> create_result { + auto file = std::ifstream{p}; + try { + auto doc = YAML::LoadFile(p.string()); + return build_recipe_from_yaml_node(doc, p); } catch(const YAML::BadFile& err) { ecsact::cli::report_error("YAML PARSE: {}", err.what()); return build_recipe_parse_error::bad_file; diff --git a/ecsact/cli/commands/build/build_recipe.hh b/ecsact/cli/commands/build/build_recipe.hh index 849d8ca..70c9872 100644 --- a/ecsact/cli/commands/build/build_recipe.hh +++ b/ecsact/cli/commands/build/build_recipe.hh @@ -1,12 +1,17 @@ #pragma once #include -#include #include #include #include #include +#include #include +#include + +namespace YAML { +class Node; +} namespace ecsact { @@ -33,6 +38,11 @@ public: std::filesystem::path p ) -> create_result; + static auto from_yaml_string( // + const std::string& str, + std::filesystem::path p + ) -> create_result; + static auto merge( // const build_recipe& a, const build_recipe& b @@ -84,6 +94,11 @@ private: build_recipe(); build_recipe(const build_recipe&); + + static auto build_recipe_from_yaml_node( // + YAML::Node doc, + std::filesystem::path p + ) -> create_result; }; struct build_recipe::create_result diff --git a/ecsact/cli/commands/build/recipe/cook.cc b/ecsact/cli/commands/build/recipe/cook.cc index e56bf0a..104e26c 100644 --- a/ecsact/cli/commands/build/recipe/cook.cc +++ b/ecsact/cli/commands/build/recipe/cook.cc @@ -118,6 +118,7 @@ static auto write_file(fs::path path, std::span data) -> void { } static auto handle_source( // + fs::path base_directory, ecsact::build_recipe::source_fetch src, const ecsact::cli::cook_recipe_options& options ) -> int { @@ -239,6 +240,7 @@ static auto handle_source( // } static auto handle_source( // + fs::path base_directory, ecsact::build_recipe::source_codegen src, const ecsact::cli::cook_recipe_options& options ) -> int { @@ -275,9 +277,15 @@ static auto handle_source( // } static auto handle_source( // + fs::path base_directory, ecsact::build_recipe::source_path src, const ecsact::cli::cook_recipe_options& options ) -> int { + auto src_path = src.path; + if(!src_path.is_absolute()) { + src_path = (base_directory / src_path).lexically_normal(); + } + auto outdir = src.outdir // ? options.work_dir / *src.outdir : options.work_dir; @@ -285,37 +293,37 @@ static auto handle_source( // auto ec = std::error_code{}; fs::create_directories(outdir, ec); - auto before_glob = path_before_glob(src.path); - auto paths = expand_path_globs(src.path, ec); + auto before_glob = path_before_glob(src_path); + auto paths = expand_path_globs(src_path, ec); if(ec) { ecsact::cli::report_error( "Failed to glob {}: {}", - src.path.generic_string(), + src_path.generic_string(), ec.message() ); return 1; } for(auto path : paths) { - if(!fs::exists(src.path)) { + if(!fs::exists(src_path)) { ecsact::cli::report_error( "Source file {} does not exist", - src.path.generic_string() + src_path.generic_string() ); return 1; } auto rel_outdir = outdir; - if(auto stripped = path_strip_prefix(src.path, before_glob)) { + if(auto stripped = path_strip_prefix(src_path, before_glob)) { rel_outdir = outdir / *stripped; } fs::create_directories(rel_outdir, ec); - fs::copy(src.path, rel_outdir, ec); + fs::copy(src_path, rel_outdir, ec); if(ec) { ecsact::cli::report_error( "Failed to copy source {} to {}: {}", - src.path.generic_string(), + src_path.generic_string(), rel_outdir.generic_string(), ec.message() ); @@ -323,7 +331,7 @@ static auto handle_source( // } else { ecsact::cli::report_info( "Copied source {} to {}", - src.path.generic_string(), + src_path.generic_string(), rel_outdir.generic_string() ); } @@ -707,7 +715,9 @@ auto ecsact::cli::cook_recipe( // for(auto& src : recipe.sources()) { exit_code = std::visit( - [&](auto& src) { return handle_source(src, recipe_options); }, + [&](auto& src) { + return handle_source(recipe.base_directory(), src, recipe_options); + }, src ); diff --git a/ecsact/cli/commands/build/test/BUILD.bazel b/ecsact/cli/commands/build/test/BUILD.bazel new file mode 100644 index 0000000..6a4abbf --- /dev/null +++ b/ecsact/cli/commands/build/test/BUILD.bazel @@ -0,0 +1,13 @@ +load("@rules_cc//cc:defs.bzl", "cc_test") +load("//bazel:copts.bzl", "copts") + +cc_test( + name = "merge_recipe_test", + copts = copts, + srcs = ["merge_recipe_test.cc"], + deps = [ + "@googletest//:gtest", + "@googletest//:gtest_main", + "//ecsact/cli/commands/build:build_recipe", + ], +) diff --git a/ecsact/cli/commands/build/test/merge_recipe_test.cc b/ecsact/cli/commands/build/test/merge_recipe_test.cc new file mode 100644 index 0000000..635ae3f --- /dev/null +++ b/ecsact/cli/commands/build/test/merge_recipe_test.cc @@ -0,0 +1,149 @@ +#include "gtest/gtest.h" +#include +#include +#include +#include "ecsact/cli/commands/build/build_recipe.hh" + +using ecsact::build_recipe; +using namespace std::string_literals; + +constexpr auto RECIPE_A = R"yaml( +name: A +sources: +- xilo/yama/zompers.cc + +imports: [] +exports: [ecsact_create_registry] +)yaml"; + +constexpr auto RECIPE_B = R"yaml( +name: B +sources: +- bad/beaver.cc + +imports: [] +exports: [ecsact_destroy_registry] +)yaml"; + +constexpr auto RECIPE_C = R"yaml( +name: C +sources: +- xilo/yama/vedder/dog.cc + +imports: [] +exports: [ecsact_clear_registry] +)yaml"; + +constexpr auto RECIPE_D = R"yaml( +name: D +sources: +- d.cc + +imports: [] +exports: [ecsact_clear_registry] +)yaml"; + +constexpr auto RECIPE_E = R"yaml( +name: E +sources: +- e.cc + +imports: [] +exports: [ecsact_destroy_registry] +)yaml"; + +auto contains_source_path(auto&& r, auto p) -> bool { + using std::ranges::find_if; + + auto itr = find_if(r, [&](auto&& src) -> bool { + auto src_path = std::get_if(&src); + if(!src_path) { + return false; + } + + return src_path->path.generic_string() == p; + }); + + return itr != r.end(); +} + +auto sources_path_str(auto&& r) -> std::string { + auto str = std::string{}; + + for(auto&& src : r) { + auto src_path = std::get_if(&src); + if(!src_path) { + continue; + } + str += " " + src_path->path.generic_string() + "\n"; + } + + return str; +} + +TEST(RecipeMerge, Correct) { + auto a = std::get(build_recipe::from_yaml_string( // + RECIPE_A, + "zeke/apple.yml" + )); + auto b = std::get(build_recipe::from_yaml_string( // + RECIPE_B, + "ban.yml" + )); + auto c = std::get(build_recipe::from_yaml_string( // + RECIPE_C, + "zeke/bonbon/quit/cover.yml" + )); + + auto ab_m = std::get(build_recipe::merge(a, b)); + + EXPECT_EQ(ab_m.base_directory(), a.base_directory()); + + EXPECT_TRUE(contains_source_path(ab_m.sources(), "xilo/yama/zompers.cc")) + << "Found:\n" + << sources_path_str(ab_m.sources()); + + EXPECT_TRUE(contains_source_path(ab_m.sources(), "../bad/beaver.cc")) + << "Found:\n" + << sources_path_str(ab_m.sources()); + + auto abc_m = std::get(build_recipe::merge(ab_m, c)); + + EXPECT_EQ(abc_m.base_directory(), ab_m.base_directory()); + + EXPECT_TRUE(contains_source_path(abc_m.sources(), "xilo/yama/zompers.cc")) + << "Found:\n" + << sources_path_str(abc_m.sources()); + + EXPECT_TRUE(contains_source_path(abc_m.sources(), "../bad/beaver.cc")) + << "Found:\n" + << sources_path_str(abc_m.sources()); + + EXPECT_TRUE( + contains_source_path(abc_m.sources(), "bonbon/quit/xilo/yama/vedder/dog.cc") + ) << "Found:\n" + << sources_path_str(abc_m.sources()); +} + +TEST(RecipeMerge, Correct2) { + auto d = std::get(build_recipe::from_yaml_string( // + RECIPE_D, + "job/d.yml" + )); + auto e = std::get(build_recipe::from_yaml_string( // + RECIPE_E, + "job/e.yml" + )); + + auto de_m = std::get(build_recipe::merge(d, e)); + + EXPECT_EQ(d.base_directory(), de_m.base_directory()); + + EXPECT_TRUE(contains_source_path(de_m.sources(), "d.cc")) + << "Found:\n" + << sources_path_str(de_m.sources()); + + EXPECT_TRUE(contains_source_path(de_m.sources(), "e.cc")) + << "Found:\n" + << sources_path_str(de_m.sources()); +}