[M5+] add resolver::nixpkgs_git_resolve fallback
This commit is contained in:
138
src/resolver/nixpkgs_git.cpp
Normal file
138
src/resolver/nixpkgs_git.cpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user