[M5+] add resolver::verify_link + provisional overlay lifecycle
This commit is contained in:
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
162
src/resolver/verify_link.cpp
Normal file
162
src/resolver/verify_link.cpp
Normal 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
|
||||
Reference in New Issue
Block a user