[M5+] wire resolver::discover into cargoxx add
This commit is contained in:
15
CHANGELOG.md
15
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
|
and `[build]` honoring `warnings_as_errors` and `sanitizers`. Source
|
||||||
paths emitted relative to `build/` (i.e. prefixed with `../`).
|
paths emitted relative to `build/` (i.e. prefixed with `../`).
|
||||||
Output is deterministic. `tests/codegen_cmake.cpp` covers 11 cases.
|
Output is deterministic. `tests/codegen_cmake.cpp` covers 11 cases.
|
||||||
|
- `cargoxx add <pkg>` now auto-resolves packages outside the curated
|
||||||
|
linkdb. On `LinkdbUnknownPackage`, `cmd_add` invokes
|
||||||
|
`resolver::discover` which: probes `nixpkgs#<pkg>` 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 <pkg>` 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`,
|
- `cargoxx.linkdb::Database::insert_provisional`,
|
||||||
`confirm_provisional`, and `abort_provisional` — three-step lifecycle
|
`confirm_provisional`, and `abort_provisional` — three-step lifecycle
|
||||||
for non-`manual` overlay rows: insert with `verified_at = 0`, run a
|
for non-`manual` overlay rows: insert with `verified_at = 0`, run a
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ target_sources(cargoxx
|
|||||||
src/resolver/conan_probe.cpp
|
src/resolver/conan_probe.cpp
|
||||||
src/resolver/vcpkg_probe.cpp
|
src/resolver/vcpkg_probe.cpp
|
||||||
src/resolver/verify_link.cpp
|
src/resolver/verify_link.cpp
|
||||||
|
src/resolver/discover.cpp
|
||||||
src/cli/cmd_new.cpp
|
src/cli/cmd_new.cpp
|
||||||
src/cli/cmd_build.cpp
|
src/cli/cmd_build.cpp
|
||||||
src/cli/cmd_run.cpp
|
src/cli/cmd_run.cpp
|
||||||
|
|||||||
@@ -196,8 +196,8 @@ auto verify_link(const Recipe& candidate,
|
|||||||
| 2. nix_cmake_scan | ✅ | `e63ac69` |
|
| 2. nix_cmake_scan | ✅ | `e63ac69` |
|
||||||
| 3. conan_probe + parse_conanfile | ✅ | `e5c173b` |
|
| 3. conan_probe + parse_conanfile | ✅ | `e5c173b` |
|
||||||
| 4. vcpkg_probe + parse_vcpkg_usage | ✅ | `941d5b3` |
|
| 4. vcpkg_probe + parse_vcpkg_usage | ✅ | `941d5b3` |
|
||||||
| 5. verify_link (tmp project + cmd_build) | ✅ | (this commit) |
|
| 5. verify_link (tmp project + cmd_build) | ✅ | `816ec99` |
|
||||||
| 6. Database::discover + cmd_add wire-up + failure caching | pending | — |
|
| 6. resolver::discover + cmd_add wire-up | ✅ | (this commit) |
|
||||||
|
|
||||||
## Testing strategy
|
## Testing strategy
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,63 @@ import std;
|
|||||||
import cargoxx.util;
|
import cargoxx.util;
|
||||||
import cargoxx.manifest;
|
import cargoxx.manifest;
|
||||||
import cargoxx.linkdb;
|
import cargoxx.linkdb;
|
||||||
|
import cargoxx.resolver;
|
||||||
|
|
||||||
namespace cargoxx::cli {
|
namespace cargoxx::cli {
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
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<std::string>& components,
|
||||||
|
const fs::path& overlay_path) -> util::Result<bool> {
|
||||||
|
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<std::string>& components,
|
||||||
|
const fs::path& overlay_path) -> util::Result<void> {
|
||||||
|
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,
|
auto cmd_add(const fs::path& project_root, const std::string& name,
|
||||||
const std::string& version_spec, std::vector<std::string> components,
|
const std::string& version_spec, std::vector<std::string> components,
|
||||||
std::optional<fs::path> overlay_path) -> util::Result<void> {
|
std::optional<fs::path> overlay_path) -> util::Result<void> {
|
||||||
@@ -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));
|
const auto effective_overlay = overlay_path.value_or(linkdb::default_overlay_path());
|
||||||
if (!db) {
|
|
||||||
return std::unexpected(db.error());
|
auto known = recipe_already_known(name, effective_version, components,
|
||||||
|
effective_overlay);
|
||||||
|
if (!known) {
|
||||||
|
return std::unexpected(known.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());
|
||||||
}
|
}
|
||||||
if (auto check = db->resolve(name, effective_version, components); !check) {
|
|
||||||
return std::unexpected(check.error());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m->dependencies.push_back(manifest::Dependency{
|
m->dependencies.push_back(manifest::Dependency{
|
||||||
|
|||||||
@@ -71,16 +71,6 @@ auto load_curated(const std::filesystem::path& path)
|
|||||||
return out;
|
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<std::string, std::vector<detail::CuratedRecipe>>& curated,
|
auto resolve_curated(const std::map<std::string, std::vector<detail::CuratedRecipe>>& curated,
|
||||||
const std::string& package, const std::string& version,
|
const std::string& package, const std::string& version,
|
||||||
@@ -136,6 +126,17 @@ auto resolve_curated(const std::map<std::string, std::vector<detail::CuratedReci
|
|||||||
|
|
||||||
} // namespace
|
} // 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<std::filesystem::path> overlay_path) -> util::Result<Database> {
|
auto Database::open(std::optional<std::filesystem::path> overlay_path) -> util::Result<Database> {
|
||||||
Database db;
|
Database db;
|
||||||
|
|
||||||
|
|||||||
@@ -131,6 +131,12 @@ class Database {
|
|||||||
std::unique_ptr<detail::OverlayState> overlay_;
|
std::unique_ptr<detail::OverlayState> 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)
|
||||||
|
// <cwd>/.cargoxx-linkdb.sqlite (final fallback)
|
||||||
|
auto default_overlay_path() -> std::filesystem::path;
|
||||||
|
|
||||||
// Pure helpers exported for unit testing.
|
// Pure helpers exported for unit testing.
|
||||||
auto substitute_components(std::string find_package, const std::vector<std::string>& components)
|
auto substitute_components(std::string find_package, const std::vector<std::string>& components)
|
||||||
-> std::string;
|
-> std::string;
|
||||||
|
|||||||
@@ -267,6 +267,14 @@ auto overlay_is_fresh(const OverlayRow& row, std::int64_t now) -> bool {
|
|||||||
if (row.source == "manual" || row.source == "curated") {
|
if (row.source == "manual" || row.source == "curated") {
|
||||||
return true;
|
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;
|
return (now - row.verified_at) < THIRTY_DAYS_SECONDS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
124
src/resolver/discover.cpp
Normal file
124
src/resolver/discover.cpp
Normal file
@@ -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<std::string>& components,
|
||||||
|
const fs::path& overlay_path, const fs::path& scratch_root,
|
||||||
|
const BuildFn& build_fn) -> util::Result<void> {
|
||||||
|
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<std::string>& components,
|
||||||
|
const fs::path& overlay_path, const fs::path& scratch_root,
|
||||||
|
const BuildFn& build_fn) -> util::Result<Discovered> {
|
||||||
|
auto info = nixpkgs_probe(name);
|
||||||
|
if (!info) {
|
||||||
|
return std::unexpected(info.error());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Candidate> 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
|
||||||
@@ -115,4 +115,25 @@ struct VerifyLinkRequest {
|
|||||||
auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn)
|
auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn)
|
||||||
-> util::Result<void>;
|
-> util::Result<void>;
|
||||||
|
|
||||||
|
// 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<std::string>& components,
|
||||||
|
const std::filesystem::path& overlay_path,
|
||||||
|
const std::filesystem::path& scratch_root, const BuildFn& build_fn)
|
||||||
|
-> util::Result<Discovered>;
|
||||||
|
|
||||||
} // namespace cargoxx::resolver
|
} // namespace cargoxx::resolver
|
||||||
|
|||||||
@@ -80,7 +80,9 @@ TEST_CASE("cmd_add with wildcard version still rejects unknown packages",
|
|||||||
auto parent = fresh_dir();
|
auto parent = fresh_dir();
|
||||||
auto root = scaffold(parent);
|
auto root = scaffold(parent);
|
||||||
|
|
||||||
|
setenv("CARGOXX_NO_AUTORESOLVE", "1", /*overwrite=*/1);
|
||||||
auto r = cmd_add(root, "obscurelib", "", {}, overlay_path(parent));
|
auto r = cmd_add(root, "obscurelib", "", {}, overlay_path(parent));
|
||||||
|
unsetenv("CARGOXX_NO_AUTORESOLVE");
|
||||||
REQUIRE_FALSE(r.has_value());
|
REQUIRE_FALSE(r.has_value());
|
||||||
REQUIRE(r.error().code == ErrorCode::LinkdbUnknownPackage);
|
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 parent = fresh_dir();
|
||||||
auto root = scaffold(parent);
|
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));
|
auto r = cmd_add(root, "obscurelib", "0.0.1", {}, overlay_path(parent));
|
||||||
|
unsetenv("CARGOXX_NO_AUTORESOLVE");
|
||||||
REQUIRE_FALSE(r.has_value());
|
REQUIRE_FALSE(r.has_value());
|
||||||
REQUIRE(r.error().code == ErrorCode::LinkdbUnknownPackage);
|
REQUIRE(r.error().code == ErrorCode::LinkdbUnknownPackage);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user