[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/vixcpp/cli/main/src/util/Resolver.cpp [Back]  [Original]

/**
 *
 *  @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;
  }
}

Web Proxy Viewer  |  New URL  |  Original Page