Signed-off-by: Julien Jerphanion <git@jjerphan.xyz>
This commit is contained in:
Julien Jerphanion 2025-04-29 17:57:06 +02:00
parent e49116bfff
commit e749bbe18b
No known key found for this signature in database
GPG Key ID: 56B690A97C2E35B8
14 changed files with 1377 additions and 78 deletions

View File

@ -196,6 +196,9 @@ set(
${LIBMAMBA_SOURCE_DIR}/solver/libsolv/repo_info.cpp
${LIBMAMBA_SOURCE_DIR}/solver/libsolv/solver.cpp
${LIBMAMBA_SOURCE_DIR}/solver/libsolv/unsolvable.cpp
# Solver resolvo implementation
${LIBMAMBA_SOURCE_DIR}/solver/resolvo/database.cpp
${LIBMAMBA_SOURCE_DIR}/solver/resolvo/solver.cpp
# Artifacts validation
${LIBMAMBA_SOURCE_DIR}/validation/errors.cpp
${LIBMAMBA_SOURCE_DIR}/validation/keys.cpp
@ -349,6 +352,9 @@ set(
${LIBMAMBA_INCLUDE_DIR}/mamba/solver/libsolv/repo_info.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/solver/libsolv/solver.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/solver/libsolv/unsolvable.hpp
# Solver resolvo implementation
${LIBMAMBA_INCLUDE_DIR}/mamba/solver/resolvo/database.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/solver/resolvo/solver.hpp
# Artifacts validation
${LIBMAMBA_INCLUDE_DIR}/mamba/validation/errors.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/validation/keys.hpp

View File

@ -8,6 +8,7 @@
#define MAMBA_API_CHANNEL_LOADER_HPP
#include "mamba/core/error_handling.hpp"
#include "mamba/solver/resolvo/database.hpp"
namespace mamba
{
@ -15,6 +16,11 @@ namespace mamba
{
class Database;
}
namespace solver::resolvo
{
class Database;
}
class Context;
class ChannelContext;
class MultiPackageCache;
@ -30,7 +36,7 @@ namespace mamba
auto load_channels(
Context& ctx,
ChannelContext& channel_context,
solver::libsolv::Database& database,
std::variant<solver::libsolv::Database, solver::resolvo::Database>& database,
MultiPackageCache& package_caches
) -> expected_t<void, mamba_aggregated_error>;

View File

@ -257,6 +257,8 @@ namespace mamba
bool repodata_use_zst = true;
std::vector<std::string> repodata_has_zst = { "https://conda.anaconda.org/conda-forge" };
bool use_resolvo_solver = false;
// FIXME: Should not be stored here
// Notice that we cannot build this map directly from mirrored_channels,
// since we need to add a single "mirror" for non mirrored channels

View File

@ -9,19 +9,24 @@
#include "mamba/core/error_handling.hpp"
#include "mamba/solver/libsolv/repo_info.hpp"
#include "mamba/solver/resolvo/database.hpp"
#include "mamba/specs/channel.hpp"
namespace mamba
{
class Context;
class PrefixData;
class SubdirData;
namespace solver::libsolv
{
class Database;
}
namespace solver::resolvo
{
class Database;
}
class Context;
class PrefixData;
class SubdirData;
void add_spdlog_logger_to_database(solver::libsolv::Database& database);
auto load_subdir_in_database( //
@ -32,8 +37,8 @@ namespace mamba
auto load_installed_packages_in_database(
const Context& ctx,
solver::libsolv::Database& database,
std::variant<solver::libsolv::Database, solver::resolvo::Database>& database,
const PrefixData& prefix
) -> solver::libsolv::RepoInfo;
) -> expected_t<void>;
}
#endif

View File

@ -0,0 +1,42 @@
// Copyright (c) 2024, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef MAMBA_SOLVER_DATABASE_HPP
#define MAMBA_SOLVER_DATABASE_HPP
#include <string>
#include <vector>
#include "mamba/fs/filesystem.hpp"
#include "mamba/specs/channel.hpp"
#include "mamba/specs/package_info.hpp"
namespace mamba::solver
{
class Database
{
public:
virtual ~Database() = default;
virtual void add_repo_from_repodata_json(
const fs::u8path& filename,
const std::string& repo_url,
const std::string& channel_id,
bool verify_artifacts = false
) = 0;
virtual void add_repo_from_packages(
const std::vector<specs::PackageInfo>& packages,
const std::string& repo_name,
bool pip_as_python_dependency = false
) = 0;
virtual void set_installed_repo(const std::string& repo_name) = 0;
};
}
#endif // MAMBA_SOLVER_DATABASE_HPP

View File

@ -0,0 +1,262 @@
// Copyright (c) 2024, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef MAMBA_SOLVER_RESOLVO_DATABASE_HPP
#define MAMBA_SOLVER_RESOLVO_DATABASE_HPP
#include <string>
#include <unordered_map>
#include <vector>
#include <resolvo/resolvo.h>
#include <resolvo/resolvo_dependency_provider.h>
#include <resolvo/resolvo_pool.h>
#include "mamba/solver/database.hpp"
#include "mamba/specs/match_spec.hpp"
#include "mamba/specs/package_info.hpp"
#include "mamba/specs/version.hpp"
namespace std
{
template <>
struct hash<::resolvo::NameId>
{
size_t operator()(const ::resolvo::NameId& id) const noexcept
{
return static_cast<size_t>(id.id);
}
};
template <>
struct hash<::resolvo::VersionSetId>
{
size_t operator()(const ::resolvo::VersionSetId& id) const noexcept
{
return static_cast<size_t>(id.id);
}
};
template <>
struct hash<::resolvo::SolvableId>
{
size_t operator()(const ::resolvo::SolvableId& id) const noexcept
{
return static_cast<size_t>(id.id);
}
};
template <>
struct hash<::resolvo::StringId>
{
size_t operator()(const ::resolvo::StringId& id) const noexcept
{
return static_cast<size_t>(id.id);
}
};
}
namespace mamba::solver::resolvo
{
// Create a template Pool class that maps a key to a set of values
template <typename ID, typename T>
struct Mapping
{
/**
* Adds the value to the Mapping and returns its associated id. If the
* value is already in the Mapping, returns the id associated with it.
*/
ID alloc(T value)
{
if (auto element = value_to_id.find(value); element != value_to_id.end())
{
return element->second;
}
auto id = ID{ static_cast<uint32_t>(id_to_value.size()) };
id_to_value[id] = value;
value_to_id[value] = id;
return id;
}
/**
* Returns the value associated with the given id.
*/
T operator[](ID id)
{
return id_to_value[id];
}
/**
* Returns the id associated with the given value.
*/
ID operator[](T value)
{
return value_to_id[value];
}
// Iterator for the Mapping
auto begin()
{
return id_to_value.begin();
}
auto end()
{
return id_to_value.end();
}
auto begin() const
{
return id_to_value.begin();
}
auto end() const
{
return id_to_value.end();
}
auto cbegin()
{
return id_to_value.cbegin();
}
auto cend()
{
return id_to_value.cend();
}
auto cbegin() const
{
return id_to_value.cbegin();
}
auto cend() const
{
return id_to_value.cend();
}
auto find(T value)
{
return value_to_id.find(value);
}
auto begin_ids()
{
return value_to_id.begin();
}
auto end_ids()
{
return value_to_id.end();
}
auto begin_ids() const
{
return value_to_id.begin();
}
auto end_ids() const
{
return value_to_id.end();
}
auto cbegin_ids()
{
return value_to_id.cbegin();
}
auto cend_ids()
{
return value_to_id.cend();
}
auto cbegin_ids() const
{
return value_to_id.cbegin();
}
auto cend_ids() const
{
return value_to_id.cend();
}
auto size() const
{
return id_to_value.size();
}
private:
std::unordered_map<T, ID> value_to_id;
std::unordered_map<ID, T> id_to_value;
};
class Database
: public mamba::solver::Database
, public ::resolvo::DependencyProvider
{
public:
Database();
~Database() override = default;
// Implementation of mamba::solver::Database interface
void add_repo_from_repodata_json(
const fs::u8path& filename,
const std::string& repo_url,
const std::string& channel_id,
bool verify_artifacts = false
) override;
void add_repo_from_packages(
const std::vector<specs::PackageInfo>& packages,
const std::string& repo_name,
bool pip_as_python_dependency = false
) override;
void set_installed_repo(const std::string& repo_name) override;
// Implementation of resolvo::DependencyProvider interface
::resolvo::String display_solvable(::resolvo::SolvableId solvable) override;
::resolvo::String display_solvable_name(::resolvo::SolvableId solvable) override;
::resolvo::String
display_merged_solvables(::resolvo::Slice<::resolvo::SolvableId> solvable) override;
::resolvo::String display_name(::resolvo::NameId name) override;
::resolvo::String display_version_set(::resolvo::VersionSetId version_set) override;
::resolvo::String display_string(::resolvo::StringId string) override;
::resolvo::NameId version_set_name(::resolvo::VersionSetId version_set_id) override;
::resolvo::NameId solvable_name(::resolvo::SolvableId solvable_id) override;
::resolvo::Candidates get_candidates(::resolvo::NameId package) override;
void sort_candidates(::resolvo::Slice<::resolvo::SolvableId> solvables) override;
::resolvo::Vector<::resolvo::SolvableId> filter_candidates(
::resolvo::Slice<::resolvo::SolvableId> candidates,
::resolvo::VersionSetId version_set_id,
bool inverse
) override;
::resolvo::Dependencies get_dependencies(::resolvo::SolvableId solvable_id) override;
// Public access to pools and helper methods
::resolvo::VersionSetId alloc_version_set(std::string_view raw_match_spec);
::resolvo::SolvableId alloc_solvable(specs::PackageInfo package_info);
std::pair<specs::Version, size_t>
find_highest_version(::resolvo::VersionSetId version_set_id);
// Pools for mapping between resolvo IDs and mamba types
Mapping<::resolvo::NameId, ::resolvo::String> name_pool;
Mapping<::resolvo::StringId, ::resolvo::String> string_pool;
Mapping<::resolvo::VersionSetId, specs::MatchSpec> version_set_pool;
Mapping<::resolvo::SolvableId, specs::PackageInfo> solvable_pool;
private:
// Maps for quick lookups
std::unordered_map<::resolvo::NameId, ::resolvo::Vector<::resolvo::SolvableId>> name_to_solvable;
std::unordered_map<::resolvo::VersionSetId, std::pair<specs::Version, size_t>>
version_set_to_max_version_and_track_features_numbers;
};
}
#endif // MAMBA_SOLVER_RESOLVO_DATABASE_HPP

View File

@ -0,0 +1,32 @@
// Copyright (c) 2024, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef MAMBA_SOLVER_RESOLVO_SOLVER_HPP
#define MAMBA_SOLVER_RESOLVO_SOLVER_HPP
#include "mamba/core/error_handling.hpp"
#include "mamba/solver/request.hpp"
#include "mamba/solver/solution.hpp"
namespace mamba::solver::resolvo
{
class Database;
class Solver
{
public:
using Outcome = std::variant<Solution>;
[[nodiscard]] auto solve(Database& database, Request&& request) -> expected_t<Outcome>;
[[nodiscard]] auto solve(Database& database, const Request& request) -> expected_t<Outcome>;
private:
auto solve_impl(Database& database, const Request& request) -> expected_t<Outcome>;
};
}
#endif

View File

@ -0,0 +1,39 @@
// Copyright (c) 2024, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef MAMBA_SOLVER_SOLVER_FACTORY_HPP
#define MAMBA_SOLVER_SOLVER_FACTORY_HPP
#include <memory>
#include "mamba/core/context.hpp"
#include "mamba/solver/libsolv/database.hpp"
#include "mamba/solver/libsolv/solver.hpp"
#include "mamba/solver/resolvo/database.hpp"
#include "mamba/solver/resolvo/solver.hpp"
namespace mamba::solver
{
/**
* Create a solver based on the configuration.
*
* @param ctx The context containing the configuration.
* @return A unique pointer to the appropriate solver.
*/
template <typename Database>
auto create_solver(const Context& ctx)
{
if (ctx.use_resolvo_solver)
{
return std::make_unique<resolvo::Solver>();
}
else
{
return std::make_unique<libsolv::Solver>();
}
}
}
#endif

View File

@ -125,7 +125,7 @@ namespace mamba
auto load_channels_impl(
Context& ctx,
ChannelContext& channel_context,
solver::libsolv::Database& database,
std::variant<solver::libsolv::Database, solver::resolvo::Database>& database,
MultiPackageCache& package_caches,
bool is_retry
) -> expected_t<void, mamba_aggregated_error>
@ -196,7 +196,14 @@ namespace mamba
if (!packages.empty())
{
database.add_repo_from_packages(packages, "packages");
if (auto* libsolv_db = std::get_if<solver::libsolv::Database>(&database))
{
libsolv_db->add_repo_from_packages(packages, "packages");
}
else if (auto* resolvo_db = std::get_if<solver::resolvo::Database>(&database))
{
resolvo_db->add_repo_from_packages(packages, "packages");
}
}
expected_t<void> download_res;
@ -243,7 +250,15 @@ namespace mamba
LOG_INFO << "Creating repo from pkgs_dir for offline";
for (const auto& c : ctx.pkgs_dirs)
{
create_repo_from_pkgs_dir(ctx, channel_context, database, c);
if (auto* libsolv_db = std::get_if<solver::libsolv::Database>(&database))
{
create_repo_from_pkgs_dir(ctx, channel_context, *libsolv_db, c);
}
else if (auto* resolvo_db = std::get_if<solver::resolvo::Database>(&database))
{
// TODO: Implement this for resolvo
throw std::runtime_error("Offline mode not supported with resolvo solver yet");
}
}
}
std::string prev_channel;
@ -251,43 +266,41 @@ namespace mamba
for (std::size_t i = 0; i < subdirs.size(); ++i)
{
auto& subdir = subdirs[i];
if (!subdir.valid_cache_found())
auto channel = subdir.channel();
if (channel != prev_channel)
{
if (!ctx.offline && subdir.is_noarch())
prev_channel = channel;
if (i != 0)
{
error_list.push_back(mamba_error(
"Subdir " + subdir.name() + " not loaded!",
mamba_error_code::subdirdata_not_loaded
));
Console::instance().print("\n");
}
continue;
Console::instance().print(fmt::format("{} {}", channel, subdir.platform()));
}
else
{
Console::instance().print(fmt::format(" {}", subdir.platform()));
}
load_subdir_in_database(ctx, database, subdir)
.transform([&](solver::libsolv::RepoInfo&& repo)
{ database.set_repo_priority(repo, priorities[i]); })
.or_else(
[&](const auto&)
{
if (is_retry)
{
std::stringstream ss;
ss << "Could not load repodata.json for " << subdir.name()
<< " after retry." << "Please check repodata source. Exiting."
<< std::endl;
error_list.push_back(
mamba_error(ss.str(), mamba_error_code::repodata_not_loaded)
);
}
else
{
LOG_WARNING << "Could not load repodata.json for " << subdir.name()
<< ". Deleting cache, and retrying.";
subdir.clear_cache_files();
loading_failed = true;
}
}
);
if (auto* libsolv_db = std::get_if<solver::libsolv::Database>(&database))
{
auto exp_repo = load_subdir_in_database(ctx, *libsolv_db, subdir);
if (!exp_repo)
{
error_list.push_back(exp_repo.error());
loading_failed = true;
continue;
}
auto repo = exp_repo.value();
if (i < priorities.size())
{
libsolv_db->set_repo_priority(repo, priorities[i]);
}
}
else if (auto* resolvo_db = std::get_if<solver::resolvo::Database>(&database))
{
// TODO: Implement this for resolvo
throw std::runtime_error("Loading subdirs not supported with resolvo solver yet");
}
}
if (loading_failed)
@ -312,7 +325,7 @@ namespace mamba
auto load_channels(
Context& ctx,
ChannelContext& channel_context,
solver::libsolv::Database& database,
std::variant<solver::libsolv::Database, solver::resolvo::Database>& database,
MultiPackageCache& package_caches
) -> expected_t<void, mamba_aggregated_error>
{

View File

@ -1259,10 +1259,7 @@ namespace mamba
out << YAML::Comment(std::string(54, '#'));
}
void dump_configurable(nl::json& node, const Configurable& c, const std::string& name)
{
c.dump_json(node, name);
}
void dump_configurable(nl::json& node, const Configurable& c, const std::string& name);
}
/********************************
@ -1630,6 +1627,17 @@ namespace mamba
}
));
insert(Configurable("experimental_resolvo_solver", false)
.group("Solver")
.set_rc_configurable()
.set_env_var_names()
.description("Use the experimental resolvo solver instead of libsolv")
.long_description(unindent(R"(
When enabled, use the experimental resolvo solver instead of libsolv.
This is an experimental feature and may not be fully functional.)"))
.set_post_merge_hook<bool>([&](bool& value)
{ m_context.use_resolvo_solver = value; }));
insert(Configurable("explicit_install", false)
.group("Solver")
.description("Use explicit install instead of solving environment"));

View File

@ -23,6 +23,7 @@
#include "mamba/download/downloader.hpp"
#include "mamba/fs/filesystem.hpp"
#include "mamba/solver/libsolv/solver.hpp"
#include "mamba/solver/resolvo/solver.hpp"
#include "mamba/util/path_manip.hpp"
#include "mamba/util/string.hpp"
@ -471,14 +472,22 @@ namespace mamba
LOG_WARNING << "No 'channels' specified";
}
solver::libsolv::Database db{
channel_context.params(),
{
ctx.experimental_matchspec_parsing ? solver::libsolv::MatchSpecParser::Mamba
: solver::libsolv::MatchSpecParser::Libsolv,
},
};
add_spdlog_logger_to_database(db);
std::variant<solver::libsolv::Database, solver::resolvo::Database> db;
if (ctx.use_resolvo_solver)
{
db.emplace<solver::resolvo::Database>();
}
else
{
db.emplace<solver::libsolv::Database>(
channel_context.params(),
{
ctx.experimental_matchspec_parsing ? solver::libsolv::MatchSpecParser::Mamba
: solver::libsolv::MatchSpecParser::Libsolv,
},
);
add_spdlog_logger_to_database(std::get<solver::libsolv::Database>(db));
}
auto exp_load = load_channels(ctx, channel_context, db, package_caches);
if (!exp_load)
@ -495,7 +504,6 @@ namespace mamba
load_installed_packages_in_database(ctx, db, prefix_data);
auto request = create_install_request(prefix_data, raw_specs, freeze_installed);
add_pins_to_request(request, ctx, prefix_data, raw_specs, no_pin, no_py_pin);
request.flags = ctx.solver_flags;
@ -506,20 +514,40 @@ namespace mamba
// Console stream prints on destruction
}
auto outcome = solver::libsolv::Solver()
.solve(
db,
request,
ctx.experimental_matchspec_parsing
? solver::libsolv::MatchSpecParser::Mamba
: solver::libsolv::MatchSpecParser::Mixed
)
.value();
auto outcome =
[&]() -> expected_t<std::variant<solver::Solution, solver::libsolv::UnSolvable>>
{
if (ctx.use_resolvo_solver)
{
auto resolvo_outcome = solver::resolvo::Solver().solve(
std::get<solver::resolvo::Database>(db),
request
);
if (!resolvo_outcome)
{
return make_unexpected(resolvo_outcome.error());
}
return std::get<solver::Solution>(resolvo_outcome.value());
}
else
{
return solver::libsolv::Solver().solve(
std::get<solver::libsolv::Database>(db),
request,
ctx.experimental_matchspec_parsing ? solver::libsolv::MatchSpecParser::Mamba : solver::libsolv::MatchSpecParser::Mixed
);
}
}();
if (auto* unsolvable = std::get_if<solver::libsolv::UnSolvable>(&outcome))
if (!outcome)
{
throw std::runtime_error(outcome.error().what());
}
if (auto* unsolvable = std::get_if<solver::libsolv::UnSolvable>(&outcome.value()))
{
unsolvable->explain_problems_to(
db,
std::get<solver::libsolv::Database>(db),
LOG_ERROR,
{
/* .unavailable= */ ctx.graphics_params.palette.failure,
@ -549,7 +577,9 @@ namespace mamba
if (ctx.output_params.json)
{
Console::instance().json_write(
{ { "success", false }, { "solver_problems", unsolvable->problems(db) } }
{ { "success", false },
{ "solver_problems",
unsolvable->problems(std::get<solver::libsolv::Database>(db)) } }
);
}
throw mamba_error(
@ -579,7 +609,7 @@ namespace mamba
ctx,
database,
request,
std::get<solver::Solution>(outcome),
std::get<solver::Solution>(outcome.value()),
package_caches
);
}(std::move(db));

View File

@ -5,6 +5,7 @@
// The full license is in the file LICENSE, distributed with this software.
#include <string_view>
#include <variant>
#include <fmt/format.h>
#include <solv/evr.h>
@ -138,9 +139,9 @@ namespace mamba
auto load_installed_packages_in_database(
const Context& ctx,
solver::libsolv::Database& database,
std::variant<solver::libsolv::Database, solver::resolvo::Database>& database,
const PrefixData& prefix
) -> solver::libsolv::RepoInfo
) -> expected_t<void>
{
// TODO(C++20): We could do a PrefixData range that returns packages without storing them.
auto pkgs = prefix.sorted_records();
@ -152,12 +153,25 @@ namespace mamba
// Not adding Pip dependency since it might needlessly make the installed/active environment
// broken if pip is not already installed (debatable).
auto repo = database.add_repo_from_packages(
pkgs,
"installed",
solver::libsolv::PipAsPythonDependency::No
);
database.set_installed_repo(repo);
return repo;
if (auto* libsolv_db = std::get_if<solver::libsolv::Database>(&database))
{
auto repo = libsolv_db->add_repo_from_packages(
pkgs,
"installed",
solver::libsolv::PipAsPythonDependency::No
);
libsolv_db->set_installed_repo(repo);
return {};
}
else if (auto* resolvo_db = std::get_if<solver::resolvo::Database>(&database))
{
resolvo_db->add_repo_from_packages(pkgs, "installed");
resolvo_db->set_installed_repo("installed");
return {};
}
else
{
return make_unexpected("Unknown database type", mamba_error_code::unknown);
}
}
}

View File

@ -0,0 +1,676 @@
// Copyright (c) 2024, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#include <simdjson.h>
#include "mamba/core/output.hpp"
#include "mamba/core/util.hpp"
#include "mamba/solver/libsolv/parameters.hpp"
#include "mamba/solver/resolvo/database.hpp"
#include "mamba/specs/channel.hpp"
#include "mamba/specs/package_info.hpp"
#include "mamba/util/string.hpp"
namespace mamba::solver::resolvo
{
namespace
{
// TODO: reuse it from `mamba/solver/libsolv/helpers.cpp`
auto lsplit_track_features(std::string_view features)
{
constexpr auto is_sep = [](char c) -> bool { return (c == ',') || util::is_space(c); };
auto [_, tail] = util::lstrip_if_parts(features, is_sep);
return util::lstrip_if_parts(tail, [&](char c) { return !is_sep(c); });
}
// TODO: factorise with the implementation from `set_solvable` in
// `mamba/solver/libsolv/helpers.cpp`
bool parse_packageinfo_json(
const std::string_view& filename,
const simdjson::dom::element& pkg,
const specs::CondaURL& repo_url,
const std::string& channel_id,
Database& database
)
{
specs::PackageInfo package_info;
package_info.channel = channel_id;
package_info.filename = filename;
package_info.package_url = (repo_url / filename).str(specs::CondaURL::Credentials::Show);
if (auto fn = pkg["fn"].get_string(); !fn.error())
{
package_info.name = fn.value_unsafe();
}
else
{
// Fallback from key entry
package_info.name = filename;
}
if (auto name = pkg["name"].get_string(); !name.error())
{
package_info.name = name.value_unsafe();
}
else
{
LOG_WARNING << R"(Found invalid name in ")" << filename << R"(")";
return false;
}
if (auto version = pkg["version"].get_string(); !version.error())
{
package_info.version = version.value_unsafe();
}
else
{
LOG_WARNING << R"(Found invalid version in ")" << filename << R"(")";
return false;
}
if (auto build_string = pkg["build"].get_string(); !build_string.error())
{
package_info.build_string = build_string.value_unsafe();
}
else
{
LOG_WARNING << R"(Found invalid build in ")" << filename << R"(")";
return false;
}
if (auto build_number = pkg["build_number"].get_uint64(); !build_number.error())
{
package_info.build_number = build_number.value_unsafe();
}
else
{
LOG_WARNING << R"(Found invalid build_number in ")" << filename << R"(")";
return false;
}
if (auto subdir = pkg["subdir"].get_c_str(); !subdir.error())
{
package_info.platform = subdir.value_unsafe();
}
else
{
LOG_WARNING << R"(Found invalid subdir in ")" << filename << R"(")";
}
if (auto size = pkg["size"].get_uint64(); !size.error())
{
package_info.size = size.value_unsafe();
}
if (auto md5 = pkg["md5"].get_c_str(); !md5.error())
{
package_info.md5 = md5.value_unsafe();
}
if (auto sha256 = pkg["sha256"].get_c_str(); !sha256.error())
{
package_info.sha256 = sha256.value_unsafe();
}
if (auto elem = pkg["noarch"]; !elem.error())
{
// TODO: is the following right?
if (auto val = elem.get_bool(); !val.error() && val.value_unsafe())
{
package_info.noarch = specs::NoArchType::No;
}
else if (auto noarch = elem.get_c_str(); !noarch.error())
{
package_info.noarch = specs::NoArchType::No;
}
}
if (auto license = pkg["license"].get_c_str(); !license.error())
{
package_info.license = license.value_unsafe();
}
// TODO conda timestamp are not Unix timestamp.
// Libsolv normalize them this way, we need to do the same here otherwise the current
// package may get arbitrary priority.
if (auto timestamp = pkg["timestamp"].get_uint64(); !timestamp.error())
{
const auto time = timestamp.value_unsafe();
// TODO: reuse it from `mamba/solver/libsolv/helpers.cpp`
constexpr auto MAX_CONDA_TIMESTAMP = 253402300799ULL;
package_info.timestamp = (time > MAX_CONDA_TIMESTAMP) ? (time / 1000) : time;
}
if (auto depends = pkg["depends"].get_array(); !depends.error())
{
for (auto elem : depends)
{
if (auto dep = elem.get_c_str(); !dep.error())
{
package_info.dependencies.emplace_back(dep.value_unsafe());
}
}
}
if (auto constrains = pkg["constrains"].get_array(); !constrains.error())
{
for (auto elem : constrains)
{
if (auto cons = elem.get_c_str(); !cons.error())
{
package_info.constrains.emplace_back(cons.value_unsafe());
}
}
}
if (auto obj = pkg["track_features"]; !obj.error())
{
if (auto track_features_arr = obj.get_array(); !track_features_arr.error())
{
for (auto elem : track_features_arr)
{
if (auto feat = elem.get_string(); !feat.error())
{
package_info.track_features.emplace_back(feat.value_unsafe());
}
}
}
else if (auto track_features_str = obj.get_string(); !track_features_str.error())
{
auto splits = lsplit_track_features(track_features_str.value_unsafe());
while (!splits[0].empty())
{
package_info.track_features.emplace_back(splits[0]);
splits = lsplit_track_features(splits[1]);
}
}
}
database.alloc_solvable(package_info);
return true;
}
}
Database::Database()
: name_pool(Mapping<::resolvo::NameId, ::resolvo::String>())
{
}
void Database::add_repo_from_repodata_json(
const fs::u8path& filename,
const std::string& repo_url,
const std::string& channel_id,
bool verify_artifacts
)
{
auto parser = simdjson::dom::parser();
const auto lock = LockFile(filename);
const auto repodata = parser.load(filename);
// An override for missing package subdir is found at the top level
auto default_subdir = std::string();
if (auto subdir = repodata.at_pointer("/info/subdir").get_string(); !subdir.error())
{
default_subdir = std::string(subdir.value_unsafe());
}
// Get `base_url` in case 'repodata_version': 2
// cf. https://github.com/conda-incubator/ceps/blob/main/cep-15.md
auto base_url = repo_url;
if (auto repodata_version = repodata["repodata_version"].get_int64();
!repodata_version.error())
{
if (repodata_version.value_unsafe() == 2)
{
if (auto url = repodata.at_pointer("/info/base_url").get_string(); !url.error())
{
base_url = std::string(url.value_unsafe());
}
}
}
const auto parsed_url = specs::CondaURL::parse(base_url)
.or_else([](specs::ParseError&& err) { throw std::move(err); })
.value();
auto signatures = std::optional<simdjson::dom::object>(std::nullopt);
if (auto maybe_sigs = repodata["signatures"].get_object();
!maybe_sigs.error() && verify_artifacts)
{
signatures = std::move(maybe_sigs).value();
}
auto added = util::flat_set<std::string_view>();
if (auto pkgs = repodata["packages.conda"].get_object(); !pkgs.error())
{
for (auto [key, value] : pkgs.value())
{
parse_packageinfo_json(key, value, parsed_url, channel_id, *this);
}
}
if (auto pkgs = repodata["packages"].get_object(); !pkgs.error())
{
for (auto [key, value] : pkgs.value())
{
parse_packageinfo_json(key, value, parsed_url, channel_id, *this);
}
}
}
void Database::add_repo_from_packages(
const std::vector<specs::PackageInfo>& packages,
const std::string& repo_name,
bool pip_as_python_dependency
)
{
for (const auto& package : packages)
{
alloc_solvable(package);
}
}
void Database::set_installed_repo(const std::string& repo_name)
{
// TODO: Implement this
}
::resolvo::String Database::display_solvable(::resolvo::SolvableId solvable)
{
const specs::PackageInfo& package_info = solvable_pool[solvable];
return ::resolvo::String{ package_info.long_str() };
}
::resolvo::String Database::display_solvable_name(::resolvo::SolvableId solvable)
{
const specs::PackageInfo& package_info = solvable_pool[solvable];
return ::resolvo::String{ package_info.name };
}
::resolvo::String
Database::display_merged_solvables(::resolvo::Slice<::resolvo::SolvableId> solvable)
{
std::string result;
for (auto& solvable_id : solvable)
{
result += solvable_pool[solvable_id].long_str();
}
return ::resolvo::String{ result };
}
::resolvo::String Database::display_name(::resolvo::NameId name)
{
return name_pool[name];
}
::resolvo::String Database::display_version_set(::resolvo::VersionSetId version_set)
{
const specs::MatchSpec match_spec = version_set_pool[version_set];
return ::resolvo::String{ match_spec.str() };
}
::resolvo::String Database::display_string(::resolvo::StringId string)
{
return string_pool[string];
}
::resolvo::NameId Database::version_set_name(::resolvo::VersionSetId version_set_id)
{
const specs::MatchSpec match_spec = version_set_pool[version_set_id];
return name_pool[::resolvo::String{ match_spec.name().str() }];
}
::resolvo::NameId Database::solvable_name(::resolvo::SolvableId solvable_id)
{
const specs::PackageInfo& package_info = solvable_pool[solvable_id];
return name_pool[::resolvo::String{ package_info.name }];
}
::resolvo::Candidates Database::get_candidates(::resolvo::NameId package)
{
::resolvo::Candidates candidates{};
candidates.favored = nullptr;
candidates.locked = nullptr;
candidates.candidates = name_to_solvable[package];
return candidates;
}
void Database::sort_candidates(::resolvo::Slice<::resolvo::SolvableId> solvables)
{
std::sort(
solvables.begin(),
solvables.end(),
[&](const ::resolvo::SolvableId& a, const ::resolvo::SolvableId& b)
{
const specs::PackageInfo& package_info_a = solvable_pool[a];
const specs::PackageInfo& package_info_b = solvable_pool[b];
// If track features are present, prefer the solvable having the least of them.
if (package_info_a.track_features.size() != package_info_b.track_features.size())
{
return package_info_a.track_features.size()
< package_info_b.track_features.size();
}
const auto a_version = specs::Version::parse(package_info_a.version).value();
const auto b_version = specs::Version::parse(package_info_b.version).value();
if (a_version != b_version)
{
return a_version > b_version;
}
if (package_info_a.build_number != package_info_b.build_number)
{
return package_info_a.build_number > package_info_b.build_number;
}
// Compare the dependencies of the variants.
std::unordered_map<::resolvo::NameId, ::resolvo::VersionSetId> a_deps;
std::unordered_map<::resolvo::NameId, ::resolvo::VersionSetId> b_deps;
for (auto dep_a : package_info_a.dependencies)
{
// TODO: have a VersionID to NameID mapping instead
specs::MatchSpec ms = specs::MatchSpec::parse(dep_a).value();
const std::string& name = ms.name().str();
auto name_id = name_pool.alloc(::resolvo::String{ name });
a_deps[name_id] = version_set_pool[ms];
}
for (auto dep_b : package_info_b.dependencies)
{
// TODO: have a VersionID to NameID mapping instead
specs::MatchSpec ms = specs::MatchSpec::parse(dep_b).value();
const std::string& name = ms.name().str();
auto name_id = name_pool.alloc(::resolvo::String{ name });
b_deps[name_id] = version_set_pool[ms];
}
auto ordering_score = 0;
for (auto [name_id, version_set_id] : a_deps)
{
if (b_deps.find(name_id) != b_deps.end())
{
auto [a_tf_version, a_n_track_features] = find_highest_version(version_set_id);
auto [b_tf_version, b_n_track_features] = find_highest_version(b_deps[name_id]
);
// Favor the solvable with higher versions of their dependencies
if (a_tf_version != b_tf_version)
{
ordering_score += a_tf_version > b_tf_version ? 1 : -1;
}
// Highly penalize the solvable if a dependencies has more track features
if (a_n_track_features != b_n_track_features)
{
ordering_score += a_n_track_features > b_n_track_features ? -100 : 100;
}
}
}
if (ordering_score != 0)
{
return ordering_score > 0;
}
return package_info_a.timestamp > package_info_b.timestamp;
}
);
}
::resolvo::Vector<::resolvo::SolvableId> Database::filter_candidates(
::resolvo::Slice<::resolvo::SolvableId> candidates,
::resolvo::VersionSetId version_set_id,
bool inverse
)
{
specs::MatchSpec match_spec = version_set_pool[version_set_id];
::resolvo::Vector<::resolvo::SolvableId> filtered;
if (inverse)
{
for (auto& solvable_id : candidates)
{
const specs::PackageInfo& package_info = solvable_pool[solvable_id];
// Is it an appropriate check? Or must another one be crafted?
if (!match_spec.contains_except_channel(package_info))
{
filtered.push_back(solvable_id);
}
}
}
else
{
for (auto& solvable_id : candidates)
{
const specs::PackageInfo& package_info = solvable_pool[solvable_id];
// Is it an appropriate check? Or must another one be crafted?
if (match_spec.contains_except_channel(package_info))
{
filtered.push_back(solvable_id);
}
}
}
return filtered;
}
::resolvo::Dependencies Database::get_dependencies(::resolvo::SolvableId solvable_id)
{
const specs::PackageInfo& package_info = solvable_pool[solvable_id];
::resolvo::Dependencies dependencies;
// TODO: do this in O(1)
for (auto& dep : package_info.dependencies)
{
const specs::MatchSpec match_spec = specs::MatchSpec::parse(dep).value();
dependencies.requirements.push_back(version_set_pool[match_spec]);
}
for (auto& constr : package_info.constrains)
{
// if constr contain " == " replace it with "=="
std::string constr2 = constr;
while (constr2.find(" == ") != std::string::npos)
{
constr2 = constr2.replace(constr2.find(" == "), 4, "==");
}
while (constr2.find(" >= ") != std::string::npos)
{
constr2 = constr2.replace(constr2.find(" >= "), 4, ">=");
}
const specs::MatchSpec match_spec = specs::MatchSpec::parse(constr2).value();
dependencies.constrains.push_back(version_set_pool[match_spec]);
}
return dependencies;
}
::resolvo::VersionSetId Database::alloc_version_set(std::string_view raw_match_spec)
{
std::string raw_match_spec_str = std::string(raw_match_spec);
// Replace all " v" with simply " " to work around the `v` prefix in some version strings
// e.g. `mingw-w64-ucrt-x86_64-crt-git v12.0.0.r2.ggc561118da h707e725_0` in
// `inform2w64-sysroot_win-64-v12.0.0.r2.ggc561118da-h707e725_0.conda` while
// (raw_match_spec_str.find(" v") != std::string::npos)
{
raw_match_spec_str = raw_match_spec_str.replace(raw_match_spec_str.find(" v"), 2, " ");
}
// Remove any presence of selector on python version in the match spec
// e.g. `pillow-heif >=0.10.0,<1.0.0<py312` -> `pillow-heif >=0.10.0,<1.0.0` in
// `infowillow-1.6.3-pyhd8ed1ab_0.conda`
for (const auto specifier : { "=py", "<py", ">py", ">=py", "<=py", "!=py" })
{
while (raw_match_spec_str.find(specifier) != std::string::npos)
{
raw_match_spec_str = raw_match_spec_str.substr(0, raw_match_spec_str.find(specifier));
}
}
// Remove any white space between version
// e.g. `kytea >=0.1.4, 0.2.0` -> `kytea >=0.1.4,0.2.0` in
// `infokonoha-4.6.3-pyhd8ed1ab_0.tar.bz2`
while (raw_match_spec_str.find(", ") != std::string::npos)
{
raw_match_spec_str = raw_match_spec_str.replace(raw_match_spec_str.find(", "), 2, ",");
}
// TODO: skip allocation for now if "*.*" is in the match spec
if (raw_match_spec_str.find("*.*") != std::string::npos)
{
return ::resolvo::VersionSetId{ 0 };
}
// NOTE: works around `openblas 0.2.18|0.2.18.*.` from
// `dlib==19.0=np110py27_blas_openblas_200` If contains "|", split on it and recurse
if (raw_match_spec_str.find("|") != std::string::npos)
{
std::vector<std::string> match_specs;
std::string match_spec;
for (char c : raw_match_spec_str)
{
if (c == '|')
{
match_specs.push_back(match_spec);
match_spec.clear();
}
else
{
match_spec += c;
}
}
match_specs.push_back(match_spec);
std::vector<::resolvo::VersionSetId> version_sets;
for (const std::string& ms : match_specs)
{
alloc_version_set(ms);
}
// Placeholder return value
return ::resolvo::VersionSetId{ 0 };
}
// NOTE: This works around some improperly encoded `constrains` in the test data, e.g.:
// `openmpi-4.1.4-ha1ae619_102`'s improperly encoded `constrains`: "cudatoolkit
// >= 10.2" `pytorch-1.13.0-cpu_py310h02c325b_0.conda`'s improperly encoded
// `constrains`: "pytorch-cpu = 1.13.0", "pytorch-gpu = 99999999"
// `fipy-3.4.2.1-py310hff52083_3.tar.bz2`'s improperly encoded `constrains` or `dep`:
// ">=4.5.2"
// Remove any with space after the binary operators
for (const std::string& op : { ">=", "<=", "==", ">", "<", "!=", "=", "==" })
{
const std::string& bad_op = op + " ";
while (raw_match_spec_str.find(bad_op) != std::string::npos)
{
raw_match_spec_str = raw_match_spec_str.substr(0, raw_match_spec_str.find(bad_op)) + op
+ raw_match_spec_str.substr(
raw_match_spec_str.find(bad_op) + bad_op.size()
);
}
// If start with binary operator, prepend NONE
if (raw_match_spec_str.find(op) == 0)
{
raw_match_spec_str = "NONE " + raw_match_spec_str;
}
}
const specs::MatchSpec match_spec = specs::MatchSpec::parse(raw_match_spec_str).value();
// Add the version set to the version set pool
auto id = version_set_pool.alloc(match_spec);
// Add name to the Name and String pools
const std::string name = match_spec.name().str();
name_pool.alloc(::resolvo::String{ name });
string_pool.alloc(::resolvo::String{ name });
// Add the MatchSpec's string representation to the Name and String pools
const std::string match_spec_str = match_spec.str();
name_pool.alloc(::resolvo::String{ match_spec_str });
string_pool.alloc(::resolvo::String{ match_spec_str });
return id;
}
::resolvo::SolvableId Database::alloc_solvable(specs::PackageInfo package_info)
{
// Add the solvable to the solvable pool
auto id = solvable_pool.alloc(package_info);
// Add name to the Name and String pools
const std::string name = package_info.name;
name_pool.alloc(::resolvo::String{ name });
string_pool.alloc(::resolvo::String{ name });
// Add the long string representation of the package to the Name and String pools
const std::string long_str = package_info.long_str();
name_pool.alloc(::resolvo::String{ long_str });
string_pool.alloc(::resolvo::String{ long_str });
for (auto& dep : package_info.dependencies)
{
alloc_version_set(dep);
}
for (auto& constr : package_info.constrains)
{
alloc_version_set(constr);
}
// Add the solvable to the name_to_solvable map
const ::resolvo::NameId name_id = name_pool.alloc(::resolvo::String{ package_info.name });
name_to_solvable[name_id].push_back(id);
return id;
}
std::pair<specs::Version, size_t>
Database::find_highest_version(::resolvo::VersionSetId version_set_id)
{
// If the version set has already been computed, return it.
if (version_set_to_max_version_and_track_features_numbers.find(version_set_id)
!= version_set_to_max_version_and_track_features_numbers.end())
{
return version_set_to_max_version_and_track_features_numbers[version_set_id];
}
const specs::MatchSpec match_spec = version_set_pool[version_set_id];
const std::string& name = match_spec.name().str();
auto name_id = name_pool.alloc(::resolvo::String{ name });
auto solvables = name_to_solvable[name_id];
auto filtered = filter_candidates(solvables, version_set_id, false);
specs::Version max_version = specs::Version();
size_t max_version_n_track_features = 0;
for (auto& solvable_id : filtered)
{
const specs::PackageInfo& package_info = solvable_pool[solvable_id];
const auto version = specs::Version::parse(package_info.version).value();
if (version == max_version)
{
max_version_n_track_features = std::min(
max_version_n_track_features,
package_info.track_features.size()
);
}
if (version > max_version)
{
max_version = version;
max_version_n_track_features = package_info.track_features.size();
}
}
auto val = std::make_pair(max_version, max_version_n_track_features);
version_set_to_max_version_and_track_features_numbers[version_set_id] = val;
return val;
}
} // namespace mamba::solver::resolvo

View File

@ -0,0 +1,164 @@
// Copyright (c) 2024, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#include "mamba/solver/resolvo/database.hpp"
#include "mamba/solver/resolvo/solver.hpp"
#include "mamba/util/variant_cmp.hpp"
namespace mamba::solver::resolvo
{
namespace
{
/**
* An arbitrary comparison function to get determinist output.
*/
auto make_request_cmp()
{
return util::make_variant_cmp(
/** index_cmp= */
[](auto lhs, auto rhs) { return lhs < rhs; },
/** alternative_cmp= */
[](const auto& lhs, const auto& rhs)
{
using Itm = std::decay_t<decltype(lhs)>;
if constexpr (!std::is_same_v<Itm, Request::UpdateAll>)
{
return lhs.spec.name().str() < rhs.spec.name().str();
}
return false;
}
);
}
auto request_to_requirements(const Request& request, Database& database)
-> std::vector<::resolvo::VersionSetId>
{
std::vector<::resolvo::VersionSetId> requirements;
requirements.reserve(request.jobs.size());
for (const auto& job : request.jobs)
{
std::visit(
[&](const auto& j)
{
using T = std::decay_t<decltype(j)>;
if constexpr (std::is_same_v<T, Request::Install>)
{
requirements.push_back(database.alloc_version_set(j.spec.name().str()));
}
},
job
);
}
return requirements;
}
auto request_to_constraints(const Request& request, Database& database)
-> std::vector<::resolvo::VersionSetId>
{
std::vector<::resolvo::VersionSetId> constraints;
constraints.reserve(request.jobs.size());
for (const auto& job : request.jobs)
{
std::visit(
[&](const auto& j)
{
using T = std::decay_t<decltype(j)>;
if constexpr (std::is_same_v<T, Request::Remove>)
{
constraints.push_back(database.alloc_version_set(j.spec.name().str()));
}
},
job
);
}
return constraints;
}
auto
result_to_solution(const ::resolvo::Vector<::resolvo::SolvableId>& result, Database& database, const Request&)
-> Solution
{
Solution solution;
solution.actions.reserve(result.size());
for (const auto& solvable_id : result)
{
const auto& solvable = database.solvable_pool[solvable_id];
specs::PackageInfo pkg;
pkg.name = solvable.name;
pkg.version = solvable.version;
pkg.build_string = solvable.build_string;
pkg.build_number = solvable.build_number;
pkg.channel = solvable.channel;
pkg.md5 = solvable.md5;
pkg.sha256 = solvable.sha256;
pkg.track_features = solvable.track_features;
pkg.dependencies = solvable.dependencies;
pkg.constrains = solvable.constrains;
pkg.timestamp = solvable.timestamp;
pkg.license = solvable.license;
pkg.size = solvable.size;
solution.actions.emplace_back(Solution::Install{ std::move(pkg) });
}
return solution;
}
}
auto Solver::solve_impl(Database& database, const Request& request) -> expected_t<Outcome>
{
auto requirements = request_to_requirements(request, database);
auto constraints = request_to_constraints(request, database);
::resolvo::Vector<::resolvo::SolvableId> result;
::resolvo::Vector<::resolvo::VersionSetId> req_vec;
for (const auto& req : requirements)
{
req_vec.push_back(req);
}
::resolvo::Vector<::resolvo::VersionSetId> constr_vec;
for (const auto& constr : constraints)
{
constr_vec.push_back(constr);
}
::resolvo::String reason = ::resolvo::solve(database, req_vec, constr_vec, result);
if (reason != "")
{
// Get the length from a string view of the reason
std::string_view reason_str_view = reason;
std::string reason_str(reason.data(), reason_str_view.size());
return make_unexpected(mamba_error("resolvo", reason_str));
}
return Outcome{ result_to_solution(result, database, request) };
}
auto Solver::solve(Database& database, Request&& request) -> expected_t<Outcome>
{
if (request.flags.order_request)
{
std::sort(request.jobs.begin(), request.jobs.end(), make_request_cmp());
}
return solve_impl(database, request);
}
auto Solver::solve(Database& database, const Request& request) -> expected_t<Outcome>
{
if (request.flags.order_request)
{
auto sorted_request = request;
std::sort(sorted_request.jobs.begin(), sorted_request.jobs.end(), make_request_cmp());
return solve_impl(database, sorted_request);
}
return solve_impl(database, request);
}
}