module; #include module cargoxx.resolver; import std; import cargoxx.util; import cargoxx.exec; namespace cargoxx::resolver { 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; " "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}; } // nix eval emits these markers when an attribute is missing on the flake. auto looks_like_missing_attribute(std::string_view stderr_text) -> bool { return stderr_text.find("does not provide attribute") != std::string_view::npos || stderr_text.find("attribute '") != std::string_view::npos && stderr_text.find("missing") != std::string_view::npos; } } // namespace auto parse_nix_eval_json(std::string_view attr, std::string_view json) -> util::Result { nlohmann::json j; try { j = nlohmann::json::parse(json); } catch (const nlohmann::json::parse_error& e) { return std::unexpected(make_error( util::ErrorCode::ResolutionNetworkError, std::format("nix eval emitted unparseable JSON: {}", e.what()))); } if (!j.is_object()) { return std::unexpected(make_error( util::ErrorCode::ResolutionNetworkError, "nix eval JSON is not an object")); } 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(); } else { return std::unexpected(make_error( util::ErrorCode::ResolutionNetworkError, "nix eval JSON lacks 'path'")); } if (j.contains("version") && j["version"].is_string()) { info.version = j["version"].get(); } if (j.contains("dev_path") && j["dev_path"].is_string()) { info.dev_path = j["dev_path"].get(); } return info; } auto nixpkgs_probe(const std::string& attr) -> util::Result { if (attr.empty()) { return std::unexpected(make_error(util::ErrorCode::ResolutionUnknownPackage, "package name is empty")); } std::vector args{ "--extra-experimental-features", "nix-command flakes", "eval", std::format("nixpkgs#{}", attr), "--json", "--apply", std::string{APPLY_FN}, }; auto r = exec::run("nix", args, exec::ExecOptions{ .cwd = {}, .env_overrides = {}, .timeout = std::chrono::seconds{60}, .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 '{}'", attr))); } return std::unexpected(make_error( util::ErrorCode::ResolutionNetworkError, std::format("nix eval failed (exit {}): {}", r->exit_code, r->stderr_text))); } return parse_nix_eval_json(attr, r->stdout_text); } } // namespace cargoxx::resolver