/**
*
* @file Resolver.cpp
* @author Gaspard Kirira
*
* Copyright 2025, Gaspard Kirira. All rights reserved.
* https://github.com/vixcpp/vix
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Vix.cpp
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace fs = std::filesystem;
using json = nlohmann::json;
namespace vix::cli::util::resolver
{
namespace
{
struct PkgSpec
{
std::string ns;
std::string name;
std::string requestedVersion;
std::string resolvedVersion;
std::string id() const
{
return ns + "/" + name;
}
};
std::string home_dir()
{
#ifdef _WIN32
const char *home = vix::utils::vix_getenv("USERPROFILE");
#else
const char *home = vix::utils::vix_getenv("HOME");
#endif
return home ? std::string(home) : std::string();
}
fs::path vix_root()
{
const std::string h = home_dir();
if (h.empty())
{
return fs::path(".vix");
}
return fs::path(h) / ".vix";
}
fs::path registry_dir()
{
return vix_root() / "registry" / "index";
}
fs::path registry_index_dir()
{
return registry_dir() / "index";
}
fs::path store_git_dir()
{
return vix_root() / "store" / "git";
}
std::string trim_copy(std::string s)
{
auto isws = [](unsigned char c)
{
return std::isspace(c) != 0;
};
while (!s.empty() && isws(static_cast(s.front())))
{
s.erase(s.begin());
}
while (!s.empty() && isws(static_cast(s.back())))
{
s.pop_back();
}
return s;
}
json read_json_file_or_throw(const fs::path &path)
{
std::ifstream in(path);
if (!in)
{
throw std::runtime_error("cannot open file: " + path.string());
}
json j;
in >> j;
return j;
}
fs::path entry_path(const std::string &ns, const std::string &name)
{
return registry_index_dir() / (ns + "." + name + ".json");
}
void ensure_registry_present_or_throw()
{
if (fs::exists(registry_dir()) && fs::exists(registry_index_dir()))
{
return;
}
throw std::runtime_error("registry not synced");
}
std::optional parse_dependency_spec(
const vix::cli::util::manifest::Dependency &dependency)
{
const std::string id = trim_copy(dependency.id);
const auto slash = id.find('/');
if (slash == std::string::npos)
{
return std::nullopt;
}
PkgSpec spec;
spec.ns = trim_copy(id.substr(0, slash));
spec.name = trim_copy(id.substr(slash + 1));
spec.requestedVersion = trim_copy(dependency.requested);
spec.resolvedVersion.clear();
if (spec.ns.empty() || spec.name.empty())
{
return std::nullopt;
}
return spec;
}
int resolve_version_or_throw(const json &entry, PkgSpec &spec)
{
if (!entry.contains("versions") || !entry["versions"].is_object())
{
throw std::runtime_error(
"invalid registry entry: missing versions for " + spec.id());
}
std::vector versions;
versions.reserve(entry["versions"].size());
for (auto it = entry["versions"].begin(); it != entry["versions"].end(); ++it)
{
versions.push_back(it.key());
}
if (versions.empty())
{
throw std::runtime_error("no versions available for: " + spec.id());
}
if (spec.requestedVersion.empty())
{
spec.resolvedVersion = vix::cli::util::semver::findLatest(versions);
return 0;
}
const auto resolved =
vix::cli::util::semver::resolveMaxSatisfying(versions, spec.requestedVersion);
if (!resolved.has_value())
{
throw std::runtime_error(
"no version matches range: " + spec.id() + "@" + spec.requestedVersion);
}
spec.resolvedVersion = *resolved;
return 0;
}
int clone_checkout_or_throw(
const std::string &repoUrl,
const std::string &idDot,
const std::string &commit,
std::string &outDir)
{
fs::create_directories(store_git_dir());
const fs::path dst = store_git_dir() / idDot / commit;
outDir = dst.string();
if (fs::exists(dst))
{
return 0;
}
fs::create_directories(dst.parent_path());
{
const std::string cmd =
"git clone -q " + repoUrl + " " + dst.string();
const int rc = vix::cli::util::run_cmd_retry_debug(cmd);
if (rc != 0)
{
throw std::runtime_error("git clone failed for: " + repoUrl);
}
}
{
const std::string cmd =
"git -C " + dst.string() +
" -c advice.detachedHead=false checkout -q " + commit;
const int rc = vix::cli::util::run_cmd_retry_debug(cmd);
if (rc != 0)
{
throw std::runtime_error("git checkout failed for commit: " + commit);
}
}
return 0;
}
std::optional parse_dep_string_v1(const std::string &raw)
{
const std::string s = trim_copy(raw);
if (s.empty())
{
return std::nullopt;
}
const auto slash = s.find('/');
if (slash == std::string::npos)
{
return std::nullopt;
}
const auto at = s.find('@', slash + 1);
PkgSpec spec;
spec.ns = trim_copy(s.substr(0, slash));
if (at == std::string::npos)
{
spec.name = trim_copy(s.substr(slash + 1));
spec.requestedVersion.clear();
}
else
{
spec.name = trim_copy(s.substr(slash + 1, at - (slash + 1)));
spec.requestedVersion = trim_copy(s.substr(at + 1));
}
spec.resolvedVersion.clear();
if (spec.ns.empty() || spec.name.empty())
{
return std::nullopt;
}
return spec;
}
std::optional parse_dep_obj_v1(const json &dependency)
{
if (!dependency.is_object())
{
return std::nullopt;
}
const std::string id = trim_copy(dependency.value("id", ""));
if (id.empty())
{
return std::nullopt;
}
std::string requested = trim_copy(dependency.value("version", ""));
if (requested.empty())
{
requested = trim_copy(dependency.value("requested", ""));
}
if (requested.empty())
{
requested = trim_copy(dependency.value("range", ""));
}
const auto slash = id.find('/');
if (slash == std::string::npos)
{
return std::nullopt;
}
PkgSpec spec;
spec.ns = trim_copy(id.substr(0, slash));
spec.name = trim_copy(id.substr(slash + 1));
spec.requestedVersion = requested;
spec.resolvedVersion.clear();
if (spec.ns.empty() || spec.name.empty())
{
return std::nullopt;
}
return spec;
}
std::vector read_vix_json_deps_v1(const fs::path &repoDir)
{
std::vector out;
const fs::path manifestPath = repoDir / "vix.json";
if (!fs::exists(manifestPath))
{
return out;
}
json root;
try
{
root = read_json_file_or_throw(manifestPath);
}
catch (...)
{
return out;
}
if (!root.is_object())
{
return out;
}
if (!root.contains("deps") || !root["deps"].is_array())
{
return out;
}
for (const auto &dependency : root["deps"])
{
if (dependency.is_object())
{
auto spec = parse_dep_obj_v1(dependency);
if (spec.has_value())
{
out.push_back(*spec);
}
continue;
}
if (dependency.is_string())
{
auto spec = parse_dep_string_v1(dependency.get());
if (spec.has_value())
{
out.push_back(*spec);
}
}
}
return out;
}
void upsert_locked_dependency(
std::vector &dependencies,
const vix::cli::util::lockfile::LockedDependency &dependency)
{
for (auto &item : dependencies)
{
if (item.id == dependency.id)
{
item = dependency;
return;
}
}
dependencies.push_back(dependency);
}
void resolve_transitive_or_throw(
PkgSpec spec,
std::unordered_set &visited,
std::vector &lockedDependencies)
{
const fs::path registryEntryPath = entry_path(spec.ns, spec.name);
if (!fs::exists(registryEntryPath))
{
throw std::runtime_error("package not found: " + spec.id());
}
const json entry = read_json_file_or_throw(registryEntryPath);
resolve_version_or_throw(entry, spec);
const json versions = entry.at("versions");
if (!versions.contains(spec.resolvedVersion))
{
throw std::runtime_error(
"version not found: " + spec.id() + "@" + spec.resolvedVersion);
}
const json versionNode = versions.at(spec.resolvedVersion);
const std::string repoUrl = entry.at("repo").at("url").get();
const std::string tag = versionNode.at("tag").get();
const std::string commit = versionNode.at("commit").get();
const std::string visitKey = spec.id() + "@" + spec.resolvedVersion;
if (visited.count(visitKey))
{
return;
}
visited.insert(visitKey);
const std::string idDot = spec.ns + "." + spec.name;
std::string installedDir;
clone_checkout_or_throw(repoUrl, idDot, commit, installedDir);
const auto contentHash = vix::cli::util::sha256_package_directory(installedDir);
const std::string hashStr = contentHash.value_or("");
upsert_locked_dependency(
lockedDependencies,
vix::cli::util::lockfile::LockedDependency{
spec.id(),
spec.requestedVersion.empty() ? spec.resolvedVersion : spec.requestedVersion,
spec.resolvedVersion,
repoUrl,
tag,
commit,
hashStr,
vix::cli::util::PACKAGE_HASH_ALGORITHM,
vix::cli::util::PACKAGE_HASH_VERSION});
const auto transitiveDependencies = read_vix_json_deps_v1(fs::path(installedDir));
for (auto transitiveSpec : transitiveDependencies)
{
resolve_transitive_or_throw(transitiveSpec, visited, lockedDependencies);
}
}
}
std::vector
resolve_project_dependencies_or_throw(
const std::vector &manifestDependencies)
{
ensure_registry_present_or_throw();
std::vector lockedDependencies;
std::unordered_set visited;
for (const auto &dependency : manifestDependencies)
{
const auto spec = parse_dependency_spec(dependency);
if (!spec.has_value())
{
throw std::runtime_error("invalid manifest dependency: " + dependency.id);
}
resolve_transitive_or_throw(*spec, visited, lockedDependencies);
}
return lockedDependencies;
}
std::vector available_registry_versions_or_throw(
const std::string &packageId)
{
ensure_registry_present_or_throw();
const auto spec = parse_dep_string_v1(packageId);
if (!spec.has_value()) throw std::runtime_error("invalid registry package id: " + packageId);
const json entry = read_json_file_or_throw(entry_path(spec->ns, spec->name));
if (!entry.contains("versions") || !entry["versions"].is_object()) throw std::runtime_error("invalid registry entry: missing versions for " + packageId);
std::vector versions;
for (auto it = entry["versions"].begin(); it != entry["versions"].end(); ++it) versions.push_back(it.key());
return versions;
}
}