[M6] preserve every probe's scratch under last-failure/<pkg>/

This commit is contained in:
2026-05-15 13:43:16 +00:00
parent c46f3aa1f0
commit 8b396bcd0f
6 changed files with 79 additions and 72 deletions

View File

@@ -77,9 +77,12 @@ auto record_lockfile_rev(const fs::path& project_root, const std::string& name,
return lockfile::write(lock, lock_path); return lockfile::write(lock, lock_path);
} }
// Drives the resolver chain (Conan → vcpkg → nix-cmake-scan), running a // Drives the resolver chain (Conan → vcpkg → nix-cmake-scan → pc-scan),
// real `cmd_build` against each candidate via verify_link. On success the // running a real `cmd_build` against each candidate via verify_link.
// overlay carries a confirmed row for the package. // On success the overlay carries a confirmed row for the package.
// Every probe's scratch project is preserved under
// `<XDG>/cargoxx/last-failure/<name>/<NN>-<probe>/` for inspection;
// the dir is wiped clean at the start of each call.
auto run_auto_resolution(const std::string& name, const std::string& version, auto run_auto_resolution(const std::string& name, const std::string& version,
const std::vector<std::string>& components, const std::vector<std::string>& components,
const fs::path& overlay_path) -> util::Result<void> { const fs::path& overlay_path) -> util::Result<void> {
@@ -87,17 +90,18 @@ auto run_auto_resolution(const std::string& name, const std::string& version,
return cmd_build(root, /*no_build=*/false, /*release=*/false, return cmd_build(root, /*no_build=*/false, /*release=*/false,
/*target=*/std::nullopt, overlay_path); /*target=*/std::nullopt, overlay_path);
}; };
const auto scratch_root = const auto scratch_root = resolver::last_failure_dir(name);
std::filesystem::temp_directory_path() / std::error_code ec;
std::format("cargoxx-discover-{}", std::random_device{}()); std::filesystem::remove_all(scratch_root, ec);
std::filesystem::create_directories(scratch_root, ec);
auto disc = resolver::discover(name, version, components, overlay_path, auto disc = resolver::discover(name, version, components, overlay_path,
scratch_root, build_fn); scratch_root, build_fn);
std::error_code ec;
std::filesystem::remove_all(scratch_root, ec);
if (!disc) { if (!disc) {
std::cerr << std::format(
"note: every probe attempt's scratch project is preserved at\n"
" {}/ — re-run cmake inside any subdir to reproduce.\n",
scratch_root.string());
return std::unexpected(disc.error()); return std::unexpected(disc.error());
} }
return {}; return {};

View File

@@ -160,14 +160,17 @@ auto cmd_build(const fs::path& project_root, bool no_build, bool release,
return cmd_build(root, /*no_build=*/false, /*release=*/false, return cmd_build(root, /*no_build=*/false, /*release=*/false,
/*target=*/std::nullopt, effective_overlay); /*target=*/std::nullopt, effective_overlay);
}; };
const auto scratch_root = const auto scratch_root = resolver::last_failure_dir(name);
std::filesystem::temp_directory_path() /
std::format("cargoxx-discover-{}", std::random_device{}());
auto disc = resolver::discover(name, version, components,
effective_overlay, scratch_root, build_fn);
std::error_code ec; std::error_code ec;
std::filesystem::remove_all(scratch_root, ec); std::filesystem::remove_all(scratch_root, ec);
std::filesystem::create_directories(scratch_root, ec);
auto disc = resolver::discover(name, version, components,
effective_overlay, scratch_root, build_fn);
if (!disc) { if (!disc) {
std::cerr << std::format(
"note: every probe attempt's scratch project is preserved at\n"
" {}/ — re-run cmake inside any subdir to reproduce.\n",
scratch_root.string());
return std::unexpected(disc.error()); return std::unexpected(disc.error());
} }
return {}; return {};

View File

@@ -86,7 +86,7 @@ struct Candidate {
auto try_verify(const Candidate& cand, const std::string& name, auto try_verify(const Candidate& cand, const std::string& name,
const std::string& version_spec, const std::string& version_spec,
const std::vector<std::string>& components, const std::vector<std::string>& components,
const fs::path& overlay_path, const fs::path& scratch_root, const fs::path& overlay_path, const fs::path& scratch_path,
const BuildFn& build_fn) -> util::Result<void> { const BuildFn& build_fn) -> util::Result<void> {
VerifyLinkRequest req{ VerifyLinkRequest req{
.candidate = cand.recipe, .candidate = cand.recipe,
@@ -95,7 +95,7 @@ auto try_verify(const Candidate& cand, const std::string& name,
.version_spec = version_spec, .version_spec = version_spec,
.components = components, .components = components,
.overlay_path = overlay_path, .overlay_path = overlay_path,
.scratch_root = scratch_root, .scratch_path = scratch_path,
}; };
return verify_link(req, build_fn); return verify_link(req, build_fn);
} }
@@ -181,9 +181,12 @@ auto discover(const std::string& name, const std::string& version_spec,
std::format("no candidate for '{}' verified", name), "", std::format("no candidate for '{}' verified", name), "",
std::nullopt, std::nullopt, std::nullopt, std::nullopt,
}; };
for (auto& cand : candidates) { for (std::size_t i = 0; i < candidates.size(); ++i) {
auto& cand = candidates[i];
auto subdir = std::format("{:02}-{}", i + 1, cand.source);
auto scratch_path = scratch_root / subdir;
auto verified = try_verify(cand, name, version_spec, components, overlay_path, auto verified = try_verify(cand, name, version_spec, components, overlay_path,
scratch_root, build_fn); scratch_path, build_fn);
if (verified) { if (verified) {
return Discovered{ return Discovered{
.recipe = std::move(cand.recipe), .recipe = std::move(cand.recipe),
@@ -196,4 +199,17 @@ auto discover(const std::string& name, const std::string& version_spec,
return std::unexpected(last_error); return std::unexpected(last_error);
} }
auto last_failure_dir(const std::string& package_name) -> fs::path {
auto base = []() -> fs::path {
if (auto* xdg = std::getenv("XDG_CACHE_HOME"); xdg && *xdg) {
return fs::path{xdg} / "cargoxx" / "last-failure";
}
if (auto* home = std::getenv("HOME"); home && *home) {
return fs::path{home} / ".cache" / "cargoxx" / "last-failure";
}
return fs::current_path() / ".cargoxx-last-failure";
}();
return base / package_name;
}
} // namespace cargoxx::resolver } // namespace cargoxx::resolver

View File

@@ -139,12 +139,17 @@ using BuildFn =
struct VerifyLinkRequest { struct VerifyLinkRequest {
linkdb::Recipe candidate; // recipe under test linkdb::Recipe candidate; // recipe under test
std::string source; // "conan" | "vcpkg" | "nix-probe" std::string source; // "conan" | "vcpkg" | "nix-probe" | "pkg-config" | …
std::string package_name; std::string package_name;
std::string version_spec; // user-supplied spec (e.g. "*", "1.2") std::string version_spec; // user-supplied spec (e.g. "*", "1.2")
std::vector<std::string> components; std::vector<std::string> components;
std::filesystem::path overlay_path; // sqlite file we read/write std::filesystem::path overlay_path; // sqlite file we read/write
std::filesystem::path scratch_root; // parent dir for the tmp project // Exact directory the verify project lives in. verify_link creates
// it (and its `src/` subdir) but never deletes it; the caller is
// responsible for cleanup. discover() puts each probe's project at
// a distinct path under `<last-failure-dir>/<NN>-<probe>/` so
// every attempt is preserved for inspection.
std::filesystem::path scratch_path;
}; };
// Scaffolds a tiny Cargoxx project under `req.scratch_root`, writes a // Scaffolds a tiny Cargoxx project under `req.scratch_root`, writes a
@@ -163,20 +168,34 @@ struct Discovered {
std::string source; std::string source;
}; };
// Walks the full auto-resolution chain for a package not present in the // Walks the full auto-resolution chain for a package not present in
// curated linkdb or the user's overlay: // the user's overlay. Each candidate produced by a probe gets its own
// 1. nixpkgs_probe(name) — confirms the attribute exists, captures // verify_link attempt at
// version + out_path // <scratch_root>/<NN>-<probe>/
// 2. for each of conan_probe, vcpkg_probe, nix_cmake_scan(out_path,…): // e.g. `01-conan/`, `02-vcpkg/`, `03-nix-probe/`, `04-pkg-config/`.
// build a candidate linkdb::Recipe, run verify_link on it, return // Subdirs are NOT cleaned up — they're meant for the user to inspect
// on first success // after a failed `cargoxx add`. The caller wipes `<scratch_root>`
// 3. all candidates failed → ResolutionUnsatisfiable // clean at the start of each invocation (cmd_add / cmd_build).
//
// Returns `Discovered` on the first verify_link success;
// `ResolutionUnsatisfiable` when all probes are exhausted; or the
// underlying error from `nixpkgs_probe`.
auto discover(const std::string& name, const std::string& version_spec, auto discover(const std::string& name, const std::string& version_spec,
const std::vector<std::string>& components, const std::vector<std::string>& components,
const std::filesystem::path& overlay_path, const std::filesystem::path& overlay_path,
const std::filesystem::path& scratch_root, const BuildFn& build_fn) const std::filesystem::path& scratch_root, const BuildFn& build_fn)
-> util::Result<Discovered>; -> util::Result<Discovered>;
// The on-disk parent dir that holds per-package verify_link scratch
// projects. Resolves to:
// $XDG_CACHE_HOME/cargoxx/last-failure/<pkg>/ (when XDG_CACHE_HOME is set)
// $HOME/.cache/cargoxx/last-failure/<pkg>/ (else if HOME is set)
// <cwd>/.cargoxx-last-failure/<pkg>/ (fallback)
// Each `cargoxx add <pkg>` (and `cmd_build`'s auto-resolve) wipes
// this dir clean, then discover() repopulates it with one subdir per
// probe attempt.
auto last_failure_dir(const std::string& package_name) -> std::filesystem::path;
// Output of devbox's /v1/resolve API. We capture only the fields cargoxx // Output of devbox's /v1/resolve API. We capture only the fields cargoxx
// uses; the response carries far more metadata (license, summary, per- // uses; the response carries far more metadata (license, summary, per-
// system store hashes) that we deliberately ignore. // system store hashes) that we deliberately ignore.

View File

@@ -31,24 +31,6 @@ auto write_text(const fs::path& path, std::string_view content) -> util::Result<
return {}; 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 } // namespace
auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn) auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn)
@@ -61,9 +43,6 @@ auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn)
}); });
} }
// 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); auto db = linkdb::Database::open(req.overlay_path);
if (!db) { if (!db) {
@@ -74,26 +53,12 @@ auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn)
!r) { !r) {
return std::unexpected(r.error()); 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};
const auto& proj_root = req.scratch_path;
std::error_code ec;
fs::create_directories(proj_root / "src", ec); fs::create_directories(proj_root / "src", ec);
if (ec) { if (ec) {
// Roll back the provisional row before bailing.
auto db = linkdb::Database::open(req.overlay_path); auto db = linkdb::Database::open(req.overlay_path);
if (db) { if (db) {
(void)db->abort_provisional(req.package_name, req.version_spec, (void)db->abort_provisional(req.package_name, req.version_spec,
@@ -129,8 +94,6 @@ auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn)
return std::unexpected(r.error()); 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) { if (auto r = write_text(proj_root / "src" / "main.cpp", "int main() {}\n"); !r) {
auto db = linkdb::Database::open(req.overlay_path); auto db = linkdb::Database::open(req.overlay_path);
if (db) { if (db) {

View File

@@ -41,7 +41,7 @@ auto make_request(const std::filesystem::path& parent) -> VerifyLinkRequest {
.version_spec = "*", .version_spec = "*",
.components = {}, .components = {},
.overlay_path = parent / "overlay.sqlite", .overlay_path = parent / "overlay.sqlite",
.scratch_root = parent / "scratch", .scratch_path = parent / "scratch" / "01-conan",
}; };
} }
@@ -101,7 +101,8 @@ TEST_CASE("verify_link rolls the provisional row back when the build fails",
REQUIRE(rec.error().code == cargoxx::util::ErrorCode::LinkdbUnknownPackage); REQUIRE(rec.error().code == cargoxx::util::ErrorCode::LinkdbUnknownPackage);
} }
TEST_CASE("verify_link cleans up its scratch project", "[resolver][verify_link]") { TEST_CASE("verify_link preserves its scratch project for inspection",
"[resolver][verify_link]") {
auto parent = fresh_dir(); auto parent = fresh_dir();
auto req = make_request(parent); auto req = make_request(parent);
std::filesystem::path captured; std::filesystem::path captured;
@@ -111,6 +112,7 @@ TEST_CASE("verify_link cleans up its scratch project", "[resolver][verify_link]"
return cargoxx::util::Result<void>{}; return cargoxx::util::Result<void>{};
}); });
REQUIRE_FALSE(captured.empty()); REQUIRE(captured == req.scratch_path);
REQUIRE_FALSE(std::filesystem::exists(captured)); REQUIRE(std::filesystem::exists(captured / "Cargoxx.toml"));
REQUIRE(std::filesystem::exists(captured / "src" / "main.cpp"));
} }