[M5+] nix_cmake_scan filters Find*.cmake shims and picks public targets

This commit is contained in:
2026-05-10 16:18:22 +00:00
parent 6e280becbe
commit b8171e3d03
3 changed files with 105 additions and 0 deletions

View File

@@ -121,6 +121,61 @@ auto scan_imported_targets(std::string_view config_text) -> std::vector<std::str
return out;
}
// Reduce a freshly-scanned list of IMPORTED targets to just the ones
// the package wants downstream consumers to link against. Strategy:
//
// * If the list contains any namespaced target (`Pkg::name`), keep
// only the namespaced ones — modern CMake's public-API convention
// (fmt::fmt, Catch2::Catch2WithMain, protobuf::libprotobuf, …).
//
// * Otherwise, the package only ships bare/legacy targets. LLVM is
// the canonical example: 213 internal libs (LLVMSupport, LLVMCore,
// …) plus the unified `LLVM` umbrella. Linking every internal
// target also pulls in their broken link interfaces (e.g.
// LLVMWindowsManifest references LibXml2::LibXml2 unconditionally,
// LLVMInterpreter references FFI::ffi). Keep only the target whose
// name matches the config stem case-insensitively (`LLVM` for
// `LLVMConfig.cmake`).
//
// * Fallback: if the stem-name filter yields nothing (no umbrella
// target exists), return the original list untouched — better a
// noisy link line than an empty recipe.
auto filter_public_targets(std::vector<std::string> targets,
std::string_view stem) -> std::vector<std::string> {
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<unsigned char>(t[i]));
auto b = std::tolower(static_cast<unsigned char>(stem[i]));
if (a != b) {
return false;
}
}
return true;
};
std::vector<std::string> 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<X>.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),

View File

@@ -58,6 +58,13 @@ struct NixCmakeCandidate {
// `<alias>::<member>` forms get picked up.
auto scan_imported_targets(std::string_view config_text) -> std::vector<std::string>;
// 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<std::string> targets,
std::string_view stem) -> std::vector<std::string>;
// 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;

View File

@@ -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<std::string>{"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<std::string>{"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<std::string>{"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<std::string>{"alpha", "beta", "gamma"});
}
TEST_CASE("scan_imported_targets dedupes duplicate target names",
"[resolver][nix_cmake_scan]") {
constexpr std::string_view text = R"(