diff --git a/CHANGELOG.md b/CHANGELOG.md index 71af483..4a975f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,19 @@ All notable changes to cargoxx will be documented in this file. 6 cases against fixtures derived from a real fmt 10.2.1 response; `tests/devbox_resolve_live.cpp` (gated by `CARGOXX_NETWORK_TESTS=1`) hits the live API. +- Fix: `nix_cmake_scan` would silently fail when the dev output had + not yet been realized on the local machine — `nixpkgs_probe`'s + `nix eval` only *computes* a store path, so packages first-time + encountered (`libclang`, anything not previously built) returned a + path that didn't exist on disk. Discovery now realizes the + candidate output via + `nix build --no-link --print-out-paths nixpkgs#.dev` (or + `.`) before scanning. New free function + `cargoxx::resolver::realize_path(flake_attr)` wraps the call. + Live verified: `cargoxx add libclang` now reaches the verify-link + step (the subsequent `find_dependency(LLVM)` failure inside Clang's + CMake config is the pre-existing cross-package transitive-dep + limit — workaround is to `cargoxx add libllvm` first). - Fix: `nix_cmake_scan` walked only the package's default output. For multi-output Nix packages — `boost`, `openssl`, `libllvm`, `libclang`, glib, … — CMake configs live in the `dev` output diff --git a/src/resolver/discover.cpp b/src/resolver/discover.cpp index 6bbef38..2a4279d 100644 --- a/src/resolver/discover.cpp +++ b/src/resolver/discover.cpp @@ -89,21 +89,31 @@ auto discover(const std::string& name, const std::string& version_spec, if (auto v = vcpkg_probe(name); v) { candidates.push_back({"vcpkg", recipe_from_vcpkg(*v, name, "vcpkg")}); } - // Multi-output nix packages keep CMake configs in the `dev` output; - // scan it first when available, fall back to the default `out`. - auto try_scan = [&](const fs::path& root) + // Multi-output nix packages keep CMake configs in the `dev` output. + // The probe above only computes paths via `nix eval`; for packages + // not yet present in the store, those paths don't exist on disk + // and the scan would fail on its `fs::exists` check. Realize each + // candidate output (downloads from the binary cache) before + // scanning. dev wins when available; fall back to the default out. + auto realize_and_scan = [&](std::string_view sub_attr) -> std::optional { - if (root.empty()) { + auto attr = + sub_attr.empty() ? name : std::format("{}.{}", name, sub_attr); + auto realized = realize_path(attr); + if (!realized) { return std::nullopt; } - if (auto r = nix_cmake_scan(root, name); r) { + if (auto r = nix_cmake_scan(fs::path{*realized}, name); r) { return std::move(*r); } return std::nullopt; }; - auto scan_hit = try_scan(info->dev_path); + std::optional scan_hit; + if (!info->dev_path.empty()) { + scan_hit = realize_and_scan("dev"); + } if (!scan_hit) { - scan_hit = try_scan(info->out_path); + scan_hit = realize_and_scan(""); } if (scan_hit) { candidates.push_back( diff --git a/src/resolver/nixpkgs_probe.cpp b/src/resolver/nixpkgs_probe.cpp index 4a475df..5358a58 100644 --- a/src/resolver/nixpkgs_probe.cpp +++ b/src/resolver/nixpkgs_probe.cpp @@ -115,4 +115,51 @@ auto nixpkgs_probe(const std::string& attr) -> util::Result { return parse_nix_eval_json(attr, r->stdout_text); } +auto realize_path(const std::string& flake_attr) -> util::Result { + if (flake_attr.empty()) { + return std::unexpected(make_error(util::ErrorCode::ResolutionUnknownPackage, + "realize_path: flake_attr is empty")); + } + + std::vector args{ + "--extra-experimental-features", "nix-command flakes", + "build", std::format("nixpkgs#{}", flake_attr), + "--no-link", "--print-out-paths", + }; + + auto r = exec::run("nix", args, + exec::ExecOptions{ + .cwd = {}, + .env_overrides = {}, + // Long enough for first-time fetch from the binary + // cache; multi-output llvm tarballs are ~hundreds of MB. + .timeout = std::chrono::seconds{600}, + .inherit_stdio = false, + }); + if (!r) { + return std::unexpected(r.error()); + } + if (r->exit_code != 0) { + if (looks_like_missing_attribute(r->stderr_text)) { + return std::unexpected(make_error( + util::ErrorCode::ResolutionUnknownPackage, + std::format("nixpkgs has no attribute '{}'", flake_attr))); + } + return std::unexpected(make_error( + util::ErrorCode::ResolutionNetworkError, + std::format("nix build failed (exit {}): {}", r->exit_code, + r->stderr_text))); + } + auto path = r->stdout_text; + while (!path.empty() && (path.back() == '\n' || path.back() == ' ')) { + path.pop_back(); + } + if (path.empty()) { + return std::unexpected(make_error( + util::ErrorCode::ResolutionNetworkError, + std::format("nix build emitted no path for '{}'", flake_attr))); + } + return path; +} + } // namespace cargoxx::resolver diff --git a/src/resolver/resolver.cppm b/src/resolver/resolver.cppm index 4e62b3d..bb2e8ae 100644 --- a/src/resolver/resolver.cppm +++ b/src/resolver/resolver.cppm @@ -33,6 +33,18 @@ auto parse_nix_eval_json(std::string_view attr, std::string_view json) // `ResolutionNetworkError` on timeout or evaluator errors. auto nixpkgs_probe(const std::string& attr) -> util::Result; +// Materializes a flake attribute on disk by shelling out to +// nix build --no-link --print-out-paths nixpkgs# +// and returns the absolute store path. The eval-only `nixpkgs_probe` +// computes the path the output *would* have, but it isn't actually +// present on disk until it's been built / fetched from the binary +// cache; nix_cmake_scan needs it on disk to walk lib/cmake. Use +// `realize_path("libclang.dev")` to scan a dev output, etc. +// +// `ResolutionUnknownPackage` for unknown attrs, `ResolutionNetworkError` +// for build / network errors. +auto realize_path(const std::string& flake_attr) -> util::Result; + // One CMake config-file's IMPORTED targets together with the find_package // expression derived from its filename stem. struct NixCmakeCandidate {