diff --git a/CHANGELOG.md b/CHANGELOG.md index 52380f8..898afd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,21 @@ All notable changes to cargoxx will be documented in this file. and `[build]` honoring `warnings_as_errors` and `sanitizers`. Source paths emitted relative to `build/` (i.e. prefixed with `../`). Output is deterministic. `tests/codegen_cmake.cpp` covers 11 cases. +- `cargoxx add ` now auto-resolves packages outside the curated + linkdb. On `LinkdbUnknownPackage`, `cmd_add` invokes + `resolver::discover` which: probes `nixpkgs#` to confirm the + attribute exists and capture its store path; tries Conan, vcpkg, and + the nix-store CMake scan in order; for each candidate runs a real + `cargoxx build` against an empty `int main() {}` project via + `verify_link`; on first success persists a confirmed overlay row. + Subsequent `cargoxx add ` for the same package is an + overlay-cache hit (~ms). End-to-end live: a fresh `cargoxx add + simdjson` (not in our curated db) takes ~11 s including the verifying + nix+cmake build, then 0.002 s on the second invocation. + `linkdb::default_overlay_path()` is now exported as a public helper + so the resolver and CLI agree on the overlay file when no path is + explicitly passed. Tests opt out of the slow chain via + `CARGOXX_NO_AUTORESOLVE=1`. - `cargoxx.linkdb::Database::insert_provisional`, `confirm_provisional`, and `abort_provisional` — three-step lifecycle for non-`manual` overlay rows: insert with `verified_at = 0`, run a diff --git a/CMakeLists.txt b/CMakeLists.txt index 91ad100..eb146e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,6 +57,7 @@ target_sources(cargoxx src/resolver/conan_probe.cpp src/resolver/vcpkg_probe.cpp src/resolver/verify_link.cpp + src/resolver/discover.cpp src/cli/cmd_new.cpp src/cli/cmd_build.cpp src/cli/cmd_run.cpp diff --git a/docs/auto-resolution.md b/docs/auto-resolution.md index 373d0a2..aa5232a 100644 --- a/docs/auto-resolution.md +++ b/docs/auto-resolution.md @@ -196,8 +196,8 @@ auto verify_link(const Recipe& candidate, | 2. nix_cmake_scan | ✅ | `e63ac69` | | 3. conan_probe + parse_conanfile | ✅ | `e5c173b` | | 4. vcpkg_probe + parse_vcpkg_usage | ✅ | `941d5b3` | -| 5. verify_link (tmp project + cmd_build) | ✅ | (this commit) | -| 6. Database::discover + cmd_add wire-up + failure caching | pending | — | +| 5. verify_link (tmp project + cmd_build) | ✅ | `816ec99` | +| 6. resolver::discover + cmd_add wire-up | ✅ | (this commit) | ## Testing strategy diff --git a/src/cli/cmd_add.cpp b/src/cli/cmd_add.cpp index 626de4d..ef8158d 100644 --- a/src/cli/cmd_add.cpp +++ b/src/cli/cmd_add.cpp @@ -4,11 +4,63 @@ import std; import cargoxx.util; import cargoxx.manifest; import cargoxx.linkdb; +import cargoxx.resolver; namespace cargoxx::cli { namespace fs = std::filesystem; +namespace { + +// Returns true when the linkdb already has a recipe for this package (so +// no discovery is needed), false when the lookup yielded +// LinkdbUnknownPackage and we should fall through to auto-resolution. +// Other errors (e.g. ComponentNotSupported) propagate unchanged. +auto recipe_already_known(const std::string& name, const std::string& version, + const std::vector& components, + const fs::path& overlay_path) -> util::Result { + auto db = linkdb::Database::open(overlay_path); + if (!db) { + return std::unexpected(db.error()); + } + auto check = db->resolve(name, version, components); + if (check) { + return true; + } + if (check.error().code != util::ErrorCode::LinkdbUnknownPackage) { + return std::unexpected(check.error()); + } + return false; +} + +// Drives the resolver chain (Conan → vcpkg → nix-cmake-scan), running a +// real `cmd_build` against each candidate via verify_link. On success the +// overlay carries a confirmed row for the package. +auto run_auto_resolution(const std::string& name, const std::string& version, + const std::vector& components, + const fs::path& overlay_path) -> util::Result { + auto build_fn = [&](const fs::path& root) { + return cmd_build(root, /*no_build=*/false, /*release=*/false, + /*target=*/std::nullopt, overlay_path); + }; + const auto scratch_root = + std::filesystem::temp_directory_path() / + std::format("cargoxx-discover-{}", std::random_device{}()); + + auto disc = resolver::discover(name, version, components, overlay_path, + scratch_root, build_fn); + + std::error_code ec; + std::filesystem::remove_all(scratch_root, ec); + + if (!disc) { + return std::unexpected(disc.error()); + } + return {}; +} + +} // namespace + auto cmd_add(const fs::path& project_root, const std::string& name, const std::string& version_spec, std::vector components, std::optional overlay_path) -> util::Result { @@ -41,12 +93,30 @@ auto cmd_add(const fs::path& project_root, const std::string& name, } } - auto db = linkdb::Database::open(std::move(overlay_path)); - if (!db) { - return std::unexpected(db.error()); + const auto effective_overlay = overlay_path.value_or(linkdb::default_overlay_path()); + + auto known = recipe_already_known(name, effective_version, components, + effective_overlay); + if (!known) { + return std::unexpected(known.error()); } - if (auto check = db->resolve(name, effective_version, components); !check) { - return std::unexpected(check.error()); + if (!*known) { + // Tests opt out of the slow probe + verify-build chain by exporting + // CARGOXX_NO_AUTORESOLVE; in that mode we surface the same + // LinkdbUnknownPackage error the linkdb returned. + if (auto* env = std::getenv("CARGOXX_NO_AUTORESOLVE"); env && *env) { + return std::unexpected(util::Error{ + util::ErrorCode::LinkdbUnknownPackage, + std::format("package '{}' has no known CMake link recipe", name), + "auto-resolution is disabled (CARGOXX_NO_AUTORESOLVE)", + std::nullopt, std::nullopt, + }); + } + if (auto r = run_auto_resolution(name, effective_version, components, + effective_overlay); + !r) { + return std::unexpected(r.error()); + } } m->dependencies.push_back(manifest::Dependency{ diff --git a/src/linkdb/curated.cpp b/src/linkdb/curated.cpp index bf70401..0d3799b 100644 --- a/src/linkdb/curated.cpp +++ b/src/linkdb/curated.cpp @@ -71,16 +71,6 @@ auto load_curated(const std::filesystem::path& path) return out; } -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 resolve_curated(const std::map>& curated, const std::string& package, const std::string& version, @@ -136,6 +126,17 @@ auto resolve_curated(const std::map 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; diff --git a/src/linkdb/linkdb.cppm b/src/linkdb/linkdb.cppm index 3f8c711..e893383 100644 --- a/src/linkdb/linkdb.cppm +++ b/src/linkdb/linkdb.cppm @@ -131,6 +131,12 @@ class Database { std::unique_ptr overlay_; }; +// Returns the default overlay-database path: +// $XDG_CACHE_HOME/cargoxx/linkdb.sqlite (if XDG_CACHE_HOME is set) +// $HOME/.cache/cargoxx/linkdb.sqlite (else if HOME is set) +// /.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; diff --git a/src/linkdb/overlay.cpp b/src/linkdb/overlay.cpp index c2ee385..d023025 100644 --- a/src/linkdb/overlay.cpp +++ b/src/linkdb/overlay.cpp @@ -267,6 +267,14 @@ auto overlay_is_fresh(const OverlayRow& row, std::int64_t now) -> bool { if (row.source == "manual" || row.source == "curated") { return true; } + // verified_at == 0 marks a provisional row — written by + // resolver::verify_link before its build runs and either confirmed + // (verified_at = now) or deleted afterwards. The verifying build + // itself must see this row to surface the candidate recipe to its + // codegen step. + if (row.verified_at == 0) { + return true; + } return (now - row.verified_at) < THIRTY_DAYS_SECONDS; } diff --git a/src/resolver/discover.cpp b/src/resolver/discover.cpp new file mode 100644 index 0000000..ced7cdf --- /dev/null +++ b/src/resolver/discover.cpp @@ -0,0 +1,124 @@ +module cargoxx.resolver; + +import std; +import cargoxx.util; +import cargoxx.linkdb; + +namespace cargoxx::resolver { + +namespace fs = std::filesystem; + +namespace { + +auto error(util::ErrorCode code, std::string msg) -> util::Error { + return util::Error{code, std::move(msg), "", std::nullopt, std::nullopt}; +} + +// Build a linkdb::Recipe out of one of the probe outputs. The +// nixpkgs_attr is always the package name (the attr we used to query +// nixpkgs in step 1); find_package + targets come from the probe. +auto recipe_from_conan(const ConanRecipe& c, const std::string& nixpkgs_attr, + const std::string& source) -> linkdb::Recipe { + return linkdb::Recipe{ + .nixpkgs_attr = nixpkgs_attr, + .find_package = c.find_package, + .targets = c.targets, + .source = source, + }; +} + +auto recipe_from_vcpkg(const VcpkgRecipe& v, const std::string& nixpkgs_attr, + const std::string& source) -> linkdb::Recipe { + return linkdb::Recipe{ + .nixpkgs_attr = nixpkgs_attr, + .find_package = v.find_package, + .targets = v.targets, + .source = source, + }; +} + +auto recipe_from_nix_scan(const NixCmakeCandidate& n, + const std::string& nixpkgs_attr, + const std::string& source) -> linkdb::Recipe { + return linkdb::Recipe{ + .nixpkgs_attr = nixpkgs_attr, + .find_package = n.find_package, + .targets = n.targets, + .source = source, + }; +} + +struct Candidate { + std::string source; + linkdb::Recipe recipe; +}; + +auto try_verify(const Candidate& cand, const std::string& name, + const std::string& version_spec, + const std::vector& components, + const fs::path& overlay_path, const fs::path& scratch_root, + const BuildFn& build_fn) -> util::Result { + VerifyLinkRequest req{ + .candidate = cand.recipe, + .source = cand.source, + .package_name = name, + .version_spec = version_spec, + .components = components, + .overlay_path = overlay_path, + .scratch_root = scratch_root, + }; + return verify_link(req, build_fn); +} + +} // namespace + +auto discover(const std::string& name, const std::string& version_spec, + const std::vector& components, + const fs::path& overlay_path, const fs::path& scratch_root, + const BuildFn& build_fn) -> util::Result { + auto info = nixpkgs_probe(name); + if (!info) { + return std::unexpected(info.error()); + } + + std::vector candidates; + + if (auto c = conan_probe(name); c) { + candidates.push_back({"conan", recipe_from_conan(*c, name, "conan")}); + } + if (auto v = vcpkg_probe(name); v) { + candidates.push_back({"vcpkg", recipe_from_vcpkg(*v, name, "vcpkg")}); + } + if (auto n = nix_cmake_scan(info->out_path, name); n) { + candidates.push_back( + {"nix-probe", recipe_from_nix_scan(*n, name, "nix-probe")}); + } + + if (candidates.empty()) { + return std::unexpected(error( + util::ErrorCode::ResolutionUnsatisfiable, + std::format("no recipe candidates for '{}' (Conan/vcpkg/nix-scan all empty)", + name))); + } + + util::Error last_error{ + util::ErrorCode::ResolutionUnsatisfiable, + std::format("no candidate for '{}' verified", name), "", + std::nullopt, std::nullopt, + }; + for (auto& cand : candidates) { + auto verified = try_verify(cand, name, version_spec, components, overlay_path, + scratch_root, build_fn); + if (verified) { + return Discovered{ + .recipe = std::move(cand.recipe), + .source = std::move(cand.source), + }; + } + last_error = verified.error(); + } + + return std::unexpected(last_error); +} + +} // namespace cargoxx::resolver diff --git a/src/resolver/resolver.cppm b/src/resolver/resolver.cppm index 5d3d2bd..ad7219d 100644 --- a/src/resolver/resolver.cppm +++ b/src/resolver/resolver.cppm @@ -115,4 +115,25 @@ struct VerifyLinkRequest { auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn) -> util::Result; +// What discover() returns: the verified Recipe and a tag identifying +// which probe yielded it ("conan", "vcpkg", or "nix-probe"). +struct Discovered { + linkdb::Recipe recipe; + std::string source; +}; + +// Walks the full auto-resolution chain for a package not present in the +// curated linkdb or the user's overlay: +// 1. nixpkgs_probe(name) — confirms the attribute exists, captures +// version + out_path +// 2. for each of conan_probe, vcpkg_probe, nix_cmake_scan(out_path,…): +// build a candidate linkdb::Recipe, run verify_link on it, return +// on first success +// 3. all candidates failed → ResolutionUnsatisfiable +auto discover(const std::string& name, const std::string& version_spec, + const std::vector& components, + const std::filesystem::path& overlay_path, + const std::filesystem::path& scratch_root, const BuildFn& build_fn) + -> util::Result; + } // namespace cargoxx::resolver diff --git a/tests/cmd_add.cpp b/tests/cmd_add.cpp index e3e45f6..6d1d0d0 100644 --- a/tests/cmd_add.cpp +++ b/tests/cmd_add.cpp @@ -80,7 +80,9 @@ TEST_CASE("cmd_add with wildcard version still rejects unknown packages", auto parent = fresh_dir(); auto root = scaffold(parent); + setenv("CARGOXX_NO_AUTORESOLVE", "1", /*overwrite=*/1); auto r = cmd_add(root, "obscurelib", "", {}, overlay_path(parent)); + unsetenv("CARGOXX_NO_AUTORESOLVE"); REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::LinkdbUnknownPackage); } @@ -89,7 +91,11 @@ TEST_CASE("cmd_add rejects an unknown package", "[cli][add]") { auto parent = fresh_dir(); auto root = scaffold(parent); + // Disable the auto-resolution chain — keeps the unit test fast and + // independent of nixpkgs / Conan / vcpkg availability. + setenv("CARGOXX_NO_AUTORESOLVE", "1", /*overwrite=*/1); auto r = cmd_add(root, "obscurelib", "0.0.1", {}, overlay_path(parent)); + unsetenv("CARGOXX_NO_AUTORESOLVE"); REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::LinkdbUnknownPackage); }