442 lines
16 KiB
C++
442 lines
16 KiB
C++
module;
|
|
|
|
#include <json.hpp>
|
|
#include <sqlite3.h>
|
|
|
|
module cargoxx.linkdb;
|
|
|
|
import std;
|
|
import cargoxx.util;
|
|
|
|
namespace cargoxx::linkdb {
|
|
|
|
namespace {
|
|
|
|
constexpr const char* SCHEMA_SQL = R"(
|
|
CREATE TABLE IF NOT EXISTS recipes (
|
|
package TEXT NOT NULL,
|
|
version_range TEXT NOT NULL,
|
|
nixpkgs_attr TEXT NOT NULL,
|
|
find_package TEXT NOT NULL,
|
|
targets TEXT NOT NULL,
|
|
components TEXT,
|
|
source TEXT NOT NULL,
|
|
verified_at INTEGER NOT NULL,
|
|
PRIMARY KEY (package, version_range, source)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS resolution_failures (
|
|
package TEXT NOT NULL,
|
|
version TEXT NOT NULL,
|
|
last_attempt INTEGER NOT NULL,
|
|
error TEXT NOT NULL,
|
|
PRIMARY KEY (package, version)
|
|
);
|
|
)";
|
|
|
|
constexpr std::int64_t THIRTY_DAYS_SECONDS = 30LL * 24 * 60 * 60;
|
|
|
|
auto sqlite_error(sqlite3* db, std::string_view ctx) -> util::Error {
|
|
return util::Error{
|
|
util::ErrorCode::LinkdbCorrupt,
|
|
std::format("{}: {}", ctx, sqlite3_errmsg(db)),
|
|
"", std::nullopt, std::nullopt,
|
|
};
|
|
}
|
|
|
|
auto column_text(sqlite3_stmt* stmt, int idx) -> std::string {
|
|
const auto* p = sqlite3_column_text(stmt, idx);
|
|
if (!p) {
|
|
return {};
|
|
}
|
|
return std::string{reinterpret_cast<const char*>(p)};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace detail {
|
|
|
|
auto overlay_open(const std::filesystem::path& path)
|
|
-> util::Result<std::unique_ptr<OverlayState>> {
|
|
std::error_code ec;
|
|
if (path.has_parent_path()) {
|
|
std::filesystem::create_directories(path.parent_path(), ec);
|
|
// ec is non-fatal; sqlite3_open will report a clearer error if it matters
|
|
}
|
|
|
|
sqlite3* db = nullptr;
|
|
auto rc = sqlite3_open(path.string().c_str(), &db);
|
|
if (rc != SQLITE_OK) {
|
|
std::string msg = std::format("cannot open overlay '{}': {}", path.string(),
|
|
db ? sqlite3_errmsg(db) : sqlite3_errstr(rc));
|
|
if (db) {
|
|
sqlite3_close(db);
|
|
}
|
|
return std::unexpected(util::Error{
|
|
util::ErrorCode::LinkdbCorrupt, std::move(msg), "", path, std::nullopt,
|
|
});
|
|
}
|
|
|
|
auto state = std::make_unique<OverlayState>(db);
|
|
|
|
char* errmsg = nullptr;
|
|
rc = sqlite3_exec(state->handle(), SCHEMA_SQL, nullptr, nullptr, &errmsg);
|
|
if (rc != SQLITE_OK) {
|
|
std::string msg =
|
|
std::format("cannot create overlay schema: {}", errmsg ? errmsg : "?");
|
|
sqlite3_free(errmsg);
|
|
return std::unexpected(util::Error{
|
|
util::ErrorCode::LinkdbCorrupt, std::move(msg), "", path, std::nullopt,
|
|
});
|
|
}
|
|
|
|
// Schema migrations. SQLite ADD COLUMN errors when the column
|
|
// already exists; treat "duplicate column" as success.
|
|
auto add_column = [&](const char* sql) -> util::Result<void> {
|
|
char* mig_err = nullptr;
|
|
if (sqlite3_exec(state->handle(), sql, nullptr, nullptr, &mig_err) !=
|
|
SQLITE_OK) {
|
|
if (mig_err && std::string_view{mig_err}.find("duplicate column") ==
|
|
std::string_view::npos) {
|
|
std::string msg = std::format("cannot migrate overlay schema: {}",
|
|
mig_err ? mig_err : "?");
|
|
sqlite3_free(mig_err);
|
|
return std::unexpected(util::Error{
|
|
util::ErrorCode::LinkdbCorrupt, std::move(msg), "", path,
|
|
std::nullopt,
|
|
});
|
|
}
|
|
sqlite3_free(mig_err);
|
|
}
|
|
return {};
|
|
};
|
|
if (auto r = add_column(
|
|
"ALTER TABLE recipes ADD COLUMN pkg_config_module TEXT");
|
|
!r) {
|
|
return std::unexpected(r.error());
|
|
}
|
|
if (auto r = add_column(
|
|
"ALTER TABLE recipes ADD COLUMN brute_force_libs TEXT");
|
|
!r) {
|
|
return std::unexpected(r.error());
|
|
}
|
|
if (auto r = add_column(
|
|
"ALTER TABLE recipes ADD COLUMN brute_force_includes TEXT");
|
|
!r) {
|
|
return std::unexpected(r.error());
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
auto overlay_insert_manual(OverlayState& state, const std::string& package,
|
|
const std::string& version_range, const Recipe& r)
|
|
-> util::Result<void> {
|
|
constexpr const char* SQL =
|
|
"INSERT OR REPLACE INTO recipes "
|
|
"(package, version_range, nixpkgs_attr, find_package, targets, components, source, "
|
|
" verified_at, pkg_config_module, brute_force_libs, brute_force_includes) "
|
|
"VALUES (?, ?, ?, ?, ?, NULL, 'manual', ?, ?, ?, ?)";
|
|
|
|
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();
|
|
auto libs_str = nlohmann::json(r.brute_force_libs).dump();
|
|
auto incs_str = nlohmann::json(r.brute_force_includes).dump();
|
|
auto now = std::chrono::duration_cast<std::chrono::seconds>(
|
|
std::chrono::system_clock::now().time_since_epoch())
|
|
.count();
|
|
|
|
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_int64(stmt, 6, now);
|
|
if (r.pkg_config_module) {
|
|
sqlite3_bind_text(stmt, 7, r.pkg_config_module->c_str(), -1, SQLITE_TRANSIENT);
|
|
} else {
|
|
sqlite3_bind_null(stmt, 7);
|
|
}
|
|
sqlite3_bind_text(stmt, 8, libs_str.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 9, incs_str.c_str(), -1, SQLITE_TRANSIENT);
|
|
|
|
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(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, pkg_config_module, brute_force_libs, brute_force_includes) "
|
|
"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();
|
|
auto libs_str = nlohmann::json(r.brute_force_libs).dump();
|
|
auto incs_str = nlohmann::json(r.brute_force_includes).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);
|
|
if (r.pkg_config_module) {
|
|
sqlite3_bind_text(stmt, 8, r.pkg_config_module->c_str(), -1, SQLITE_TRANSIENT);
|
|
} else {
|
|
sqlite3_bind_null(stmt, 8);
|
|
}
|
|
sqlite3_bind_text(stmt, 9, libs_str.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 10, incs_str.c_str(), -1, SQLITE_TRANSIENT);
|
|
|
|
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_evict_auto(OverlayState& state, const std::string& package)
|
|
-> util::Result<void> {
|
|
// `manual` rows are user-curated via `cargoxx linkdb add`; everything
|
|
// else (`nix-probe`, `conan`, `vcpkg`, …) was synthesized by the
|
|
// resolver from the current cargoxx logic and is safe to drop —
|
|
// the next add will re-discover with the latest scanner.
|
|
constexpr const char* SQL =
|
|
"DELETE FROM recipes WHERE package = ? AND source != 'manual'";
|
|
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 evict"));
|
|
}
|
|
sqlite3_bind_text(stmt, 1, package.c_str(), -1, SQLITE_TRANSIENT);
|
|
auto rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
if (rc != SQLITE_DONE) {
|
|
return std::unexpected(sqlite_error(db, "step evict"));
|
|
}
|
|
return {};
|
|
}
|
|
|
|
auto overlay_query(OverlayState& state, const std::string& package)
|
|
-> util::Result<std::vector<OverlayRow>> {
|
|
constexpr const char* SQL =
|
|
"SELECT version_range, nixpkgs_attr, find_package, targets, source, verified_at, "
|
|
" pkg_config_module, brute_force_libs, brute_force_includes "
|
|
"FROM recipes WHERE package = ?";
|
|
|
|
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 select"));
|
|
}
|
|
sqlite3_bind_text(stmt, 1, package.c_str(), -1, SQLITE_TRANSIENT);
|
|
|
|
std::vector<OverlayRow> out;
|
|
int rc;
|
|
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
|
|
OverlayRow row;
|
|
row.version_range = column_text(stmt, 0);
|
|
row.nixpkgs_attr = column_text(stmt, 1);
|
|
row.find_package = column_text(stmt, 2);
|
|
auto targets_text = column_text(stmt, 3);
|
|
try {
|
|
row.targets =
|
|
nlohmann::json::parse(targets_text).get<std::vector<std::string>>();
|
|
} catch (const nlohmann::json::exception&) {
|
|
sqlite3_finalize(stmt);
|
|
return std::unexpected(util::Error{
|
|
util::ErrorCode::LinkdbCorrupt,
|
|
std::format("overlay row for '{}' has malformed targets JSON", package),
|
|
"", std::nullopt, std::nullopt,
|
|
});
|
|
}
|
|
row.source = column_text(stmt, 4);
|
|
row.verified_at = sqlite3_column_int64(stmt, 5);
|
|
if (sqlite3_column_type(stmt, 6) != SQLITE_NULL) {
|
|
row.pkg_config_module = column_text(stmt, 6);
|
|
}
|
|
auto parse_str_array = [&](int col, std::vector<std::string>& out_arr) {
|
|
if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
|
|
return;
|
|
}
|
|
try {
|
|
auto txt = column_text(stmt, col);
|
|
if (txt.empty()) {
|
|
return;
|
|
}
|
|
out_arr =
|
|
nlohmann::json::parse(txt).get<std::vector<std::string>>();
|
|
} catch (const nlohmann::json::exception&) {
|
|
// legacy/manual rows may have stored garbage; ignore
|
|
}
|
|
};
|
|
parse_str_array(7, row.brute_force_libs);
|
|
parse_str_array(8, row.brute_force_includes);
|
|
out.push_back(std::move(row));
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
if (rc != SQLITE_DONE) {
|
|
return std::unexpected(sqlite_error(db, "step select"));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
} // namespace detail
|
|
|
|
auto Database::add_manual(const std::string& package, const std::string& version_range,
|
|
const Recipe& r) -> util::Result<void> {
|
|
if (!overlay_) {
|
|
return std::unexpected(util::Error{
|
|
util::ErrorCode::Internal,
|
|
"no overlay database is open",
|
|
"", std::nullopt, std::nullopt,
|
|
});
|
|
}
|
|
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);
|
|
}
|
|
|
|
auto Database::evict_auto_recipes(const std::string& package)
|
|
-> util::Result<void> {
|
|
if (auto ok = require_overlay(overlay_); !ok) {
|
|
return std::unexpected(ok.error());
|
|
}
|
|
return detail::overlay_evict_auto(*overlay_, package);
|
|
}
|
|
|
|
} // namespace cargoxx::linkdb
|