[M5+] add resolver::nixpkgs_git_resolve fallback

This commit is contained in:
2026-05-10 12:19:25 +00:00
parent df2c25b559
commit cb82e918d8
7 changed files with 622 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
module cargoxx.resolver;
import std;
import cargoxx.util;
import cargoxx.exec;
namespace cargoxx::resolver {
namespace fs = std::filesystem;
namespace {
constexpr std::string_view NIXPKGS_REPO_URL = "https://github.com/NixOS/nixpkgs.git";
auto error(util::ErrorCode code, std::string msg) -> util::Error {
return util::Error{code, std::move(msg), "", std::nullopt, std::nullopt};
}
auto default_clone_path() -> fs::path {
if (auto* xdg = std::getenv("XDG_CACHE_HOME"); xdg && *xdg) {
return fs::path{xdg} / "cargoxx" / "nixpkgs";
}
if (auto* home = std::getenv("HOME"); home && *home) {
return fs::path{home} / ".cache" / "cargoxx" / "nixpkgs";
}
return fs::current_path() / ".cargoxx-nixpkgs";
}
auto ensure_clone(const fs::path& repo_path) -> util::Result<void> {
std::error_code ec;
if (fs::exists(repo_path / ".git", ec)) {
return {};
}
fs::create_directories(repo_path.parent_path(), ec);
if (ec) {
return std::unexpected(error(
util::ErrorCode::ResolutionNetworkError,
std::format("cannot create '{}': {}", repo_path.parent_path().string(),
ec.message())));
}
auto r = exec::run("git",
{"clone", std::string{NIXPKGS_REPO_URL}, repo_path.string()},
exec::ExecOptions{
.cwd = {},
.env_overrides = {},
.timeout = std::chrono::seconds{1800}, // up to 30 min
.inherit_stdio = true, // user wants progress
});
if (!r) {
return std::unexpected(r.error());
}
if (r->exit_code != 0) {
return std::unexpected(error(
util::ErrorCode::ResolutionNetworkError,
std::format("git clone of nixpkgs failed (exit {})", r->exit_code)));
}
return {};
}
} // namespace
auto pick_youngest_commit(std::string_view git_log_output) -> std::optional<std::string> {
std::string best_sha;
std::int64_t best_time = -1;
std::size_t pos = 0;
while (pos < git_log_output.size()) {
auto eol = git_log_output.find('\n', pos);
auto line = git_log_output.substr(
pos, eol == std::string_view::npos ? git_log_output.size() - pos
: eol - pos);
auto space = line.find(' ');
if (space != std::string_view::npos) {
auto sha = line.substr(0, space);
auto ts = line.substr(space + 1);
std::int64_t t = 0;
auto [ptr, ec] = std::from_chars(ts.data(), ts.data() + ts.size(), t);
if (ec == std::errc{} && t > best_time) {
best_time = t;
best_sha = std::string{sha};
}
}
if (eol == std::string_view::npos) {
break;
}
pos = eol + 1;
}
if (best_sha.empty()) {
return std::nullopt;
}
return best_sha;
}
auto nixpkgs_git_resolve(const std::string& name, const std::string& version,
std::optional<fs::path> repo_path) -> util::Result<std::string> {
if (name.empty() || version.empty()) {
return std::unexpected(error(util::ErrorCode::ResolutionUnknownPackage,
"nixpkgs_git_resolve: name and version are required"));
}
auto repo = repo_path.value_or(default_clone_path());
if (auto ok = ensure_clone(repo); !ok) {
return std::unexpected(ok.error());
}
// -S looks for changes that added or removed the literal string. We
// restrict the path filter to pkgs/ to keep noise down. The youngest
// commit wins (highest committer timestamp).
auto needle = std::format("version = \"{}\"", version);
auto r = exec::run(
"git",
{"-C", repo.string(), "log", "--all", "-S", needle,
"--pretty=%H %ct", "--", "pkgs/"},
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) {
return std::unexpected(error(
util::ErrorCode::ResolutionNetworkError,
std::format("git log failed (exit {}): {}", r->exit_code, r->stderr_text)));
}
auto sha = pick_youngest_commit(r->stdout_text);
if (!sha) {
return std::unexpected(error(
util::ErrorCode::ResolutionVersionNotFound,
std::format("nixpkgs git: no commit introduced 'version = \"{}\"' for {}",
version, name)));
}
return *sha;
}
} // namespace cargoxx::resolver

View File

@@ -158,4 +158,20 @@ auto parse_devbox_resolve(std::string_view json)
auto devbox_resolve(const std::string& name, const std::string& version)
-> util::Result<DevboxResolution>;
// Pure: parse the output of
// git log --all -S 'version = "<v>"' --pretty='%H %ct' -- pkgs/
// (one commit per line: "<40-char-sha> <unix-epoch-seconds>") and
// return the youngest matching SHA, or nullopt on empty input.
auto pick_youngest_commit(std::string_view git_log_output)
-> std::optional<std::string>;
// Searches a local nixpkgs clone for the youngest commit that
// introduced `version = "<version>"` under pkgs/. `repo_path`
// defaults to ~/.cache/cargoxx/nixpkgs/ — when the directory doesn't
// exist the repo is cloned lazily (multi-GB, only on first miss).
// 404-equivalent: ResolutionVersionNotFound.
auto nixpkgs_git_resolve(const std::string& name, const std::string& version,
std::optional<std::filesystem::path> repo_path = std::nullopt)
-> util::Result<std::string>;
} // namespace cargoxx::resolver