[M6] resolver: CMake builtin FindModule scan probe
This commit is contained in:
@@ -59,6 +59,7 @@ target_sources(cargoxx
|
|||||||
../src/manifest/writer.cpp
|
../src/manifest/writer.cpp
|
||||||
../src/resolver/conan_probe.cpp
|
../src/resolver/conan_probe.cpp
|
||||||
../src/resolver/discover.cpp
|
../src/resolver/discover.cpp
|
||||||
|
../src/resolver/findmodule_scan.cpp
|
||||||
../src/resolver/nix_cmake_scan.cpp
|
../src/resolver/nix_cmake_scan.cpp
|
||||||
../src/resolver/nixpkgs_git.cpp
|
../src/resolver/nixpkgs_git.cpp
|
||||||
../src/resolver/nixpkgs_probe.cpp
|
../src/resolver/nixpkgs_probe.cpp
|
||||||
|
|||||||
@@ -78,6 +78,17 @@ auto recipe_from_pc(const PcCandidate& p, const std::string& nixpkgs_attr,
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto recipe_from_findmodule(const FindModuleCandidate& fm,
|
||||||
|
const std::string& nixpkgs_attr,
|
||||||
|
const std::string& source) -> linkdb::Recipe {
|
||||||
|
return linkdb::Recipe{
|
||||||
|
.nixpkgs_attr = nixpkgs_attr,
|
||||||
|
.find_package = fm.find_package,
|
||||||
|
.targets = fm.targets,
|
||||||
|
.source = source,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
struct Candidate {
|
struct Candidate {
|
||||||
std::string source;
|
std::string source;
|
||||||
linkdb::Recipe recipe;
|
linkdb::Recipe recipe;
|
||||||
@@ -159,6 +170,10 @@ auto discover(const std::string& name, const std::string& version_spec,
|
|||||||
candidates.push_back(
|
candidates.push_back(
|
||||||
{"nix-probe", recipe_from_nix_scan(*scan_hit, name, "nix-probe")});
|
{"nix-probe", recipe_from_nix_scan(*scan_hit, name, "nix-probe")});
|
||||||
}
|
}
|
||||||
|
if (auto fm = findmodule_scan(name); fm) {
|
||||||
|
candidates.push_back(
|
||||||
|
{"cmake-findmodule", recipe_from_findmodule(*fm, name, "cmake-findmodule")});
|
||||||
|
}
|
||||||
if (!realized_dev_path.empty()) {
|
if (!realized_dev_path.empty()) {
|
||||||
if (auto p = pc_scan(fs::path{realized_dev_path}, name); p) {
|
if (auto p = pc_scan(fs::path{realized_dev_path}, name); p) {
|
||||||
pc_hit = std::move(*p);
|
pc_hit = std::move(*p);
|
||||||
|
|||||||
205
src/resolver/findmodule_scan.cpp
Normal file
205
src/resolver/findmodule_scan.cpp
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
module cargoxx.resolver;
|
||||||
|
|
||||||
|
import std;
|
||||||
|
import cargoxx.exec;
|
||||||
|
import cargoxx.util;
|
||||||
|
|
||||||
|
namespace cargoxx::resolver {
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Mirrors nix_cmake_scan / pc_scan. Kept local; Phase A refactor will
|
||||||
|
// extract this to a shared `name_match` module.
|
||||||
|
auto normalize(std::string_view s) -> std::string {
|
||||||
|
std::string out;
|
||||||
|
out.reserve(s.size());
|
||||||
|
for (char c : s) {
|
||||||
|
out += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||||
|
}
|
||||||
|
if (out.size() > 3 && out.starts_with("lib")) {
|
||||||
|
out.erase(0, 3);
|
||||||
|
}
|
||||||
|
auto is_vchar = [](char c) {
|
||||||
|
return std::isdigit(static_cast<unsigned char>(c)) || c == '.'
|
||||||
|
|| c == '-' || c == '_';
|
||||||
|
};
|
||||||
|
std::size_t end = out.size();
|
||||||
|
while (end > 0 && is_vchar(out[end - 1])) {
|
||||||
|
--end;
|
||||||
|
}
|
||||||
|
bool has_digit = false;
|
||||||
|
for (auto i = end; i < out.size(); ++i) {
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(out[i]))) {
|
||||||
|
has_digit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (has_digit) {
|
||||||
|
out.erase(end);
|
||||||
|
if (!out.empty() && (out.back() == '-' || out.back() == '_')) {
|
||||||
|
out.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::string compact;
|
||||||
|
compact.reserve(out.size());
|
||||||
|
for (char c : out) {
|
||||||
|
if (std::isalnum(static_cast<unsigned char>(c))) {
|
||||||
|
compact += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return compact;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto match_score(std::string_view stem, std::string_view pkg) -> int {
|
||||||
|
auto s = normalize(stem);
|
||||||
|
auto q = normalize(pkg);
|
||||||
|
if (q.empty()) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (s == q) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!s.empty() && (s.starts_with(q) || q.starts_with(s))) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find CMake's bundled Modules dir. `cmake -E capabilities` emits JSON
|
||||||
|
// with a `cmakeRoot` field; modules live at `${cmakeRoot}/Modules/`.
|
||||||
|
// We parse the value with a tiny string search rather than dragging
|
||||||
|
// nlohmann::json through this module — the field's value is always a
|
||||||
|
// quoted string immediately after the literal `"cmakeRoot":`.
|
||||||
|
auto find_modules_dir() -> std::optional<fs::path> {
|
||||||
|
auto r = exec::run("cmake",
|
||||||
|
{"-E", "capabilities"},
|
||||||
|
exec::ExecOptions{
|
||||||
|
.cwd = fs::current_path(),
|
||||||
|
.env_overrides = {},
|
||||||
|
.timeout = std::chrono::seconds{5},
|
||||||
|
.inherit_stdio = false,
|
||||||
|
});
|
||||||
|
if (!r || r->exit_code != 0) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
std::string_view body = r->stdout_text;
|
||||||
|
constexpr std::string_view key = "\"cmakeRoot\":\"";
|
||||||
|
auto pos = body.find(key);
|
||||||
|
if (pos == std::string_view::npos) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
pos += key.size();
|
||||||
|
auto end = body.find('"', pos);
|
||||||
|
if (end == std::string_view::npos) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
fs::path modules = fs::path{std::string{body.substr(pos, end - pos)}} / "Modules";
|
||||||
|
std::error_code ec;
|
||||||
|
if (!fs::exists(modules, ec) || ec) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip a leading "Find" and trailing ".cmake" from a filename to get
|
||||||
|
// the find_package stem.
|
||||||
|
auto module_stem(const fs::path& path) -> std::string {
|
||||||
|
auto s = path.stem().string(); // e.g. "FindSQLite3"
|
||||||
|
if (s.starts_with("Find")) {
|
||||||
|
s.erase(0, 4);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto findmodule_scan(const std::string& package_name)
|
||||||
|
-> util::Result<FindModuleCandidate> {
|
||||||
|
auto modules_dir = find_modules_dir();
|
||||||
|
if (!modules_dir) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::ResolutionUnknownPackage,
|
||||||
|
"cannot locate CMake's bundled Modules directory",
|
||||||
|
"", std::nullopt, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Match {
|
||||||
|
int score;
|
||||||
|
std::string stem;
|
||||||
|
fs::path path;
|
||||||
|
};
|
||||||
|
std::vector<Match> matches;
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
for (const auto& entry : fs::directory_iterator{
|
||||||
|
*modules_dir, fs::directory_options::skip_permission_denied, ec}) {
|
||||||
|
if (!entry.is_regular_file()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto name = entry.path().filename().string();
|
||||||
|
if (!name.starts_with("Find") || !name.ends_with(".cmake")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto stem = module_stem(entry.path());
|
||||||
|
matches.push_back(Match{
|
||||||
|
.score = match_score(stem, package_name),
|
||||||
|
.stem = std::move(stem),
|
||||||
|
.path = entry.path(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.empty()) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::ResolutionUnknownPackage,
|
||||||
|
std::format("no Find*.cmake under '{}'", modules_dir->string()),
|
||||||
|
"", std::nullopt, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ranges::stable_sort(matches, [](const Match& a, const Match& b) {
|
||||||
|
if (a.score != b.score) {
|
||||||
|
return a.score < b.score;
|
||||||
|
}
|
||||||
|
if (a.stem.size() != b.stem.size()) {
|
||||||
|
return a.stem.size() < b.stem.size();
|
||||||
|
}
|
||||||
|
return a.stem < b.stem;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matches.front().score >= 2) {
|
||||||
|
return std::unexpected(util::Error{
|
||||||
|
util::ErrorCode::ResolutionUnknownPackage,
|
||||||
|
std::format("no Find*.cmake matches package name '{}'", package_name),
|
||||||
|
"", std::nullopt, std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& winner = matches.front();
|
||||||
|
|
||||||
|
// FindModules rarely declare IMPORTED targets the same way Config
|
||||||
|
// files do, so scan_imported_targets often comes back empty.
|
||||||
|
// Default to CMake's modern convention `<X>::<X>` and let the
|
||||||
|
// namespaced-from-the-module-body pick override when present.
|
||||||
|
std::vector<std::string> targets;
|
||||||
|
std::ifstream in{winner.path};
|
||||||
|
if (in) {
|
||||||
|
std::string text{std::istreambuf_iterator<char>{in}, {}};
|
||||||
|
targets = scan_imported_targets(text);
|
||||||
|
}
|
||||||
|
if (targets.empty()) {
|
||||||
|
targets.push_back(std::format("{}::{}", winner.stem, winner.stem));
|
||||||
|
} else {
|
||||||
|
targets = filter_public_targets(std::move(targets), winner.stem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FindModuleCandidate{
|
||||||
|
.find_package = std::format("{} REQUIRED", winner.stem),
|
||||||
|
.targets = std::move(targets),
|
||||||
|
.module_file = winner.path,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cargoxx::resolver
|
||||||
@@ -77,6 +77,23 @@ auto nix_cmake_scan(const std::filesystem::path& store_path,
|
|||||||
const std::string& package_name)
|
const std::string& package_name)
|
||||||
-> util::Result<NixCmakeCandidate>;
|
-> util::Result<NixCmakeCandidate>;
|
||||||
|
|
||||||
|
// A CMake builtin FindModule-shaped recipe. CMake ships ~160 `Find*.cmake`
|
||||||
|
// modules with the installation; for libraries that have no `Config.cmake`
|
||||||
|
// but a corresponding builtin (sqlite has `FindSQLite3.cmake`, openssl has
|
||||||
|
// `FindOpenSSL.cmake`, threads has `FindThreads.cmake`, …) the recipe
|
||||||
|
// emits `find_package(<X> REQUIRED)` without the CONFIG keyword.
|
||||||
|
struct FindModuleCandidate {
|
||||||
|
std::string find_package; // e.g. "SQLite3 REQUIRED"
|
||||||
|
std::vector<std::string> targets; // e.g. ["SQLite::SQLite3"]
|
||||||
|
std::filesystem::path module_file; // the FindX.cmake we matched
|
||||||
|
};
|
||||||
|
|
||||||
|
// Walks CMake's bundled `Modules/Find*.cmake` and picks the best match
|
||||||
|
// for `package_name`. Returns ResolutionUnknownPackage when no module
|
||||||
|
// scores acceptably.
|
||||||
|
auto findmodule_scan(const std::string& package_name)
|
||||||
|
-> util::Result<FindModuleCandidate>;
|
||||||
|
|
||||||
// A pkg-config-shaped recipe: the package ships a `.pc` file rather
|
// A pkg-config-shaped recipe: the package ships a `.pc` file rather
|
||||||
// than a CMake config. Consumed via `find_package(PkgConfig REQUIRED)`
|
// than a CMake config. Consumed via `find_package(PkgConfig REQUIRED)`
|
||||||
// + `pkg_check_modules(<NAME> REQUIRED IMPORTED_TARGET <pc_module>)`,
|
// + `pkg_check_modules(<NAME> REQUIRED IMPORTED_TARGET <pc_module>)`,
|
||||||
|
|||||||
Reference in New Issue
Block a user