[M2] add curated linkdb + semver matcher

This commit is contained in:
2026-05-08 11:42:08 +00:00
parent 361b936648
commit d5715428ea
12 changed files with 26378 additions and 0 deletions

147
src/linkdb/curated.cpp Normal file
View File

@@ -0,0 +1,147 @@
module;
#include <json.hpp>
module cargoxx.linkdb;
import std;
import cargoxx.util;
#ifndef CARGOXX_LINKDB_DEFAULT_PATH
#define CARGOXX_LINKDB_DEFAULT_PATH ""
#endif
namespace cargoxx::linkdb {
namespace {
auto curated_source_error(std::string msg) -> util::Error {
return util::Error{
util::ErrorCode::LinkdbCorrupt, std::move(msg), "", std::nullopt, std::nullopt,
};
}
auto load_curated(const std::filesystem::path& path)
-> util::Result<std::map<std::string, std::vector<detail::CuratedRecipe>>> {
std::ifstream in{path};
if (!in) {
return std::unexpected(curated_source_error(
std::format("cannot open curated linkdb at '{}'", path.string())));
}
nlohmann::json j;
try {
in >> j;
} catch (const nlohmann::json::parse_error& e) {
return std::unexpected(curated_source_error(
std::format("curated linkdb is not valid JSON: {}", e.what())));
}
if (!j.is_object() || !j.contains("packages") || !j["packages"].is_object()) {
return std::unexpected(
curated_source_error("curated linkdb missing top-level 'packages' object"));
}
std::map<std::string, std::vector<detail::CuratedRecipe>> out;
for (const auto& [name, recipes] : j["packages"].items()) {
if (!recipes.is_array()) {
return std::unexpected(curated_source_error(
std::format("packages.{} must be an array", name)));
}
std::vector<detail::CuratedRecipe> bucket;
bucket.reserve(recipes.size());
for (const auto& r : recipes) {
detail::CuratedRecipe rec;
try {
rec.version_range = r.at("version").get<std::string>();
rec.nixpkgs_attr = r.at("nixpkgs_attr").get<std::string>();
rec.find_package = r.at("find_package").get<std::string>();
rec.targets = r.at("targets").get<std::vector<std::string>>();
} catch (const nlohmann::json::exception& e) {
return std::unexpected(curated_source_error(std::format(
"packages.{} has a malformed recipe: {}", name, e.what())));
}
if (r.contains("components") && r["components"] == "supported") {
rec.components_supported = true;
}
bucket.push_back(std::move(rec));
}
out.emplace(name, std::move(bucket));
}
return out;
}
} // namespace
auto Database::open() -> util::Result<Database> {
Database db;
auto curated = load_curated(CARGOXX_LINKDB_DEFAULT_PATH);
if (!curated) {
return std::unexpected(curated.error());
}
db.curated_ = std::move(*curated);
return db;
}
auto Database::resolve(const std::string& package, const std::string& version,
const std::vector<std::string>& components) -> util::Result<Recipe> {
auto it = curated_.find(package);
if (it == curated_.end() || it->second.empty()) {
return std::unexpected(util::Error{
util::ErrorCode::LinkdbUnknownPackage,
std::format("package '{}' has no known CMake link recipe", package),
"file an issue at <repo>/issues/new, or add a manual recipe via cargoxx linkdb add",
std::nullopt, std::nullopt,
});
}
const detail::CuratedRecipe* match = nullptr;
for (const auto& r : it->second) {
if (util::satisfies(version, r.version_range)) {
match = &r;
break;
}
}
if (!match) {
return std::unexpected(util::Error{
util::ErrorCode::LinkdbUnknownPackage,
std::format("no curated recipe for {} {} matches", package, version),
"", std::nullopt, std::nullopt,
});
}
if (match->components_supported && components.empty()) {
return std::unexpected(util::Error{
util::ErrorCode::LinkdbComponentNotSupported,
std::format("package '{}' requires at least one component", package),
"specify components in Cargoxx.toml: { version = \"...\", components = [\"...\"] }",
std::nullopt, std::nullopt,
});
}
if (!match->components_supported && !components.empty()) {
return std::unexpected(util::Error{
util::ErrorCode::LinkdbComponentNotSupported,
std::format("package '{}' does not declare component support", package),
"", std::nullopt, std::nullopt,
});
}
Recipe out{
.nixpkgs_attr = match->nixpkgs_attr,
.find_package = substitute_components(match->find_package, components),
.targets = expand_targets(match->targets, components),
.source = "curated",
};
return out;
}
auto Database::add_manual(const std::string&, const std::string&, const Recipe&)
-> util::Result<void> {
return std::unexpected(util::Error{
util::ErrorCode::NotImplemented,
"manual link recipes are not implemented in this milestone",
"", std::nullopt, std::nullopt,
});
}
} // namespace cargoxx::linkdb

View File

@@ -1,3 +1,56 @@
export module cargoxx.linkdb;
import std;
import cargoxx.util;
export namespace cargoxx::linkdb {
struct Recipe {
std::string nixpkgs_attr;
std::string find_package; // post-substitution
std::vector<std::string> targets; // post-substitution
std::string source; // 'curated' | 'manual' | etc.
bool operator==(const Recipe&) const = default;
};
} // namespace cargoxx::linkdb
namespace cargoxx::linkdb::detail {
struct CuratedRecipe {
std::string version_range;
std::string nixpkgs_attr;
std::string find_package;
std::vector<std::string> targets;
bool components_supported = false;
};
} // namespace cargoxx::linkdb::detail
export namespace cargoxx::linkdb {
class Database {
public:
static auto open() -> util::Result<Database>;
auto resolve(const std::string& package, const std::string& version,
const std::vector<std::string>& components = {})
-> util::Result<Recipe>;
auto add_manual(const std::string& package, const std::string& version_range,
const Recipe& r) -> util::Result<void>;
private:
Database() = default;
std::map<std::string, std::vector<detail::CuratedRecipe>> curated_;
};
// Pure helpers exported for unit testing.
auto substitute_components(std::string find_package, const std::vector<std::string>& components)
-> std::string;
auto expand_targets(const std::vector<std::string>& templates,
const std::vector<std::string>& components) -> std::vector<std::string>;
} // namespace cargoxx::linkdb

55
src/linkdb/recipe.cpp Normal file
View File

@@ -0,0 +1,55 @@
module cargoxx.linkdb;
import std;
namespace cargoxx::linkdb {
namespace {
auto replace_all(std::string s, std::string_view marker, std::string_view value) -> std::string {
std::string out;
out.reserve(s.size());
std::size_t pos = 0;
while (pos < s.size()) {
auto next = s.find(marker, pos);
if (next == std::string::npos) {
out.append(s, pos);
break;
}
out.append(s, pos, next - pos);
out.append(value);
pos = next + marker.size();
}
return out;
}
} // namespace
auto substitute_components(std::string find_package, const std::vector<std::string>& components)
-> std::string {
std::string joined;
for (std::size_t i = 0; i < components.size(); ++i) {
if (i > 0) {
joined += ' ';
}
joined += components[i];
}
return replace_all(std::move(find_package), "{{components}}", joined);
}
auto expand_targets(const std::vector<std::string>& templates,
const std::vector<std::string>& components) -> std::vector<std::string> {
std::vector<std::string> out;
for (const auto& t : templates) {
if (t.find("{{component}}") != std::string::npos) {
for (const auto& c : components) {
out.push_back(replace_all(t, "{{component}}", c));
}
} else {
out.push_back(t);
}
}
return out;
}
} // namespace cargoxx::linkdb