Files
cargoxx/src/resolver/pc_scan.cpp

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