[M5+] add resolver::devbox_resolve (search.devbox.sh)

This commit is contained in:
2026-05-10 12:09:58 +00:00
parent afe1856e11
commit df2c25b559
7 changed files with 295 additions and 0 deletions

View File

@@ -136,4 +136,26 @@ auto discover(const std::string& name, const std::string& version_spec,
const std::filesystem::path& scratch_root, const BuildFn& build_fn)
-> util::Result<Discovered>;
// 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.
struct DevboxResolution {
std::string name;
std::string version;
std::string commit_hash;
std::vector<std::string> attr_paths;
};
// Pure: parse the JSON body of GET https://search.devbox.sh/v1/resolve.
// `commit_hash` must be present and non-empty; other fields tolerate
// absence by leaving themselves blank.
auto parse_devbox_resolve(std::string_view json)
-> util::Result<DevboxResolution>;
// Calls GET https://search.devbox.sh/v1/resolve?name=<n>&version=<v>
// via curl. 404 → ResolutionUnknownPackage; transport / parse errors →
// ResolutionNetworkError.
auto devbox_resolve(const std::string& name, const std::string& version)
-> util::Result<DevboxResolution>;
} // namespace cargoxx::resolver

View File

@@ -0,0 +1,128 @@
module;
#include <json.hpp>
module cargoxx.resolver;
import std;
import cargoxx.util;
import cargoxx.exec;
namespace cargoxx::resolver {
namespace {
auto error(util::ErrorCode code, std::string msg) -> util::Error {
return util::Error{code, std::move(msg), "", std::nullopt, std::nullopt};
}
} // namespace
auto parse_devbox_resolve(std::string_view json) -> util::Result<DevboxResolution> {
nlohmann::json j;
try {
j = nlohmann::json::parse(json);
} catch (const nlohmann::json::parse_error& e) {
return std::unexpected(error(
util::ErrorCode::ResolutionNetworkError,
std::format("devbox /v1/resolve returned unparseable JSON: {}", e.what())));
}
if (!j.is_object()) {
return std::unexpected(error(util::ErrorCode::ResolutionNetworkError,
"devbox /v1/resolve response is not an object"));
}
DevboxResolution out;
if (auto it = j.find("name"); it != j.end() && it->is_string()) {
out.name = it->get<std::string>();
}
if (auto it = j.find("version"); it != j.end() && it->is_string()) {
out.version = it->get<std::string>();
}
if (auto it = j.find("commit_hash"); it != j.end() && it->is_string()) {
out.commit_hash = it->get<std::string>();
}
if (out.commit_hash.empty()) {
// Some responses omit the top-level commit_hash and surface it only
// per-system (`systems.<plat>.commit_hash`). Fall back to the first
// non-empty value we find under `systems`.
if (auto sys_it = j.find("systems");
sys_it != j.end() && sys_it->is_object()) {
for (const auto& [_, sys] : sys_it->items()) {
if (auto ch = sys.find("commit_hash");
ch != sys.end() && ch->is_string()) {
out.commit_hash = ch->get<std::string>();
if (!out.commit_hash.empty()) {
break;
}
}
}
}
}
if (auto it = j.find("attr_paths");
it != j.end() && it->is_array()) {
for (const auto& el : *it) {
if (el.is_string()) {
out.attr_paths.push_back(el.get<std::string>());
}
}
}
if (out.attr_paths.empty()) {
// attr_paths sometimes only appear under `systems.<plat>`.
if (auto sys_it = j.find("systems");
sys_it != j.end() && sys_it->is_object() && !sys_it->empty()) {
const auto& any_sys = sys_it->begin().value();
if (auto ap = any_sys.find("attr_paths");
ap != any_sys.end() && ap->is_array()) {
for (const auto& el : *ap) {
if (el.is_string()) {
out.attr_paths.push_back(el.get<std::string>());
}
}
}
}
}
if (out.commit_hash.empty()) {
return std::unexpected(error(
util::ErrorCode::ResolutionVersionNotFound,
"devbox response carried no commit_hash"));
}
return out;
}
auto devbox_resolve(const std::string& name, const std::string& version)
-> util::Result<DevboxResolution> {
if (name.empty() || version.empty()) {
return std::unexpected(error(util::ErrorCode::ResolutionUnknownPackage,
"devbox_resolve: name and version are required"));
}
auto url = std::format(
"https://search.devbox.sh/v1/resolve?name={}&version={}", name, version);
auto r = exec::run("curl", {"-fsSL", "--max-time", "10", url},
exec::ExecOptions{
.cwd = {},
.env_overrides = {},
.timeout = std::chrono::seconds{15},
.inherit_stdio = false,
});
if (!r) {
return std::unexpected(r.error());
}
if (r->exit_code == 22) {
return std::unexpected(error(
util::ErrorCode::ResolutionUnknownPackage,
std::format("devbox /v1/resolve has no entry for {} {}", name, version)));
}
if (r->exit_code != 0) {
return std::unexpected(error(
util::ErrorCode::ResolutionNetworkError,
std::format("curl failed (exit {}): {}", r->exit_code, r->stderr_text)));
}
return parse_devbox_resolve(r->stdout_text);
}
} // namespace cargoxx::resolver