[M5+] nix_cmake_scan filters Find*.cmake shims and picks public targets
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"(
|
||||
|
||||
Reference in New Issue
Block a user