diff --git a/src/cps/loader.cpp b/src/cps/loader.cpp index f2c2e45..ed0bb9a 100644 --- a/src/cps/loader.cpp +++ b/src/cps/loader.cpp @@ -249,6 +249,8 @@ namespace cps::loader { .value_or(std::vector{}); auto const location = CPS_TRY(get_optional(comp, name, "location")); auto const link_location = CPS_TRY(get_optional(comp, name, "link_location")); + auto const link_requires = CPS_TRY(get_optional>(comp, name, "link_requires")) + .value_or(std::vector{}); auto const require = CPS_TRY(get_optional>(comp, name, "requires")) .value_or(std::vector{}); @@ -268,6 +270,7 @@ namespace cps::loader { .definitions = std::move(definitions), .link_flags = std::move(link_flags), .link_libraries = std::move(link_libraries), + .link_requires = std::move(link_requires), .location = std::move(location), .link_location = std::move(link_location), .require = std::move(require), diff --git a/src/cps/loader.hpp b/src/cps/loader.hpp index 3e2e0f3..84c0ea8 100644 --- a/src/cps/loader.hpp +++ b/src/cps/loader.hpp @@ -70,7 +70,7 @@ namespace cps::loader { std::vector link_flags; // TODO: std::vector link_languages; std::vector link_libraries; - // TODO: link_requires + std::vector link_requires; std::optional location; std::optional link_location; std::vector require; // requires is a keyword diff --git a/src/cps/pc_compat/pc_loader.cpp b/src/cps/pc_compat/pc_loader.cpp index 6a9df37..1a6ccf1 100644 --- a/src/cps/pc_compat/pc_loader.cpp +++ b/src/cps/pc_compat/pc_loader.cpp @@ -119,6 +119,7 @@ namespace cps::pc_compat { .definitions = loader::Defines{}, .link_flags = link_flags, .link_libraries = {}, + .link_requires = {}, // TODO: Currently lib location is hard coded to appease assertions. This would // need to implement linker-like search to replicate current behavior. .location = fmt::format("@prefix@/lib/{}.a", name), diff --git a/src/cps/search.cpp b/src/cps/search.cpp index 8bb3804..118fa92 100644 --- a/src/cps/search.cpp +++ b/src/cps/search.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include namespace fs = std::filesystem; @@ -31,6 +32,12 @@ namespace cps::search { using version::to_string; + /// @brief Details about how a required componenet should be used + struct ComponentDetails { + /// @brief If this component is only required for linking, not compiling + bool link_only; + }; + /// @brief A CPS file, along with the components in that CPS file to /// load class Dependency { @@ -40,7 +47,7 @@ namespace cps::search { /// @brief The loaded CPS file loader::Package package; /// @brief the components from that CPS file to use - std::vector components; + std::unordered_map components; }; /// @brief A DAG node @@ -376,65 +383,86 @@ namespace cps::search { /// @brief Calculate the required components in the graph /// @param node The node to process /// @param components the components required from this node - void set_components(std::shared_ptr node, const std::vector & components, - bool default_components) { - // If this package needs the default_components, then add to the list of used components + void set_components(std::shared_ptr node, std::vector components, bool default_components, + bool link_only = false) { + const auto & component_updater = [&node, &link_only](std::string name) { + if (const auto & entry = node->data.components.find(name); entry != node->data.components.end()) { + entry->second.link_only |= link_only; + } else { + node->data.components.emplace(std::move(name), ComponentDetails{link_only}); + } + }; + + // Set the components that this package's dependees want if (default_components && node->data.package.default_components) { - const std::vector & defs = node->data.package.default_components.value(); - node->data.components.insert(node->data.components.end(), defs.begin(), defs.end()); + const auto & defs = node->data.package.default_components.value(); + components.insert(components.end(), defs.begin(), defs.end()); } // Then add all of the explicitly listed components - node->data.components.insert(node->data.components.end(), components.begin(), components.end()); + std::for_each(components.begin(), components.end(), component_updater); // Handle self requirements first // This takes the form `"requires": [":a", ":b"]` // These must be handled before child dependencies, as they may alter the requirements placed on the // children.. - std::vector self_requires = node->data.components; + std::vector> self_requires; + std::transform(node->data.components.begin(), node->data.components.end(), + std::back_insert_iterator(self_requires), + [](std::pair entry) { return entry; }); std::unordered_set processed; bool self_defaults = default_components; while (!self_requires.empty()) { - const std::string c_name = self_requires.back(); + const auto [this_name, this_comp] = self_requires.back(); self_requires.pop_back(); - if (processed.find(c_name) != processed.end()) { + if (processed.find(this_name) != processed.end()) { continue; } - processed.emplace(c_name); + processed.emplace(this_name); - const loader::Component & component = node->data.package.components.at(c_name); + const loader::Component & component = node->data.package.components.at(this_name); auto && required = process_requires(component.require); if (auto && self = required.find(""); self != required.end()) { // Don't insert these twice + std::vector self_comps = self->second.components; if (!self_defaults && self->second.defaults && node->data.package.default_components) { self_defaults = true; const std::vector & defs = node->data.package.default_components.value(); - node->data.components.insert(node->data.components.end(), defs.begin(), defs.end()); + self_comps.insert(self_comps.end(), defs.begin(), defs.end()); } - for (auto && comp : self->second.components) { + std::for_each(self_comps.begin(), self_comps.end(), component_updater); + + for (auto && comp : self_comps) { if (processed.find(comp) != processed.end()) { continue; } - self_requires.emplace_back(comp); - node->data.components.emplace_back(comp); + self_requires.emplace_back(std::make_pair(comp, ComponentDetails{link_only})); } } } // Walk the list of components for this component, adding component // requirements recursively for external requirements. - for (const std::string & c_name : node->data.components) { + for (const auto & [this_name, this_comp] : node->data.components) { // It's possible that the Package::Requires section listed // dependencies we don't actually need. If we don't need them we // can trim the graph std::vector> trimmed; // This *should* be validated such that we won't have an exception - const loader::Component & component = node->data.package.components.at(c_name); + const loader::Component & component = node->data.package.components.at(this_name); auto && required = process_requires(component.require); for (std::shared_ptr & child : node->depends) { if (auto && child_comps = required.find(child->data.package.name); child_comps != required.end()) { trimmed.emplace_back(child); - set_components(child, child_comps->second.components, child_comps->second.defaults); + set_components(child, child_comps->second.components, child_comps->second.defaults, link_only); + } + } + auto && link_required = process_requires(component.link_requires); + for (std::shared_ptr & child : node->depends) { + if (auto && child_comps = link_required.find(child->data.package.name); + child_comps != link_required.end()) { + trimmed.emplace_back(child); + set_components(child, child_comps->second.components, child_comps->second.defaults, true); } } node->depends = trimmed; @@ -485,12 +513,12 @@ namespace cps::search { return s; }; - for (const auto & c_name : node->data.components) { + for (const auto & [comp_name, cps_comp] : node->data.components) { // We should have already errored if this is not the case - auto && f = node->data.package.components.find(c_name); + auto && f = node->data.package.components.find(comp_name); utils::assert_fn( f != node->data.package.components.end(), - fmt::format("Could not find component {} of package {}", c_name, node->data.package.name)); + fmt::format("Could not find component {} of package {}", comp_name, node->data.package.name)); auto && comp = f->second; // Convert prefix at this point because: @@ -498,14 +526,17 @@ namespace cps::search { // from // 2. if we do it at the search point we have to plumb overrides // deep into that - merge_result(comp.includes, result.includes, prefix_replacer); - merge_result(comp.definitions, result.definitions); - merge_result(comp.compile_flags, result.compile_flags); + if (!cps_comp.link_only) { + merge_result(comp.includes, result.includes, prefix_replacer); + merge_result(comp.definitions, result.definitions); + merge_result(comp.compile_flags, result.compile_flags); + } merge_result(comp.link_libraries, result.link_libraries); merge_result(comp.link_flags, result.link_flags); if (comp.type != loader::Type::interface) { if (!comp.location) { - return tl::make_unexpected(fmt::format("Component `{}` requires 'location' attribute", c_name)); + return tl::make_unexpected( + fmt::format("Component `{}` requires 'location' attribute", comp_name)); } result.link_location.emplace_back( prefix_replacer(comp.link_location.value_or(comp.location.value()))); diff --git a/tests/cases/cps-config.toml b/tests/cases/cps-config.toml index 0afe768..c7947fa 100644 --- a/tests/cases/cps-config.toml +++ b/tests/cases/cps-config.toml @@ -133,3 +133,15 @@ name = "requires component from self nested" cps = "full" args = ["flags", "--component", "requires-self", "--cflags", "--print-errors"] expected = "-fvectorize -I/usr/local/include -I/opt/include -DBAR=2 -DFOO=1 -DOTHER" + +[[case]] +name = "link requires nested" +cps = "link-requires" +args = ["flags", "--component", "nested", "--cflags", "--libs", "--print-errors"] +expected = "-lfake" + +[[case]] +name = "same component twice" +cps = "multiple-components" +args = ["flags", "--component", "same-component-twice", "--cflags", "--print-errors"] +expected = "-I/something" diff --git a/tests/cases/pkg-config-compat.toml b/tests/cases/pkg-config-compat.toml index 65c2dde..4a81b45 100644 --- a/tests/cases/pkg-config-compat.toml +++ b/tests/cases/pkg-config-compat.toml @@ -59,3 +59,9 @@ name = "parsing pc file" cps = "pc-variables" args = ["pkg-config", "--cflags"] expected = "-I/home/kaniini/pkg/include/libfoo" + +[[case]] +name = "link requires" +cps = "link-requires" +args = ["pkg-config", "--cflags", "--libs", "--print-errors"] +expected = "-L/usr/lib/ -flto -l/something/lib/libfoo.so -lbar" diff --git a/tests/cps-files/lib/cps/link-requires.cps b/tests/cps-files/lib/cps/link-requires.cps new file mode 100644 index 0000000..5ba3b72 --- /dev/null +++ b/tests/cps-files/lib/cps/link-requires.cps @@ -0,0 +1,27 @@ +{ + "name": "link-requires", + "cps_version": "0.13.0", + "version": "1.0.0", + "requires": { + "full": null, + "multiple-components": null + }, + "prefix": "/prefix", + "components": { + "default": { + "type": "interface", + "link_requires": [ + "full" + ] + }, + "nested": { + "type": "interface", + "link_requires": [ + "multiple-components:link-requires" + ] + } + }, + "default_components": [ + "default" + ] +} diff --git a/tests/cps-files/lib/cps/multiple-components.cps b/tests/cps-files/lib/cps/multiple-components.cps index 4a794d2..775c241 100644 --- a/tests/cps-files/lib/cps/multiple-components.cps +++ b/tests/cps-files/lib/cps/multiple-components.cps @@ -68,6 +68,19 @@ "requires": [ "minimal:sample0" ] + }, + "link-requires": { + "type": "interface", + "link_requires": [ + "minimal:sample1" + ] + }, + "same-component-twice": { + "type": "interface", + "requires": [ + ":sample3", + ":sample4" + ] } }, "default_components": [