[M5+] add resolver::verify_link + provisional overlay lifecycle

This commit is contained in:
2026-05-10 10:32:58 +00:00
parent 941d5b3284
commit 816ec993cd
9 changed files with 490 additions and 2 deletions

View File

@@ -71,6 +71,25 @@ auto overlay_insert_manual(OverlayState& state, const std::string& package,
const cargoxx::linkdb::Recipe& r)
-> cargoxx::util::Result<void>;
// Insert a row from a non-manual probe. `verified_at = 0` flags the row as
// provisional; the verify-link step bumps it to the current epoch on
// success or deletes the row on failure.
auto overlay_insert_provisional(OverlayState& state, const std::string& package,
const std::string& version_range,
const cargoxx::linkdb::Recipe& r,
const std::string& source)
-> cargoxx::util::Result<void>;
auto overlay_confirm_provisional(OverlayState& state, const std::string& package,
const std::string& version_range,
const std::string& source)
-> cargoxx::util::Result<void>;
auto overlay_delete_recipe(OverlayState& state, const std::string& package,
const std::string& version_range,
const std::string& source)
-> cargoxx::util::Result<void>;
auto overlay_query(OverlayState& state, const std::string& package)
-> cargoxx::util::Result<std::vector<OverlayRow>>;
@@ -92,6 +111,20 @@ class Database {
auto add_manual(const std::string& package, const std::string& version_range,
const Recipe& r) -> util::Result<void>;
// Provisional-recipe lifecycle, used by the resolver's verify-link step.
// `source` should be one of "conan", "vcpkg", "nix-probe", or any other
// probe identifier (NOT "manual"/"curated", which have their own
// contracts).
auto insert_provisional(const std::string& package,
const std::string& version_range, const Recipe& r,
const std::string& source) -> util::Result<void>;
auto confirm_provisional(const std::string& package,
const std::string& version_range,
const std::string& source) -> util::Result<void>;
auto abort_provisional(const std::string& package,
const std::string& version_range,
const std::string& source) -> util::Result<void>;
private:
Database() = default;
std::map<std::string, std::vector<detail::CuratedRecipe>> curated_;

View File

@@ -128,6 +128,98 @@ auto overlay_insert_manual(OverlayState& state, const std::string& package,
return {};
}
namespace {
auto overlay_insert(OverlayState& state, const std::string& package,
const std::string& version_range, const Recipe& r,
const std::string& source, std::int64_t verified_at)
-> util::Result<void> {
constexpr const char* SQL =
"INSERT OR REPLACE INTO recipes "
"(package, version_range, nixpkgs_attr, find_package, targets, components, source, "
" verified_at) "
"VALUES (?, ?, ?, ?, ?, NULL, ?, ?)";
sqlite3* db = state.handle();
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db, SQL, -1, &stmt, nullptr) != SQLITE_OK) {
return std::unexpected(sqlite_error(db, "prepare insert"));
}
auto targets_str = nlohmann::json(r.targets).dump();
sqlite3_bind_text(stmt, 1, package.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, version_range.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, r.nixpkgs_attr.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 4, r.find_package.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 5, targets_str.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 6, source.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(stmt, 7, verified_at);
auto rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
return std::unexpected(sqlite_error(db, "step insert"));
}
return {};
}
} // namespace
auto overlay_insert_provisional(OverlayState& state, const std::string& package,
const std::string& version_range, const Recipe& r,
const std::string& source) -> util::Result<void> {
return overlay_insert(state, package, version_range, r, source, /*verified_at=*/0);
}
auto overlay_confirm_provisional(OverlayState& state, const std::string& package,
const std::string& version_range,
const std::string& source) -> util::Result<void> {
constexpr const char* SQL =
"UPDATE recipes SET verified_at = ? "
"WHERE package = ? AND version_range = ? AND source = ?";
sqlite3* db = state.handle();
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db, SQL, -1, &stmt, nullptr) != SQLITE_OK) {
return std::unexpected(sqlite_error(db, "prepare update"));
}
auto now = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
sqlite3_bind_int64(stmt, 1, now);
sqlite3_bind_text(stmt, 2, package.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, version_range.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 4, source.c_str(), -1, SQLITE_TRANSIENT);
auto rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
return std::unexpected(sqlite_error(db, "step update"));
}
return {};
}
auto overlay_delete_recipe(OverlayState& state, const std::string& package,
const std::string& version_range,
const std::string& source) -> util::Result<void> {
constexpr const char* SQL =
"DELETE FROM recipes WHERE package = ? AND version_range = ? AND source = ?";
sqlite3* db = state.handle();
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db, SQL, -1, &stmt, nullptr) != SQLITE_OK) {
return std::unexpected(sqlite_error(db, "prepare delete"));
}
sqlite3_bind_text(stmt, 1, package.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, version_range.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, source.c_str(), -1, SQLITE_TRANSIENT);
auto rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
return std::unexpected(sqlite_error(db, "step delete"));
}
return {};
}
auto overlay_query(OverlayState& state, const std::string& package)
-> util::Result<std::vector<OverlayRow>> {
constexpr const char* SQL =
@@ -192,4 +284,45 @@ auto Database::add_manual(const std::string& package, const std::string& version
return detail::overlay_insert_manual(*overlay_, package, version_range, r);
}
namespace {
auto require_overlay(const std::unique_ptr<detail::OverlayState>& o)
-> util::Result<void> {
if (!o) {
return std::unexpected(util::Error{
util::ErrorCode::Internal, "no overlay database is open", "",
std::nullopt, std::nullopt,
});
}
return {};
}
} // namespace
auto Database::insert_provisional(const std::string& package,
const std::string& version_range,
const Recipe& r, const std::string& source)
-> util::Result<void> {
if (auto ok = require_overlay(overlay_); !ok) {
return std::unexpected(ok.error());
}
return detail::overlay_insert_provisional(*overlay_, package, version_range, r, source);
}
auto Database::confirm_provisional(const std::string& package,
const std::string& version_range,
const std::string& source) -> util::Result<void> {
if (auto ok = require_overlay(overlay_); !ok) {
return std::unexpected(ok.error());
}
return detail::overlay_confirm_provisional(*overlay_, package, version_range, source);
}
auto Database::abort_provisional(const std::string& package,
const std::string& version_range,
const std::string& source) -> util::Result<void> {
if (auto ok = require_overlay(overlay_); !ok) {
return std::unexpected(ok.error());
}
return detail::overlay_delete_recipe(*overlay_, package, version_range, source);
}
} // namespace cargoxx::linkdb

View File

@@ -4,6 +4,7 @@ import std;
import cargoxx.util;
import cargoxx.exec;
import cargoxx.linkdb;
import cargoxx.manifest;
export namespace cargoxx::resolver {
@@ -89,4 +90,29 @@ auto parse_vcpkg_usage(std::string_view usage_text)
// ResolutionUnknownPackage; transport errors → ResolutionNetworkError.
auto vcpkg_probe(const std::string& name) -> util::Result<VcpkgRecipe>;
// Caller-supplied closure that runs `cargoxx build` (or any equivalent
// build) on a project rooted at the given path. Injected so the resolver
// stays decoupled from `cargoxx.cli`.
using BuildFn =
std::function<util::Result<void>(const std::filesystem::path& project_root)>;
struct VerifyLinkRequest {
linkdb::Recipe candidate; // recipe under test
std::string source; // "conan" | "vcpkg" | "nix-probe"
std::string package_name;
std::string version_spec; // user-supplied spec (e.g. "*", "1.2")
std::vector<std::string> components;
std::filesystem::path overlay_path; // sqlite file we read/write
std::filesystem::path scratch_root; // parent dir for the tmp project
};
// Scaffolds a tiny Cargoxx project under `req.scratch_root`, writes a
// provisional overlay row pointing at `req.candidate`, runs `build_fn` on
// the project (typically `cli::cmd_build`), and depending on the build
// result either confirms the provisional row (verified_at = now) or
// deletes it. Cleans the tmp project regardless. Returns success only
// when the build itself succeeded.
auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn)
-> util::Result<void>;
} // namespace cargoxx::resolver

View File

@@ -0,0 +1,162 @@
module cargoxx.resolver;
import std;
import cargoxx.util;
import cargoxx.linkdb;
import cargoxx.manifest;
namespace cargoxx::resolver {
namespace fs = std::filesystem;
namespace {
auto io_error(std::string msg, fs::path path) -> util::Error {
return util::Error{
util::ErrorCode::Internal, std::move(msg), "", std::move(path), std::nullopt,
};
}
auto write_text(const fs::path& path, std::string_view content) -> util::Result<void> {
std::ofstream out{path};
if (!out) {
return std::unexpected(
io_error(std::format("cannot open for writing: {}", path.string()), path));
}
out << content;
if (!out) {
return std::unexpected(
io_error(std::format("write failed: {}", path.string()), path));
}
return {};
}
class TmpProject {
public:
explicit TmpProject(fs::path root) : root_(std::move(root)) {}
~TmpProject() {
std::error_code ec;
fs::remove_all(root_, ec);
}
TmpProject(const TmpProject&) = delete;
TmpProject& operator=(const TmpProject&) = delete;
TmpProject(TmpProject&&) = delete;
TmpProject& operator=(TmpProject&&) = delete;
[[nodiscard]] auto path() const -> const fs::path& { return root_; }
private:
fs::path root_;
};
} // namespace
auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn)
-> util::Result<void> {
if (req.package_name.empty()) {
return std::unexpected(util::Error{
util::ErrorCode::Internal,
"verify_link: package_name is empty",
"", std::nullopt, std::nullopt,
});
}
// Insert the provisional overlay row. It persists through the build's
// own Database::open() call, which is how the candidate recipe gets
// surfaced to cmake_lists codegen via Database::resolve.
{
auto db = linkdb::Database::open(req.overlay_path);
if (!db) {
return std::unexpected(db.error());
}
if (auto r = db->insert_provisional(req.package_name, req.version_spec,
req.candidate, req.source);
!r) {
return std::unexpected(r.error());
}
} // close db before re-opening it inside build_fn
// Scaffold a tmp project. We bypass cargoxx::cli::cmd_new to avoid a
// resolver-on-cli dependency cycle; the manifest + src/main.cpp are
// exactly what cmd_build needs for its codegen.
std::error_code ec;
fs::create_directories(req.scratch_root, ec);
if (ec) {
return std::unexpected(io_error(
std::format("cannot create scratch root: {}", ec.message()),
req.scratch_root));
}
auto proj_root = req.scratch_root /
std::format("cargoxx-verify-{}-{}", req.package_name,
std::random_device{}());
TmpProject scope{proj_root};
fs::create_directories(proj_root / "src", ec);
if (ec) {
// Roll back the provisional row before bailing.
auto db = linkdb::Database::open(req.overlay_path);
if (db) {
(void)db->abort_provisional(req.package_name, req.version_spec,
req.source);
}
return std::unexpected(io_error(
std::format("cannot create '{}': {}", (proj_root / "src").string(),
ec.message()),
proj_root));
}
manifest::Manifest m{
.package = manifest::Package{
.name = req.package_name + "_verify",
.version = "0.0.0",
.edition = manifest::Edition::Cpp23,
.authors = {},
.license = std::nullopt,
},
.dependencies = {manifest::Dependency{
.name = req.package_name,
.version_spec = req.version_spec,
.components = req.components,
}},
.build = {},
};
if (auto r = manifest::write(m, proj_root / "Cargoxx.toml"); !r) {
auto db = linkdb::Database::open(req.overlay_path);
if (db) {
(void)db->abort_provisional(req.package_name, req.version_spec,
req.source);
}
return std::unexpected(r.error());
}
// Empty main — exercises find_package + target + linker without
// requiring per-package symbol knowledge.
if (auto r = write_text(proj_root / "src" / "main.cpp", "int main() {}\n"); !r) {
auto db = linkdb::Database::open(req.overlay_path);
if (db) {
(void)db->abort_provisional(req.package_name, req.version_spec,
req.source);
}
return std::unexpected(r.error());
}
auto build_result = build_fn(proj_root);
// Always re-open the db to flip the provisional row's fate.
auto db = linkdb::Database::open(req.overlay_path);
if (!db) {
return std::unexpected(db.error());
}
if (build_result) {
if (auto r = db->confirm_provisional(req.package_name, req.version_spec,
req.source);
!r) {
return std::unexpected(r.error());
}
return {};
}
(void)db->abort_provisional(req.package_name, req.version_spec, req.source);
return std::unexpected(build_result.error());
}
} // namespace cargoxx::resolver