[M5+] add resolver::devbox_resolve (search.devbox.sh)
This commit is contained in:
@@ -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
|
||||
|
||||
128
src/resolver/search_devbox.cpp
Normal file
128
src/resolver/search_devbox.cpp
Normal 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
|
||||
Reference in New Issue
Block a user