[M5+] add resolver::nixpkgs_probe (nix eval wrapper)

This commit is contained in:
2026-05-10 09:52:06 +00:00
parent f3d18b7939
commit 1c7ff39f64
7 changed files with 239 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
module;
#include <json.hpp>
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.
constexpr std::string_view APPLY_FN =
"p: { version = p.version or \"\"; path = p.outPath; }";
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<NixpkgsInfo> {
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 = {}};
if (j.contains("path") && j["path"].is_string()) {
info.out_path = j["path"].get<std::string>();
} 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<std::string>();
}
return info;
}
auto nixpkgs_probe(const std::string& attr) -> util::Result<NixpkgsInfo> {
if (attr.empty()) {
return std::unexpected(make_error(util::ErrorCode::ResolutionUnknownPackage,
"package name is empty"));
}
std::vector<std::string> 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