253 lines
8.0 KiB
C++
253 lines
8.0 KiB
C++
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(<name> ... IMPORTED ...)` and
|
|
// `add_library(<alias> ALIAS <real>)` forms. Returns the bare target names.
|
|
auto collect_targets(std::string_view text) -> std::vector<std::string> {
|
|
std::vector<std::string> 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<unsigned char>(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(<name> [STATIC|SHARED|...] [IMPORTED] ...)
|
|
// or
|
|
// add_library(<alias> ALIAS <real>)
|
|
std::vector<std::string_view> toks;
|
|
std::size_t tp = 0;
|
|
while (tp < args.size()) {
|
|
while (tp < args.size() &&
|
|
std::isspace(static_cast<unsigned char>(args[tp]))) {
|
|
++tp;
|
|
}
|
|
if (tp >= args.size()) {
|
|
break;
|
|
}
|
|
std::size_t start = tp;
|
|
while (tp < args.size() &&
|
|
!std::isspace(static_cast<unsigned char>(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<std::string> {
|
|
auto targets = collect_targets(config_text);
|
|
// Stable-dedup: preserve first-occurrence order, drop duplicates.
|
|
std::vector<std::string> 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<unsigned char>(a[i]));
|
|
auto bl = std::tolower(static_cast<unsigned char>(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<unsigned char>(longer[i]));
|
|
auto b = std::tolower(static_cast<unsigned char>(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<NixCmakeCandidate> {
|
|
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<Match> 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(<self>-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<std::string> 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<char>{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
|