diff --git a/CHANGELOG.md b/CHANGELOG.md index 6150690..71af483 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,18 @@ 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` walked only the package's default output. For + multi-output Nix packages — `boost`, `openssl`, `libllvm`, + `libclang`, glib, … — CMake configs live in the `dev` output + (`/lib/cmake/llvm/LLVMConfig.cmake`), not under the default + output, so `cargoxx add libllvm` (or any other multi-output dep) + failed with "Conan/vcpkg/nix-scan all empty". `NixpkgsInfo` now + carries an optional `dev_path`; the `nix eval` apply expression + reports `if p ? dev then p.dev.outPath else ""`. `discover` scans + the dev output preferentially and falls back to `out`. Live + verified: `cargoxx add libllvm` now reaches the verify-link step + (the linker-time libstdc++/libc++ ABI mismatch is a separate + documented limit and applies to abseil-cpp too). - Fix: pinned-dep `buildInputs` line used the curated linkdb's `nixpkgs_attr` (e.g. `fmt_10`) regardless of the pinned rev, so `cargoxx add fmt@12.1.0` emitted `pkgs_nixpkgs_fmt_12_1_0.fmt_10` diff --git a/src/resolver/discover.cpp b/src/resolver/discover.cpp index ced7cdf..6bbef38 100644 --- a/src/resolver/discover.cpp +++ b/src/resolver/discover.cpp @@ -89,9 +89,25 @@ 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")}); } - if (auto n = nix_cmake_scan(info->out_path, name); n) { + // 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) + -> std::optional { + if (root.empty()) { + return std::nullopt; + } + if (auto r = nix_cmake_scan(root, name); r) { + return std::move(*r); + } + return std::nullopt; + }; + auto scan_hit = try_scan(info->dev_path); + if (!scan_hit) { + scan_hit = try_scan(info->out_path); + } + if (scan_hit) { candidates.push_back( - {"nix-probe", recipe_from_nix_scan(*n, name, "nix-probe")}); + {"nix-probe", recipe_from_nix_scan(*scan_hit, name, "nix-probe")}); } if (candidates.empty()) { diff --git a/src/resolver/nixpkgs_probe.cpp b/src/resolver/nixpkgs_probe.cpp index 6876cc0..4a475df 100644 --- a/src/resolver/nixpkgs_probe.cpp +++ b/src/resolver/nixpkgs_probe.cpp @@ -15,8 +15,14 @@ namespace { // `outPath` is a magic attribute name that triggers nix's derivation // coercion in --json mode (the attrset gets serialized as a bare path // string). Renaming the field to `path` keeps it a proper JSON object. +// +// `dev_path` is captured for multi-output packages (boost, openssl, +// llvm, …) where CMake configs live under the `dev` output rather +// than `out`. `p ? dev` is false for single-output packages, leaving +// the field as the empty string. constexpr std::string_view APPLY_FN = - "p: { version = p.version or \"\"; path = p.outPath; }"; + "p: { version = p.version or \"\"; path = p.outPath; " + "dev_path = if p ? dev then p.dev.outPath else \"\"; }"; auto make_error(util::ErrorCode code, std::string msg) -> util::Error { return util::Error{code, std::move(msg), "", std::nullopt, std::nullopt}; @@ -48,7 +54,9 @@ auto parse_nix_eval_json(std::string_view attr, std::string_view json) "nix eval JSON is not an object")); } - NixpkgsInfo info{.attr = std::string{attr}, .version = {}, .out_path = {}}; + NixpkgsInfo info{ + .attr = std::string{attr}, .version = {}, .out_path = {}, .dev_path = {}, + }; if (j.contains("path") && j["path"].is_string()) { info.out_path = j["path"].get(); @@ -62,6 +70,10 @@ auto parse_nix_eval_json(std::string_view attr, std::string_view json) info.version = j["version"].get(); } + if (j.contains("dev_path") && j["dev_path"].is_string()) { + info.dev_path = j["dev_path"].get(); + } + return info; } diff --git a/src/resolver/resolver.cppm b/src/resolver/resolver.cppm index 3c0c04b..4e62b3d 100644 --- a/src/resolver/resolver.cppm +++ b/src/resolver/resolver.cppm @@ -10,11 +10,17 @@ export namespace cargoxx::resolver { // What `nix eval nixpkgs#` reports for a package: a confirmation that // the attribute exists, a best-effort version string, and the realized -// nix-store path so later probes can scan its installed CMake configs. +// nix-store path(s) so later probes can scan its installed CMake configs. +// +// Multi-output packages (boost, openssl, llvm, ...) expose CMake configs +// in their `dev` output, not in the default `out`. When the package has +// a separate dev output its store path is captured here so callers can +// scan it preferentially. struct NixpkgsInfo { - std::string attr; // the queried name, e.g. "simdjson" - std::string version; // empty when the derivation has no version - std::string out_path; // absolute /nix/store/... path + std::string attr; // the queried name, e.g. "simdjson" + std::string version; // empty when the derivation has no version + std::string out_path; // default output's absolute /nix/store/... path + std::string dev_path; // dev output's path; empty when no dev output }; // Pure parser exposed for unit testing. Accepts the raw JSON returned by diff --git a/tests/nixpkgs_probe_parse.cpp b/tests/nixpkgs_probe_parse.cpp index 8b98fbf..c492e7d 100644 --- a/tests/nixpkgs_probe_parse.cpp +++ b/tests/nixpkgs_probe_parse.cpp @@ -53,3 +53,29 @@ TEST_CASE("parse_nix_eval_json fails on a non-object root", REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::ResolutionNetworkError); } + +TEST_CASE("parse_nix_eval_json captures dev_path when present", + "[resolver][nixpkgs]") { + constexpr std::string_view input = R"({ + "version": "21.1.8", + "path": "/nix/store/abc-llvm-21.1.8", + "dev_path": "/nix/store/def-llvm-21.1.8-dev" + })"; + auto r = parse_nix_eval_json("libllvm", input); + REQUIRE(r.has_value()); + REQUIRE(r->out_path == "/nix/store/abc-llvm-21.1.8"); + REQUIRE(r->dev_path == "/nix/store/def-llvm-21.1.8-dev"); +} + +TEST_CASE("parse_nix_eval_json leaves dev_path empty for single-output packages", + "[resolver][nixpkgs]") { + constexpr std::string_view input = R"({ + "version": "2.12.2", + "path": "/nix/store/xyz-hello-2.12.2", + "dev_path": "" + })"; + auto r = parse_nix_eval_json("hello", input); + REQUIRE(r.has_value()); + REQUIRE(r->out_path == "/nix/store/xyz-hello-2.12.2"); + REQUIRE(r->dev_path.empty()); +}