162 lines
4.6 KiB
C++
162 lines
4.6 KiB
C++
module cargoxx.resolver;
|
|
|
|
import std;
|
|
import cargoxx.util;
|
|
|
|
namespace cargoxx::resolver {
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace {
|
|
|
|
// Mirrors `normalize` in nix_cmake_scan.cpp. Kept local rather than
|
|
// extracted so each scanner is self-contained; a future Phase A
|
|
// refactor will hoist this into a shared 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;
|
|
}
|
|
|
|
// Same scoring as nix_cmake_scan: 0 exact, 1 prefix-either, 2 fallback.
|
|
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;
|
|
}
|
|
|
|
// Defensive: read a few KB to verify the .pc has at least one Name: or
|
|
// Libs: line. We don't parse the file — `pkg_check_modules` does that
|
|
// at CMake time — but a sanity check rejects empty/junk files.
|
|
auto looks_like_pc(const fs::path& path) -> bool {
|
|
std::ifstream in{path};
|
|
if (!in) {
|
|
return false;
|
|
}
|
|
char buf[4096];
|
|
in.read(buf, sizeof(buf));
|
|
std::string_view chunk{buf, static_cast<std::size_t>(in.gcount())};
|
|
return chunk.find("\nName:") != std::string_view::npos ||
|
|
chunk.starts_with("Name:") ||
|
|
chunk.find("\nLibs:") != std::string_view::npos ||
|
|
chunk.starts_with("Libs:");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
auto pc_scan(const fs::path& store_path, const std::string& package_name)
|
|
-> util::Result<PcCandidate> {
|
|
const auto pc_dir = store_path / "lib" / "pkgconfig";
|
|
std::error_code ec;
|
|
if (!fs::exists(pc_dir, ec) || ec) {
|
|
return std::unexpected(util::Error{
|
|
util::ErrorCode::ResolutionUnknownPackage,
|
|
std::format("no pkgconfig directory under '{}'", pc_dir.string()),
|
|
"", store_path, std::nullopt,
|
|
});
|
|
}
|
|
|
|
struct Match {
|
|
int score;
|
|
std::string stem;
|
|
fs::path path;
|
|
};
|
|
std::vector<Match> matches;
|
|
|
|
for (const auto& entry : fs::directory_iterator{
|
|
pc_dir, fs::directory_options::skip_permission_denied, ec}) {
|
|
if (!entry.is_regular_file()) {
|
|
continue;
|
|
}
|
|
if (entry.path().extension() != ".pc") {
|
|
continue;
|
|
}
|
|
if (!looks_like_pc(entry.path())) {
|
|
continue;
|
|
}
|
|
auto stem = entry.path().stem().string();
|
|
matches.push_back(Match{
|
|
.score = match_score(stem, package_name),
|
|
.stem = stem,
|
|
.path = entry.path(),
|
|
});
|
|
}
|
|
|
|
if (matches.empty()) {
|
|
return std::unexpected(util::Error{
|
|
util::ErrorCode::ResolutionUnknownPackage,
|
|
std::format("no usable .pc file under '{}'", pc_dir.string()),
|
|
"", store_path, 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 .pc file under '{}' matches package name '{}'",
|
|
pc_dir.string(), package_name),
|
|
"", store_path, std::nullopt,
|
|
});
|
|
}
|
|
|
|
return PcCandidate{
|
|
.pc_module = std::move(matches.front().stem),
|
|
.pc_file = std::move(matches.front().path),
|
|
};
|
|
}
|
|
|
|
} // namespace cargoxx::resolver
|