diff --git a/src/resolver/nix_cmake_scan.cpp b/src/resolver/nix_cmake_scan.cpp index 592a037..a44ca62 100644 --- a/src/resolver/nix_cmake_scan.cpp +++ b/src/resolver/nix_cmake_scan.cpp @@ -121,6 +121,61 @@ auto scan_imported_targets(std::string_view config_text) -> std::vector targets, + std::string_view stem) -> std::vector { + auto has_namespace = std::ranges::any_of(targets, [](const auto& t) { + return t.find("::") != std::string::npos; + }); + if (has_namespace) { + std::erase_if(targets, [](const auto& t) { + return t.find("::") == std::string::npos; + }); + return targets; + } + auto stem_ci_eq = [&](std::string_view t) { + if (t.size() != stem.size()) { + return false; + } + for (std::size_t i = 0; i < t.size(); ++i) { + auto a = std::tolower(static_cast(t[i])); + auto b = std::tolower(static_cast(stem[i])); + if (a != b) { + return false; + } + } + return true; + }; + std::vector keep; + for (auto& t : targets) { + if (stem_ci_eq(t)) { + keep.push_back(std::move(t)); + } + } + if (keep.empty()) { + return targets; + } + return keep; +} + namespace { // Normalize a name for cross-shape comparison: @@ -253,6 +308,17 @@ auto nix_cmake_scan(const fs::path& store_path, const std::string& package_name) if (!sib.is_regular_file() || sib.path().extension() != ".cmake") { continue; } + // Skip CMake FindModule shims (Find.cmake). These declare + // cross-package IMPORTED targets like `FFI::ffi`, + // `LibEdit::LibEdit`, `zstd::libzstd_*` as fallbacks for + // when the host project doesn't have those libraries on + // CMAKE_PREFIX_PATH. Folding them into the recipe makes the + // generated `target_link_libraries` reference targets that + // don't exist in our buildInputs. + auto sib_stem = sib.path().stem().string(); + if (sib_stem.starts_with("Find")) { + continue; + } std::ifstream in{sib.path()}; if (!in) { continue; @@ -269,6 +335,7 @@ auto nix_cmake_scan(const fs::path& store_path, const std::string& package_name) } auto stem = config_stem_to_package(entry.path().filename().string()); + targets = filter_public_targets(std::move(targets), stem); auto parent_dir_name = pkg_dir.filename().string(); NixCmakeCandidate cand{ .find_package = std::format("{} CONFIG REQUIRED", stem), diff --git a/src/resolver/resolver.cppm b/src/resolver/resolver.cppm index bb2e8ae..8847780 100644 --- a/src/resolver/resolver.cppm +++ b/src/resolver/resolver.cppm @@ -58,6 +58,13 @@ struct NixCmakeCandidate { // `::` forms get picked up. auto scan_imported_targets(std::string_view config_text) -> std::vector; +// Pure: pick the public-API subset out of a freshly scanned target +// list. See nix_cmake_scan.cpp for the rule (namespaced > stem-named +// umbrella > pass-through). Exported so unit tests can drive it +// without scaffolding an on-disk store. +auto filter_public_targets(std::vector targets, + std::string_view stem) -> std::vector; + // Pure: turn a CMake config filename into the find_package name. // `fmtConfig.cmake` / `fmt-config.cmake` -> `fmt`. auto config_stem_to_package(std::string_view filename) -> std::string; diff --git a/tests/nix_cmake_scan_parse.cpp b/tests/nix_cmake_scan_parse.cpp index d999bc9..275b650 100644 --- a/tests/nix_cmake_scan_parse.cpp +++ b/tests/nix_cmake_scan_parse.cpp @@ -5,6 +5,7 @@ import cargoxx.util; import std; using cargoxx::resolver::config_stem_to_package; +using cargoxx::resolver::filter_public_targets; using cargoxx::resolver::nix_cmake_scan; using cargoxx::resolver::scan_imported_targets; using cargoxx::util::ErrorCode; @@ -64,6 +65,36 @@ TEST_CASE("scan_imported_targets does not match _add_library or similar", REQUIRE(out.empty()); } +TEST_CASE("filter_public_targets keeps namespaced targets when present", + "[resolver][nix_cmake_scan]") { + auto out = filter_public_targets( + {"fmt::fmt", "fmt::fmt-header-only", "fmt_internal_helper"}, "fmt"); + REQUIRE(out == std::vector{"fmt::fmt", "fmt::fmt-header-only"}); +} + +TEST_CASE("filter_public_targets picks the umbrella when no targets are namespaced", + "[resolver][nix_cmake_scan]") { + // Mirrors LLVMExports: 213 bare targets, only `LLVM` is the + // intended downstream entry point. + auto out = filter_public_targets( + {"LLVM", "LLVMSupport", "LLVMCore", "LLVMWindowsManifest"}, "LLVM"); + REQUIRE(out == std::vector{"LLVM"}); +} + +TEST_CASE("filter_public_targets is case-insensitive when picking the umbrella", + "[resolver][nix_cmake_scan]") { + auto out = filter_public_targets({"GRPC", "grpc_internal"}, "gRPC"); + REQUIRE(out == std::vector{"GRPC"}); +} + +TEST_CASE("filter_public_targets keeps everything when no umbrella matches", + "[resolver][nix_cmake_scan]") { + // Stem doesn't match any bare target — fall back to passthrough so + // we still emit a non-empty link line. + auto out = filter_public_targets({"alpha", "beta", "gamma"}, "Whatever"); + REQUIRE(out == std::vector{"alpha", "beta", "gamma"}); +} + TEST_CASE("scan_imported_targets dedupes duplicate target names", "[resolver][nix_cmake_scan]") { constexpr std::string_view text = R"(