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(std::tolower(static_cast(c))); } if (out.size() > 3 && out.starts_with("lib")) { out.erase(0, 3); } auto is_vchar = [](char c) { return std::isdigit(static_cast(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(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(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(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 { 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 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