From 653b9fbb8ddf8a852d20a5376bdf2d5771a49f6c Mon Sep 17 00:00:00 2001 From: Amadey Vorontsov Date: Wed, 13 May 2026 23:28:36 +0000 Subject: [PATCH] [M5+] drop curated linkdb JSON; SQLite overlay is the single source --- CMakeLists.txt | 6 +- data/linkdb.json | 212 ------------------------------------- src/linkdb/curated.cpp | 195 ---------------------------------- src/linkdb/database.cpp | 78 ++++++++++++++ src/linkdb/linkdb.cppm | 16 --- src/linkdb/recipe.cpp | 55 ---------- tests/cmd_add.cpp | 45 ++++---- tests/cmd_build.cpp | 58 ++++++---- tests/cmd_remove.cpp | 20 ++++ tests/linkdb_lookup.cpp | 142 +++++-------------------- tests/linkdb_overlay.cpp | 16 ++- tests/verify_link_unit.cpp | 6 +- 12 files changed, 193 insertions(+), 656 deletions(-) delete mode 100644 data/linkdb.json delete mode 100644 src/linkdb/curated.cpp create mode 100644 src/linkdb/database.cpp delete mode 100644 src/linkdb/recipe.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d0fd2a7..28e27a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,9 +35,6 @@ find_package(reproc REQUIRED) # ----- 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 @@ -46,8 +43,7 @@ target_sources(cargoxx src/manifest/writer.cpp src/layout/layout.cpp src/lockfile/lockfile.cpp - src/linkdb/recipe.cpp - src/linkdb/curated.cpp + src/linkdb/database.cpp src/linkdb/overlay.cpp src/codegen/flake.cpp src/codegen/cmake.cpp diff --git a/data/linkdb.json b/data/linkdb.json deleted file mode 100644 index 2aef7a5..0000000 --- a/data/linkdb.json +++ /dev/null @@ -1,212 +0,0 @@ -{ - "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", - "targets": ["Boost::headers"] - } - ], - "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"] - } - ] - } -} diff --git a/src/linkdb/curated.cpp b/src/linkdb/curated.cpp deleted file mode 100644 index 0d3799b..0000000 --- a/src/linkdb/curated.cpp +++ /dev/null @@ -1,195 +0,0 @@ -module; - -#include - -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::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> 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 bucket; - bucket.reserve(recipes.size()); - for (const auto& r : recipes) { - detail::CuratedRecipe rec; - try { - rec.version_range = r.at("version").get(); - rec.nixpkgs_attr = r.at("nixpkgs_attr").get(); - rec.find_package = r.at("find_package").get(); - rec.targets = r.at("targets").get>(); - } 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; -} - - -auto resolve_curated(const std::map>& curated, - const std::string& package, const std::string& version, - const std::vector& components) -> util::Result { - 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 /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, - }); - } - - return Recipe{ - .nixpkgs_attr = match->nixpkgs_attr, - .find_package = substitute_components(match->find_package, components), - .targets = expand_targets(match->targets, components), - .source = "curated", - }; -} - -} // namespace - -auto default_overlay_path() -> std::filesystem::path { - namespace fs = std::filesystem; - if (auto* xdg = std::getenv("XDG_CACHE_HOME"); xdg && *xdg) { - return fs::path{xdg} / "cargoxx" / "linkdb.sqlite"; - } - if (auto* home = std::getenv("HOME"); home && *home) { - return fs::path{home} / ".cache" / "cargoxx" / "linkdb.sqlite"; - } - return fs::current_path() / ".cargoxx-linkdb.sqlite"; -} - -auto Database::open(std::optional overlay_path) -> util::Result { - Database db; - - auto curated = load_curated(CARGOXX_LINKDB_DEFAULT_PATH); - if (!curated) { - return std::unexpected(curated.error()); - } - db.curated_ = std::move(*curated); - - auto path = overlay_path.value_or(default_overlay_path()); - auto handle = detail::overlay_open(path); - if (!handle) { - return std::unexpected(handle.error()); - } - db.overlay_ = std::move(*handle); - - return db; -} - -auto Database::resolve(const std::string& package, const std::string& version, - const std::vector& components) -> util::Result { - if (overlay_) { - auto rows = detail::overlay_query(*overlay_, package); - if (!rows) { - return std::unexpected(rows.error()); - } - const auto now = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); - for (const auto& row : *rows) { - if (!detail::overlay_is_fresh(row, now)) { - continue; - } - if (!util::satisfies(version, row.version_range)) { - continue; - } - if (!components.empty()) { - return std::unexpected(util::Error{ - util::ErrorCode::LinkdbComponentNotSupported, - std::format("overlay recipe for '{}' does not support components", package), - "", std::nullopt, std::nullopt, - }); - } - return Recipe{ - .nixpkgs_attr = row.nixpkgs_attr, - .find_package = row.find_package, - .targets = row.targets, - .source = row.source, - }; - } - } - - return resolve_curated(curated_, package, version, components); -} - -} // namespace cargoxx::linkdb diff --git a/src/linkdb/database.cpp b/src/linkdb/database.cpp new file mode 100644 index 0000000..a888182 --- /dev/null +++ b/src/linkdb/database.cpp @@ -0,0 +1,78 @@ +module cargoxx.linkdb; + +import std; +import cargoxx.util; + +namespace cargoxx::linkdb { + +auto default_overlay_path() -> std::filesystem::path { + namespace fs = std::filesystem; + if (auto* xdg = std::getenv("XDG_CACHE_HOME"); xdg && *xdg) { + return fs::path{xdg} / "cargoxx" / "linkdb.sqlite"; + } + if (auto* home = std::getenv("HOME"); home && *home) { + return fs::path{home} / ".cache" / "cargoxx" / "linkdb.sqlite"; + } + return fs::current_path() / ".cargoxx-linkdb.sqlite"; +} + +auto Database::open(std::optional overlay_path) + -> util::Result { + Database db; + auto path = overlay_path.value_or(default_overlay_path()); + auto handle = detail::overlay_open(path); + if (!handle) { + return std::unexpected(handle.error()); + } + db.overlay_ = std::move(*handle); + return db; +} + +auto Database::resolve(const std::string& package, const std::string& version, + const std::vector& components) + -> util::Result { + if (!overlay_) { + return std::unexpected(util::Error{ + util::ErrorCode::Internal, "no overlay database is open", "", + std::nullopt, std::nullopt, + }); + } + auto rows = detail::overlay_query(*overlay_, package); + if (!rows) { + return std::unexpected(rows.error()); + } + const auto now = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + for (const auto& row : *rows) { + if (!detail::overlay_is_fresh(row, now)) { + continue; + } + if (!util::satisfies(version, row.version_range)) { + continue; + } + if (!components.empty()) { + return std::unexpected(util::Error{ + util::ErrorCode::LinkdbComponentNotSupported, + std::format("overlay recipe for '{}' does not support components", + package), + "", std::nullopt, std::nullopt, + }); + } + return Recipe{ + .nixpkgs_attr = row.nixpkgs_attr, + .find_package = row.find_package, + .targets = row.targets, + .source = row.source, + }; + } + return std::unexpected(util::Error{ + util::ErrorCode::LinkdbUnknownPackage, + std::format("package '{}' has no known CMake link recipe", package), + "run `cargoxx add {0}` to discover and verify a recipe via the resolver, " + "or add a manual recipe via `cargoxx linkdb add`", + std::nullopt, std::nullopt, + }); +} + +} // namespace cargoxx::linkdb diff --git a/src/linkdb/linkdb.cppm b/src/linkdb/linkdb.cppm index 67a8e30..5796fc0 100644 --- a/src/linkdb/linkdb.cppm +++ b/src/linkdb/linkdb.cppm @@ -22,14 +22,6 @@ struct Recipe { namespace cargoxx::linkdb::detail { -struct CuratedRecipe { - std::string version_range; - std::string nixpkgs_attr; - std::string find_package; - std::vector targets; - bool components_supported = false; -}; - struct OverlayRow { std::string version_range; std::string nixpkgs_attr; @@ -140,7 +132,6 @@ class Database { private: Database() = default; - std::map> curated_; std::unique_ptr overlay_; }; @@ -150,11 +141,4 @@ class Database { // /.cargoxx-linkdb.sqlite (final fallback) auto default_overlay_path() -> std::filesystem::path; -// Pure helpers exported for unit testing. -auto substitute_components(std::string find_package, const std::vector& components) - -> std::string; - -auto expand_targets(const std::vector& templates, - const std::vector& components) -> std::vector; - } // namespace cargoxx::linkdb diff --git a/src/linkdb/recipe.cpp b/src/linkdb/recipe.cpp deleted file mode 100644 index cdd49db..0000000 --- a/src/linkdb/recipe.cpp +++ /dev/null @@ -1,55 +0,0 @@ -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& 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& templates, - const std::vector& components) -> std::vector { - std::vector 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 diff --git a/tests/cmd_add.cpp b/tests/cmd_add.cpp index 6955b6f..faf044b 100644 --- a/tests/cmd_add.cpp +++ b/tests/cmd_add.cpp @@ -1,12 +1,15 @@ #include import cargoxx.cli; +import cargoxx.linkdb; import cargoxx.manifest; import cargoxx.util; import std; using cargoxx::cli::cmd_add; using cargoxx::cli::cmd_new; +using cargoxx::linkdb::Database; +using cargoxx::linkdb::Recipe; using cargoxx::util::ErrorCode; namespace manifest = cargoxx::manifest; @@ -36,11 +39,25 @@ auto scaffold(const std::filesystem::path& parent) -> std::filesystem::path { return parent / "app"; } +auto seed_fmt(const std::filesystem::path& overlay) { + auto db = Database::open(overlay); + REQUIRE(db.has_value()); + REQUIRE(db->add_manual("fmt", "*", + Recipe{ + .nixpkgs_attr = "fmt_10", + .find_package = "fmt CONFIG REQUIRED", + .targets = {"fmt::fmt"}, + .source = "manual", + }) + .has_value()); +} + } // namespace TEST_CASE("cmd_add appends a string-form dependency", "[cli][add]") { auto parent = fresh_dir(); auto root = scaffold(parent); + seed_fmt(overlay_path(parent)); auto r = cmd_add(root, "fmt", "10.2.0", {}, overlay_path(parent)); REQUIRE(r.has_value()); @@ -53,25 +70,10 @@ TEST_CASE("cmd_add appends a string-form dependency", "[cli][add]") { REQUIRE(m->dependencies[0].components.empty()); } -TEST_CASE("cmd_add stores components when provided", "[cli][add]") { - auto parent = fresh_dir(); - auto root = scaffold(parent); - - auto r = cmd_add(root, "abseil-cpp", "20240116.0", {"strings", "base"}, - overlay_path(parent)); - REQUIRE(r.has_value()); - - auto m = manifest::parse(root / "Cargoxx.toml"); - REQUIRE(m.has_value()); - REQUIRE(m->dependencies.size() == 1); - REQUIRE(m->dependencies[0].name == "abseil-cpp"); - REQUIRE(m->dependencies[0].components == - std::vector{"strings", "base"}); -} - TEST_CASE("cmd_add accepts an empty version and stores '*'", "[cli][add]") { auto parent = fresh_dir(); auto root = scaffold(parent); + seed_fmt(overlay_path(parent)); auto r = cmd_add(root, "fmt", "", {}, overlay_path(parent)); REQUIRE(r.has_value()); @@ -105,6 +107,7 @@ TEST_CASE("cmd_add rejects an unknown package", "[cli][add]") { TEST_CASE("cmd_add rejects an already-declared dep", "[cli][add]") { auto parent = fresh_dir(); auto root = scaffold(parent); + seed_fmt(overlay_path(parent)); REQUIRE(cmd_add(root, "fmt", "10.2.0", {}, overlay_path(parent)).has_value()); @@ -112,13 +115,3 @@ TEST_CASE("cmd_add rejects an already-declared dep", "[cli][add]") { REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::ManifestInvalidField); } - -TEST_CASE("cmd_add rejects componentized package without components", - "[cli][add]") { - auto parent = fresh_dir(); - auto root = scaffold(parent); - - auto r = cmd_add(root, "abseil-cpp", "20240116.0", {}, overlay_path(parent)); - REQUIRE_FALSE(r.has_value()); - REQUIRE(r.error().code == ErrorCode::LinkdbComponentNotSupported); -} diff --git a/tests/cmd_build.cpp b/tests/cmd_build.cpp index b962a64..e7b0b9e 100644 --- a/tests/cmd_build.cpp +++ b/tests/cmd_build.cpp @@ -1,6 +1,7 @@ #include import cargoxx.cli; +import cargoxx.linkdb; import cargoxx.manifest; import cargoxx.lockfile; import cargoxx.util; @@ -8,6 +9,8 @@ import std; using cargoxx::cli::cmd_build; using cargoxx::cli::cmd_new; +using cargoxx::linkdb::Database; +using cargoxx::linkdb::Recipe; using cargoxx::util::ErrorCode; namespace manifest = cargoxx::manifest; namespace lockfile = cargoxx::lockfile; @@ -43,6 +46,13 @@ auto add_dep(const std::filesystem::path& root, const std::string& name, REQUIRE(manifest::write(*m, path).has_value()); } +auto seed_recipe(const std::filesystem::path& overlay, const std::string& package, + const std::string& version_range, const Recipe& r) { + auto db = Database::open(overlay); + REQUIRE(db.has_value()); + REQUIRE(db->add_manual(package, version_range, r).has_value()); +} + } // namespace TEST_CASE("cmd_build generates files for a no-deps binary project", @@ -82,12 +92,19 @@ TEST_CASE("cmd_build generates files for a library project", "[cli][build]") { REQUIRE(cmake_text.find("add_executable") == std::string::npos); } -TEST_CASE("cmd_build resolves a curated dep into find_package + targets", +TEST_CASE("cmd_build resolves a manually-seeded dep into find_package + targets", "[cli][build]") { auto parent = fresh_dir(); REQUIRE(cmd_new("app", false, parent).has_value()); auto root = parent / "app"; add_dep(root, "fmt", "10.2.0"); + seed_recipe(overlay_path(parent), "fmt", ">=10.0.0", + Recipe{ + .nixpkgs_attr = "fmt_10", + .find_package = "fmt CONFIG REQUIRED", + .targets = {"fmt::fmt"}, + .source = "manual", + }); auto r = cmd_build(root, true, false, std::nullopt, overlay_path(parent)); REQUIRE(r.has_value()); @@ -100,35 +117,33 @@ TEST_CASE("cmd_build resolves a curated dep into find_package + targets", REQUIRE(flake_text.find("pkgs.fmt_10") != std::string::npos); } -TEST_CASE("cmd_build resolves a componentized dep", "[cli][build]") { - auto parent = fresh_dir(); - REQUIRE(cmd_new("app", false, parent).has_value()); - auto root = parent / "app"; - add_dep(root, "abseil-cpp", "20240116.0", {"strings", "base"}); - - auto r = cmd_build(root, true, false, std::nullopt, overlay_path(parent)); - REQUIRE(r.has_value()); - - auto cmake_text = read_file(root / "build" / "CMakeLists.txt"); - REQUIRE(cmake_text.find("find_package(absl CONFIG REQUIRED)") != - std::string::npos); - REQUIRE(cmake_text.find("absl::strings") != std::string::npos); - REQUIRE(cmake_text.find("absl::base") != std::string::npos); -} - TEST_CASE("cmd_build synthesizes a lockfile entry per dep", "[cli][build]") { auto parent = fresh_dir(); REQUIRE(cmd_new("app", false, parent).has_value()); auto root = parent / "app"; add_dep(root, "fmt", "10.2.0"); add_dep(root, "spdlog", "1.13.0"); + seed_recipe(overlay_path(parent), "fmt", ">=10.0.0", + Recipe{ + .nixpkgs_attr = "fmt_10", + .find_package = "fmt CONFIG REQUIRED", + .targets = {"fmt::fmt"}, + .source = "manual", + }); + seed_recipe(overlay_path(parent), "spdlog", "*", + Recipe{ + .nixpkgs_attr = "spdlog", + .find_package = "spdlog CONFIG REQUIRED", + .targets = {"spdlog::spdlog"}, + .source = "manual", + }); auto r = cmd_build(root, true, false, std::nullopt, overlay_path(parent)); REQUIRE(r.has_value()); auto lock = lockfile::parse(root / "Cargoxx.lock"); REQUIRE(lock.has_value()); - REQUIRE(lock->packages.size() == 3); // root + fmt + spdlog + REQUIRE(lock->packages.size() == 3); REQUIRE(lock->packages[0].name == "app"); REQUIRE(lock->packages[0].dependencies.size() == 2); } @@ -172,6 +187,13 @@ TEST_CASE("cmd_build is idempotent — second run produces identical files", REQUIRE(cmd_new("app", false, parent).has_value()); auto root = parent / "app"; add_dep(root, "fmt", "10.2.0"); + seed_recipe(overlay_path(parent), "fmt", ">=10.0.0", + Recipe{ + .nixpkgs_attr = "fmt_10", + .find_package = "fmt CONFIG REQUIRED", + .targets = {"fmt::fmt"}, + .source = "manual", + }); REQUIRE(cmd_build(root, true, false, std::nullopt, overlay_path(parent)).has_value()); auto first_cmake = read_file(root / "build" / "CMakeLists.txt"); diff --git a/tests/cmd_remove.cpp b/tests/cmd_remove.cpp index 60ae5ab..2d041d8 100644 --- a/tests/cmd_remove.cpp +++ b/tests/cmd_remove.cpp @@ -1,6 +1,7 @@ #include import cargoxx.cli; +import cargoxx.linkdb; import cargoxx.manifest; import cargoxx.util; import std; @@ -8,6 +9,8 @@ import std; using cargoxx::cli::cmd_add; using cargoxx::cli::cmd_new; using cargoxx::cli::cmd_remove; +using cargoxx::linkdb::Database; +using cargoxx::linkdb::Recipe; using cargoxx::util::ErrorCode; namespace manifest = cargoxx::manifest; @@ -36,11 +39,26 @@ auto scaffold(const std::filesystem::path& parent) -> std::filesystem::path { return parent / "app"; } +auto seed_recipe(const std::filesystem::path& overlay, const std::string& name, + const std::string& nixpkgs_attr) { + auto db = Database::open(overlay); + REQUIRE(db.has_value()); + REQUIRE(db->add_manual(name, "*", + Recipe{ + .nixpkgs_attr = nixpkgs_attr, + .find_package = std::format("{} CONFIG REQUIRED", name), + .targets = {std::format("{}::{}", name, name)}, + .source = "manual", + }) + .has_value()); +} + } // namespace TEST_CASE("cmd_remove drops the dependency", "[cli][remove]") { auto parent = fresh_dir(); auto root = scaffold(parent); + seed_recipe(overlay_path(parent), "fmt", "fmt_10"); REQUIRE(cmd_add(root, "fmt", "10.2.0", {}, overlay_path(parent)).has_value()); REQUIRE(cmd_remove(root, "fmt").has_value()); @@ -53,6 +71,8 @@ TEST_CASE("cmd_remove drops the dependency", "[cli][remove]") { TEST_CASE("cmd_remove leaves other deps in place", "[cli][remove]") { auto parent = fresh_dir(); auto root = scaffold(parent); + seed_recipe(overlay_path(parent), "fmt", "fmt_10"); + seed_recipe(overlay_path(parent), "spdlog", "spdlog"); REQUIRE(cmd_add(root, "fmt", "10.2.0", {}, overlay_path(parent)).has_value()); REQUIRE(cmd_add(root, "spdlog", "1.13.0", {}, overlay_path(parent)).has_value()); diff --git a/tests/linkdb_lookup.cpp b/tests/linkdb_lookup.cpp index eabe0f9..cc153a8 100644 --- a/tests/linkdb_lookup.cpp +++ b/tests/linkdb_lookup.cpp @@ -6,8 +6,6 @@ import std; using cargoxx::linkdb::Database; using cargoxx::linkdb::Recipe; -using cargoxx::linkdb::expand_targets; -using cargoxx::linkdb::substitute_components; using cargoxx::util::ErrorCode; namespace { @@ -27,134 +25,48 @@ auto open_db() -> Database { } // namespace -TEST_CASE("Database::open loads the curated linkdb", "[linkdb]") { +TEST_CASE("Database::open succeeds against a fresh overlay path", "[linkdb]") { auto db = open_db(); (void)db; } -TEST_CASE("resolve returns the curated recipe for fmt 10", "[linkdb]") { +TEST_CASE("resolve fails on an empty database", "[linkdb]") { auto db = open_db(); + auto rec = db.resolve("fmt", "10.2.0"); + REQUIRE_FALSE(rec.has_value()); + REQUIRE(rec.error().code == ErrorCode::LinkdbUnknownPackage); +} + +TEST_CASE("resolve returns a manually-added recipe", "[linkdb]") { + auto db = open_db(); + auto add = db.add_manual("fmt", ">=10.0.0", + Recipe{ + .nixpkgs_attr = "fmt_10", + .find_package = "fmt CONFIG REQUIRED", + .targets = {"fmt::fmt"}, + .source = "manual", + }); + REQUIRE(add.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{"fmt::fmt"}); - REQUIRE(rec->source == "curated"); + REQUIRE(rec->source == "manual"); } -TEST_CASE("resolve returns the older fmt recipe for fmt 8", "[linkdb]") { - auto db = open_db(); - 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 = open_db(); - auto rec = db.resolve("obscurelib", "0.0.1"); - REQUIRE_FALSE(rec.has_value()); - REQUIRE(rec.error().code == ErrorCode::LinkdbUnknownPackage); -} - -TEST_CASE("resolve substitutes abseil-cpp components", "[linkdb]") { - auto db = open_db(); - auto rec = db.resolve("abseil-cpp", "20240116.0", {"strings", "base"}); - REQUIRE(rec.has_value()); - REQUIRE(rec->find_package == "absl CONFIG REQUIRED"); - REQUIRE(rec->targets == - std::vector{"absl::strings", "absl::base"}); -} - -TEST_CASE("resolve fails when a componentized package gets no components", - "[linkdb]") { - auto db = open_db(); - auto rec = db.resolve("abseil-cpp", "20240116.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", +TEST_CASE("resolve fails when components are passed but the row is non-componentized", "[linkdb]") { auto db = open_db(); + (void)db.add_manual("fmt", "*", + Recipe{ + .nixpkgs_attr = "fmt_10", + .find_package = "fmt CONFIG REQUIRED", + .targets = {"fmt::fmt"}, + .source = "manual", + }); 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 = open_db(); - auto rec = db.resolve("openssl", "3.2.0"); - REQUIRE(rec.has_value()); - REQUIRE(rec->find_package == "OpenSSL REQUIRED"); - REQUIRE(rec->targets == - std::vector{"OpenSSL::SSL", "OpenSSL::Crypto"}); -} - -TEST_CASE("resolve covers all 25 curated packages", "[linkdb]") { - auto db = open_db(); - - struct Sample { - std::string name; - std::string version; - std::vector components; - }; - const std::vector samples = { - {"fmt", "10.2.0", {}}, - {"spdlog", "1.13.0", {}}, - {"nlohmann_json", "3.11.0", {}}, - {"boost", "1.84.0", {}}, - {"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{"Boost::filesystem", "Boost::system"}); -} - -TEST_CASE("expand_targets keeps non-templated targets verbatim", - "[linkdb][substitute]") { - REQUIRE(expand_targets({"OpenSSL::SSL", "OpenSSL::Crypto"}, {}) == - std::vector{"OpenSSL::SSL", "OpenSSL::Crypto"}); -} diff --git a/tests/linkdb_overlay.cpp b/tests/linkdb_overlay.cpp index ff9f109..b1a7ce8 100644 --- a/tests/linkdb_overlay.cpp +++ b/tests/linkdb_overlay.cpp @@ -47,18 +47,17 @@ TEST_CASE("add_manual then resolve returns the manual recipe", REQUIRE(got->source == "manual"); } -TEST_CASE("manual entry overrides curated for the same package", - "[linkdb][overlay]") { +TEST_CASE("manual entry resolves on subsequent open", "[linkdb][overlay]") { auto db = Database::open(fresh_overlay()); REQUIRE(db.has_value()); - Recipe override_r{ + Recipe r{ .nixpkgs_attr = "fmt_pinned", .find_package = "fmt CONFIG REQUIRED", .targets = {"fmt::fmt"}, .source = "manual", }; - REQUIRE(db->add_manual("fmt", ">=10.0.0", override_r).has_value()); + REQUIRE(db->add_manual("fmt", ">=10.0.0", r).has_value()); auto got = db->resolve("fmt", "10.2.0"); REQUIRE(got.has_value()); @@ -79,13 +78,10 @@ TEST_CASE("manual entry is constrained by version_range", }; REQUIRE(db->add_manual("fmt", ">=11.0.0", r).has_value()); - // 10.x falls outside the manual range and falls through to curated - auto curated = db->resolve("fmt", "10.2.0"); - REQUIRE(curated.has_value()); - REQUIRE(curated->source == "curated"); - REQUIRE(curated->nixpkgs_attr == "fmt_10"); + auto miss = db->resolve("fmt", "10.2.0"); + REQUIRE_FALSE(miss.has_value()); + REQUIRE(miss.error().code == ErrorCode::LinkdbUnknownPackage); - // 11.x matches the manual range auto manual = db->resolve("fmt", "11.0.0"); REQUIRE(manual.has_value()); REQUIRE(manual->source == "manual"); diff --git a/tests/verify_link_unit.cpp b/tests/verify_link_unit.cpp index 3edd4c2..45a3e9f 100644 --- a/tests/verify_link_unit.cpp +++ b/tests/verify_link_unit.cpp @@ -94,13 +94,11 @@ TEST_CASE("verify_link rolls the provisional row back when the build fails", REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == cargoxx::util::ErrorCode::BuildCmakeFailed); - // The conan-source row must be gone; resolve falls through to the - // curated linkdb (which has its own fmt recipe with source = "curated"). auto db = Database::open(req.overlay_path); REQUIRE(db.has_value()); auto rec = db->resolve("fmt", "10.2.0", {}); - REQUIRE(rec.has_value()); - REQUIRE(rec->source == "curated"); + REQUIRE_FALSE(rec.has_value()); + REQUIRE(rec.error().code == cargoxx::util::ErrorCode::LinkdbUnknownPackage); } TEST_CASE("verify_link cleans up its scratch project", "[resolver][verify_link]") {