diff --git a/src/cli/cmd_add.cpp b/src/cli/cmd_add.cpp index 98a8da2..4c14c78 100644 --- a/src/cli/cmd_add.cpp +++ b/src/cli/cmd_add.cpp @@ -77,9 +77,12 @@ auto record_lockfile_rev(const fs::path& project_root, const std::string& name, return lockfile::write(lock, lock_path); } -// 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. +// Drives the resolver chain (Conan → vcpkg → nix-cmake-scan → pc-scan), +// running a real `cmd_build` against each candidate via verify_link. +// On success the overlay carries a confirmed row for the package. +// Every probe's scratch project is preserved under +// `/cargoxx/last-failure//-/` 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, const std::vector& components, const fs::path& overlay_path) -> util::Result { @@ -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, /*target=*/std::nullopt, overlay_path); }; - const auto scratch_root = - std::filesystem::temp_directory_path() / - std::format("cargoxx-discover-{}", std::random_device{}()); + const auto scratch_root = resolver::last_failure_dir(name); + std::error_code ec; + std::filesystem::remove_all(scratch_root, ec); + std::filesystem::create_directories(scratch_root, ec); 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) { + 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 {}; diff --git a/src/cli/cmd_build.cpp b/src/cli/cmd_build.cpp index ad111b4..442090f 100644 --- a/src/cli/cmd_build.cpp +++ b/src/cli/cmd_build.cpp @@ -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, /*target=*/std::nullopt, effective_overlay); }; - const auto scratch_root = - 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); + const auto scratch_root = resolver::last_failure_dir(name); std::error_code 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) { + 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 {}; diff --git a/src/resolver/discover.cpp b/src/resolver/discover.cpp index a630bd8..b5ddd17 100644 --- a/src/resolver/discover.cpp +++ b/src/resolver/discover.cpp @@ -86,7 +86,7 @@ struct Candidate { auto try_verify(const Candidate& cand, const std::string& name, const std::string& version_spec, const std::vector& 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 { VerifyLinkRequest req{ .candidate = cand.recipe, @@ -95,7 +95,7 @@ auto try_verify(const Candidate& cand, const std::string& name, .version_spec = version_spec, .components = components, .overlay_path = overlay_path, - .scratch_root = scratch_root, + .scratch_path = scratch_path, }; 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::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, - scratch_root, build_fn); + scratch_path, build_fn); if (verified) { return Discovered{ .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); } +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 diff --git a/src/resolver/resolver.cppm b/src/resolver/resolver.cppm index c090e81..f6b5a2c 100644 --- a/src/resolver/resolver.cppm +++ b/src/resolver/resolver.cppm @@ -139,12 +139,17 @@ using BuildFn = struct VerifyLinkRequest { 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 version_spec; // user-supplied spec (e.g. "*", "1.2") std::vector components; 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 `/-/` so + // every attempt is preserved for inspection. + std::filesystem::path scratch_path; }; // Scaffolds a tiny Cargoxx project under `req.scratch_root`, writes a @@ -163,20 +168,34 @@ struct Discovered { 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 +// Walks the full auto-resolution chain for a package not present in +// the user's overlay. Each candidate produced by a probe gets its own +// verify_link attempt at +// /-/ +// e.g. `01-conan/`, `02-vcpkg/`, `03-nix-probe/`, `04-pkg-config/`. +// Subdirs are NOT cleaned up — they're meant for the user to inspect +// after a failed `cargoxx add`. The caller wipes `` +// 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, const std::vector& components, const std::filesystem::path& overlay_path, const std::filesystem::path& scratch_root, const BuildFn& build_fn) -> util::Result; +// The on-disk parent dir that holds per-package verify_link scratch +// projects. Resolves to: +// $XDG_CACHE_HOME/cargoxx/last-failure// (when XDG_CACHE_HOME is set) +// $HOME/.cache/cargoxx/last-failure// (else if HOME is set) +// /.cargoxx-last-failure// (fallback) +// Each `cargoxx add ` (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 // uses; the response carries far more metadata (license, summary, per- // system store hashes) that we deliberately ignore. diff --git a/src/resolver/verify_link.cpp b/src/resolver/verify_link.cpp index a12991e..079252e 100644 --- a/src/resolver/verify_link.cpp +++ b/src/resolver/verify_link.cpp @@ -31,24 +31,6 @@ auto write_text(const fs::path& path, std::string_view content) -> util::Result< 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) @@ -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); if (!db) { @@ -74,26 +53,12 @@ auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn) !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}; + const auto& proj_root = req.scratch_path; + std::error_code ec; 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, @@ -129,8 +94,6 @@ auto verify_link(const VerifyLinkRequest& req, const BuildFn& build_fn) 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) { diff --git a/tests/verify_link_unit.cpp b/tests/verify_link_unit.cpp index 45a3e9f..b96835e 100644 --- a/tests/verify_link_unit.cpp +++ b/tests/verify_link_unit.cpp @@ -41,7 +41,7 @@ auto make_request(const std::filesystem::path& parent) -> VerifyLinkRequest { .version_spec = "*", .components = {}, .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); } -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 req = make_request(parent); std::filesystem::path captured; @@ -111,6 +112,7 @@ TEST_CASE("verify_link cleans up its scratch project", "[resolver][verify_link]" return cargoxx::util::Result{}; }); - REQUIRE_FALSE(captured.empty()); - REQUIRE_FALSE(std::filesystem::exists(captured)); + REQUIRE(captured == req.scratch_path); + REQUIRE(std::filesystem::exists(captured / "Cargoxx.toml")); + REQUIRE(std::filesystem::exists(captured / "src" / "main.cpp")); }