module; #include module cargoxx.resolver; import std; import cargoxx.exec; import cargoxx.util; namespace cargoxx::resolver { namespace fs = std::filesystem; namespace { auto network_error(std::string msg) -> util::Error { return util::Error{util::ErrorCode::ResolutionNetworkError, std::move(msg), "", std::nullopt, std::nullopt}; } auto fetch_tree_paths(const std::string& url) -> util::Result> { auto r = exec::run("curl", {"-fsSL", "--max-time", "20", url}, exec::ExecOptions{ .cwd = {}, .env_overrides = {}, .timeout = std::chrono::seconds{30}, .inherit_stdio = false, }); if (!r) { return std::unexpected(r.error()); } if (r->exit_code != 0) { return std::unexpected(network_error(std::format( "curl failed (exit {}): {}", r->exit_code, r->stderr_text))); } nlohmann::json j; try { j = nlohmann::json::parse(r->stdout_text); } catch (const nlohmann::json::parse_error& e) { return std::unexpected( network_error(std::format("tree listing not valid JSON: {}", e.what()))); } if (!j.contains("tree") || !j["tree"].is_array()) { return std::unexpected(network_error("tree listing missing 'tree' array")); } std::vector out; for (const auto& entry : j["tree"]) { if (entry.contains("path") && entry["path"].is_string()) { out.push_back(entry["path"].get()); } } return out; } auto cache_root() -> fs::path { if (auto* xdg = std::getenv("XDG_CACHE_HOME"); xdg && *xdg) { return fs::path{xdg} / "cargoxx"; } if (auto* home = std::getenv("HOME"); home && *home) { return fs::path{home} / ".cache" / "cargoxx"; } return fs::temp_directory_path() / "cargoxx"; } constexpr auto INDEX_TTL = std::chrono::hours{24}; auto load_or_fetch(const std::string& cache_key, const std::string& url) -> util::Result> { auto path = cache_root() / std::format("{}-index.txt", cache_key); std::error_code ec; if (fs::exists(path, ec) && !ec) { auto age = std::chrono::system_clock::now() - std::chrono::file_clock::to_sys(fs::last_write_time(path)); if (age < INDEX_TTL) { std::ifstream in{path}; if (in) { std::vector out; std::string line; while (std::getline(in, line)) { if (!line.empty()) { out.push_back(std::move(line)); } } if (!out.empty()) { return out; } } } } auto fresh = fetch_tree_paths(url); if (!fresh) { return std::unexpected(fresh.error()); } fs::create_directories(path.parent_path(), ec); if (std::ofstream out{path}; out) { for (const auto& p : *fresh) { out << p << '\n'; } } return fresh; } // Levenshtein top-k filter with a max-distance gate of ⌈len/4⌉ (min 1). auto top_fuzzy(std::string_view query, const std::vector& corpus, std::size_t k) -> std::vector { const std::size_t cap = std::max(1, (query.size() + 3) / 4); struct Scored { std::size_t dist; std::string name; }; std::vector scored; scored.reserve(corpus.size()); for (const auto& c : corpus) { auto d = util::levenshtein(query, c); if (d <= cap) { scored.push_back({d, c}); } } std::ranges::sort(scored, [](const auto& a, const auto& b) { if (a.dist != b.dist) { return a.dist < b.dist; } return a.name < b.name; }); std::vector out; for (std::size_t i = 0; i < std::min(k, scored.size()); ++i) { out.push_back(std::move(scored[i].name)); } return out; } constexpr auto FUZZY_K = std::size_t{3}; } // namespace auto conan_probe_fuzzy(const std::string& name) -> util::Result { if (auto exact = conan_probe(name); exact) { return exact; } auto index = load_or_fetch( "conan", "https://api.github.com/repos/conan-io/conan-center-index/git/trees/master:recipes"); if (!index) { return std::unexpected(util::Error{ util::ErrorCode::ResolutionUnknownPackage, std::format("no Conan recipe for '{}' and index fetch failed", name), "", std::nullopt, std::nullopt, }); } auto candidates = top_fuzzy(name, *index, FUZZY_K); for (const auto& cand : candidates) { if (auto r = conan_probe(cand); r) { return r; } } return std::unexpected(util::Error{ util::ErrorCode::ResolutionUnknownPackage, std::format("no Conan recipe matches '{}' (tried exact + fuzzy top-{})", name, FUZZY_K), "", std::nullopt, std::nullopt, }); } auto vcpkg_probe_fuzzy(const std::string& name) -> util::Result { if (auto exact = vcpkg_probe(name); exact) { return exact; } auto index = load_or_fetch( "vcpkg", "https://api.github.com/repos/microsoft/vcpkg/git/trees/master:ports"); if (!index) { return std::unexpected(util::Error{ util::ErrorCode::ResolutionUnknownPackage, std::format("no vcpkg port for '{}' and index fetch failed", name), "", std::nullopt, std::nullopt, }); } auto candidates = top_fuzzy(name, *index, FUZZY_K); for (const auto& cand : candidates) { if (auto r = vcpkg_probe(cand); r) { return r; } } return std::unexpected(util::Error{ util::ErrorCode::ResolutionUnknownPackage, std::format("no vcpkg port matches '{}' (tried exact + fuzzy top-{})", name, FUZZY_K), "", std::nullopt, std::nullopt, }); } } // namespace cargoxx::resolver