module cargoxx.resolver; import std; import cargoxx.util; namespace cargoxx::resolver { namespace fs = std::filesystem; auto config_stem_to_package(std::string_view filename) -> std::string { std::string s{filename}; // Drop any directory prefix. if (auto slash = s.find_last_of('/'); slash != std::string::npos) { s.erase(0, slash + 1); } constexpr std::array suffixes = {".cmake"}; for (auto suf : suffixes) { if (s.ends_with(suf)) { s.erase(s.size() - std::string_view{suf}.size()); } } constexpr std::array stems = {std::string_view{"Config"}, std::string_view{"-config"}}; for (auto stem : stems) { if (s.ends_with(stem)) { s.erase(s.size() - stem.size()); break; } } return s; } namespace { // Walks `text` looking for `add_library( ... IMPORTED ...)` and // `add_library( ALIAS )` forms. Returns the bare target names. auto collect_targets(std::string_view text) -> std::vector { std::vector out; constexpr std::string_view marker = "add_library"; std::size_t pos = 0; while (pos < text.size()) { auto next = text.find(marker, pos); if (next == std::string_view::npos) { break; } // Must be at line start or preceded by whitespace/punct (avoid // matching `_add_library` etc.). if (next > 0) { char prev = text[next - 1]; if (std::isalnum(static_cast(prev)) || prev == '_') { pos = next + marker.size(); continue; } } // Find the opening '('. auto open = text.find('(', next + marker.size()); if (open == std::string_view::npos) { break; } auto close = text.find(')', open + 1); if (close == std::string_view::npos) { break; } auto args = text.substr(open + 1, close - open - 1); // Tokenize by whitespace. CMake's add_library has the form // add_library( [STATIC|SHARED|...] [IMPORTED] ...) // or // add_library( ALIAS ) std::vector toks; std::size_t tp = 0; while (tp < args.size()) { while (tp < args.size() && std::isspace(static_cast(args[tp]))) { ++tp; } if (tp >= args.size()) { break; } std::size_t start = tp; while (tp < args.size() && !std::isspace(static_cast(args[tp]))) { ++tp; } toks.push_back(args.substr(start, tp - start)); } if (toks.size() >= 2) { const auto& name = toks[0]; bool imported = false; bool alias = false; for (std::size_t i = 1; i < toks.size(); ++i) { if (toks[i] == "IMPORTED") { imported = true; } if (toks[i] == "ALIAS") { alias = true; } } if (imported || alias) { out.emplace_back(name); } } pos = close + 1; } return out; } } // namespace auto scan_imported_targets(std::string_view config_text) -> std::vector { auto targets = collect_targets(config_text); // Stable-dedup: preserve first-occurrence order, drop duplicates. std::vector out; out.reserve(targets.size()); for (auto& t : targets) { if (std::ranges::find(out, t) == out.end()) { out.push_back(std::move(t)); } } return out; } namespace { // Score a stem against the queried package name. Lower is better. // 0 — exact match (case-insensitive) // 1 — prefix match (one is a case-insensitive prefix of the other) // 2 — fallback (any other non-empty target list) auto match_score(std::string_view stem, std::string_view pkg) -> int { auto eq_ci = [](std::string_view a, std::string_view b) { if (a.size() != b.size()) { return false; } for (std::size_t i = 0; i < a.size(); ++i) { auto al = std::tolower(static_cast(a[i])); auto bl = std::tolower(static_cast(b[i])); if (al != bl) { return false; } } return true; }; if (eq_ci(stem, pkg)) { return 0; } auto starts_with_ci = [](std::string_view longer, std::string_view shorter) { if (shorter.size() > longer.size()) { return false; } for (std::size_t i = 0; i < shorter.size(); ++i) { auto a = std::tolower(static_cast(longer[i])); auto b = std::tolower(static_cast(shorter[i])); if (a != b) { return false; } } return true; }; if (starts_with_ci(stem, pkg) || starts_with_ci(pkg, stem)) { return 1; } return 2; } auto is_config_filename(const fs::path& p) -> bool { if (p.extension() != ".cmake") { return false; } auto stem = p.stem().string(); return stem.ends_with("Config") || stem.ends_with("-config"); } } // namespace auto nix_cmake_scan(const fs::path& store_path, const std::string& package_name) -> util::Result { const auto cmake_dir = store_path / "lib" / "cmake"; std::error_code ec; if (!fs::exists(cmake_dir, ec) || ec) { return std::unexpected(util::Error{ util::ErrorCode::ResolutionUnknownPackage, std::format("no CMake configs under '{}'", cmake_dir.string()), "", store_path, std::nullopt, }); } struct Match { int score; NixCmakeCandidate cand; }; std::vector matches; // Walk the tree once, find every *Config.cmake / *-config.cmake. The // IMPORTED targets are usually in a sibling *-targets.cmake (via the // standard `include(-targets.cmake)` pattern), so for each // config file we scan every .cmake file in its parent directory. for (const auto& entry : fs::recursive_directory_iterator{ cmake_dir, fs::directory_options::skip_permission_denied}) { if (!entry.is_regular_file()) { continue; } if (!is_config_filename(entry.path())) { continue; } std::vector targets; const auto pkg_dir = entry.path().parent_path(); for (const auto& sib : fs::directory_iterator{pkg_dir}) { if (!sib.is_regular_file() || sib.path().extension() != ".cmake") { continue; } std::ifstream in{sib.path()}; if (!in) { continue; } std::string text{std::istreambuf_iterator{in}, {}}; for (auto& t : scan_imported_targets(text)) { if (std::ranges::find(targets, t) == targets.end()) { targets.push_back(std::move(t)); } } } if (targets.empty()) { continue; } auto stem = config_stem_to_package(entry.path().filename().string()); NixCmakeCandidate cand{ .find_package = std::format("{} CONFIG REQUIRED", stem), .targets = std::move(targets), .config_file = entry.path(), }; matches.push_back(Match{ .score = match_score(stem, package_name), .cand = std::move(cand), }); } if (matches.empty()) { return std::unexpected(util::Error{ util::ErrorCode::ResolutionUnknownPackage, std::format("no IMPORTED targets found under '{}'", cmake_dir.string()), "", store_path, std::nullopt, }); } std::ranges::stable_sort(matches, {}, &Match::score); return std::move(matches.front().cand); } } // namespace cargoxx::resolver