[M2] add curated linkdb + semver matcher
This commit is contained in:
14
CHANGELOG.md
14
CHANGELOG.md
@@ -43,3 +43,17 @@ All notable changes to cargoxx will be documented in this file.
|
||||
Codegen of `flake.nix` / `CMakeLists.txt` is intentionally deferred to M3.
|
||||
CLI11 v2.6.2 vendored at `third_party/CLI11.hpp`; entry point is now
|
||||
`cargoxx::cli::run(argc, argv)`. `tests/cmd_new.cpp` covers 9 cases.
|
||||
- `util::satisfies(version, range)` — minimal semver range matcher
|
||||
supporting `*`, `==/>=/<=/>/<` operators, and comma-separated AND
|
||||
clauses. Versions are zero-padded to three components.
|
||||
`tests/semver_satisfies.cpp` covers 7 cases.
|
||||
- `cargoxx.linkdb`: `Recipe`, `Database::open()`, `Database::resolve(...)`
|
||||
for curated lookups. The curated database ships at `data/linkdb.json`
|
||||
with all 25 packages from `SPEC.md` §11. Component-substitution helpers
|
||||
expand `{{components}}` (space-joined) in `find_package` and fan out
|
||||
`{{component}}` per-target. Path is injected via the
|
||||
`CARGOXX_LINKDB_DEFAULT_PATH` compile definition. nlohmann/json 3.12.0
|
||||
vendored at `third_party/json.hpp`. SQLite overlay and `add_manual`
|
||||
are deferred to the M2 follow-up commit.
|
||||
`tests/linkdb_lookup.cpp` covers 13 cases including a smoke test that
|
||||
resolves all 25 curated packages.
|
||||
|
||||
@@ -32,12 +32,18 @@ endif()
|
||||
# ----- cargoxx library: module units + implementation units -----
|
||||
add_library(cargoxx STATIC)
|
||||
target_include_directories(cargoxx SYSTEM PRIVATE third_party)
|
||||
target_compile_definitions(cargoxx PRIVATE
|
||||
CARGOXX_LINKDB_DEFAULT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/data/linkdb.json"
|
||||
)
|
||||
target_sources(cargoxx
|
||||
PRIVATE
|
||||
src/util/error.cpp
|
||||
src/util/semver.cpp
|
||||
src/manifest/parser.cpp
|
||||
src/manifest/writer.cpp
|
||||
src/layout/layout.cpp
|
||||
src/linkdb/recipe.cpp
|
||||
src/linkdb/curated.cpp
|
||||
src/cli/cmd_new.cpp
|
||||
src/cli/run.cpp
|
||||
PUBLIC
|
||||
|
||||
213
data/linkdb.json
Normal file
213
data/linkdb.json
Normal file
@@ -0,0 +1,213 @@
|
||||
{
|
||||
"version": 1,
|
||||
"packages": {
|
||||
"fmt": [
|
||||
{
|
||||
"version": ">=10.0.0",
|
||||
"nixpkgs_attr": "fmt_10",
|
||||
"find_package": "fmt CONFIG REQUIRED",
|
||||
"targets": ["fmt::fmt"]
|
||||
},
|
||||
{
|
||||
"version": ">=8.0.0,<10.0.0",
|
||||
"nixpkgs_attr": "fmt_8",
|
||||
"find_package": "fmt CONFIG REQUIRED",
|
||||
"targets": ["fmt::fmt"]
|
||||
}
|
||||
],
|
||||
"spdlog": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "spdlog",
|
||||
"find_package": "spdlog CONFIG REQUIRED",
|
||||
"targets": ["spdlog::spdlog"]
|
||||
}
|
||||
],
|
||||
"nlohmann_json": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "nlohmann_json",
|
||||
"find_package": "nlohmann_json CONFIG REQUIRED",
|
||||
"targets": ["nlohmann_json::nlohmann_json"]
|
||||
}
|
||||
],
|
||||
"boost": [
|
||||
{
|
||||
"version": ">=1.70.0",
|
||||
"nixpkgs_attr": "boost",
|
||||
"find_package": "Boost REQUIRED COMPONENTS {{components}}",
|
||||
"targets": ["Boost::{{component}}"],
|
||||
"components": "supported"
|
||||
}
|
||||
],
|
||||
"openssl": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "openssl",
|
||||
"find_package": "OpenSSL REQUIRED",
|
||||
"targets": ["OpenSSL::SSL", "OpenSSL::Crypto"]
|
||||
}
|
||||
],
|
||||
"zlib": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "zlib",
|
||||
"find_package": "ZLIB REQUIRED",
|
||||
"targets": ["ZLIB::ZLIB"]
|
||||
}
|
||||
],
|
||||
"sqlite3": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "sqlite",
|
||||
"find_package": "SQLite3 REQUIRED",
|
||||
"targets": ["SQLite::SQLite3"]
|
||||
}
|
||||
],
|
||||
"curl": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "curl",
|
||||
"find_package": "CURL REQUIRED",
|
||||
"targets": ["CURL::libcurl"]
|
||||
}
|
||||
],
|
||||
"protobuf": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "protobuf",
|
||||
"find_package": "Protobuf REQUIRED",
|
||||
"targets": ["protobuf::libprotobuf"]
|
||||
}
|
||||
],
|
||||
"grpc": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "grpc",
|
||||
"find_package": "gRPC CONFIG REQUIRED",
|
||||
"targets": ["gRPC::grpc++"]
|
||||
}
|
||||
],
|
||||
"abseil-cpp": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "abseil-cpp",
|
||||
"find_package": "absl CONFIG REQUIRED",
|
||||
"targets": ["absl::{{component}}"],
|
||||
"components": "supported"
|
||||
}
|
||||
],
|
||||
"gtest": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "gtest",
|
||||
"find_package": "GTest CONFIG REQUIRED",
|
||||
"targets": ["GTest::gtest", "GTest::gtest_main"]
|
||||
}
|
||||
],
|
||||
"catch2": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "catch2_3",
|
||||
"find_package": "Catch2 CONFIG REQUIRED",
|
||||
"targets": ["Catch2::Catch2WithMain"]
|
||||
}
|
||||
],
|
||||
"eigen": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "eigen",
|
||||
"find_package": "Eigen3 CONFIG REQUIRED",
|
||||
"targets": ["Eigen3::Eigen"]
|
||||
}
|
||||
],
|
||||
"tbb": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "tbb",
|
||||
"find_package": "TBB CONFIG REQUIRED",
|
||||
"targets": ["TBB::tbb"]
|
||||
}
|
||||
],
|
||||
"libpng": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "libpng",
|
||||
"find_package": "PNG REQUIRED",
|
||||
"targets": ["PNG::PNG"]
|
||||
}
|
||||
],
|
||||
"libjpeg": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "libjpeg",
|
||||
"find_package": "JPEG REQUIRED",
|
||||
"targets": ["JPEG::JPEG"]
|
||||
}
|
||||
],
|
||||
"freetype": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "freetype",
|
||||
"find_package": "Freetype REQUIRED",
|
||||
"targets": ["Freetype::Freetype"]
|
||||
}
|
||||
],
|
||||
"glfw": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "glfw",
|
||||
"find_package": "glfw3 CONFIG REQUIRED",
|
||||
"targets": ["glfw"]
|
||||
}
|
||||
],
|
||||
"glm": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "glm",
|
||||
"find_package": "glm CONFIG REQUIRED",
|
||||
"targets": ["glm::glm"]
|
||||
}
|
||||
],
|
||||
"sdl2": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "SDL2",
|
||||
"find_package": "SDL2 CONFIG REQUIRED",
|
||||
"targets": ["SDL2::SDL2"]
|
||||
}
|
||||
],
|
||||
"cli11": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "cli11",
|
||||
"find_package": "CLI11 CONFIG REQUIRED",
|
||||
"targets": ["CLI11::CLI11"]
|
||||
}
|
||||
],
|
||||
"cxxopts": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "cxxopts",
|
||||
"find_package": "cxxopts CONFIG REQUIRED",
|
||||
"targets": ["cxxopts::cxxopts"]
|
||||
}
|
||||
],
|
||||
"range-v3": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "range-v3",
|
||||
"find_package": "range-v3 CONFIG REQUIRED",
|
||||
"targets": ["range-v3::range-v3"]
|
||||
}
|
||||
],
|
||||
"magic_enum": [
|
||||
{
|
||||
"version": "*",
|
||||
"nixpkgs_attr": "magic-enum",
|
||||
"find_package": "magic_enum CONFIG REQUIRED",
|
||||
"targets": ["magic_enum::magic_enum"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
147
src/linkdb/curated.cpp
Normal file
147
src/linkdb/curated.cpp
Normal 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
|
||||
@@ -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
55
src/linkdb/recipe.cpp
Normal 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
|
||||
142
src/util/semver.cpp
Normal file
142
src/util/semver.cpp
Normal file
@@ -0,0 +1,142 @@
|
||||
module cargoxx.util;
|
||||
|
||||
import std;
|
||||
|
||||
namespace cargoxx::util {
|
||||
|
||||
namespace {
|
||||
|
||||
using Version = std::array<int, 3>;
|
||||
|
||||
auto parse_version(std::string_view s) -> std::optional<Version> {
|
||||
Version v{0, 0, 0};
|
||||
std::size_t idx = 0;
|
||||
std::size_t pos = 0;
|
||||
while (pos < s.size() && idx < 3) {
|
||||
std::size_t end = pos;
|
||||
while (end < s.size() && std::isdigit(static_cast<unsigned char>(s[end]))) {
|
||||
++end;
|
||||
}
|
||||
if (end == pos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
int n = 0;
|
||||
auto [_, ec] = std::from_chars(s.data() + pos, s.data() + end, n);
|
||||
if (ec != std::errc{}) {
|
||||
return std::nullopt;
|
||||
}
|
||||
v[idx++] = n;
|
||||
pos = end;
|
||||
if (pos < s.size() && s[pos] == '.') {
|
||||
++pos;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
auto trim(std::string_view s) -> std::string_view {
|
||||
while (!s.empty() && std::isspace(static_cast<unsigned char>(s.front()))) {
|
||||
s.remove_prefix(1);
|
||||
}
|
||||
while (!s.empty() && std::isspace(static_cast<unsigned char>(s.back()))) {
|
||||
s.remove_suffix(1);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
struct Clause {
|
||||
enum class Op { Eq, Ge, Le, Gt, Lt };
|
||||
Op op;
|
||||
Version v;
|
||||
};
|
||||
|
||||
auto parse_clause(std::string_view raw) -> std::optional<Clause> {
|
||||
auto s = trim(raw);
|
||||
if (s.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
Clause c{Clause::Op::Eq, {}};
|
||||
if (s.starts_with(">=")) {
|
||||
c.op = Clause::Op::Ge;
|
||||
s.remove_prefix(2);
|
||||
} else if (s.starts_with("<=")) {
|
||||
c.op = Clause::Op::Le;
|
||||
s.remove_prefix(2);
|
||||
} else if (s.starts_with("==")) {
|
||||
c.op = Clause::Op::Eq;
|
||||
s.remove_prefix(2);
|
||||
} else if (s.starts_with('>')) {
|
||||
c.op = Clause::Op::Gt;
|
||||
s.remove_prefix(1);
|
||||
} else if (s.starts_with('<')) {
|
||||
c.op = Clause::Op::Lt;
|
||||
s.remove_prefix(1);
|
||||
}
|
||||
s = trim(s);
|
||||
auto v = parse_version(s);
|
||||
if (!v) {
|
||||
return std::nullopt;
|
||||
}
|
||||
c.v = *v;
|
||||
return c;
|
||||
}
|
||||
|
||||
auto cmp(const Version& a, const Version& b) -> int {
|
||||
for (std::size_t i = 0; i < a.size(); ++i) {
|
||||
if (a[i] != b[i]) {
|
||||
return a[i] < b[i] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto check(const Clause& c, const Version& v) -> bool {
|
||||
int r = cmp(v, c.v);
|
||||
switch (c.op) {
|
||||
case Clause::Op::Eq:
|
||||
return r == 0;
|
||||
case Clause::Op::Ge:
|
||||
return r >= 0;
|
||||
case Clause::Op::Le:
|
||||
return r <= 0;
|
||||
case Clause::Op::Gt:
|
||||
return r > 0;
|
||||
case Clause::Op::Lt:
|
||||
return r < 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto satisfies(std::string_view version, std::string_view range) -> bool {
|
||||
range = trim(range);
|
||||
if (range == "*") {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto v = parse_version(trim(version));
|
||||
if (!v) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t pos = 0;
|
||||
while (pos <= range.size()) {
|
||||
auto next = range.find(',', pos);
|
||||
auto clause_str =
|
||||
range.substr(pos, next == std::string_view::npos ? range.size() - pos : next - pos);
|
||||
auto c = parse_clause(clause_str);
|
||||
if (!c || !check(*c, *v)) {
|
||||
return false;
|
||||
}
|
||||
if (next == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
pos = next + 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace cargoxx::util
|
||||
@@ -47,4 +47,19 @@ using Result = std::expected<T, Error>;
|
||||
|
||||
auto format(const Error& e) -> std::string;
|
||||
|
||||
// Returns true if `version` (e.g. "10.2", "1.84.0") satisfies `range`.
|
||||
//
|
||||
// Supported range syntax:
|
||||
// "*" any version
|
||||
// "==X.Y.Z" exact match
|
||||
// ">=X.Y.Z" greater-or-equal
|
||||
// "<=X.Y.Z" less-or-equal
|
||||
// ">X.Y.Z" greater-than
|
||||
// "<X.Y.Z" less-than
|
||||
// "<a>,<b>,..." comma-separated AND of clauses
|
||||
// "X.Y.Z" (bare) treated as "==X.Y.Z"
|
||||
//
|
||||
// Versions with fewer components are zero-padded ("10.2" → 10.2.0).
|
||||
auto satisfies(std::string_view version, std::string_view range) -> bool;
|
||||
|
||||
} // namespace cargoxx::util
|
||||
|
||||
@@ -8,7 +8,9 @@ function(cargoxx_add_test name)
|
||||
endfunction()
|
||||
|
||||
cargoxx_add_test(util_error)
|
||||
cargoxx_add_test(semver_satisfies)
|
||||
cargoxx_add_test(manifest_parse)
|
||||
cargoxx_add_test(manifest_write)
|
||||
cargoxx_add_test(layout_discovery)
|
||||
cargoxx_add_test(linkdb_lookup)
|
||||
cargoxx_add_test(cmd_new)
|
||||
|
||||
151
tests/linkdb_lookup.cpp
Normal file
151
tests/linkdb_lookup.cpp
Normal file
@@ -0,0 +1,151 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
import cargoxx.linkdb;
|
||||
import cargoxx.util;
|
||||
import std;
|
||||
|
||||
using cargoxx::linkdb::Database;
|
||||
using cargoxx::linkdb::Recipe;
|
||||
using cargoxx::linkdb::expand_targets;
|
||||
using cargoxx::linkdb::substitute_components;
|
||||
using cargoxx::util::ErrorCode;
|
||||
|
||||
TEST_CASE("Database::open loads the curated linkdb", "[linkdb]") {
|
||||
auto r = Database::open();
|
||||
REQUIRE(r.has_value());
|
||||
}
|
||||
|
||||
TEST_CASE("resolve returns the curated recipe for fmt 10", "[linkdb]") {
|
||||
auto db = Database::open();
|
||||
REQUIRE(db.has_value());
|
||||
auto rec = db->resolve("fmt", "10.2.0");
|
||||
REQUIRE(rec.has_value());
|
||||
REQUIRE(rec->nixpkgs_attr == "fmt_10");
|
||||
REQUIRE(rec->find_package == "fmt CONFIG REQUIRED");
|
||||
REQUIRE(rec->targets == std::vector<std::string>{"fmt::fmt"});
|
||||
REQUIRE(rec->source == "curated");
|
||||
}
|
||||
|
||||
TEST_CASE("resolve returns the older fmt recipe for fmt 8", "[linkdb]") {
|
||||
auto db = Database::open();
|
||||
REQUIRE(db.has_value());
|
||||
auto rec = db->resolve("fmt", "8.1.0");
|
||||
REQUIRE(rec.has_value());
|
||||
REQUIRE(rec->nixpkgs_attr == "fmt_8");
|
||||
}
|
||||
|
||||
TEST_CASE("resolve fails for an unknown package", "[linkdb]") {
|
||||
auto db = Database::open();
|
||||
REQUIRE(db.has_value());
|
||||
auto rec = db->resolve("obscurelib", "0.0.1");
|
||||
REQUIRE_FALSE(rec.has_value());
|
||||
REQUIRE(rec.error().code == ErrorCode::LinkdbUnknownPackage);
|
||||
}
|
||||
|
||||
TEST_CASE("resolve substitutes boost components", "[linkdb]") {
|
||||
auto db = Database::open();
|
||||
REQUIRE(db.has_value());
|
||||
auto rec = db->resolve("boost", "1.84.0", {"filesystem", "system"});
|
||||
REQUIRE(rec.has_value());
|
||||
REQUIRE(rec->find_package == "Boost REQUIRED COMPONENTS filesystem system");
|
||||
REQUIRE(rec->targets ==
|
||||
std::vector<std::string>{"Boost::filesystem", "Boost::system"});
|
||||
}
|
||||
|
||||
TEST_CASE("resolve fails when a componentized package gets no components",
|
||||
"[linkdb]") {
|
||||
auto db = Database::open();
|
||||
REQUIRE(db.has_value());
|
||||
auto rec = db->resolve("boost", "1.84.0");
|
||||
REQUIRE_FALSE(rec.has_value());
|
||||
REQUIRE(rec.error().code == ErrorCode::LinkdbComponentNotSupported);
|
||||
}
|
||||
|
||||
TEST_CASE("resolve fails when components are passed to a non-componentized package",
|
||||
"[linkdb]") {
|
||||
auto db = Database::open();
|
||||
REQUIRE(db.has_value());
|
||||
auto rec = db->resolve("fmt", "10.2.0", {"core"});
|
||||
REQUIRE_FALSE(rec.has_value());
|
||||
REQUIRE(rec.error().code == ErrorCode::LinkdbComponentNotSupported);
|
||||
}
|
||||
|
||||
TEST_CASE("resolve handles wildcard versions", "[linkdb]") {
|
||||
auto db = Database::open();
|
||||
REQUIRE(db.has_value());
|
||||
auto rec = db->resolve("openssl", "3.2.0");
|
||||
REQUIRE(rec.has_value());
|
||||
REQUIRE(rec->find_package == "OpenSSL REQUIRED");
|
||||
REQUIRE(rec->targets ==
|
||||
std::vector<std::string>{"OpenSSL::SSL", "OpenSSL::Crypto"});
|
||||
}
|
||||
|
||||
TEST_CASE("resolve covers all 25 curated packages", "[linkdb]") {
|
||||
auto db = Database::open();
|
||||
REQUIRE(db.has_value());
|
||||
|
||||
struct Sample {
|
||||
std::string name;
|
||||
std::string version;
|
||||
std::vector<std::string> components;
|
||||
};
|
||||
const std::vector<Sample> samples = {
|
||||
{"fmt", "10.2.0", {}},
|
||||
{"spdlog", "1.13.0", {}},
|
||||
{"nlohmann_json", "3.11.0", {}},
|
||||
{"boost", "1.84.0", {"system"}},
|
||||
{"openssl", "3.2.0", {}},
|
||||
{"zlib", "1.3.0", {}},
|
||||
{"sqlite3", "3.45.0", {}},
|
||||
{"curl", "8.5.0", {}},
|
||||
{"protobuf", "25.0.0", {}},
|
||||
{"grpc", "1.60.0", {}},
|
||||
{"abseil-cpp", "20240116.0", {"strings"}},
|
||||
{"gtest", "1.14.0", {}},
|
||||
{"catch2", "3.5.0", {}},
|
||||
{"eigen", "3.4.0", {}},
|
||||
{"tbb", "2021.10.0", {}},
|
||||
{"libpng", "1.6.40", {}},
|
||||
{"libjpeg", "3.0.1", {}},
|
||||
{"freetype", "2.13.2", {}},
|
||||
{"glfw", "3.3.9", {}},
|
||||
{"glm", "0.9.9.8", {}},
|
||||
{"sdl2", "2.28.5", {}},
|
||||
{"cli11", "2.4.1", {}},
|
||||
{"cxxopts", "3.2.0", {}},
|
||||
{"range-v3", "0.12.0", {}},
|
||||
{"magic_enum", "0.9.5", {}},
|
||||
};
|
||||
|
||||
for (const auto& s : samples) {
|
||||
auto rec = db->resolve(s.name, s.version, s.components);
|
||||
INFO("resolving " << s.name);
|
||||
REQUIRE(rec.has_value());
|
||||
REQUIRE_FALSE(rec->nixpkgs_attr.empty());
|
||||
REQUIRE_FALSE(rec->find_package.empty());
|
||||
REQUIRE_FALSE(rec->targets.empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("substitute_components is a no-op when marker is absent",
|
||||
"[linkdb][substitute]") {
|
||||
REQUIRE(substitute_components("foo bar", {"a", "b"}) == "foo bar");
|
||||
}
|
||||
|
||||
TEST_CASE("substitute_components joins components with spaces",
|
||||
"[linkdb][substitute]") {
|
||||
REQUIRE(substitute_components("X {{components}} Y", {"a", "b", "c"}) ==
|
||||
"X a b c Y");
|
||||
}
|
||||
|
||||
TEST_CASE("expand_targets fans out per-component templates",
|
||||
"[linkdb][substitute]") {
|
||||
REQUIRE(expand_targets({"Boost::{{component}}"}, {"filesystem", "system"}) ==
|
||||
std::vector<std::string>{"Boost::filesystem", "Boost::system"});
|
||||
}
|
||||
|
||||
TEST_CASE("expand_targets keeps non-templated targets verbatim",
|
||||
"[linkdb][substitute]") {
|
||||
REQUIRE(expand_targets({"OpenSSL::SSL", "OpenSSL::Crypto"}, {}) ==
|
||||
std::vector<std::string>{"OpenSSL::SSL", "OpenSSL::Crypto"});
|
||||
}
|
||||
54
tests/semver_satisfies.cpp
Normal file
54
tests/semver_satisfies.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
import cargoxx.util;
|
||||
|
||||
using cargoxx::util::satisfies;
|
||||
|
||||
TEST_CASE("satisfies returns true for wildcard", "[util][semver]") {
|
||||
REQUIRE(satisfies("0.0.0", "*"));
|
||||
REQUIRE(satisfies("99.99.99", "*"));
|
||||
REQUIRE(satisfies("1.0", "*"));
|
||||
}
|
||||
|
||||
TEST_CASE("satisfies handles >= ranges", "[util][semver]") {
|
||||
REQUIRE(satisfies("10.2.0", ">=10.0.0"));
|
||||
REQUIRE(satisfies("10.0.0", ">=10.0.0"));
|
||||
REQUIRE_FALSE(satisfies("9.9.9", ">=10.0.0"));
|
||||
}
|
||||
|
||||
TEST_CASE("satisfies handles compound ranges", "[util][semver]") {
|
||||
REQUIRE(satisfies("10.2.0", ">=10.0.0,<11.0.0"));
|
||||
REQUIRE(satisfies("8.5.3", ">=8.0.0,<10.0.0"));
|
||||
REQUIRE_FALSE(satisfies("11.0.0", ">=10.0.0,<11.0.0"));
|
||||
REQUIRE_FALSE(satisfies("7.0.0", ">=8.0.0,<10.0.0"));
|
||||
}
|
||||
|
||||
TEST_CASE("satisfies handles short version strings", "[util][semver]") {
|
||||
REQUIRE(satisfies("10.2", ">=10.0.0"));
|
||||
REQUIRE(satisfies("10", ">=10.0.0"));
|
||||
REQUIRE_FALSE(satisfies("9", ">=10.0.0"));
|
||||
}
|
||||
|
||||
TEST_CASE("satisfies treats bare version as exact match", "[util][semver]") {
|
||||
REQUIRE(satisfies("1.2.3", "1.2.3"));
|
||||
REQUIRE_FALSE(satisfies("1.2.4", "1.2.3"));
|
||||
}
|
||||
|
||||
TEST_CASE("satisfies handles all comparison operators", "[util][semver]") {
|
||||
REQUIRE(satisfies("2.0.0", "==2.0.0"));
|
||||
REQUIRE_FALSE(satisfies("2.0.1", "==2.0.0"));
|
||||
|
||||
REQUIRE(satisfies("2.0.1", ">2.0.0"));
|
||||
REQUIRE_FALSE(satisfies("2.0.0", ">2.0.0"));
|
||||
|
||||
REQUIRE(satisfies("1.9.9", "<2.0.0"));
|
||||
REQUIRE_FALSE(satisfies("2.0.0", "<2.0.0"));
|
||||
|
||||
REQUIRE(satisfies("2.0.0", "<=2.0.0"));
|
||||
REQUIRE_FALSE(satisfies("2.0.1", "<=2.0.0"));
|
||||
}
|
||||
|
||||
TEST_CASE("satisfies returns false on malformed inputs", "[util][semver]") {
|
||||
REQUIRE_FALSE(satisfies("not-a-version", ">=1.0.0"));
|
||||
REQUIRE_FALSE(satisfies("1.0.0", "garbage"));
|
||||
}
|
||||
25526
third_party/json.hpp
vendored
Normal file
25526
third_party/json.hpp
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user