From 5f18efa0abb33b6ae553a265fe2d5b47d0ef8fe0 Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Thu, 7 May 2026 14:28:43 +0200 Subject: [PATCH 01/20] feat: Introduce `resolvo` as a solver Signed-off-by: Julien Jerphanion --- dev/environment-dev.yml | 1 + dev/environment-micromamba-static.yml | 1 + libmamba/CMakeLists.txt | 12 + libmamba/include/mamba/core/context.hpp | 1 + libmamba/include/mamba/solver/database.hpp | 62 + .../include/mamba/solver/database_utils.hpp | 24 + .../include/mamba/solver/libsolv/database.hpp | 14 + .../include/mamba/solver/resolvo/database.hpp | 260 +++ .../include/mamba/solver/resolvo/solver.hpp | 32 + .../include/mamba/solver/solver_factory.hpp | 45 + libmamba/include/mamba/specs/package_info.hpp | 9 + libmamba/src/solver/resolvo/database.cpp | 699 +++++++ libmamba/src/solver/resolvo/solver.cpp | 182 ++ libmamba/src/specs/package_info.cpp | 144 ++ .../tests/src/solver/resolvo/test_solver.cpp | 1628 +++++++++++++++++ 15 files changed, 3114 insertions(+) create mode 100644 libmamba/include/mamba/solver/database.hpp create mode 100644 libmamba/include/mamba/solver/database_utils.hpp create mode 100644 libmamba/include/mamba/solver/resolvo/database.hpp create mode 100644 libmamba/include/mamba/solver/resolvo/solver.hpp create mode 100644 libmamba/include/mamba/solver/solver_factory.hpp create mode 100644 libmamba/src/solver/resolvo/database.cpp create mode 100644 libmamba/src/solver/resolvo/solver.cpp create mode 100644 libmamba/tests/src/solver/resolvo/test_solver.cpp diff --git a/dev/environment-dev.yml b/dev/environment-dev.yml index 3fe56f778b..f661c484a4 100644 --- a/dev/environment-dev.yml +++ b/dev/environment-dev.yml @@ -17,6 +17,7 @@ dependencies: - libarchive>=3.8 lgpl_* - libcurl >=7.86 - libsodium + - resolvo-cpp==0.1.0 - libsolv >=0.7.18 - libmsgpack-c - nlohmann_json diff --git a/dev/environment-micromamba-static.yml b/dev/environment-micromamba-static.yml index 703333834d..f4a7031a51 100644 --- a/dev/environment-micromamba-static.yml +++ b/dev/environment-micromamba-static.yml @@ -18,6 +18,7 @@ dependencies: - simdjson-static >=3.3.0 - spdlog >=1.16.0 - fmt >=11.1.0 + - resolvo-cpp==0.1.0 - libsolv-static >=0.7.24 - yaml-cpp-static >=0.8.0 - reproc-static >=14.2.4.post0 diff --git a/libmamba/CMakeLists.txt b/libmamba/CMakeLists.txt index 2f10878112..2d6d4d06e9 100644 --- a/libmamba/CMakeLists.txt +++ b/libmamba/CMakeLists.txt @@ -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 @@ -355,12 +358,18 @@ set( ${LIBMAMBA_INCLUDE_DIR}/mamba/solver/problems_graph.hpp ${LIBMAMBA_INCLUDE_DIR}/mamba/solver/request.hpp ${LIBMAMBA_INCLUDE_DIR}/mamba/solver/solution.hpp + ${LIBMAMBA_INCLUDE_DIR}/mamba/solver/database.hpp + ${LIBMAMBA_INCLUDE_DIR}/mamba/solver/database_utils.hpp + ${LIBMAMBA_INCLUDE_DIR}/mamba/solver/solver_factory.hpp # Solver libsolv implementation ${LIBMAMBA_INCLUDE_DIR}/mamba/solver/libsolv/database.hpp ${LIBMAMBA_INCLUDE_DIR}/mamba/solver/libsolv/parameters.hpp ${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 @@ -447,6 +456,7 @@ find_package(yaml-cpp CONFIG REQUIRED) find_package(reproc CONFIG REQUIRED) find_package(reproc++ CONFIG REQUIRED) find_package(Libsolv MODULE REQUIRED) +find_package(Resolvo CONFIG REQUIRED) find_package(msgpack-c CONFIG REQUIRED) add_subdirectory(ext/solv-cpp) @@ -504,6 +514,7 @@ macro(libmamba_create_target target_name linkage output_name) solv::libsolv_static solv::libsolvext_static solv::cpp + Resolvo::Resolvo ) if(UNIX) @@ -688,6 +699,7 @@ macro(libmamba_create_target target_name linkage output_name) solv::libsolv solv::libsolvext solv::cpp + Resolvo::Resolvo ) # CMake 3.17 provides a LibArchive::LibArchive target that could be used instead of # LIBRARIES/INCLUDE_DIRS diff --git a/libmamba/include/mamba/core/context.hpp b/libmamba/include/mamba/core/context.hpp index 71160c2e1f..f0f1450106 100644 --- a/libmamba/include/mamba/core/context.hpp +++ b/libmamba/include/mamba/core/context.hpp @@ -103,6 +103,7 @@ namespace mamba // Configurable bool mamba_repodata_parsing = true; bool experimental_matchspec_parsing = false; + bool experimental_resolvo_solver = false; bool debug = false; bool use_uv = false; diff --git a/libmamba/include/mamba/solver/database.hpp b/libmamba/include/mamba/solver/database.hpp new file mode 100644 index 0000000000..6613debc80 --- /dev/null +++ b/libmamba/include/mamba/solver/database.hpp @@ -0,0 +1,62 @@ +// Copyright (c) 2026, 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 +#include +#include + +#include "mamba/fs/filesystem.hpp" +#include "mamba/specs/channel.hpp" +#include "mamba/specs/match_spec.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& packages, + const std::string& repo_name, + bool pip_as_python_dependency = false + ) = 0; + + virtual void set_installed_repo(const std::string& repo_name) = 0; + + virtual bool has_package(const specs::MatchSpec& spec) = 0; + }; + + namespace libsolv + { + class Database; + } + + namespace resolvo + { + class Database; + } + + using DatabaseVariant = std::variant; + + // Remove or comment out the inline database_has_package function if DatabaseVariant is not + // visible or causes errors inline auto database_has_package(DatabaseVariant& database, const + // specs::MatchSpec& spec) -> bool; +} + +#endif // MAMBA_SOLVER_DATABASE_HPP diff --git a/libmamba/include/mamba/solver/database_utils.hpp b/libmamba/include/mamba/solver/database_utils.hpp new file mode 100644 index 0000000000..4a7b9986db --- /dev/null +++ b/libmamba/include/mamba/solver/database_utils.hpp @@ -0,0 +1,24 @@ +#ifndef MAMBA_SOLVER_DATABASE_UTILS_HPP +#define MAMBA_SOLVER_DATABASE_UTILS_HPP + +#include + +#include "mamba/solver/database.hpp" + +namespace mamba::solver +{ + inline bool database_has_package(DatabaseVariant& database, const specs::MatchSpec& spec) + { + if (auto* libsolv_db = std::get_if(&database)) + { + return libsolv_db->has_package(spec); + } + else if (auto* resolvo_db = std::get_if(&database)) + { + return resolvo_db->has_package(spec); + } + throw std::runtime_error("Invalid database variant"); + } +} + +#endif // MAMBA_SOLVER_DATABASE_UTILS_HPP diff --git a/libmamba/include/mamba/solver/libsolv/database.hpp b/libmamba/include/mamba/solver/libsolv/database.hpp index 41b828a07e..a1b3066b08 100644 --- a/libmamba/include/mamba/solver/libsolv/database.hpp +++ b/libmamba/include/mamba/solver/libsolv/database.hpp @@ -164,6 +164,20 @@ namespace mamba::solver::libsolv template void for_each_package_depending_on(const specs::MatchSpec& ms, Func&&); + bool has_package(const specs::MatchSpec& spec) + { + bool found = false; + for_each_package_matching( + spec, + [&](const auto&) + { + found = true; + return util::LoopControl::Break; + } + ); + return found; + } + /** * An access control wrapper. * diff --git a/libmamba/include/mamba/solver/resolvo/database.hpp b/libmamba/include/mamba/solver/resolvo/database.hpp new file mode 100644 index 0000000000..256b0dd4f8 --- /dev/null +++ b/libmamba/include/mamba/solver/resolvo/database.hpp @@ -0,0 +1,260 @@ +// Copyright (c) 2026, 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 +#include +#include + +#include +#include +#include + +#include "mamba/solver/database.hpp" +#include "mamba/solver/libsolv/parameters.hpp" +#include "mamba/solver/libsolv/repo_info.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(id.id); + } + }; + + template <> + struct hash<::resolvo::VersionSetId> + { + size_t operator()(const ::resolvo::VersionSetId& id) const noexcept + { + return static_cast(id.id); + } + }; + + template <> + struct hash<::resolvo::SolvableId> + { + size_t operator()(const ::resolvo::SolvableId& id) const noexcept + { + return static_cast(id.id); + } + }; + + template <> + struct hash<::resolvo::StringId> + { + size_t operator()(const ::resolvo::StringId& id) const noexcept + { + return static_cast(id.id); + } + }; + + template <> + struct hash<::resolvo::VersionSetUnionId> + { + size_t operator()(const ::resolvo::VersionSetUnionId& id) const noexcept + { + return static_cast(id.id); + } + }; + + template <> + struct hash<::resolvo::ConditionId> + { + size_t operator()(const ::resolvo::ConditionId& id) const noexcept + { + return static_cast(id.id); + } + }; +} + +namespace mamba::solver::resolvo +{ + // Create a template Pool class that maps a key to a set of values + template + struct bijective_map + { + /** + * Adds the value to the bijective_map and returns its associated id. If the + * value is already in the bijective_map, 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(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 bijective_map + auto begin_values() const + { + return id_to_value.begin(); + } + + auto end_values() const + { + return id_to_value.end(); + } + + auto cbegin_values() const + { + return id_to_value.cbegin(); + } + + auto cend_values() const + { + return id_to_value.cend(); + } + + auto find(T value) const + { + return value_to_id.find(value); + } + + auto begin_keys() const + { + return value_to_id.begin(); + } + + auto end_keys() const + { + return value_to_id.end(); + } + + auto cbegin_keys() const + { + return value_to_id.cbegin(); + } + + auto cend_keys() const + { + return value_to_id.cend(); + } + + auto size() const + { + return id_to_value.size(); + } + + private: + + std::unordered_map value_to_id; + std::unordered_map id_to_value; + }; + + class Database final + : public mamba::solver::Database + , public ::resolvo::DependencyProvider + { + public: + + explicit Database(specs::ChannelResolveParams channel_params); + ~Database() override = default; + + [[nodiscard]] auto channel_params() const -> const specs::ChannelResolveParams&; + + // 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& 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::Slice<::resolvo::VersionSetId> + version_sets_in_union(::resolvo::VersionSetUnionId version_set_union_id) override; + ::resolvo::Condition resolve_condition(::resolvo::ConditionId condition) 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 + find_highest_version(::resolvo::VersionSetId version_set_id); + + // Pools for mapping between resolvo IDs and mamba types + bijective_map<::resolvo::NameId, ::resolvo::String> name_pool; + bijective_map<::resolvo::StringId, ::resolvo::String> string_pool; + bijective_map<::resolvo::VersionSetId, specs::MatchSpec> version_set_pool; + bijective_map<::resolvo::SolvableId, specs::PackageInfo> solvable_pool; + + bool has_package(const specs::MatchSpec& spec) + { + auto candidates = get_candidates( + name_pool.alloc(::resolvo::String(spec.name().to_string())) + ); + return !candidates.candidates.empty(); + } + + private: + + // Maps for quick lookups + std::unordered_map<::resolvo::NameId, ::resolvo::Vector<::resolvo::SolvableId>> name_to_solvable; + std::unordered_map<::resolvo::VersionSetUnionId, ::resolvo::Vector<::resolvo::VersionSetId>> + version_set_unions; + std::unordered_map<::resolvo::ConditionId, ::resolvo::Condition> conditions; + std::unordered_map<::resolvo::VersionSetId, std::pair> + version_set_to_max_version_and_track_features_numbers; + + specs::ChannelResolveParams m_channel_params; + }; +} + +#endif // MAMBA_SOLVER_RESOLVO_DATABASE_HPP diff --git a/libmamba/include/mamba/solver/resolvo/solver.hpp b/libmamba/include/mamba/solver/resolvo/solver.hpp new file mode 100644 index 0000000000..f57c46b880 --- /dev/null +++ b/libmamba/include/mamba/solver/resolvo/solver.hpp @@ -0,0 +1,32 @@ +// Copyright (c) 2026, 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; + + [[nodiscard]] auto solve(Database& database, Request&& request) -> expected_t; + [[nodiscard]] auto solve(Database& database, const Request& request) -> expected_t; + + private: + + auto solve_impl(Database& database, const Request& request) -> expected_t; + }; +} +#endif diff --git a/libmamba/include/mamba/solver/solver_factory.hpp b/libmamba/include/mamba/solver/solver_factory.hpp new file mode 100644 index 0000000000..95ae025234 --- /dev/null +++ b/libmamba/include/mamba/solver/solver_factory.hpp @@ -0,0 +1,45 @@ +// Copyright (c) 2026, 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 +#include + +#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 +{ + /** + * Type alias for the database variant that can hold either libsolv or resolvo database. + */ + using DatabaseVariant = std::variant; + + /** + * Create a solver based on the configuration. + * + * @param ctx The context containing the configuration. + * @return A unique pointer to the appropriate solver. + */ + template + auto create_solver(const Context& ctx) + { + if (ctx.experimental_resolvo_solver) + { + return std::make_unique(); + } + else + { + return std::make_unique(); + } + } +} +#endif diff --git a/libmamba/include/mamba/specs/package_info.hpp b/libmamba/include/mamba/specs/package_info.hpp index 34bb98bc5e..f5cb25a98b 100644 --- a/libmamba/include/mamba/specs/package_info.hpp +++ b/libmamba/include/mamba/specs/package_info.hpp @@ -12,6 +12,7 @@ #include #include +#include #include "mamba/specs/error.hpp" #include "mamba/specs/platform.hpp" @@ -19,6 +20,8 @@ namespace mamba::specs { + class CondaURL; + namespace defaulted_key { inline constexpr std::string_view initialized = "_initialized"; @@ -123,6 +126,12 @@ namespace mamba::specs PackageType package_type = PackageType::Unknown; [[nodiscard]] static auto from_url(std::string_view url) -> expected_parse_t; + [[nodiscard]] static auto from_json( + const std::string_view& filename, + simdjson::ondemand::object& pkg, + const CondaURL& repo_url, + const std::string& channel_id + ) -> expected_parse_t; [[nodiscard]] auto url_for_channel(std::string_view channel_mirror_url) const -> std::string; [[nodiscard]] auto url_for_channel_platform(std::string_view channel_mirror_platform_url) const -> std::string; diff --git a/libmamba/src/solver/resolvo/database.cpp b/libmamba/src/solver/resolvo/database.cpp new file mode 100644 index 0000000000..ec727b77c6 --- /dev/null +++ b/libmamba/src/solver/resolvo/database.cpp @@ -0,0 +1,699 @@ +// Copyright (c) 2026, 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 + +#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 +{ + + Database::Database(specs::ChannelResolveParams channel_params) + : name_pool(bijective_map<::resolvo::NameId, ::resolvo::String>()) + , m_channel_params(std::move(channel_params)) + { + } + + auto Database::channel_params() const -> const specs::ChannelResolveParams& + { + return m_channel_params; + } + + void Database::add_repo_from_repodata_json( + const fs::u8path& filename, + const std::string& repo_url, + const std::string& channel_id, + [[maybe_unused]] bool verify_artifacts + ) + { + // BEWARE: + // We use below `simdjson`'s "on-demand" parser, which does not tolerate reading the same + // value more than once. This means we need to make sure that the objects and their fields + // are read and/or concretized only once and if we need to use them more than once we need + // to persist them in local memory. This is why the code below tries hard to pre-read the + // data needed in several parts of the computing in a way that prevents jumping up and down + // the hierarchy of json objects. When this rule is not followed, the parsing might end + // earlier than expected or might skip data that are read when they shouldn't be, leading to + // *runtime issues* that might not be visible at first. Because of these reasons, be careful + // when modifying the following parsing code. + + auto parser = simdjson::ondemand::parser(); + const auto lock = LockFile(filename); + + // The json storage must be kept alive as long as we are reading the json data. + const auto json_content = simdjson::padded_string::load(filename.string()); + + // Note that with the "on-demand" parser, documents/values/objects act as iterators + // to go through the document. + auto repodata_doc = parser.iterate(json_content); + + const auto repodata_version = [&] + { + if (auto version = repodata_doc["repodata_version"].get_int64(); !version.error()) + { + return version.value(); + } + else + { + return std::int64_t{ 1 }; + } + }(); + + auto repodata_info = [&] + { + if (auto value = repodata_doc["info"]; !value.error()) + { + if (auto object = value.get_object(); !object.error()) + { + return std::make_optional(object); + } + } + return decltype(std::make_optional(repodata_doc["info"].get_object())){}; + }(); + + // An override for missing package subdir could be found at the top level + const auto default_subdir = [&] + { + if (repodata_info) + { + if (auto subdir = repodata_info.value()["subdir"]; !subdir.error()) + { + return std::string(subdir.get_string().value_unsafe()); + } + } + + return std::string{}; + }(); + + // Get `base_url` in case 'repodata_version': 2 + // cf. https://github.com/conda-incubator/ceps/blob/main/cep-15.md + const auto base_url = [&] + { + if (repodata_version == 2 && repodata_info) + { + if (auto url = repodata_info.value()["base_url"]; !url.error()) + { + return std::string(url.get_string().value_unsafe()); + } + } + + return repo_url; + }(); + + const auto parsed_url = specs::CondaURL::parse(base_url) + .or_else([](specs::ParseError&& err) { throw std::move(err); }) + .value(); + + // TODO: it does not seems resolvo can handle setting signatures on solvables for now + // auto signatures = [&] + // { + // auto maybe_sigs = repodata_doc["signatures"]; + // if (!maybe_sigs.error() && verify_artifacts) + // { + // return std::make_optional(maybe_sigs); + // } + // else + // { + // LOG_DEBUG << "No signatures available or requested. Downloading without verifying + // artifacts."; return decltype(std::make_optional(maybe_sigs)){}; + // } + // }(); + + // Process packages.conda first + if (auto pkgs = repodata_doc["packages.conda"]; !pkgs.error()) + { + if (auto packages_as_object = pkgs.get_object(); !packages_as_object.error()) + { + for (auto field : packages_as_object) + { + if (!field.error()) + { + const std::string key(field.unescaped_key().value()); + if (auto value = field.value(); !value.error()) + { + if (auto pkg_obj = value.get_object(); !pkg_obj.error()) + { + auto package_info = specs::PackageInfo::from_json( + filename.string(), + pkg_obj.value(), + parsed_url, + channel_id + ); + if (!package_info) + { + LOG_WARNING << package_info.error().what(); + } + alloc_solvable(package_info.value()); + } + } + } + } + } + } + + // Then process packages + if (auto pkgs = repodata_doc["packages"]; !pkgs.error()) + { + if (auto packages_as_object = pkgs.get_object(); !packages_as_object.error()) + { + for (auto field : packages_as_object) + { + if (!field.error()) + { + const std::string key(field.unescaped_key().value()); + if (auto value = field.value(); !value.error()) + { + if (auto pkg_obj = value.get_object(); !pkg_obj.error()) + { + auto package_info = specs::PackageInfo::from_json( + filename.string(), + pkg_obj.value(), + parsed_url, + channel_id + ); + if (!package_info) + { + LOG_WARNING << package_info.error().what(); + } + alloc_solvable(package_info.value()); + } + } + } + } + } + } + } + + void Database::add_repo_from_packages( + const std::vector& packages, + [[maybe_unused]] const std::string& repo_name, + [[maybe_unused]] bool pip_as_python_dependency + ) + { + for (const auto& package : packages) + { + alloc_solvable(package); + } + } + + void Database::set_installed_repo([[maybe_unused]] const std::string& repo_name) + { + // TODO: Implement this + } + + /** + * Allocates a new requirement and return the id of the requirement. + */ + ::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 `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" }) + { + 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 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); + 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 char* op : { ">=", "<=", "==", ">", "<", "!=", "=", "==" }) + { + const std::string bad_op = std::string(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().to_string(); + 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.to_string(); + name_pool.alloc(::resolvo::String{ match_spec_str }); + string_pool.alloc(::resolvo::String{ match_spec_str }); + return id; + } + + /** + * Allocates a new solvable and returns its id. + * + * - Adds the solvable to the solvable pool. + * - Adds the name to the Name and String pools. + * - Adds the long string representation of the package to the Name and String pools. + * - Allocates version sets for dependencies and constrains. + * - Adds the solvable to the name_to_solvable map. + */ + ::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 auto name_id = name_pool.alloc(::resolvo::String{ package_info.name }); + name_to_solvable[name_id].push_back(id); + + return id; + } + + /** + * Returns a user-friendly string representation of the specified solvable. + * + * When formatting the solvable, it should it include both the name of + * the package and any other identifying properties. + */ + ::resolvo::String Database::display_solvable(::resolvo::SolvableId solvable) + { + const specs::PackageInfo& package_info = solvable_pool[solvable]; + return ::resolvo::String{ package_info.long_str() }; + } + + /** + * Returns a user-friendly string representation of the name of the + * specified solvable. + */ + ::resolvo::String Database::display_solvable_name(::resolvo::SolvableId solvable) + { + const specs::PackageInfo& package_info = solvable_pool[solvable]; + return ::resolvo::String{ package_info.name }; + } + + /** + * Returns a string representation of multiple solvables merged together. + * + * When formatting the solvables, both the name of the packages and any + * other identifying properties should be included. + */ + ::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 }; + } + + /** + * Returns an object that can be used to display the given name in a + * user-friendly way. + */ + ::resolvo::String Database::display_name(::resolvo::NameId name) + { + return name_pool[name]; + } + + /** + * Returns a user-friendly string representation of the specified version + * set. + * + * The name of the package should *not* be included in the display. Where + * appropriate, this information is added. + */ + ::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.to_string() }; + } + + /** + * Returns the string representation of the specified string. + */ + ::resolvo::String Database::display_string(::resolvo::StringId string) + { + return string_pool[string]; + } + + /** + * Returns the name of the package that the specified version set is + * associated with. + */ + ::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().to_string() }]; + } + + /** + * Returns the name of the package for the given solvable. + */ + ::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::Slice<::resolvo::VersionSetId> + Database::version_sets_in_union(::resolvo::VersionSetUnionId version_set_union_id) + { + if (auto it = version_set_unions.find(version_set_union_id); it != version_set_unions.end()) + { + return it->second; + } + static ::resolvo::Vector<::resolvo::VersionSetId> empty{}; + return empty; + } + + ::resolvo::Condition Database::resolve_condition(::resolvo::ConditionId condition_id) + { + if (auto it = conditions.find(condition_id); it != conditions.end()) + { + return it->second; + } + return ::resolvo::Condition(::resolvo::VersionSetId{ 0 }); + } + + /** + * Obtains a list of solvables that should be considered when a package + * with the given name is requested. + */ + ::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; + } + + /** + * Finds the highest version and the minimum number of track features for a given version set. + * + * - If the version set has already been computed, returns the cached value. + * - Filters candidates for the version set. + * - Iterates over filtered candidates to find the maximum version and the minimum number of + * track features. + * - Caches and returns the result. + */ + std::pair + 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().to_string(); + 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; + } + + /** + * Sort the specified solvables based on which solvable to try first. The + * solver will iteratively try to select the highest version. If a + * conflict is found with the highest version the next version is + * tried. This continues until a solution is found. + */ + 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().to_string(); + 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().to_string(); + 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; + } + ); + } + + /** + * Given a set of solvables, return the solvables that match the given + * version set or if `inverse` is true, the solvables that do *not* match + * the version set. + */ + ::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; + } + + /** + * Returns the dependencies for the specified solvable. + */ + ::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( + ::resolvo::ConditionalRequirement(::resolvo::Requirement(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; + } +} diff --git a/libmamba/src/solver/resolvo/solver.cpp b/libmamba/src/solver/resolvo/solver.cpp new file mode 100644 index 0000000000..a00e039153 --- /dev/null +++ b/libmamba/src/solver/resolvo/solver.cpp @@ -0,0 +1,182 @@ +// Copyright (c) 2026, 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; + if constexpr (!std::is_same_v) + { + return lhs.spec.name().to_string() < rhs.spec.name().to_string(); + } + 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; + if constexpr (std::is_same_v) + { + requirements.push_back( + database.alloc_version_set(j.spec.name().to_string()) + ); + } + }, + 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; + if constexpr (std::is_same_v) + { + constraints.push_back( + database.alloc_version_set(j.spec.name().to_string()) + ); + } + }, + 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 + { + 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::Vector<::resolvo::ConditionalRequirement> req_conditional; + for (const auto& req : req_vec) + { + req_conditional.push_back(::resolvo::ConditionalRequirement(::resolvo::Requirement(req))); + } + + ::resolvo::Problem problem = { + .requirements = req_conditional, + .constraints = constr_vec, + .soft_requirements = {}, + }; + + ::resolvo::String reason = ::resolvo::solve(database, problem, 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(reason_str, mamba_error_code::satisfiablitity_error); + } + + return Outcome{ result_to_solution(result, database, request) }; + } + + auto Solver::solve(Database& database, Request&& request) -> expected_t + { + 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 + { + 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); + } +} diff --git a/libmamba/src/specs/package_info.cpp b/libmamba/src/specs/package_info.cpp index 443b6a27a4..abbe29a017 100644 --- a/libmamba/src/specs/package_info.cpp +++ b/libmamba/src/specs/package_info.cpp @@ -642,6 +642,150 @@ namespace mamba::specs pkg.constrains = j.value("constrains", std::vector()); } + auto PackageInfo::from_json( + const std::string_view& filename, + simdjson::ondemand::object& pkg, + const CondaURL& repo_url, + const std::string& channel_id + ) -> expected_parse_t + { + PackageInfo package_info; + package_info.channel = channel_id; + package_info.filename = std::string(filename); + package_info.package_url = (repo_url / filename).str(CondaURL::Credentials::Show); + + if (auto name = pkg["name"]; !name.error()) + { + package_info.name = name.get_string().value_unsafe(); + } + else + { + return make_unexpected_parse(fmt::format(R"(Found invalid name in "{}")", filename)); + } + if (auto version = pkg["version"]; !version.error()) + { + package_info.version = version.get_string().value_unsafe(); + } + else + { + return make_unexpected_parse(fmt::format(R"(Found invalid version in "{}")", filename)); + } + if (auto build_string = pkg["build"]; !build_string.error()) + { + package_info.build_string = build_string.get_string().value_unsafe(); + } + else + { + return make_unexpected_parse(fmt::format(R"(Found invalid build in "{}")", filename)); + } + if (auto build_number = pkg["build_number"]; !build_number.error()) + { + package_info.build_number = build_number.get_uint64().value_unsafe(); + } + else + { + return make_unexpected_parse( + fmt::format(R"(Found invalid build_number in "{}")", filename) + ); + } + + if (auto subdir = pkg["subdir"]; !subdir.error()) + { + package_info.platform = subdir.get_string().value_unsafe(); + } + if (auto size = pkg["size"]; !size.error()) + { + package_info.size = size.get_uint64().value_unsafe(); + } + if (auto md5 = pkg["md5"]; !md5.error()) + { + package_info.md5 = md5.get_string().value_unsafe(); + } + if (auto sha256 = pkg["sha256"]; !sha256.error()) + { + package_info.sha256 = sha256.get_string().value_unsafe(); + } + if (auto elem = pkg["noarch"]; !elem.error()) + { + if (auto noarch = elem.get_bool(); !noarch.error() && noarch.value_unsafe()) + { + package_info.noarch = NoArchType::Generic; + } + else if (elem.is_string()) + { + package_info.noarch = NoArchType::Generic; + } + } + if (auto license = pkg["license"]; !license.error()) + { + package_info.license = license.get_string().value_unsafe(); + } + if (auto timestamp = pkg["timestamp"]; !timestamp.error()) + { + const auto time = timestamp.get_uint64().value_unsafe(); + constexpr auto MAX_CONDA_TIMESTAMP = 253402300799ULL; + package_info.timestamp = (time > MAX_CONDA_TIMESTAMP) ? (time / 1000) : time; + } + if (auto depends = pkg["depends"]; !depends.error()) + { + if (auto arr = depends.get_array(); !arr.error()) + { + for (auto elem : arr) + { + if (!elem.error() && elem.is_string()) + { + package_info.dependencies.emplace_back(elem.get_string().value_unsafe()); + } + } + } + } + if (auto constrains = pkg["constrains"]; !constrains.error()) + { + if (auto arr = constrains.get_array(); !arr.error()) + { + for (auto elem : arr) + { + if (!elem.error() && elem.is_string()) + { + package_info.constrains.emplace_back(elem.get_string().value_unsafe()); + } + } + } + } + if (auto track_features = pkg["track_features"]; !track_features.error()) + { + if (auto arr = track_features.get_array(); !arr.error()) + { + for (auto elem : arr) + { + if (auto feat = elem.get_string(); !feat.error()) + { + package_info.track_features.emplace_back(feat.value()); + } + } + } + else if (auto feat_str = track_features.get_string(); !feat_str.error()) + { + const auto split_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); }); + }; + auto splits = split_features(feat_str.value()); + while (!splits[0].empty()) + { + package_info.track_features.emplace_back(splits[0]); + splits = split_features(splits[1]); + } + } + } + + package_info.defaulted_keys = { std::string(defaulted_key::initialized) }; + return package_info; + } + auto PackageInfo::url_for_channel(std::string_view channel_mirror_url) const -> std::string { // TODO: add more input checks diff --git a/libmamba/tests/src/solver/resolvo/test_solver.cpp b/libmamba/tests/src/solver/resolvo/test_solver.cpp new file mode 100644 index 0000000000..caa6191b05 --- /dev/null +++ b/libmamba/tests/src/solver/resolvo/test_solver.cpp @@ -0,0 +1,1628 @@ +// Copyright (c) 2026, 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 +#include + +#include +#include +#include +#include +#include + +#include "mamba/api/install.hpp" // for parsing YAML specs +#include "mamba/core/util.hpp" // for LockFile +#include "mamba/specs/channel.hpp" +#include "mamba/specs/package_info.hpp" + +// TODO: move PackageTypes and MAX_CONDA_TIMESTAMP to a common place +#include "mamba/core/virtual_packages.hpp" +#include "mamba/solver/libsolv/database.hpp" +#include "mamba/solver/libsolv/parameters.hpp" // for PackageTypes +#include "mamba/solver/libsolv/solver.hpp" + +#include "mambatests.hpp" + + +using namespace mamba; +using namespace mamba::specs; +using namespace mamba::solver; + +using namespace resolvo; + +template <> +struct std::hash +{ + std::size_t operator()(const VersionSetId& id) const + { + return std::hash{}(id.id); + } +}; + +template <> +struct std::hash +{ + std::size_t operator()(const SolvableId& id) const + { + return std::hash{}(id.id); + } +}; + +template <> +struct std::hash +{ + std::size_t operator()(const NameId& id) const + { + return std::hash{}(id.id); + } +}; + +template <> +struct std::hash +{ + std::size_t operator()(const StringId& id) const + { + return std::hash{}(id.id); + } +}; + +// Create a template Pool class that maps a key to a set of values +template +struct bijective_map +{ + bijective_map() = default; + ~bijective_map() = default; + + /** + * Adds the value to the bijective_map and returns its associated id. If the + * value is already in the bijective_map, 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(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 bijective_map + 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 value_to_id; + std::unordered_map id_to_value; +}; + +struct PackageDatabase : public DependencyProvider +{ + virtual ~PackageDatabase() = default; + + ::bijective_map name_pool; + ::bijective_map string_pool; + + // MatchSpec are VersionSet in resolvo's semantics + ::bijective_map version_set_pool; + + // PackageInfo are Solvable in resolvo's semantics + ::bijective_map solvable_pool; + + // PackageName to Vector + std::unordered_map> name_to_solvable; + + // VersionSetId to max version + // TODO use `SolvableId` instead of `std::pair`? + std::unordered_map> + version_set_to_max_version_and_track_features_numbers; + + /** + * Allocates a new requirement and return the id of the requirement. + */ + VersionSetId 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 `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" }) + { + 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 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 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 version_sets; + for (const std::string& ms : match_specs) + { + alloc_version_set(ms); + } + // Placeholder return value + return 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 MatchSpec match_spec = 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().to_string(); + name_pool.alloc(String{ name }); + string_pool.alloc(String{ name }); + + // Add the MatchSpec's string representation to the Name and String pools + const std::string match_spec_str = match_spec.to_string(); + name_pool.alloc(String{ match_spec_str }); + string_pool.alloc(String{ match_spec_str }); + return id; + } + + SolvableId alloc_solvable(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(String{ name }); + string_pool.alloc(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(String{ long_str }); + string_pool.alloc(String{ long_str }); + + for (auto& dep : package_info.dependencies) + { + alloc_version_set(dep); + } + for (auto& constr : package_info.constrains) + { + alloc_version_set(constr); + } + + // const size_t n_track_features = package_info.track_features.size(); + // if(n_track_features > 0) + // { + // std::cout << "PackageInfo has " << package_info.long_str() << " has " << + // n_track_features << " track features" << std::endl; for (auto tf + // :package_info.track_features) + // { + // // Add the track feature to the Name and String pools + // std::cout << " - " << tf < solvable) override + { + std::string result; + for (auto& solvable_id : solvable) + { + result += solvable_pool[solvable_id].long_str(); + } + return String{ result }; + } + + /** + * Returns an object that can be used to display the given name in a + * user-friendly way. + */ + String display_name(NameId name) override + { + return name_pool[name]; + } + + /** + * Returns a user-friendly string representation of the specified version + * set. + * + * The name of the package should *not* be included in the display. Where + * appropriate, this information is added. + */ + String display_version_set(VersionSetId version_set) override + { + const MatchSpec match_spec = version_set_pool[version_set]; + return String{ match_spec.to_string() }; + } + + /** + * Returns the string representation of the specified string. + */ + String display_string(StringId string) override + { + return string_pool[string]; + } + + /** + * Returns the name of the package that the specified version set is + * associated with. + */ + NameId version_set_name(VersionSetId version_set_id) override + { + const MatchSpec match_spec = version_set_pool[version_set_id]; + // std::cout << "Getting name id for version_set_id " << match_spec.name().to_string() << + // std::endl; + return name_pool[String{ match_spec.name().to_string() }]; + } + + /** + * Returns the name of the package for the given solvable. + */ + NameId solvable_name(SolvableId solvable_id) override + { + const PackageInfo& package_info = solvable_pool[solvable_id]; + // std::cout << "Getting name id for solvable " << package_info.long_str() << std::endl; + return name_pool[String{ package_info.name }]; + } + + /** + * Obtains a list of solvables that should be considered when a package + * with the given name is requested. + */ + Candidates get_candidates(NameId package) override + { + Candidates candidates{}; + candidates.favored = nullptr; + candidates.locked = nullptr; + candidates.candidates = name_to_solvable[package]; + return candidates; + } + + std::pair find_highest_version(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 MatchSpec match_spec = version_set_pool[version_set_id]; + + const std::string& name = match_spec.name().to_string(); + + auto name_id = name_pool.alloc(String{ name }); + + auto solvables = name_to_solvable[name_id]; + + auto filtered = filter_candidates(solvables, version_set_id, false); + + Version max_version = Version(); + size_t max_version_n_track_features = 0; + + for (auto& solvable_id : filtered) + { + const PackageInfo& package_info = solvable_pool[solvable_id]; + const auto version = 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; + } + + /** + * Sort the specified solvables based on which solvable to try first. The + * solver will iteratively try to select the highest version. If a + * conflict is found with the highest version the next version is + * tried. This continues until a solution is found. + */ + void sort_candidates(Slice solvables) override + { + std::sort( + solvables.begin(), + solvables.end(), + [&](const SolvableId& a, const SolvableId& b) + { + const PackageInfo& package_info_a = solvable_pool[a]; + const 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 = Version::parse(package_info_a.version).value(); + const auto b_version = 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 a_deps; + std::unordered_map b_deps; + for (auto dep_a : package_info_a.dependencies) + { + // TODO: have a VersionID to NameID mapping instead + MatchSpec ms = MatchSpec::parse(dep_a).value(); + const std::string& name = ms.name().to_string(); + auto name_id = name_pool.alloc(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 + MatchSpec ms = MatchSpec::parse(dep_b).value(); + const std::string& name = ms.name().to_string(); + auto name_id = name_pool.alloc(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; + } + ); + } + + /** + * Given a set of solvables, return the solvables that match the given + * version set or if `inverse` is true, the solvables that do *not* match + * the version set. + */ + Vector + filter_candidates(Slice candidates, VersionSetId version_set_id, bool inverse) override + { + MatchSpec match_spec = version_set_pool[version_set_id]; + Vector filtered; + + // std::cout << "Candidates to filter " << match_spec.to_string() << std::endl; + // + // for(auto& solvable_id : candidates) { + // const PackageInfo& package_info = solvable_pool[solvable_id]; + // std::cout << " - " << package_info.long_str() << std::endl; + // } + + if (inverse) + { + for (auto& solvable_id : candidates) + { + const 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 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); + } + } + } + // std::cout << "Filtered candidates for " << match_spec.to_string() << std::endl; + // + // for(auto& solvable_id : filtered) { + // const PackageInfo& package_info = solvable_pool[solvable_id]; + // std::cout << " - " << package_info.long_str() << std::endl; + // } + + return filtered; + } + + /** + * Returns the dependencies for the specified solvable. + */ + Dependencies get_dependencies(SolvableId solvable_id) override + { + const PackageInfo& package_info = solvable_pool[solvable_id]; + // std::cout << "Getting dependencies for " << package_info.long_str() << std::endl; + + Dependencies dependencies; + + // TODO: do this in O(1) + for (auto& dep : package_info.dependencies) + { + // std::cout << "Parsing dep " << dep << std::endl; + const MatchSpec match_spec = MatchSpec::parse(dep).value(); + dependencies.requirements.push_back(version_set_pool[match_spec]); + } + for (auto& constr : package_info.constrains) + { + // std::cout << "Parsing constr " << constr << std::endl; + // 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 MatchSpec match_spec = MatchSpec::parse(constr2).value(); + dependencies.constrains.push_back(version_set_pool[match_spec]); + } + + return dependencies; + } +}; + +bool +parse_packageinfo_json( + const std::string_view& filename, + simdjson::ondemand::object& pkg, + const specs::CondaURL& repo_url, + const std::string& channel_id, + PackageDatabase& db +) +{ + auto maybe_package_info = PackageInfo::from_json(filename, pkg, repo_url, channel_id); + if (!maybe_package_info) + { + return false; + } + db.alloc_solvable(maybe_package_info.value()); + return true; +} + +void +parse_repodata_json( + PackageDatabase& db, + const fs::u8path& filename, + const std::string& repo_url, + const std::string& channel_id, + bool verify_artifacts +) +{ + auto parser = simdjson::ondemand::parser(); + const auto lock = LockFile(filename); + + // The json storage must be kept alive as long as we are reading the json data. + const auto json_content = simdjson::padded_string::load(filename.string()); + + // Note that with the "on-demand" parser, documents/values/objects act as iterators + // to go through the document. + auto repodata = parser.iterate(json_content); + + // Get `base_url` in case 'repodata_version': 2 + // cf. https://github.com/conda-incubator/ceps/blob/main/cep-15.md + const auto base_url = [&] + { + if (auto repodata_version = repodata["repodata_version"]; !repodata_version.error()) + { + if (auto info = repodata["info"]; !info.error()) + { + if (auto url = info["base_url"]; !url.error()) + { + return std::string(url.get_string().value_unsafe()); + } + } + } + + return repo_url; + }(); + + const auto parsed_url = specs::CondaURL::parse(base_url) + .or_else([](specs::ParseError&& err) { throw std::move(err); }) + .value(); + + // TODO: it does not seems resolvo can handle setting signatures on solvables for now + // auto signatures = [&] + // { + // auto maybe_sigs = repodata["signatures"]; + // if (!maybe_sigs.error() && verify_artifacts) + // { + // return std::make_optional(maybe_sigs); + // } + // else + // { + // LOG_DEBUG << "No signatures available or requested. Downloading without verifying + // artifacts."; return decltype(std::make_optional(maybe_sigs)){}; + // } + // }(); + + // Process packages.conda first + if (auto pkgs = repodata["packages.conda"]; !pkgs.error()) + { + if (auto packages_as_object = pkgs.get_object(); !packages_as_object.error()) + { + for (auto field : packages_as_object) + { + if (!field.error()) + { + const std::string key(field.unescaped_key().value()); + if (auto value = field.value(); !value.error()) + { + if (auto pkg_obj = value.get_object(); !pkg_obj.error()) + { + parse_packageinfo_json( + filename.string(), + pkg_obj.value(), + parsed_url, + channel_id, + db + ); + } + } + } + } + } + } + + // Then process packages + if (auto pkgs = repodata["packages"]; !pkgs.error()) + { + if (auto packages_as_object = pkgs.get_object(); !packages_as_object.error()) + { + for (auto field : packages_as_object) + { + if (!field.error()) + { + const std::string key(field.unescaped_key().value()); + if (auto value = field.value(); !value.error()) + { + if (auto pkg_obj = value.get_object(); !pkg_obj.error()) + { + parse_packageinfo_json( + filename.string(), + pkg_obj.value(), + parsed_url, + channel_id, + db + ); + } + } + } + } + } + } +} + +// from `src/test_solver.cpp` +auto +find_actions_with_name(const Solution& solution, std::string_view name) + -> std::vector; +auto +find_actions(const Solution& solution) -> std::vector; +auto +extract_package_to_install(const Solution& solution) -> std::vector; + +// wget https://conda.anaconda.org/conda-forge/linux-64/repodata.json +// wget https://conda.anaconda.org/conda-forge/noarch/repodata.json + +mamba::solver::libsolv::Database +create_libsolv_db() +{ + auto libsolv_db = mamba::solver::libsolv::Database( + { + /* .platforms= */ { "linux-64", "noarch" }, + /* .channel_alias= */ specs::CondaURL::parse("https://conda.anaconda.org/").value(), + } + ); + + + const auto repo_linux = libsolv_db.add_repo_from_repodata_json( + "/tmp/linux-64/repodata.json", + "https://conda.anaconda.org/conda-forge/linux-64", + "conda-forge", + libsolv::PipAsPythonDependency::No + ); + + const auto repo_noarch = libsolv_db.add_repo_from_repodata_json( + "/tmp/noarch/repodata.json", + "https://conda.anaconda.org/conda-forge/noarch", + "conda-forge", + libsolv::PipAsPythonDependency::Yes + ); + + // Not adding Pip dependency since it might needlessly make the installed/active environment + // broken if pip is not already installed (debatable). + + auto repo = libsolv_db.add_repo_from_packages( + get_virtual_packages("linux-64"), + "virtual", + solver::libsolv::PipAsPythonDependency::No + ); + libsolv_db.set_installed_repo(repo); + + return libsolv_db; +}; + +PackageDatabase +create_resolvo_db() +{ + PackageDatabase resolvo_db; + + parse_repodata_json( + resolvo_db, + "/tmp/linux-64/repodata.json", + "https://conda.anaconda.org/conda-forge/linux-64/repodata.json", + "conda-forge", + false + ); + + parse_repodata_json( + resolvo_db, + "/tmp/noarch/repodata.json", + "https://conda.anaconda.org/conda-forge/noarch/repodata.json", + "conda-forge", + false + ); + + for (const auto& package : get_virtual_packages("linux-64")) + { + resolvo_db.alloc_solvable(package); + } + + return resolvo_db; +} + +mamba::solver::libsolv::Database libsolv_db = create_libsolv_db(); +PackageDatabase resolvo_db = create_resolvo_db(); + +std::vector +libsolv_resolve(const std::vector& specs) +{ + // libsolv's specification and resolution + + Request::job_list jobs; + + std::transform( + specs.begin(), + specs.end(), + std::back_inserter(jobs), + [](const std::string& spec) { return Request::Install{ MatchSpec::parse(spec).value() }; } + ); + + const auto request = Request{ + /* .flags= */ {}, + /* .jobs= */ jobs, + }; + + std::cout << "Start with libsolv" << std::endl; + auto tick_libsolv = std::chrono::steady_clock::now(); + const auto outcome = libsolv::Solver().solve(libsolv_db, request); + auto tack_libsolv = std::chrono::steady_clock::now(); + std::cout << "End with libsolv" << std::endl; + std::cout + << "Elapsed time: " + << std::chrono::duration_cast(tack_libsolv - tick_libsolv).count() + << "ms" << std::endl; + + REQUIRE(outcome.has_value()); + if (std::holds_alternative(outcome.value())) + { + const auto& solution = std::get(outcome.value()); + + std::vector libsolv_resolution = extract_package_to_install(solution); + std::sort( + libsolv_resolution.begin(), + libsolv_resolution.end(), + [&](const PackageInfo& a, const PackageInfo& b) { return a.name < b.name; } + ); + return libsolv_resolution; + } + return {}; +} + +std::vector +resolvo_resolve(const std::vector& specs) +{ + // resolvo's specification and resolution + resolvo::Vector requirements; + for (const auto& spec : specs) + { + requirements.push_back(resolvo_db.alloc_version_set(spec)); + } + + resolvo::Vector constraints = {}; + resolvo::Vector result; + + std::cout << "Start with resolvo" << std::endl; + auto tick_resolvo = std::chrono::steady_clock::now(); + String reason = resolvo::solve(resolvo_db, requirements, constraints, result); + auto tack_resolvo = std::chrono::steady_clock::now(); + std::cout << "End with resolvo" << std::endl; + std::cout + << "Elapsed time: " + << std::chrono::duration_cast(tack_resolvo - tick_resolvo).count() + << "ms" << std::endl; + + if (reason == "") + { + std::vector resolvo_resolution; + for (auto solvable_id : result) + { + PackageInfo package_info = resolvo_db.solvable_pool[solvable_id]; + // Skip virtual package (i.e. whose `package_info.name` starts with "__") + if (package_info.name.find("__") != 0) + { + resolvo_resolution.push_back(package_info); + } + } + + std::sort( + resolvo_resolution.begin(), + resolvo_resolution.end(), + [&](const PackageInfo& a, const PackageInfo& b) { return a.name < b.name; } + ); + return resolvo_resolution; + } + return {}; +} + +TEST_CASE("solver::resolvo") +{ + using namespace specs::match_spec_literals; + + using PackageInfo = PackageInfo; + + SECTION("Addition of PackageInfo to PackageDatabase") + { + PackageDatabase database; + + PackageInfo scikit_learn("scikit-learn", "1.5.0", "py310h981052a_0", 0); + scikit_learn.dependencies.emplace_back("numpy >=1.20.0,<2.0a0"); + scikit_learn.dependencies.emplace_back("scipy >=1.6.0,<2.0a0"); + scikit_learn.dependencies.emplace_back("joblib >=1.0.1,<2.0a0"); + scikit_learn.dependencies.emplace_back("threadpoolctl >=2.1.0,<3.0a0"); + + auto solvable = database.alloc_solvable(scikit_learn); + + REQUIRE(solvable.id == 0); + REQUIRE(database.solvable_pool[solvable].name == "scikit-learn"); + REQUIRE(database.solvable_pool[solvable].version == "1.5.0"); + REQUIRE(database.solvable_pool[solvable].build_string == "py310h981052a_0"); + REQUIRE(database.solvable_pool[solvable].build_number == 0); + + auto deps = database.get_dependencies(solvable); + REQUIRE(deps.requirements.size() == 4); + REQUIRE(deps.constrains.size() == 0); + + REQUIRE( + database.version_set_pool[deps.requirements[0]].to_string() + == "numpy[version=\">=1.20.0,<2.0a0\"]" + ); + REQUIRE( + database.version_set_pool[deps.requirements[1]].to_string() + == "scipy[version=\">=1.6.0,<2.0a0\"]" + ); + REQUIRE( + database.version_set_pool[deps.requirements[2]].to_string() + == "joblib[version=\">=1.0.1,<2.0a0\"]" + ); + REQUIRE( + database.version_set_pool[deps.requirements[3]].to_string() + == "threadpoolctl[version=\">=2.1.0,<3.0a0\"]" + ); + + REQUIRE(database.name_pool.find(String{ "scikit-learn" }) != database.name_pool.end_ids()); + REQUIRE(database.name_pool.find(String{ "numpy" }) != database.name_pool.end_ids()); + REQUIRE(database.name_pool.find(String{ "scipy" }) != database.name_pool.end_ids()); + REQUIRE(database.name_pool.find(String{ "joblib" }) != database.name_pool.end_ids()); + REQUIRE(database.name_pool.find(String{ "threadpoolctl" }) != database.name_pool.end_ids()); + + REQUIRE(database.string_pool.find(String{ "scikit-learn" }) != database.string_pool.end_ids()); + REQUIRE(database.string_pool.find(String{ "numpy" }) != database.string_pool.end_ids()); + REQUIRE(database.string_pool.find(String{ "scipy" }) != database.string_pool.end_ids()); + REQUIRE(database.string_pool.find(String{ "joblib" }) != database.string_pool.end_ids()); + REQUIRE(database.string_pool.find(String{ "threadpoolctl" }) != database.string_pool.end_ids()); + } + + SECTION("Filter solvables") + { + PackageDatabase database; + + PackageInfo skl0("scikit-learn", "1.4.0", "py310h981052a_0", 0); + auto sol0 = database.alloc_solvable(skl0); + + PackageInfo skl1("scikit-learn", "1.5.0", "py310h981052a_1", 1); + auto sol1 = database.alloc_solvable(skl1); + + PackageInfo skl2("scikit-learn", "1.5.1", "py310h981052a_0", 0); + auto sol2 = database.alloc_solvable(skl2); + + PackageInfo skl3("scikit-learn", "1.5.1", "py310h981052a_2", 2); + auto sol3 = database.alloc_solvable(skl3); + + auto solvables = Vector{ sol0, sol1, sol2, sol3 }; + + // Filter on scikit-learn + auto all = database.filter_candidates( + solvables, + database.alloc_version_set("scikit-learn"), + false + ); + REQUIRE(all.size() == 4); + REQUIRE(all[0] == sol0); + REQUIRE(all[1] == sol1); + REQUIRE(all[2] == sol2); + REQUIRE(all[3] == sol3); + + // Inverse filter on scikit-learn + auto none = database.filter_candidates( + solvables, + database.alloc_version_set("scikit-learn"), + true + ); + REQUIRE(none.size() == 0); + + // Filter on scikit-learn==1.5.1 + auto one = database.filter_candidates( + solvables, + database.alloc_version_set("scikit-learn==1.5.1"), + false + ); + REQUIRE(one.size() == 2); + REQUIRE(one[0] == sol2); + REQUIRE(one[1] == sol3); + + // Inverse filter on scikit-learn==1.5.1 + auto three = database.filter_candidates( + solvables, + database.alloc_version_set("scikit-learn==1.5.1"), + true + ); + REQUIRE(three.size() == 2); + REQUIRE(three[0] == sol0); + REQUIRE(three[1] == sol1); + + // Filter on scikit-learn<1.5.1 + auto two = database.filter_candidates( + solvables, + database.alloc_version_set("scikit-learn<1.5.1"), + false + ); + REQUIRE(two.size() == 2); + REQUIRE(two[0] == sol0); + REQUIRE(two[1] == sol1); + + // Filter on build number 0 + auto build = database.filter_candidates( + solvables, + database.alloc_version_set("scikit-learn[build_number==0]"), + false + ); + REQUIRE(build.size() == 2); + REQUIRE(build[0] == sol0); + REQUIRE(build[1] == sol2); + + // Filter on build number 2 + auto build_bis = database.filter_candidates( + solvables, + database.alloc_version_set("scikit-learn[build_number==2]"), + false + ); + REQUIRE(build_bis.size() == 1); + REQUIRE(build_bis[0] == sol3); + + // Filter on build number 3 + auto build_ter = database.filter_candidates( + solvables, + database.alloc_version_set("scikit-learn[build_number==3]"), + false + ); + REQUIRE(build_ter.size() == 0); + } + + SECTION("Sort solvables increasing order") + { + PackageDatabase database; + + PackageInfo skl0("scikit-learn", "1.5.2", "py310h981052a_0", 0); + auto sol0 = database.alloc_solvable(skl0); + + PackageInfo skl1("scikit-learn", "1.5.0", "py310h981052a_1", 1); + auto sol1 = database.alloc_solvable(skl1); + + PackageInfo skl2("scikit-learn", "1.5.1", "py310h981052a_2", 2); + auto sol2 = database.alloc_solvable(skl2); + + PackageInfo skl3("scikit-learn", "1.5.0", "py310h981052a_2", 2); + auto sol3 = database.alloc_solvable(skl3); + + PackageInfo skl4("scikit-learn", "1.5.1", "py310h981052a_1", 1); + auto sol4 = database.alloc_solvable(skl4); + + Vector solvables = { sol0, sol1, sol2, sol3, sol4 }; + + database.sort_candidates(solvables); + + REQUIRE(solvables[0] == sol0); + REQUIRE(solvables[1] == sol2); + REQUIRE(solvables[2] == sol4); + REQUIRE(solvables[3] == sol3); + REQUIRE(solvables[4] == sol1); + } + + SECTION("Sort solvables (build number only)") + { + PackageDatabase database; + + PackageInfo skl0("scikit-learn", "1.5.0", "py310h981052a_0", 0); + auto sol0 = database.alloc_solvable(skl0); + + PackageInfo skl1("scikit-learn", "1.5.0", "py310h981052a_3", 3); + auto sol1 = database.alloc_solvable(skl1); + + PackageInfo skl2("scikit-learn", "1.5.0", "py310h981052a_2", 2); + auto sol2 = database.alloc_solvable(skl2); + + PackageInfo skl3("scikit-learn", "1.5.0", "py310h981052a_1", 1); + auto sol3 = database.alloc_solvable(skl3); + + PackageInfo skl4("scikit-learn", "1.5.0", "py310h981052a_4", 4); + auto sol4 = database.alloc_solvable(skl4); + + PackageInfo skl5("scikit-learn", "1.5.0", "py310h981052a_5", 5); + skl5.timestamp = 1337; + auto sol5 = database.alloc_solvable(skl5); + + PackageInfo skl6("scikit-learn", "1.5.0", "py310h981052a_5", 5); + skl6.timestamp = 42; + auto sol6 = database.alloc_solvable(skl6); + + PackageInfo skl7("scikit-learn", "1.5.0", "py310h981052a_5", 5); + skl7.timestamp = 2000; + auto sol7 = database.alloc_solvable(skl7); + + Vector solvables = { sol0, sol1, sol2, sol3, sol4, sol5, sol6, sol7 }; + + database.sort_candidates(solvables); + + REQUIRE(solvables[0] == sol7); + REQUIRE(solvables[1] == sol5); + REQUIRE(solvables[2] == sol6); + REQUIRE(solvables[3] == sol4); + REQUIRE(solvables[4] == sol1); + REQUIRE(solvables[5] == sol2); + REQUIRE(solvables[6] == sol3); + REQUIRE(solvables[7] == sol0); + } + + SECTION("Trivial problem") + { + PackageDatabase database; + // NOTE: the problem can only be solved when two `Solvable` are added to the + // `PackageDatabase` + PackageInfo scikit_learn("scikit-learn", "1.5.0", "py310h981052a_0", 0); + database.alloc_solvable(scikit_learn); + + resolvo::Vector requirements = { + database.alloc_version_set("scikit-learn==1.5.0"), + }; + resolvo::Vector constraints = {}; + + resolvo::Vector result; + String reason = resolvo::solve(database, requirements, constraints, result); + + REQUIRE(reason == ""); + REQUIRE(result.size() == 1); + REQUIRE(database.solvable_pool[result[0]] == scikit_learn); + } + + SECTION("Parse linux-64/repodata.json") + { + PackageDatabase database; + + parse_repodata_json( + database, + "/tmp/linux-64/repodata.json", + "https://conda.anaconda.org/conda-forge/linux-64/repodata.json", + "conda-forge", + false + ); + + std::cout << "Number of solvables: " << database.solvable_pool.size() << std::endl; + } + + SECTION("Parse noarch/repodata.json") + { + PackageDatabase database; + + parse_repodata_json( + database, + "/tmp/noarch/repodata.json", + "https://conda.anaconda.org/conda-forge/noarch/repodata.json", + "conda-forge", + false + ); + + std::cout << "Number of solvables: " << database.solvable_pool.size() << std::endl; + } +} + +TEST_CASE("Test consistency with libsolv (environment creation)") +{ + using namespace specs::match_spec_literals; + + using PackageInfo = PackageInfo; + + SECTION("numpy") + { + const auto request = Request{ + /* .flags= */ {}, + /* .jobs= */ { Request::Install{ "numpy"_ms } }, + }; + const auto outcome = libsolv::Solver().solve(libsolv_db, request); + + REQUIRE(outcome.has_value()); + REQUIRE(std::holds_alternative(outcome.value())); + const auto& solution = std::get(outcome.value()); + + REQUIRE_FALSE(solution.actions.empty()); + + // Numpy is last because of topological sort + REQUIRE(std::holds_alternative(solution.actions.back())); + REQUIRE(std::get(solution.actions.back()).install.name == "numpy"); + REQUIRE(find_actions_with_name(solution, "numpy").size() == 1); + + const auto python_actions = find_actions_with_name(solution, "python"); + REQUIRE(python_actions.size() == 1); + REQUIRE(std::holds_alternative(python_actions.front())); + + resolvo::Vector requirements = { + resolvo_db.alloc_version_set("numpy"), + }; + + resolvo::Vector constraints = {}; + resolvo::Vector result; + String reason = resolvo::solve(resolvo_db, requirements, constraints, result); + + REQUIRE(reason == ""); + REQUIRE(result.size() == 31); + REQUIRE(resolvo_db.solvable_pool[result[0]].name == "numpy"); + } + + SECTION("scikit-learn") + { + const auto request = Request{ + /* .flags= */ {}, + /* .jobs= */ { Request::Install{ "scikit-learn"_ms } }, + }; + + const auto outcome = libsolv::Solver().solve(libsolv_db, request); + + REQUIRE(outcome.has_value()); + REQUIRE(std::holds_alternative(outcome.value())); + const auto& solution = std::get(outcome.value()); + + REQUIRE_FALSE(solution.actions.empty()); + + // scikit-learn is last because of topological sort + REQUIRE(std::holds_alternative(solution.actions.back())); + REQUIRE(std::get(solution.actions.back()).install.name == "scikit-learn"); + REQUIRE(find_actions_with_name(solution, "scikit-learn").size() == 1); + + const auto python_actions = find_actions_with_name(solution, "scikit-learn"); + REQUIRE(python_actions.size() == 1); + REQUIRE(std::holds_alternative(python_actions.front())); + + resolvo::Vector requirements = { + resolvo_db.alloc_version_set("scikit-learn"), + }; + + resolvo::Vector constraints = {}; + resolvo::Vector result; + String reason = resolvo::solve(resolvo_db, requirements, constraints, result); + + REQUIRE(reason == ""); + REQUIRE(result.size() == 36); + REQUIRE(resolvo_db.solvable_pool[result[0]].name == "scikit-learn"); + } + + SECTION("scikit-learn explicit") + { + // Note: currently, pip is added to the environment when python is added + // we add it here to make resolvo's results consistent with libsolv's. + std::vector specs_to_install = { "scikit-learn==1.6.1=py313h8ef605b_0", "pip" }; + + std::vector known_resolution = { + PackageInfo("_libgcc_mutex", "0.1", "conda_forge", 0), + PackageInfo("_openmp_mutex", "4.5", "2_gnu", 0), + PackageInfo("bzip2", "1.0.8", "h4bc722e_7", 0), + PackageInfo("ca-certificates", "2025.1.31", "hbcca054_0", 0), + PackageInfo("joblib", "1.4.2", "pyhd8ed1ab_1", 0), + PackageInfo("ld_impl_linux-64", "2.43", "h712a8e2_2", 0), + PackageInfo("libblas", "3.9.0", "28_h59b9bed_openblas", 0), + PackageInfo("libcblas", "3.9.0", "28_he106b2a_openblas", 0), + PackageInfo("libexpat", "2.6.4", "h5888daf_0", 0), + PackageInfo("libffi", "3.4.6", "h2dba641_0", 0), + PackageInfo("libgcc", "14.2.0", "h77fa898_1", 0), + PackageInfo("libgcc-ng", "14.2.0", "h69a702a_1", 0), + PackageInfo("libgfortran", "14.2.0", "h69a702a_1", 0), + PackageInfo("libgfortran5", "14.2.0", "hd5240d6_1", 0), + PackageInfo("libgomp", "14.2.0", "h77fa898_1", 0), + PackageInfo("liblapack", "3.9.0", "28_h7ac8fdf_openblas", 0), + PackageInfo("liblzma", "5.6.4", "hb9d3cd8_0", 0), + PackageInfo("libmpdec", "4.0.0", "h4bc722e_0", 0), + PackageInfo("libopenblas", "0.3.28", "pthreads_h94d23a6_1", 0), + PackageInfo("libsqlite", "3.48.0", "hee588c1_1", 0), + PackageInfo("libstdcxx", "14.2.0", "hc0a3c3a_1", 0), + PackageInfo("libuuid", "2.38.1", "h0b41bf4_0", 0), + PackageInfo("libzlib", "1.3.1", "hb9d3cd8_2", 0), + PackageInfo("ncurses", "6.5", "h2d0b736_3", 0), + PackageInfo("numpy", "2.2.3", "py313h17eae1a_0", 0), + PackageInfo("openssl", "3.4.1", "h7b32b05_0", 0), + // Omitted as added by the environment creation + PackageInfo("pip", "25.0.1", "pyh145f28c_0", 0), + PackageInfo("python", "3.13.2", "hf636f53_100_cp313", 0), + PackageInfo("python_abi", "3.13", "5_cp313", 0), + PackageInfo("readline", "8.2", "h8228510_1", 0), + PackageInfo("scikit-learn", "1.6.1", "py313h8ef605b_0", 0), + PackageInfo("scipy", "1.15.1", "py313h750cbce_0", 0), + PackageInfo("setuptools", "75.8.0", "pyhff2d567_0", 0), + PackageInfo("threadpoolctl", "3.5.0", "pyhc1e730c_0", 0), + PackageInfo("tk", "8.6.13", "noxft_h4845f30_101", 0), + PackageInfo("tzdata", "2025a", "h78e105d_0", 0), + }; + + std::sort( + known_resolution.begin(), + known_resolution.end(), + [&](const PackageInfo& a, const PackageInfo& b) { return a.name < b.name; } + ); + + // libsolv's specification and resolution + + Request::job_list jobs; + + std::transform( + specs_to_install.begin(), + specs_to_install.end(), + std::back_inserter(jobs), + [](const std::string& spec) + { return Request::Install{ MatchSpec::parse(spec).value() }; } + ); + + const auto request = Request{ + /* .flags= */ {}, + /* .jobs= */ jobs, + }; + + const auto outcome = libsolv::Solver().solve(libsolv_db, request); + + REQUIRE(outcome.has_value()); + REQUIRE(std::holds_alternative(outcome.value())); + const auto& solution = std::get(outcome.value()); + + REQUIRE(solution.actions.size() == known_resolution.size()); + + std::vector libsolv_resolution = extract_package_to_install(solution); + std::sort( + libsolv_resolution.begin(), + libsolv_resolution.end(), + [&](const PackageInfo& a, const PackageInfo& b) { return a.name < b.name; } + ); + + // resolvo's specification and resolution + resolvo::Vector requirements; + for (const auto& spec : specs_to_install) + { + requirements.push_back(resolvo_db.alloc_version_set(spec)); + } + + resolvo::Vector constraints = {}; + resolvo::Vector result; + String reason = resolvo::solve(resolvo_db, requirements, constraints, result); + + REQUIRE(reason == ""); + REQUIRE(result.size() == known_resolution.size()); + + std::vector resolvo_resolution; + std::transform( + result.begin(), + result.end(), + std::back_inserter(resolvo_resolution), + [&](const resolvo::SolvableId& solvable_id) + { return resolvo_db.solvable_pool[solvable_id]; } + ); + + std::sort( + resolvo_resolution.begin(), + resolvo_resolution.end(), + [&](const PackageInfo& a, const PackageInfo& b) { return a.name < b.name; } + ); + + // Check libsolv's PackageInfo against the know resolution + for (size_t i = 0; i < libsolv_resolution.size(); i++) + { + const PackageInfo& package_info = libsolv_resolution[i]; + const PackageInfo& known_package_info = known_resolution[i]; + REQUIRE(package_info.name == known_package_info.name); + REQUIRE(package_info.version == known_package_info.version); + REQUIRE(package_info.build_string == known_package_info.build_string); + } + + // Check resolvo's PackageInfo against the know resolution + for (size_t i = 0; i < resolvo_resolution.size(); i++) + { + const PackageInfo& package_info = resolvo_resolution[i]; + const PackageInfo& known_package_info = known_resolution[i]; + REQUIRE(package_info.name == known_package_info.name); + REQUIRE(package_info.version == known_package_info.version); + REQUIRE(package_info.build_string == known_package_info.build_string); + } + } + + SECTION("Known hard specifications") + { + for (const std::vector& specs_to_install : + std::initializer_list>{ + // See: https://github.com/mamba-org/rattler/issues/684 + // {"arrow-cpp", "libabseil"}, + // {"mlflow=2.12.2"}, + // {"orange3=3.36.2"}, + // {"ray-dashboard=2.6.3"}, + // {"ray-default=2.6.3"}, + // {"spark-nlp=5.1.2"}, + // {"spyder=5.5.1"}, + // {"streamlit-faker=0.0.2"}, + // // See: + // https://github.com/conda-forge/rubinenv-feedstock/blob/main/recipe/meta.yaml#L45-L191 + // {"rubin-env-nosysroot"}, + // {"rubin-env"}, + // {"rubin-env-rsp"}, + // {"rubin-env-developer"} + }) + { + // See: https://github.com/mamba-org/rattler/issues/684 + std::vector libsolv_resolution = libsolv_resolve(specs_to_install); + + // Print all the packages from libsolv + std::cout << "libsolv resolution:" << std::endl; + for (const auto& package_info : libsolv_resolution) + { + std::cout << " - " << package_info.long_str() << std::endl; + } + + std::cout << std::endl; + std::vector resolvo_resolution = resolvo_resolve(specs_to_install); + + // Print all the packages from resolvo + std::cout << "resolvo resolution:" << std::endl; + for (const auto& package_info : resolvo_resolution) + { + std::cout << " - " << package_info.long_str() << std::endl; + } + + REQUIRE(resolvo_resolution.size() > 0); + REQUIRE(libsolv_resolution.size() > 0); + + // Check libsolv's PackageInfo against libsolv's + REQUIRE(resolvo_resolution.size() == libsolv_resolution.size()); + for (size_t i = 0; i < std::min(resolvo_resolution.size(), libsolv_resolution.size()); i++) + { + const PackageInfo& resolvo_package_info = resolvo_resolution[i]; + const PackageInfo& libsolv_package_info = libsolv_resolution[i]; + // Currently something in the parsing of the repodata.json must be different. + // TODO: find the difference and use `PackageInfo::operator==` instead + REQUIRE(resolvo_package_info.name == libsolv_package_info.name); + REQUIRE(resolvo_package_info.version == libsolv_package_info.version); + REQUIRE(resolvo_package_info.build_string == libsolv_package_info.build_string); + } + } + } + + SECTION("Find the highest version of hypothesis") + { + // Some builds of hypothesis depends on attrs and vice-versa + // We test that this complete correctly. + auto vid = resolvo_db.alloc_version_set("hypothesis"); + auto [version, n_track_features] = resolvo_db.find_highest_version(vid); + REQUIRE(n_track_features == 0); + std::cout << "Version: " << version.to_string() << std::endl; + REQUIRE(version > Version::parse("6.105.1").value()); + } + + SECTION("Consistency with libsolv: Celery & Dash") + { + std::vector specs_to_install = { "celery", + "dash", + "dash-core-components", + "dash-html-components", + "dash-table" }; + + // Print all the dependencies + std::cout << "Specification to install:" << std::endl; + for (const auto& dep : specs_to_install) + { + std::cout << " - " << dep << std::endl; + } + + std::vector libsolv_resolution = libsolv_resolve(specs_to_install); + + // Print all the packages from libsolv + std::cout << "libsolv resolution:" << std::endl; + for (const auto& package_info : libsolv_resolution) + { + std::cout << " - " << package_info.long_str() << std::endl; + } + + std::cout << std::endl; + + std::vector resolvo_resolution = resolvo_resolve(specs_to_install); + + // Print all the packages from resolvo + std::cout << "resolvo resolution:" << std::endl; + for (const auto& package_info : resolvo_resolution) + { + std::cout << " - " << package_info.long_str() << std::endl; + } + + std::cout << std::endl; + + // Check libsolv's PackageInfo against libsolv's + REQUIRE(resolvo_resolution.size() == libsolv_resolution.size()); + for (size_t i = 0; i < libsolv_resolution.size(); i++) + { + const PackageInfo& resolvo_package_info = resolvo_resolution[i]; + const PackageInfo& libsolv_package_info = libsolv_resolution[i]; + // Currently something in the parsing of the repodata.json must be different. + // TODO: find the difference and use `PackageInfo::operator==` instead + REQUIRE(resolvo_package_info.name == libsolv_package_info.name); + REQUIRE(resolvo_package_info.version == libsolv_package_info.version); + REQUIRE(resolvo_package_info.build_string == libsolv_package_info.build_string); + } + } +} From 8200dc0e8b3c35a82af5edcafd4428588292040d Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Thu, 7 May 2026 14:59:39 +0200 Subject: [PATCH 02/20] Add configuration and CLI flag Signed-off-by: Julien Jerphanion --- libmamba/src/api/configuration.cpp | 36 ++++++++++++++++++++++++++++++ libmamba/src/api/install.cpp | 7 +++++- libmamba/src/api/update.cpp | 7 +++++- libmamba/src/api/utils.cpp | 8 ++++--- libmamba/src/api/utils.hpp | 3 ++- micromamba/src/common_options.cpp | 9 ++++++++ 6 files changed, 64 insertions(+), 6 deletions(-) diff --git a/libmamba/src/api/configuration.cpp b/libmamba/src/api/configuration.cpp index f1e2ae3251..274d1e9732 100644 --- a/libmamba/src/api/configuration.cpp +++ b/libmamba/src/api/configuration.cpp @@ -5,6 +5,7 @@ // The full license is in the file LICENSE, distributed with this software. #include +#include #include #include @@ -1394,6 +1395,41 @@ namespace mamba ) .set_env_var_names()); + insert(Configurable("solver", std::string("libsolv")) + .group("Basic") + .description("Solver backend to use (`libsolv` or `resolvo`).") + .set_rc_configurable() + .set_env_var_names() + .set_post_merge_hook( + [&](std::string& value) + { + std::string normalized = value; + std::transform( + normalized.begin(), + normalized.end(), + normalized.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); } + ); + + if (normalized == "libsolv") + { + m_context.experimental_resolvo_solver = false; + value = normalized; + return; + } + if (normalized == "resolvo") + { + m_context.experimental_resolvo_solver = true; + value = normalized; + return; + } + + LOG_ERROR << "Invalid value for `solver`: " << value + << ". Expected `libsolv` or `resolvo`."; + throw std::runtime_error("Aborting."); + } + )); + insert(Configurable("debug", &m_context.debug) .group("Basic") .set_env_var_names() diff --git a/libmamba/src/api/install.cpp b/libmamba/src/api/install.cpp index 6b145ebe1f..2649de3cc0 100644 --- a/libmamba/src/api/install.cpp +++ b/libmamba/src/api/install.cpp @@ -608,7 +608,12 @@ namespace mamba // Console stream prints on destruction } - auto outcome = solve_request_with_status(ctx.experimental_matchspec_parsing, db, request); + auto outcome = solve_request_with_status( + ctx.experimental_matchspec_parsing, + db, + request, + ctx.experimental_resolvo_solver ? "resolvo" : "libsolv" + ); if (handle_unsolvable_with_retry( outcome, diff --git a/libmamba/src/api/update.cpp b/libmamba/src/api/update.cpp index c8cdce8a73..9f99ae950e 100644 --- a/libmamba/src/api/update.cpp +++ b/libmamba/src/api/update.cpp @@ -186,7 +186,12 @@ namespace mamba // Console stream prints on destruction } - auto outcome = solve_request_with_status(ctx.experimental_matchspec_parsing, db, request); + auto outcome = solve_request_with_status( + ctx.experimental_matchspec_parsing, + db, + request, + ctx.experimental_resolvo_solver ? "resolvo" : "libsolv" + ); if (handle_unsolvable_with_retry( outcome, diff --git a/libmamba/src/api/utils.cpp b/libmamba/src/api/utils.cpp index 98769076f4..3591260edf 100644 --- a/libmamba/src/api/utils.cpp +++ b/libmamba/src/api/utils.cpp @@ -485,13 +485,15 @@ namespace mamba solver::libsolv::Solver::Outcome solve_request_with_status( bool experimental_matchspec_parsing, solver::libsolv::Database& db, - const solver::Request& request + const solver::Request& request, + std::string_view solver_name ) { + const auto resolving_label = fmt::format("Resolving Environment with {}", solver_name); if (Console::can_report_status()) { Console::instance().print_in_place( - fmt::format("{:<85} {:>20}", "Resolving Environment", "⧖ Starting") + fmt::format("{:<85} {:>20}", resolving_label, "⧖ Starting") ); } const auto started_at = std::chrono::steady_clock::now(); @@ -509,7 +511,7 @@ namespace mamba Console::instance().print_in_place( fmt::format( "{:<85} {:>20}", - "Resolving Environment", + resolving_label, done_with_duration(std::chrono::steady_clock::now() - started_at) ), true diff --git a/libmamba/src/api/utils.hpp b/libmamba/src/api/utils.hpp index ba61ad4a54..178c67da6c 100644 --- a/libmamba/src/api/utils.hpp +++ b/libmamba/src/api/utils.hpp @@ -117,7 +117,8 @@ namespace mamba solver::libsolv::Solver::Outcome solve_request_with_status( bool experimental_matchspec_parsing, solver::libsolv::Database& db, - const solver::Request& request + const solver::Request& request, + std::string_view solver_name = "libsolv" ); /** diff --git a/micromamba/src/common_options.cpp b/micromamba/src/common_options.cpp index 5c2de81717..f7670b26c7 100644 --- a/micromamba/src/common_options.cpp +++ b/micromamba/src/common_options.cpp @@ -472,6 +472,15 @@ init_install_options(CLI::App* subcom, Configuration& config) repo_parsing.description() ); + auto& solver = config.at("solver"); + static const std::map solver_map = { + { "libsolv", "libsolv" }, + { "resolvo", "resolvo" }, + }; + subcom->add_option("--solver", solver.get_cli_config(), solver.description()) + ->transform(CLI::CheckedTransformer(solver_map, CLI::ignore_case)) + ->option_text("libsolv|resolvo"); + auto& platform = config.at("platform"); subcom->add_option("--platform", platform.get_cli_config(), platform.description()) ->option_text("PLATFORM"); From 0ced4f4d6557f90cc9e353c86c79e122ea125f71 Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Thu, 7 May 2026 15:05:23 +0200 Subject: [PATCH 03/20] Solver switch Signed-off-by: Julien Jerphanion --- libmamba/src/api/install.cpp | 2 +- libmamba/src/api/update.cpp | 2 +- libmamba/src/api/utils.cpp | 50 ++++++++++++++++++++++++++++-------- libmamba/src/api/utils.hpp | 2 +- 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/libmamba/src/api/install.cpp b/libmamba/src/api/install.cpp index 2649de3cc0..2d29c64324 100644 --- a/libmamba/src/api/install.cpp +++ b/libmamba/src/api/install.cpp @@ -612,7 +612,7 @@ namespace mamba ctx.experimental_matchspec_parsing, db, request, - ctx.experimental_resolvo_solver ? "resolvo" : "libsolv" + ctx.experimental_resolvo_solver ); if (handle_unsolvable_with_retry( diff --git a/libmamba/src/api/update.cpp b/libmamba/src/api/update.cpp index 9f99ae950e..f8fe376fed 100644 --- a/libmamba/src/api/update.cpp +++ b/libmamba/src/api/update.cpp @@ -190,7 +190,7 @@ namespace mamba ctx.experimental_matchspec_parsing, db, request, - ctx.experimental_resolvo_solver ? "resolvo" : "libsolv" + ctx.experimental_resolvo_solver ); if (handle_unsolvable_with_retry( diff --git a/libmamba/src/api/utils.cpp b/libmamba/src/api/utils.cpp index 3591260edf..32ea404757 100644 --- a/libmamba/src/api/utils.cpp +++ b/libmamba/src/api/utils.cpp @@ -33,7 +33,10 @@ #include "mamba/core/util_os.hpp" #include "mamba/fs/filesystem.hpp" #include "mamba/solver/libsolv/database.hpp" +#include "mamba/solver/libsolv/solver.hpp" #include "mamba/solver/request.hpp" +#include "mamba/solver/resolvo/database.hpp" +#include "mamba/solver/resolvo/solver.hpp" #include "mamba/specs/match_spec.hpp" #include "mamba/specs/version_spec.hpp" #include "mamba/util/environment.hpp" @@ -486,9 +489,11 @@ namespace mamba bool experimental_matchspec_parsing, solver::libsolv::Database& db, const solver::Request& request, - std::string_view solver_name + bool use_resolvo ) { + const auto solver_name = use_resolvo ? std::string_view("resolvo") + : std::string_view("libsolv"); const auto resolving_label = fmt::format("Resolving Environment with {}", solver_name); if (Console::can_report_status()) { @@ -497,15 +502,40 @@ namespace mamba ); } const auto started_at = std::chrono::steady_clock::now(); - auto outcome = solver::libsolv::Solver() - .solve( - db, - request, - experimental_matchspec_parsing - ? solver::libsolv::MatchSpecParser::Mamba - : solver::libsolv::MatchSpecParser::Mixed - ) - .value(); + auto outcome = [&]() -> solver::libsolv::Solver::Outcome + { + if (!use_resolvo) + { + LOG_DEBUG << "Using solver backend: libsolv"; + return solver::libsolv::Solver() + .solve( + db, + request, + experimental_matchspec_parsing ? solver::libsolv::MatchSpecParser::Mamba + : solver::libsolv::MatchSpecParser::Mixed + ) + .value(); + } + + LOG_DEBUG << "Using solver backend: resolvo"; + solver::resolvo::Database resolvo_db(db.channel_params()); + auto all_packages_spec = specs::MatchSpec::parse("*") + .or_else([](specs::ParseError&& err) + { throw std::move(err); }) + .value(); + std::vector packages; + db.for_each_package_matching( + all_packages_spec, + [&](specs::PackageInfo&& pkg) + { + packages.emplace_back(std::move(pkg)); + return util::LoopControl::Continue; + } + ); + resolvo_db.add_repo_from_packages(packages, "all", false); + auto resolvo_outcome = solver::resolvo::Solver().solve(resolvo_db, request).value(); + return solver::libsolv::Solver::Outcome{ std::get(resolvo_outcome) }; + }(); if (Console::can_report_status()) { Console::instance().print_in_place( diff --git a/libmamba/src/api/utils.hpp b/libmamba/src/api/utils.hpp index 178c67da6c..b1274131bc 100644 --- a/libmamba/src/api/utils.hpp +++ b/libmamba/src/api/utils.hpp @@ -118,7 +118,7 @@ namespace mamba bool experimental_matchspec_parsing, solver::libsolv::Database& db, const solver::Request& request, - std::string_view solver_name = "libsolv" + bool use_resolvo = false ); /** From 48b3651d58e6b6d62334608114df723ccfdcaf45 Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Thu, 7 May 2026 15:09:37 +0200 Subject: [PATCH 04/20] Avoid virtual packages Signed-off-by: Julien Jerphanion --- libmamba/src/api/utils.cpp | 10 ++++++++-- libmamba/src/solver/resolvo/solver.cpp | 5 +++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/libmamba/src/api/utils.cpp b/libmamba/src/api/utils.cpp index 32ea404757..20ab7127ec 100644 --- a/libmamba/src/api/utils.cpp +++ b/libmamba/src/api/utils.cpp @@ -533,8 +533,14 @@ namespace mamba } ); resolvo_db.add_repo_from_packages(packages, "all", false); - auto resolvo_outcome = solver::resolvo::Solver().solve(resolvo_db, request).value(); - return solver::libsolv::Solver::Outcome{ std::get(resolvo_outcome) }; + auto maybe_resolvo_outcome = solver::resolvo::Solver().solve(resolvo_db, request); + if (!maybe_resolvo_outcome) + { + throw maybe_resolvo_outcome.error(); + } + return solver::libsolv::Solver::Outcome{ + std::get(std::move(maybe_resolvo_outcome).value()) + }; }(); if (Console::can_report_status()) { diff --git a/libmamba/src/solver/resolvo/solver.cpp b/libmamba/src/solver/resolvo/solver.cpp index a00e039153..100e7d59a3 100644 --- a/libmamba/src/solver/resolvo/solver.cpp +++ b/libmamba/src/solver/resolvo/solver.cpp @@ -6,6 +6,7 @@ #include "mamba/solver/resolvo/database.hpp" #include "mamba/solver/resolvo/solver.hpp" +#include "mamba/util/string.hpp" #include "mamba/util/variant_cmp.hpp" namespace mamba::solver::resolvo @@ -95,6 +96,10 @@ namespace mamba::solver::resolvo for (const auto& solvable_id : result) { const auto& solvable = database.solvable_pool[solvable_id]; + if (util::starts_with(solvable.name, "__")) + { + continue; + } specs::PackageInfo pkg; pkg.name = solvable.name; pkg.version = solvable.version; From 0623fe3033b901d6631d25680e82cbc3cd803d75 Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Thu, 7 May 2026 15:40:30 +0200 Subject: [PATCH 05/20] Resolution failure handling Signed-off-by: Julien Jerphanion --- libmamba/src/api/install.cpp | 39 +++++++++++-- libmamba/src/api/update.cpp | 27 +++++++-- libmamba/src/api/utils.cpp | 104 ++++++++++++++++++++--------------- 3 files changed, 113 insertions(+), 57 deletions(-) diff --git a/libmamba/src/api/install.cpp b/libmamba/src/api/install.cpp index 2d29c64324..e7a07dd279 100644 --- a/libmamba/src/api/install.cpp +++ b/libmamba/src/api/install.cpp @@ -608,12 +608,39 @@ namespace mamba // Console stream prints on destruction } - auto outcome = solve_request_with_status( - ctx.experimental_matchspec_parsing, - db, - request, - ctx.experimental_resolvo_solver - ); + solver::libsolv::Solver::Outcome outcome; + try + { + outcome = solve_request_with_status( + ctx.experimental_matchspec_parsing, + db, + request, + ctx.experimental_resolvo_solver + ); + } + catch (const mamba_error& error) + { + if (ctx.experimental_resolvo_solver + && (error.error_code() == mamba_error_code::satisfiablitity_error) && !is_retry) + { + bool retry = true; + install_specs_impl( + ctx, + channel_context, + config, + raw_specs, + create_env, + remove_prefix_on_failure, + retry + ); + return; + } + if (ctx.experimental_resolvo_solver && freeze_installed) + { + Console::instance().print("Possible hints:\n - 'freeze_installed' is turned on\n"); + } + throw; + } if (handle_unsolvable_with_retry( outcome, diff --git a/libmamba/src/api/update.cpp b/libmamba/src/api/update.cpp index f8fe376fed..80d96e6fbb 100644 --- a/libmamba/src/api/update.cpp +++ b/libmamba/src/api/update.cpp @@ -186,12 +186,27 @@ namespace mamba // Console stream prints on destruction } - auto outcome = solve_request_with_status( - ctx.experimental_matchspec_parsing, - db, - request, - ctx.experimental_resolvo_solver - ); + solver::libsolv::Solver::Outcome outcome; + try + { + outcome = solve_request_with_status( + ctx.experimental_matchspec_parsing, + db, + request, + ctx.experimental_resolvo_solver + ); + } + catch (const mamba_error& error) + { + if (ctx.experimental_resolvo_solver + && (error.error_code() == mamba_error_code::satisfiablitity_error) && !is_retry) + { + bool retry = true; + update_impl(ctx, channel_context, config, raw_update_specs, update_params, retry); + return; + } + throw; + } if (handle_unsolvable_with_retry( outcome, diff --git a/libmamba/src/api/utils.cpp b/libmamba/src/api/utils.cpp index 20ab7127ec..a8d6a2ae62 100644 --- a/libmamba/src/api/utils.cpp +++ b/libmamba/src/api/utils.cpp @@ -502,58 +502,72 @@ namespace mamba ); } const auto started_at = std::chrono::steady_clock::now(); - auto outcome = [&]() -> solver::libsolv::Solver::Outcome + try { - if (!use_resolvo) + auto outcome = [&]() -> solver::libsolv::Solver::Outcome { - LOG_DEBUG << "Using solver backend: libsolv"; - return solver::libsolv::Solver() - .solve( - db, - request, - experimental_matchspec_parsing ? solver::libsolv::MatchSpecParser::Mamba - : solver::libsolv::MatchSpecParser::Mixed - ) - .value(); - } - - LOG_DEBUG << "Using solver backend: resolvo"; - solver::resolvo::Database resolvo_db(db.channel_params()); - auto all_packages_spec = specs::MatchSpec::parse("*") - .or_else([](specs::ParseError&& err) - { throw std::move(err); }) - .value(); - std::vector packages; - db.for_each_package_matching( - all_packages_spec, - [&](specs::PackageInfo&& pkg) + if (!use_resolvo) { - packages.emplace_back(std::move(pkg)); - return util::LoopControl::Continue; + LOG_DEBUG << "Using solver backend: libsolv"; + return solver::libsolv::Solver() + .solve( + db, + request, + experimental_matchspec_parsing ? solver::libsolv::MatchSpecParser::Mamba + : solver::libsolv::MatchSpecParser::Mixed + ) + .value(); } - ); - resolvo_db.add_repo_from_packages(packages, "all", false); - auto maybe_resolvo_outcome = solver::resolvo::Solver().solve(resolvo_db, request); - if (!maybe_resolvo_outcome) - { - throw maybe_resolvo_outcome.error(); + + LOG_DEBUG << "Using solver backend: resolvo"; + solver::resolvo::Database resolvo_db(db.channel_params()); + auto all_packages_spec = specs::MatchSpec::parse("*") + .or_else([](specs::ParseError&& err) + { throw std::move(err); }) + .value(); + std::vector packages; + db.for_each_package_matching( + all_packages_spec, + [&](specs::PackageInfo&& pkg) + { + packages.emplace_back(std::move(pkg)); + return util::LoopControl::Continue; + } + ); + resolvo_db.add_repo_from_packages(packages, "all", false); + auto maybe_resolvo_outcome = solver::resolvo::Solver().solve(resolvo_db, request); + if (!maybe_resolvo_outcome) + { + throw maybe_resolvo_outcome.error(); + } + return solver::libsolv::Solver::Outcome{ + std::get(std::move(maybe_resolvo_outcome).value()) + }; + }(); + if (Console::can_report_status()) + { + Console::instance().print_in_place( + fmt::format( + "{:<85} {:>20}", + resolving_label, + done_with_duration(std::chrono::steady_clock::now() - started_at) + ), + true + ); } - return solver::libsolv::Solver::Outcome{ - std::get(std::move(maybe_resolvo_outcome).value()) - }; - }(); - if (Console::can_report_status()) + return outcome; + } + catch (...) { - Console::instance().print_in_place( - fmt::format( - "{:<85} {:>20}", - resolving_label, - done_with_duration(std::chrono::steady_clock::now() - started_at) - ), - true - ); + if (Console::can_report_status()) + { + Console::instance().print_in_place( + fmt::format("{:<85} {:>20}", resolving_label, "✗ Failed"), + true + ); + } + throw; } - return outcome; } solver::libsolv::Database From d083cb064392e9eed9d0488dfe7ef8000626e02f Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Thu, 7 May 2026 16:29:07 +0200 Subject: [PATCH 06/20] Update resolvo C++ tests Signed-off-by: Julien Jerphanion --- .../tests/src/solver/resolvo/test_solver.cpp | 1103 ++--------------- 1 file changed, 123 insertions(+), 980 deletions(-) diff --git a/libmamba/tests/src/solver/resolvo/test_solver.cpp b/libmamba/tests/src/solver/resolvo/test_solver.cpp index caa6191b05..fe243f89ec 100644 --- a/libmamba/tests/src/solver/resolvo/test_solver.cpp +++ b/libmamba/tests/src/solver/resolvo/test_solver.cpp @@ -23,6 +23,8 @@ #include "mamba/solver/libsolv/database.hpp" #include "mamba/solver/libsolv/parameters.hpp" // for PackageTypes #include "mamba/solver/libsolv/solver.hpp" +#include "mamba/solver/resolvo/database.hpp" +#include "mamba/solver/resolvo/solver.hpp" #include "mambatests.hpp" @@ -33,811 +35,6 @@ using namespace mamba::solver; using namespace resolvo; -template <> -struct std::hash -{ - std::size_t operator()(const VersionSetId& id) const - { - return std::hash{}(id.id); - } -}; - -template <> -struct std::hash -{ - std::size_t operator()(const SolvableId& id) const - { - return std::hash{}(id.id); - } -}; - -template <> -struct std::hash -{ - std::size_t operator()(const NameId& id) const - { - return std::hash{}(id.id); - } -}; - -template <> -struct std::hash -{ - std::size_t operator()(const StringId& id) const - { - return std::hash{}(id.id); - } -}; - -// Create a template Pool class that maps a key to a set of values -template -struct bijective_map -{ - bijective_map() = default; - ~bijective_map() = default; - - /** - * Adds the value to the bijective_map and returns its associated id. If the - * value is already in the bijective_map, 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(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 bijective_map - 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 value_to_id; - std::unordered_map id_to_value; -}; - -struct PackageDatabase : public DependencyProvider -{ - virtual ~PackageDatabase() = default; - - ::bijective_map name_pool; - ::bijective_map string_pool; - - // MatchSpec are VersionSet in resolvo's semantics - ::bijective_map version_set_pool; - - // PackageInfo are Solvable in resolvo's semantics - ::bijective_map solvable_pool; - - // PackageName to Vector - std::unordered_map> name_to_solvable; - - // VersionSetId to max version - // TODO use `SolvableId` instead of `std::pair`? - std::unordered_map> - version_set_to_max_version_and_track_features_numbers; - - /** - * Allocates a new requirement and return the id of the requirement. - */ - VersionSetId 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 `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" }) - { - 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 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 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 version_sets; - for (const std::string& ms : match_specs) - { - alloc_version_set(ms); - } - // Placeholder return value - return 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 MatchSpec match_spec = 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().to_string(); - name_pool.alloc(String{ name }); - string_pool.alloc(String{ name }); - - // Add the MatchSpec's string representation to the Name and String pools - const std::string match_spec_str = match_spec.to_string(); - name_pool.alloc(String{ match_spec_str }); - string_pool.alloc(String{ match_spec_str }); - return id; - } - - SolvableId alloc_solvable(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(String{ name }); - string_pool.alloc(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(String{ long_str }); - string_pool.alloc(String{ long_str }); - - for (auto& dep : package_info.dependencies) - { - alloc_version_set(dep); - } - for (auto& constr : package_info.constrains) - { - alloc_version_set(constr); - } - - // const size_t n_track_features = package_info.track_features.size(); - // if(n_track_features > 0) - // { - // std::cout << "PackageInfo has " << package_info.long_str() << " has " << - // n_track_features << " track features" << std::endl; for (auto tf - // :package_info.track_features) - // { - // // Add the track feature to the Name and String pools - // std::cout << " - " << tf < solvable) override - { - std::string result; - for (auto& solvable_id : solvable) - { - result += solvable_pool[solvable_id].long_str(); - } - return String{ result }; - } - - /** - * Returns an object that can be used to display the given name in a - * user-friendly way. - */ - String display_name(NameId name) override - { - return name_pool[name]; - } - - /** - * Returns a user-friendly string representation of the specified version - * set. - * - * The name of the package should *not* be included in the display. Where - * appropriate, this information is added. - */ - String display_version_set(VersionSetId version_set) override - { - const MatchSpec match_spec = version_set_pool[version_set]; - return String{ match_spec.to_string() }; - } - - /** - * Returns the string representation of the specified string. - */ - String display_string(StringId string) override - { - return string_pool[string]; - } - - /** - * Returns the name of the package that the specified version set is - * associated with. - */ - NameId version_set_name(VersionSetId version_set_id) override - { - const MatchSpec match_spec = version_set_pool[version_set_id]; - // std::cout << "Getting name id for version_set_id " << match_spec.name().to_string() << - // std::endl; - return name_pool[String{ match_spec.name().to_string() }]; - } - - /** - * Returns the name of the package for the given solvable. - */ - NameId solvable_name(SolvableId solvable_id) override - { - const PackageInfo& package_info = solvable_pool[solvable_id]; - // std::cout << "Getting name id for solvable " << package_info.long_str() << std::endl; - return name_pool[String{ package_info.name }]; - } - - /** - * Obtains a list of solvables that should be considered when a package - * with the given name is requested. - */ - Candidates get_candidates(NameId package) override - { - Candidates candidates{}; - candidates.favored = nullptr; - candidates.locked = nullptr; - candidates.candidates = name_to_solvable[package]; - return candidates; - } - - std::pair find_highest_version(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 MatchSpec match_spec = version_set_pool[version_set_id]; - - const std::string& name = match_spec.name().to_string(); - - auto name_id = name_pool.alloc(String{ name }); - - auto solvables = name_to_solvable[name_id]; - - auto filtered = filter_candidates(solvables, version_set_id, false); - - Version max_version = Version(); - size_t max_version_n_track_features = 0; - - for (auto& solvable_id : filtered) - { - const PackageInfo& package_info = solvable_pool[solvable_id]; - const auto version = 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; - } - - /** - * Sort the specified solvables based on which solvable to try first. The - * solver will iteratively try to select the highest version. If a - * conflict is found with the highest version the next version is - * tried. This continues until a solution is found. - */ - void sort_candidates(Slice solvables) override - { - std::sort( - solvables.begin(), - solvables.end(), - [&](const SolvableId& a, const SolvableId& b) - { - const PackageInfo& package_info_a = solvable_pool[a]; - const 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 = Version::parse(package_info_a.version).value(); - const auto b_version = 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 a_deps; - std::unordered_map b_deps; - for (auto dep_a : package_info_a.dependencies) - { - // TODO: have a VersionID to NameID mapping instead - MatchSpec ms = MatchSpec::parse(dep_a).value(); - const std::string& name = ms.name().to_string(); - auto name_id = name_pool.alloc(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 - MatchSpec ms = MatchSpec::parse(dep_b).value(); - const std::string& name = ms.name().to_string(); - auto name_id = name_pool.alloc(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; - } - ); - } - - /** - * Given a set of solvables, return the solvables that match the given - * version set or if `inverse` is true, the solvables that do *not* match - * the version set. - */ - Vector - filter_candidates(Slice candidates, VersionSetId version_set_id, bool inverse) override - { - MatchSpec match_spec = version_set_pool[version_set_id]; - Vector filtered; - - // std::cout << "Candidates to filter " << match_spec.to_string() << std::endl; - // - // for(auto& solvable_id : candidates) { - // const PackageInfo& package_info = solvable_pool[solvable_id]; - // std::cout << " - " << package_info.long_str() << std::endl; - // } - - if (inverse) - { - for (auto& solvable_id : candidates) - { - const 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 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); - } - } - } - // std::cout << "Filtered candidates for " << match_spec.to_string() << std::endl; - // - // for(auto& solvable_id : filtered) { - // const PackageInfo& package_info = solvable_pool[solvable_id]; - // std::cout << " - " << package_info.long_str() << std::endl; - // } - - return filtered; - } - - /** - * Returns the dependencies for the specified solvable. - */ - Dependencies get_dependencies(SolvableId solvable_id) override - { - const PackageInfo& package_info = solvable_pool[solvable_id]; - // std::cout << "Getting dependencies for " << package_info.long_str() << std::endl; - - Dependencies dependencies; - - // TODO: do this in O(1) - for (auto& dep : package_info.dependencies) - { - // std::cout << "Parsing dep " << dep << std::endl; - const MatchSpec match_spec = MatchSpec::parse(dep).value(); - dependencies.requirements.push_back(version_set_pool[match_spec]); - } - for (auto& constr : package_info.constrains) - { - // std::cout << "Parsing constr " << constr << std::endl; - // 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 MatchSpec match_spec = MatchSpec::parse(constr2).value(); - dependencies.constrains.push_back(version_set_pool[match_spec]); - } - - return dependencies; - } -}; - -bool -parse_packageinfo_json( - const std::string_view& filename, - simdjson::ondemand::object& pkg, - const specs::CondaURL& repo_url, - const std::string& channel_id, - PackageDatabase& db -) -{ - auto maybe_package_info = PackageInfo::from_json(filename, pkg, repo_url, channel_id); - if (!maybe_package_info) - { - return false; - } - db.alloc_solvable(maybe_package_info.value()); - return true; -} - -void -parse_repodata_json( - PackageDatabase& db, - const fs::u8path& filename, - const std::string& repo_url, - const std::string& channel_id, - bool verify_artifacts -) -{ - auto parser = simdjson::ondemand::parser(); - const auto lock = LockFile(filename); - - // The json storage must be kept alive as long as we are reading the json data. - const auto json_content = simdjson::padded_string::load(filename.string()); - - // Note that with the "on-demand" parser, documents/values/objects act as iterators - // to go through the document. - auto repodata = parser.iterate(json_content); - - // Get `base_url` in case 'repodata_version': 2 - // cf. https://github.com/conda-incubator/ceps/blob/main/cep-15.md - const auto base_url = [&] - { - if (auto repodata_version = repodata["repodata_version"]; !repodata_version.error()) - { - if (auto info = repodata["info"]; !info.error()) - { - if (auto url = info["base_url"]; !url.error()) - { - return std::string(url.get_string().value_unsafe()); - } - } - } - - return repo_url; - }(); - - const auto parsed_url = specs::CondaURL::parse(base_url) - .or_else([](specs::ParseError&& err) { throw std::move(err); }) - .value(); - - // TODO: it does not seems resolvo can handle setting signatures on solvables for now - // auto signatures = [&] - // { - // auto maybe_sigs = repodata["signatures"]; - // if (!maybe_sigs.error() && verify_artifacts) - // { - // return std::make_optional(maybe_sigs); - // } - // else - // { - // LOG_DEBUG << "No signatures available or requested. Downloading without verifying - // artifacts."; return decltype(std::make_optional(maybe_sigs)){}; - // } - // }(); - - // Process packages.conda first - if (auto pkgs = repodata["packages.conda"]; !pkgs.error()) - { - if (auto packages_as_object = pkgs.get_object(); !packages_as_object.error()) - { - for (auto field : packages_as_object) - { - if (!field.error()) - { - const std::string key(field.unescaped_key().value()); - if (auto value = field.value(); !value.error()) - { - if (auto pkg_obj = value.get_object(); !pkg_obj.error()) - { - parse_packageinfo_json( - filename.string(), - pkg_obj.value(), - parsed_url, - channel_id, - db - ); - } - } - } - } - } - } - - // Then process packages - if (auto pkgs = repodata["packages"]; !pkgs.error()) - { - if (auto packages_as_object = pkgs.get_object(); !packages_as_object.error()) - { - for (auto field : packages_as_object) - { - if (!field.error()) - { - const std::string key(field.unescaped_key().value()); - if (auto value = field.value(); !value.error()) - { - if (auto pkg_obj = value.get_object(); !pkg_obj.error()) - { - parse_packageinfo_json( - filename.string(), - pkg_obj.value(), - parsed_url, - channel_id, - db - ); - } - } - } - } - } - } -} - // from `src/test_solver.cpp` auto find_actions_with_name(const Solution& solution, std::string_view name) @@ -888,37 +85,7 @@ create_libsolv_db() return libsolv_db; }; -PackageDatabase -create_resolvo_db() -{ - PackageDatabase resolvo_db; - - parse_repodata_json( - resolvo_db, - "/tmp/linux-64/repodata.json", - "https://conda.anaconda.org/conda-forge/linux-64/repodata.json", - "conda-forge", - false - ); - - parse_repodata_json( - resolvo_db, - "/tmp/noarch/repodata.json", - "https://conda.anaconda.org/conda-forge/noarch/repodata.json", - "conda-forge", - false - ); - - for (const auto& package : get_virtual_packages("linux-64")) - { - resolvo_db.alloc_solvable(package); - } - - return resolvo_db; -} - mamba::solver::libsolv::Database libsolv_db = create_libsolv_db(); -PackageDatabase resolvo_db = create_resolvo_db(); std::vector libsolv_resolve(const std::vector& specs) @@ -965,50 +132,62 @@ libsolv_resolve(const std::vector& specs) return {}; } -std::vector -resolvo_resolve(const std::vector& specs) +mamba::solver::resolvo::Database +create_resolvo_libmamba_db() { - // resolvo's specification and resolution - resolvo::Vector requirements; - for (const auto& spec : specs) - { - requirements.push_back(resolvo_db.alloc_version_set(spec)); - } + auto resolvo_db = mamba::solver::resolvo::Database( + { + /* .platforms= */ { "linux-64", "noarch" }, + /* .channel_alias= */ specs::CondaURL::parse("https://conda.anaconda.org/").value(), + } + ); - resolvo::Vector constraints = {}; - resolvo::Vector result; + resolvo_db.add_repo_from_repodata_json( + "/tmp/linux-64/repodata.json", + "https://conda.anaconda.org/conda-forge/linux-64", + "conda-forge", + false + ); + resolvo_db.add_repo_from_repodata_json( + "/tmp/noarch/repodata.json", + "https://conda.anaconda.org/conda-forge/noarch", + "conda-forge", + false + ); + resolvo_db.add_repo_from_packages(get_virtual_packages("linux-64"), "virtual", false); + return resolvo_db; +} - std::cout << "Start with resolvo" << std::endl; - auto tick_resolvo = std::chrono::steady_clock::now(); - String reason = resolvo::solve(resolvo_db, requirements, constraints, result); - auto tack_resolvo = std::chrono::steady_clock::now(); - std::cout << "End with resolvo" << std::endl; - std::cout - << "Elapsed time: " - << std::chrono::duration_cast(tack_resolvo - tick_resolvo).count() - << "ms" << std::endl; +mamba::solver::resolvo::Database resolvo_db = create_resolvo_libmamba_db(); - if (reason == "") - { - std::vector resolvo_resolution; - for (auto solvable_id : result) - { - PackageInfo package_info = resolvo_db.solvable_pool[solvable_id]; - // Skip virtual package (i.e. whose `package_info.name` starts with "__") - if (package_info.name.find("__") != 0) - { - resolvo_resolution.push_back(package_info); - } - } +std::vector +resolvo_libmamba_resolve(const std::vector& specs) +{ + auto resolvo_db = create_resolvo_libmamba_db(); - std::sort( - resolvo_resolution.begin(), - resolvo_resolution.end(), - [&](const PackageInfo& a, const PackageInfo& b) { return a.name < b.name; } - ); - return resolvo_resolution; - } - return {}; + Request::job_list jobs; + std::transform( + specs.begin(), + specs.end(), + std::back_inserter(jobs), + [](const std::string& spec) { return Request::Install{ MatchSpec::parse(spec).value() }; } + ); + + const auto request = Request{ + /* .flags= */ {}, + /* .jobs= */ jobs, + }; + + const auto outcome = mamba::solver::resolvo::Solver().solve(resolvo_db, request); + REQUIRE(outcome.has_value()); + const auto& solution = std::get(outcome.value()); + auto resolution = extract_package_to_install(solution); + std::sort( + resolution.begin(), + resolution.end(), + [&](const PackageInfo& a, const PackageInfo& b) { return a.name < b.name; } + ); + return resolution; } TEST_CASE("solver::resolvo") @@ -1019,7 +198,12 @@ TEST_CASE("solver::resolvo") SECTION("Addition of PackageInfo to PackageDatabase") { - PackageDatabase database; + mamba::solver::resolvo::Database database( + { + /* .platforms= */ { "linux-64", "noarch" }, + /* .channel_alias= */ specs::CondaURL::parse("https://conda.anaconda.org/").value(), + } + ); PackageInfo scikit_learn("scikit-learn", "1.5.0", "py310h981052a_0", 0); scikit_learn.dependencies.emplace_back("numpy >=1.20.0,<2.0a0"); @@ -1071,7 +255,12 @@ TEST_CASE("solver::resolvo") SECTION("Filter solvables") { - PackageDatabase database; + mamba::solver::resolvo::Database database( + { + /* .platforms= */ { "linux-64", "noarch" }, + /* .channel_alias= */ specs::CondaURL::parse("https://conda.anaconda.org/").value(), + } + ); PackageInfo skl0("scikit-learn", "1.4.0", "py310h981052a_0", 0); auto sol0 = database.alloc_solvable(skl0); @@ -1167,7 +356,12 @@ TEST_CASE("solver::resolvo") SECTION("Sort solvables increasing order") { - PackageDatabase database; + mamba::solver::resolvo::Database database( + { + /* .platforms= */ { "linux-64", "noarch" }, + /* .channel_alias= */ specs::CondaURL::parse("https://conda.anaconda.org/").value(), + } + ); PackageInfo skl0("scikit-learn", "1.5.2", "py310h981052a_0", 0); auto sol0 = database.alloc_solvable(skl0); @@ -1197,7 +391,12 @@ TEST_CASE("solver::resolvo") SECTION("Sort solvables (build number only)") { - PackageDatabase database; + mamba::solver::resolvo::Database database( + { + /* .platforms= */ { "linux-64", "noarch" }, + /* .channel_alias= */ specs::CondaURL::parse("https://conda.anaconda.org/").value(), + } + ); PackageInfo skl0("scikit-learn", "1.5.0", "py310h981052a_0", 0); auto sol0 = database.alloc_solvable(skl0); @@ -1242,7 +441,12 @@ TEST_CASE("solver::resolvo") SECTION("Trivial problem") { - PackageDatabase database; + mamba::solver::resolvo::Database database( + { + /* .platforms= */ { "linux-64", "noarch" }, + /* .channel_alias= */ specs::CondaURL::parse("https://conda.anaconda.org/").value(), + } + ); // NOTE: the problem can only be solved when two `Solvable` are added to the // `PackageDatabase` PackageInfo scikit_learn("scikit-learn", "1.5.0", "py310h981052a_0", 0); @@ -1263,12 +467,16 @@ TEST_CASE("solver::resolvo") SECTION("Parse linux-64/repodata.json") { - PackageDatabase database; + mamba::solver::resolvo::Database database( + { + /* .platforms= */ { "linux-64", "noarch" }, + /* .channel_alias= */ specs::CondaURL::parse("https://conda.anaconda.org/").value(), + } + ); - parse_repodata_json( - database, + database.add_repo_from_repodata_json( "/tmp/linux-64/repodata.json", - "https://conda.anaconda.org/conda-forge/linux-64/repodata.json", + "https://conda.anaconda.org/conda-forge/linux-64", "conda-forge", false ); @@ -1278,12 +486,16 @@ TEST_CASE("solver::resolvo") SECTION("Parse noarch/repodata.json") { - PackageDatabase database; + mamba::solver::resolvo::Database database( + { + /* .platforms= */ { "linux-64", "noarch" }, + /* .channel_alias= */ specs::CondaURL::parse("https://conda.anaconda.org/").value(), + } + ); - parse_repodata_json( - database, + database.add_repo_from_repodata_json( "/tmp/noarch/repodata.json", - "https://conda.anaconda.org/conda-forge/noarch/repodata.json", + "https://conda.anaconda.org/conda-forge/noarch", "conda-forge", false ); @@ -1300,75 +512,32 @@ TEST_CASE("Test consistency with libsolv (environment creation)") SECTION("numpy") { - const auto request = Request{ - /* .flags= */ {}, - /* .jobs= */ { Request::Install{ "numpy"_ms } }, - }; - const auto outcome = libsolv::Solver().solve(libsolv_db, request); - - REQUIRE(outcome.has_value()); - REQUIRE(std::holds_alternative(outcome.value())); - const auto& solution = std::get(outcome.value()); - - REQUIRE_FALSE(solution.actions.empty()); - - // Numpy is last because of topological sort - REQUIRE(std::holds_alternative(solution.actions.back())); - REQUIRE(std::get(solution.actions.back()).install.name == "numpy"); - REQUIRE(find_actions_with_name(solution, "numpy").size() == 1); - - const auto python_actions = find_actions_with_name(solution, "python"); - REQUIRE(python_actions.size() == 1); - REQUIRE(std::holds_alternative(python_actions.front())); - - resolvo::Vector requirements = { - resolvo_db.alloc_version_set("numpy"), - }; + const std::vector specs_to_install = { "numpy" }; + const auto libsolv_resolution = libsolv_resolve(specs_to_install); + const auto resolvo_resolution = resolvo_libmamba_resolve(specs_to_install); - resolvo::Vector constraints = {}; - resolvo::Vector result; - String reason = resolvo::solve(resolvo_db, requirements, constraints, result); - - REQUIRE(reason == ""); - REQUIRE(result.size() == 31); - REQUIRE(resolvo_db.solvable_pool[result[0]].name == "numpy"); + REQUIRE(resolvo_resolution.size() == libsolv_resolution.size()); + for (std::size_t i = 0; i < libsolv_resolution.size(); ++i) + { + REQUIRE(resolvo_resolution[i].name == libsolv_resolution[i].name); + REQUIRE(resolvo_resolution[i].version == libsolv_resolution[i].version); + REQUIRE(resolvo_resolution[i].build_string == libsolv_resolution[i].build_string); + } } SECTION("scikit-learn") { - const auto request = Request{ - /* .flags= */ {}, - /* .jobs= */ { Request::Install{ "scikit-learn"_ms } }, - }; - - const auto outcome = libsolv::Solver().solve(libsolv_db, request); - - REQUIRE(outcome.has_value()); - REQUIRE(std::holds_alternative(outcome.value())); - const auto& solution = std::get(outcome.value()); - - REQUIRE_FALSE(solution.actions.empty()); - - // scikit-learn is last because of topological sort - REQUIRE(std::holds_alternative(solution.actions.back())); - REQUIRE(std::get(solution.actions.back()).install.name == "scikit-learn"); - REQUIRE(find_actions_with_name(solution, "scikit-learn").size() == 1); + const std::vector specs_to_install = { "scikit-learn" }; + const auto libsolv_resolution = libsolv_resolve(specs_to_install); + const auto resolvo_resolution = resolvo_libmamba_resolve(specs_to_install); - const auto python_actions = find_actions_with_name(solution, "scikit-learn"); - REQUIRE(python_actions.size() == 1); - REQUIRE(std::holds_alternative(python_actions.front())); - - resolvo::Vector requirements = { - resolvo_db.alloc_version_set("scikit-learn"), - }; - - resolvo::Vector constraints = {}; - resolvo::Vector result; - String reason = resolvo::solve(resolvo_db, requirements, constraints, result); - - REQUIRE(reason == ""); - REQUIRE(result.size() == 36); - REQUIRE(resolvo_db.solvable_pool[result[0]].name == "scikit-learn"); + REQUIRE(resolvo_resolution.size() == libsolv_resolution.size()); + for (std::size_t i = 0; i < libsolv_resolution.size(); ++i) + { + REQUIRE(resolvo_resolution[i].name == libsolv_resolution[i].name); + REQUIRE(resolvo_resolution[i].version == libsolv_resolution[i].version); + REQUIRE(resolvo_resolution[i].build_string == libsolv_resolution[i].build_string); + } } SECTION("scikit-learn explicit") @@ -1455,34 +624,8 @@ TEST_CASE("Test consistency with libsolv (environment creation)") [&](const PackageInfo& a, const PackageInfo& b) { return a.name < b.name; } ); - // resolvo's specification and resolution - resolvo::Vector requirements; - for (const auto& spec : specs_to_install) - { - requirements.push_back(resolvo_db.alloc_version_set(spec)); - } - - resolvo::Vector constraints = {}; - resolvo::Vector result; - String reason = resolvo::solve(resolvo_db, requirements, constraints, result); - - REQUIRE(reason == ""); - REQUIRE(result.size() == known_resolution.size()); - - std::vector resolvo_resolution; - std::transform( - result.begin(), - result.end(), - std::back_inserter(resolvo_resolution), - [&](const resolvo::SolvableId& solvable_id) - { return resolvo_db.solvable_pool[solvable_id]; } - ); - - std::sort( - resolvo_resolution.begin(), - resolvo_resolution.end(), - [&](const PackageInfo& a, const PackageInfo& b) { return a.name < b.name; } - ); + std::vector resolvo_resolution = resolvo_libmamba_resolve(specs_to_install); + REQUIRE(resolvo_resolution.size() == known_resolution.size()); // Check libsolv's PackageInfo against the know resolution for (size_t i = 0; i < libsolv_resolution.size(); i++) @@ -1537,7 +680,7 @@ TEST_CASE("Test consistency with libsolv (environment creation)") } std::cout << std::endl; - std::vector resolvo_resolution = resolvo_resolve(specs_to_install); + std::vector resolvo_resolution = resolvo_libmamba_resolve(specs_to_install); // Print all the packages from resolvo std::cout << "resolvo resolution:" << std::endl; @@ -1601,7 +744,7 @@ TEST_CASE("Test consistency with libsolv (environment creation)") std::cout << std::endl; - std::vector resolvo_resolution = resolvo_resolve(specs_to_install); + std::vector resolvo_resolution = resolvo_libmamba_resolve(specs_to_install); // Print all the packages from resolvo std::cout << "resolvo resolution:" << std::endl; From 313d00715b120247576b6a3d88381eeb69a612a2 Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Thu, 7 May 2026 17:07:59 +0200 Subject: [PATCH 07/20] test: Solver parity Signed-off-by: Julien Jerphanion --- libmamba/tests/CMakeLists.txt | 1 + .../src/solver/test_solver_backend_parity.cpp | 212 ++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 libmamba/tests/src/solver/test_solver_backend_parity.cpp diff --git a/libmamba/tests/CMakeLists.txt b/libmamba/tests/CMakeLists.txt index db0e38d240..c6916b4e35 100644 --- a/libmamba/tests/CMakeLists.txt +++ b/libmamba/tests/CMakeLists.txt @@ -71,6 +71,7 @@ set( src/specs/test_version.cpp # Solver tests src/solver/test_helpers.cpp + src/solver/test_solver_backend_parity.cpp src/solver/test_problems_graph.cpp src/solver/test_request.cpp src/solver/test_solution.cpp diff --git a/libmamba/tests/src/solver/test_solver_backend_parity.cpp b/libmamba/tests/src/solver/test_solver_backend_parity.cpp new file mode 100644 index 0000000000..dd66d4cf96 --- /dev/null +++ b/libmamba/tests/src/solver/test_solver_backend_parity.cpp @@ -0,0 +1,212 @@ +// Copyright (c) 2026, 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 +#include +#include +#include + +#include + +#include "mamba/api/channel_loader.hpp" +#include "mamba/core/channel_context.hpp" +#include "mamba/core/context.hpp" +#include "mamba/core/package_cache.hpp" +#include "mamba/core/subdir_index.hpp" +#include "mamba/core/util.hpp" +#include "mamba/core/util_scope.hpp" +#include "mamba/core/virtual_packages.hpp" +#include "mamba/solver/libsolv/database.hpp" +#include "mamba/solver/libsolv/solver.hpp" +#include "mamba/solver/request.hpp" +#include "mamba/solver/resolvo/database.hpp" +#include "mamba/solver/resolvo/solver.hpp" +#include "mamba/specs/match_spec.hpp" + +#include "mambatests.hpp" + +namespace mamba +{ + std::vector extract_package_names_from_specs(const std::vector& specs); + void add_python_related_roots_if_python_requested(std::vector& root_packages); +} + +using namespace mamba; +using namespace mamba::solver; + +namespace +{ + using PackageKey = std::tuple; + + auto extract_root_packages(const std::vector& specs) -> std::vector + { + auto roots = extract_package_names_from_specs(specs); + add_python_related_roots_if_python_requested(roots); + return roots; + } + + auto make_request(const std::vector& specs, const Request::Flags& flags) -> Request + { + Request request; + request.flags = flags; + for (const auto& spec : specs) + { + auto parsed = specs::MatchSpec::parse(spec); + REQUIRE(parsed.has_value()); + request.jobs.emplace_back(Request::Install{ parsed.value() }); + } + return request; + } + + auto as_key_set(const Solution& solution) -> std::set + { + std::set out; + for (const auto& pkg : solution.packages_to_install()) + { + out.emplace(pkg.name, pkg.version, pkg.build_string, pkg.build_number); + } + return out; + } + + auto make_resolvo_db_from_libsolv(const libsolv::Database& libsolv_db) + -> mamba::solver::resolvo::Database + { + auto resolvo_db = mamba::solver::resolvo::Database(libsolv_db.channel_params()); + const auto all = specs::MatchSpec::parse("*"); + REQUIRE(all.has_value()); + std::vector packages; + const_cast(libsolv_db) + .for_each_package_matching( + all.value(), + [&](specs::PackageInfo&& pkg) + { + packages.emplace_back(std::move(pkg)); + return util::LoopControl::Continue; + } + ); + resolvo_db.add_repo_from_packages(packages, "all", false); + return resolvo_db; + } + + struct CaseData + { + std::string name; + std::vector channels; + std::string platform; + std::vector specs; + }; +} + +TEST_CASE("Solver backend parity on sharded specs", "[mamba::solver][parity][.integration]") +{ + auto& ctx = mambatests::context(); + const auto saved_channels = ctx.channels; + const auto saved_platform = ctx.platform; + const auto saved_offline = ctx.offline; + const auto saved_use_shards = ctx.use_sharded_repodata; + on_scope_exit restore_ctx{ [&] + { + ctx.channels = saved_channels; + ctx.platform = saved_platform; + ctx.offline = saved_offline; + ctx.use_sharded_repodata = saved_use_shards; + } }; + + const TemporaryDirectory tmp_dir; + const fs::u8path cache_dir = tmp_dir.path() / "cache"; + fs::create_directories(cache_dir); + ctx.pkgs_dirs = { cache_dir }; + create_cache_dir(cache_dir); + + const std::vector cases = { + { "python", { "https://prefix.dev/conda-forge" }, "linux-64", { "python" } }, + { "python-numpy-pandas", + { "https://prefix.dev/conda-forge" }, + "linux-64", + { "python", "numpy", "pandas" } }, + { "python-range", { "https://prefix.dev/conda-forge" }, "linux-64", { "python>=3.10,<3.12" } }, + { "jupyter", { "https://prefix.dev/conda-forge" }, "linux-64", { "jupyter" } }, + { "python-pin", { "https://prefix.dev/conda-forge" }, "linux-64", { "python=3.11" } }, + { "scikit-learn", { "https://prefix.dev/conda-forge" }, "linux-64", { "scikit-learn" } }, + { "pyarrow", { "https://prefix.dev/conda-forge" }, "linux-64", { "pyarrow" } }, + { "tensorflow", { "conda-forge" }, "linux-64", { "tensorflow" } }, + { "emscripten-xeus", + { "https://prefix.dev/emscripten-forge-4x", "https://prefix.dev/conda-forge" }, + "emscripten-wasm32", + { "python>=3.11", + "pybind11", + "nlohmann_json", + "pybind11_json", + "numpy", + "pytest", + "bzip2", + "sqlite", + "zlib", + "libffi", + "xtl", + "pyjs", + "xeus", + "xeus-lite" } }, + { "emscripten-obspy", + { "https://repo.prefix.dev/emscripten-forge-4x", "https://repo.prefix.dev/conda-forge" }, + "emscripten-wasm32", + { "python", "obspy", "matplotlib", "numpy", "scipy", "pyjs", "requests", "pyodide-http" } }, + }; + + for (const auto& c : cases) + { + DYNAMIC_SECTION(c.name) + { + ctx.channels = c.channels; + ctx.platform = c.platform; + ctx.offline = false; + ctx.use_sharded_repodata = true; + + auto channel_context = ChannelContext::make_conda_compatible(ctx); + init_channels(ctx, channel_context); + + libsolv::Database libsolv_db{ + channel_context.params(), + { ctx.experimental_matchspec_parsing ? libsolv::MatchSpecParser::Mamba + : libsolv::MatchSpecParser::Libsolv }, + }; + MultiPackageCache package_caches{ { cache_dir }, ctx.validation_params }; + auto load = load_channels( + ctx, + channel_context, + libsolv_db, + package_caches, + extract_root_packages(c.specs) + ); + REQUIRE(load.has_value()); + + libsolv_db.add_repo_from_packages( + get_virtual_packages(ctx.platform), + "virtual", + solver::libsolv::PipAsPythonDependency::No + ); + + const auto request = make_request(c.specs, ctx.solver_flags); + + auto libsolv_outcome = libsolv::Solver().solve( + libsolv_db, + request, + ctx.experimental_matchspec_parsing ? libsolv::MatchSpecParser::Mamba + : libsolv::MatchSpecParser::Libsolv + ); + REQUIRE(libsolv_outcome.has_value()); + REQUIRE(std::holds_alternative(libsolv_outcome.value())); + const auto& libsolv_solution = std::get(libsolv_outcome.value()); + + auto resolvo_db = make_resolvo_db_from_libsolv(libsolv_db); + auto resolvo_outcome = mamba::solver::resolvo::Solver().solve(resolvo_db, request); + REQUIRE(resolvo_outcome.has_value()); + const auto& resolvo_solution = std::get(resolvo_outcome.value()); + + REQUIRE(as_key_set(resolvo_solution) == as_key_set(libsolv_solution)); + } + } +} From 152697d3a900540192e00a1242bcfbe28271deab Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Thu, 28 May 2026 14:59:27 +0200 Subject: [PATCH 08/20] Direct assignement Signed-off-by: Julien Jerphanion --- libmamba/src/solver/resolvo/solver.cpp | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/libmamba/src/solver/resolvo/solver.cpp b/libmamba/src/solver/resolvo/solver.cpp index 100e7d59a3..75ad24e1cf 100644 --- a/libmamba/src/solver/resolvo/solver.cpp +++ b/libmamba/src/solver/resolvo/solver.cpp @@ -100,20 +100,9 @@ namespace mamba::solver::resolvo { continue; } - 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; + // Preserve full package metadata (url, filename, platform, hashes, etc.) + // so downstream transaction/cache logic behaves exactly like libsolv. + specs::PackageInfo pkg = solvable; solution.actions.emplace_back(Solution::Install{ std::move(pkg) }); } From e64294b4e53434590b03be7e4893cbde6a6c70a3 Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Thu, 28 May 2026 15:30:23 +0200 Subject: [PATCH 09/20] Cache dependency provider lookups to avoid repeated Matchspec parsing Signed-off-by: Julien Jerphanion --- .../include/mamba/solver/resolvo/database.hpp | 3 + libmamba/src/solver/resolvo/database.cpp | 98 +++++++------------ 2 files changed, 41 insertions(+), 60 deletions(-) diff --git a/libmamba/include/mamba/solver/resolvo/database.hpp b/libmamba/include/mamba/solver/resolvo/database.hpp index 256b0dd4f8..aa4e414828 100644 --- a/libmamba/include/mamba/solver/resolvo/database.hpp +++ b/libmamba/include/mamba/solver/resolvo/database.hpp @@ -247,6 +247,9 @@ namespace mamba::solver::resolvo // Maps for quick lookups std::unordered_map<::resolvo::NameId, ::resolvo::Vector<::resolvo::SolvableId>> name_to_solvable; + std::unordered_map<::resolvo::SolvableId, ::resolvo::Dependencies> solvable_to_dependencies; + std::unordered_map<::resolvo::SolvableId, std::unordered_map<::resolvo::NameId, ::resolvo::VersionSetId>> + solvable_to_dependency_version_sets; std::unordered_map<::resolvo::VersionSetUnionId, ::resolvo::Vector<::resolvo::VersionSetId>> version_set_unions; std::unordered_map<::resolvo::ConditionId, ::resolvo::Condition> conditions; diff --git a/libmamba/src/solver/resolvo/database.cpp b/libmamba/src/solver/resolvo/database.cpp index ec727b77c6..0692eed94a 100644 --- a/libmamba/src/solver/resolvo/database.cpp +++ b/libmamba/src/solver/resolvo/database.cpp @@ -326,6 +326,37 @@ namespace mamba::solver::resolvo */ ::resolvo::SolvableId Database::alloc_solvable(specs::PackageInfo package_info) { + ::resolvo::Dependencies dependencies; + std::unordered_map<::resolvo::NameId, ::resolvo::VersionSetId> dependency_version_sets; + dependency_version_sets.reserve(package_info.dependencies.size()); + + for (const auto& dep : package_info.dependencies) + { + const auto version_set_id = alloc_version_set(dep); + const auto match_spec = version_set_pool[version_set_id]; + const auto dep_name_id = name_pool.alloc( + ::resolvo::String{ match_spec.name().to_string() } + ); + dependencies.requirements.push_back( + ::resolvo::ConditionalRequirement(::resolvo::Requirement(version_set_id)) + ); + dependency_version_sets[dep_name_id] = version_set_id; + } + for (const 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, ">="); + } + dependencies.constrains.push_back(alloc_version_set(constr2)); + } + // Add the solvable to the solvable pool auto id = solvable_pool.alloc(package_info); @@ -339,18 +370,11 @@ namespace mamba::solver::resolvo 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 auto name_id = name_pool.alloc(::resolvo::String{ package_info.name }); name_to_solvable[name_id].push_back(id); + solvable_to_dependencies[id] = std::move(dependencies); + solvable_to_dependency_version_sets[id] = std::move(dependency_version_sets); return id; } @@ -564,34 +588,16 @@ namespace mamba::solver::resolvo } // 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().to_string(); - 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().to_string(); - auto name_id = name_pool.alloc(::resolvo::String{ name }); - - b_deps[name_id] = version_set_pool[ms]; - } + const auto& a_deps = solvable_to_dependency_version_sets.at(a); + const auto& b_deps = solvable_to_dependency_version_sets.at(b); auto ordering_score = 0; for (auto [name_id, version_set_id] : a_deps) { - if (b_deps.find(name_id) != b_deps.end()) + if (const auto b_it = b_deps.find(name_id); b_it != 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]); + auto [b_tf_version, b_n_track_features] = find_highest_version(b_it->second); // Favor the solvable with higher versions of their dependencies if (a_tf_version != b_tf_version) @@ -666,34 +672,6 @@ namespace mamba::solver::resolvo */ ::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( - ::resolvo::ConditionalRequirement(::resolvo::Requirement(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; + return solvable_to_dependencies.at(solvable_id); } } From ac80640677ee5afd96e90beff6bc8f1b05bd4054 Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Thu, 28 May 2026 16:05:18 +0200 Subject: [PATCH 10/20] Another mapping Signed-off-by: Julien Jerphanion --- libmamba/include/mamba/solver/resolvo/database.hpp | 1 + libmamba/src/solver/resolvo/database.cpp | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/libmamba/include/mamba/solver/resolvo/database.hpp b/libmamba/include/mamba/solver/resolvo/database.hpp index aa4e414828..f4fc08df65 100644 --- a/libmamba/include/mamba/solver/resolvo/database.hpp +++ b/libmamba/include/mamba/solver/resolvo/database.hpp @@ -255,6 +255,7 @@ namespace mamba::solver::resolvo std::unordered_map<::resolvo::ConditionId, ::resolvo::Condition> conditions; std::unordered_map<::resolvo::VersionSetId, std::pair> version_set_to_max_version_and_track_features_numbers; + std::unordered_map normalized_matchspec_to_version_set_id; specs::ChannelResolveParams m_channel_params; }; diff --git a/libmamba/src/solver/resolvo/database.cpp b/libmamba/src/solver/resolvo/database.cpp index 0692eed94a..c191afffbd 100644 --- a/libmamba/src/solver/resolvo/database.cpp +++ b/libmamba/src/solver/resolvo/database.cpp @@ -275,6 +275,12 @@ namespace mamba::solver::resolvo return ::resolvo::VersionSetId{ 0 }; } + if (const auto it = normalized_matchspec_to_version_set_id.find(raw_match_spec_str); + it != normalized_matchspec_to_version_set_id.end()) + { + return it->second; + } + // 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 @@ -312,6 +318,7 @@ namespace mamba::solver::resolvo const std::string match_spec_str = match_spec.to_string(); name_pool.alloc(::resolvo::String{ match_spec_str }); string_pool.alloc(::resolvo::String{ match_spec_str }); + normalized_matchspec_to_version_set_id.emplace(raw_match_spec_str, id); return id; } From 4f2be899088d1f2a6ee0b4320924c5b78be8d94d Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Fri, 29 May 2026 12:44:55 +0200 Subject: [PATCH 11/20] Update resolvo-cpp in environments' specifications Signed-off-by: Julien Jerphanion --- dev/environment-dev.yml | 2 +- dev/environment-micromamba-static.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/environment-dev.yml b/dev/environment-dev.yml index f661c484a4..049b59da40 100644 --- a/dev/environment-dev.yml +++ b/dev/environment-dev.yml @@ -17,7 +17,7 @@ dependencies: - libarchive>=3.8 lgpl_* - libcurl >=7.86 - libsodium - - resolvo-cpp==0.1.0 + - resolvo-cpp==0.3.0 - libsolv >=0.7.18 - libmsgpack-c - nlohmann_json diff --git a/dev/environment-micromamba-static.yml b/dev/environment-micromamba-static.yml index f4a7031a51..03bde080d9 100644 --- a/dev/environment-micromamba-static.yml +++ b/dev/environment-micromamba-static.yml @@ -18,7 +18,7 @@ dependencies: - simdjson-static >=3.3.0 - spdlog >=1.16.0 - fmt >=11.1.0 - - resolvo-cpp==0.1.0 + - resolvo-cpp==0.3.0 - libsolv-static >=0.7.24 - yaml-cpp-static >=0.8.0 - reproc-static >=14.2.4.post0 From 1d47fd7d7f56d0ac18bc3ad6920b231b2415f484 Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Fri, 29 May 2026 12:45:23 +0200 Subject: [PATCH 12/20] Override `has_package` Signed-off-by: Julien Jerphanion --- libmamba/include/mamba/solver/resolvo/database.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libmamba/include/mamba/solver/resolvo/database.hpp b/libmamba/include/mamba/solver/resolvo/database.hpp index f4fc08df65..3a32b79cce 100644 --- a/libmamba/include/mamba/solver/resolvo/database.hpp +++ b/libmamba/include/mamba/solver/resolvo/database.hpp @@ -235,7 +235,7 @@ namespace mamba::solver::resolvo bijective_map<::resolvo::VersionSetId, specs::MatchSpec> version_set_pool; bijective_map<::resolvo::SolvableId, specs::PackageInfo> solvable_pool; - bool has_package(const specs::MatchSpec& spec) + bool has_package(const specs::MatchSpec& spec) override { auto candidates = get_candidates( name_pool.alloc(::resolvo::String(spec.name().to_string())) From fedfe17c7994a404f4b810061d508ecc9a522755 Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Fri, 29 May 2026 13:03:52 +0200 Subject: [PATCH 13/20] Adapt linkage of resolvo and of simdjson Signed-off-by: Julien Jerphanion --- libmamba/CMakeLists.txt | 12 ++++++++---- libmamba/tests/CMakeLists.txt | 10 +++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/libmamba/CMakeLists.txt b/libmamba/CMakeLists.txt index 2d6d4d06e9..cb1aec756b 100644 --- a/libmamba/CMakeLists.txt +++ b/libmamba/CMakeLists.txt @@ -506,11 +506,11 @@ macro(libmamba_create_target target_name linkage output_name) target_link_libraries( ${target_name} - PUBLIC fmt::fmt-header-only yaml-cpp::yaml-cpp ${MSGPACK_TARGET} + PUBLIC + fmt::fmt-header-only yaml-cpp::yaml-cpp ${MSGPACK_TARGET} simdjson::simdjson_static PRIVATE reproc reproc++ - simdjson::simdjson_static solv::libsolv_static solv::libsolvext_static solv::cpp @@ -686,7 +686,12 @@ macro(libmamba_create_target target_name linkage output_name) target_link_libraries( ${target_name} PUBLIC - ${LIBSOLV_LIBRARIES} ${LIBSOLVEXT_LIBRARIES} yaml-cpp::yaml-cpp fmt::fmt msgpack-c + ${LIBSOLV_LIBRARIES} + ${LIBSOLVEXT_LIBRARIES} + yaml-cpp::yaml-cpp + fmt::fmt + msgpack-c + simdjson::simdjson PRIVATE ${LibArchive_LIBRARIES} ${CURL_LIBRARIES} @@ -694,7 +699,6 @@ macro(libmamba_create_target target_name linkage output_name) BZip2::BZip2 reproc reproc++ - simdjson::simdjson zstd::libzstd_shared solv::libsolv solv::libsolvext diff --git a/libmamba/tests/CMakeLists.txt b/libmamba/tests/CMakeLists.txt index c6916b4e35..7776540582 100644 --- a/libmamba/tests/CMakeLists.txt +++ b/libmamba/tests/CMakeLists.txt @@ -155,9 +155,17 @@ endif() target_link_libraries( test_libmamba PUBLIC mamba::libmamba reproc reproc++ - PRIVATE Catch2::Catch2WithMain Threads::Threads + PRIVATE Catch2::Catch2WithMain Threads::Threads Resolvo::Resolvo ) +# Shared libmamba links simdjson and resolvo privately; tests that include those headers need them +# too. +if(TARGET simdjson::simdjson) + target_link_libraries(test_libmamba PRIVATE simdjson::simdjson) +elseif(TARGET simdjson::simdjson_static) + target_link_libraries(test_libmamba PRIVATE simdjson::simdjson_static) +endif() + # Link zstd if test code uses it directly (e.g., test_shard_utils.cpp) The test code includes # and calls ZSTD functions directly, so it needs to link against zstd if(TARGET zstd::libzstd_static) From 80e4a59d066994c16caa2d304d53bc409da36a23 Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Fri, 29 May 2026 13:05:05 +0200 Subject: [PATCH 14/20] ci: Add resolvo-cpp host dependency to feedstock recipe Signed-off-by: Julien Jerphanion --- .github/workflows/static_build.yml | 76 +++++++++++++++++++++++++ cmake/modules/FixResolvoWindows.cmake | 40 +++++++++++++ dev/micromamba_windows_allowed_dlls.tsv | 1 + libmamba/CMakeLists.txt | 1 + 4 files changed, 118 insertions(+) create mode 100644 cmake/modules/FixResolvoWindows.cmake diff --git a/.github/workflows/static_build.yml b/.github/workflows/static_build.yml index 9a872c71cf..e472f47250 100644 --- a/.github/workflows/static_build.yml +++ b/.github/workflows/static_build.yml @@ -65,6 +65,62 @@ jobs: repository: conda-forge/micromamba-feedstock ref: dev path: micromamba-feedstock + # Temporary: use shared resolvo-cpp until static builds are on conda-forge. + - name: Add resolvo-cpp host dependency to feedstock recipe + run: | + python <<'PY' + from pathlib import Path + + path = Path("micromamba-feedstock/recipe/meta.yaml") + text = path.read_text() + dep = " - resolvo-cpp 0.3.0\n" + if "resolvo-cpp" not in text: + needle = " - simdjson-static >=3.3.0\n" + if needle not in text: + raise SystemExit(f"Could not find anchor {needle!r} in {path}") + path.write_text(text.replace(needle, needle + dep, 1)) + PY + # Temporary: ship libresolvo_cpp in the micromamba package until resolvo-cpp is static on conda-forge. + - name: Patch feedstock build.sh for resolvo DSO + run: | + python <<'PY' + from pathlib import Path + + path = Path("micromamba-feedstock/recipe/build.sh") + text = path.read_text() + if "libresolvo_cpp" in text: + print(f"{path} already copies libresolvo_cpp") + else: + needle = "cmake --install build/\n\n# remove everything related to" + block = """cmake --install build/ + + # Temporary: resolvo-cpp is only available as a shared library until static builds are + # published on conda-forge. Ship the DSO in $PREFIX/lib so @rpath/../lib resolves when + # micromamba is used outside a full conda env (e.g. CI artifact tests). + install -d "${PREFIX}/lib" + _resolvo_copied=0 + for _resolvo_lib in \\ + "${BUILD_PREFIX}/lib/libresolvo_cpp.dylib" \\ + "${BUILD_PREFIX}/lib/libresolvo_cpp.so" \\ + "${PREFIX}/lib/libresolvo_cpp.dylib" \\ + "${PREFIX}/lib/libresolvo_cpp.so"; do + if [[ -f "${_resolvo_lib}" ]]; then + cp -f "${_resolvo_lib}" "${PREFIX}/lib/" + _resolvo_copied=1 + break + fi + done + if [[ "${_resolvo_copied}" -eq 0 ]]; then + echo "libresolvo_cpp not found in BUILD_PREFIX or PREFIX" >&2 + exit 1 + fi + + # remove everything related to""" + if needle not in text: + raise SystemExit(f"Could not find anchor in {path}") + path.write_text(text.replace(needle, block, 1)) + print(f"Patched {path} to copy libresolvo_cpp into the package") + PY - name: Disable output validation if: ${{ matrix.platform == 'osx' }} run: | @@ -141,6 +197,25 @@ jobs: "micromamba-feedstock/build_artifacts/${{ matrix.platform }}-${{ matrix.arch }}/"micromamba-*.tar.bz2 "pkg/" mkdir -p "${{ github.workspace }}/artifacts" cp pkg/bin/micromamba "${{ github.workspace }}/artifacts" + # Temporary: static micromamba links shared resolvo-cpp (@rpath/../lib) until + # resolvo-cpp static libraries are published on conda-forge. Copy the DSO for + # artifact tests (the conda package only declares it as a run dependency). + mkdir -p "${{ github.workspace }}/lib" + shopt -s nullglob + _resolvo_copied=0 + for _resolvo_lib in \ + pkg/lib/libresolvo_cpp.* \ + "${MAMBA_ROOT_PREFIX}/envs/mambabuild/lib/libresolvo_cpp."* \ + "${MAMBA_ROOT_PREFIX}"/pkgs/resolvo-cpp-*/lib/libresolvo_cpp.* \ + micromamba-feedstock/build_artifacts/*/_h_env*/lib/libresolvo_cpp.*; do + cp "${_resolvo_lib}" "${{ github.workspace }}/lib/" + _resolvo_copied=1 + break + done + if [[ "${_resolvo_copied}" -eq 0 ]]; then + echo "libresolvo_cpp shared library not found for artifact tests" >&2 + exit 1 + fi - name: Test basic commands if: ${{ matrix.arch != 'aarch64' && matrix.arch != 'ppc64le' }} @@ -186,6 +261,7 @@ jobs: cpp-expected nlohmann_json simdjson-static>=3.3.0 + resolvo-cpp==0.3.0 spdlog>=1.16.0 fmt>=11.1.0 yaml-cpp-static>=0.8.0 diff --git a/cmake/modules/FixResolvoWindows.cmake b/cmake/modules/FixResolvoWindows.cmake new file mode 100644 index 0000000000..1b66521827 --- /dev/null +++ b/cmake/modules/FixResolvoWindows.cmake @@ -0,0 +1,40 @@ +# Temporary workaround until resolvo-cpp static libraries are published on conda-forge. resolvo-cpp +# 0.3.0 on Windows installs Library/bin/resolvo_cpp.dll and Library/lib/resolvo_cpp.lib, but +# ResolvoConfig.cmake still references the 0.2.x layout (Library/lib/resolvo_cpp.dll and +# resolvo_cpp.dll.lib). + +if(NOT WIN32 OR NOT TARGET resolvo_cpp-shared) + return() +endif() + +if(DEFINED ENV{LIBRARY_PREFIX}) + set(_resolvo_prefix "$ENV{LIBRARY_PREFIX}") +elseif(DEFINED ENV{CONDA_PREFIX}) + set(_resolvo_prefix "$ENV{CONDA_PREFIX}/Library") +else() + get_target_property(_resolvo_implib resolvo_cpp-shared IMPORTED_IMPLIB) + if(_resolvo_implib) + get_filename_component(_resolvo_prefix "${_resolvo_implib}" DIRECTORY) + get_filename_component(_resolvo_prefix "${_resolvo_prefix}" DIRECTORY) + endif() +endif() + +if(NOT _resolvo_prefix) + message(WARNING "Could not determine resolvo-cpp prefix for Windows import fix") + return() +endif() + +set(_resolvo_dll "${_resolvo_prefix}/bin/resolvo_cpp.dll") +set(_resolvo_lib "${_resolvo_prefix}/lib/resolvo_cpp.lib") + +if(EXISTS "${_resolvo_lib}" AND EXISTS "${_resolvo_dll}") + set_target_properties( + resolvo_cpp-shared + PROPERTIES IMPORTED_IMPLIB "${_resolvo_lib}" IMPORTED_LOCATION "${_resolvo_dll}" + ) + message(STATUS "Adjusted Resolvo Windows import lib to ${_resolvo_lib}") +endif() + +unset(_resolvo_prefix) +unset(_resolvo_dll) +unset(_resolvo_lib) diff --git a/dev/micromamba_windows_allowed_dlls.tsv b/dev/micromamba_windows_allowed_dlls.tsv index 70b6076a66..29ed348dff 100644 --- a/dev/micromamba_windows_allowed_dlls.tsv +++ b/dev/micromamba_windows_allowed_dlls.tsv @@ -28,3 +28,4 @@ api-ms-win-crt-utility-l1-1-0.dll UCRT (ucrt) UCRT forwarder: utility routines ( MSVCP140.dll MSVC runtime (vc14_runtime) Microsoft C++ standard library runtime (/MD builds). VCRUNTIME140.dll MSVC runtime (vc14_runtime) Microsoft C runtime helpers (exceptions, EH scaffolding). VCRUNTIME140_1.dll MSVC runtime (vc14_runtime) Additional MSVC C++ exception-handling support on x64. +resolvo_cpp.dll resolvo-cpp (conda-forge) Temporary: shared resolvo C++ bindings until resolvo-cpp static libraries are published on conda-forge. diff --git a/libmamba/CMakeLists.txt b/libmamba/CMakeLists.txt index cb1aec756b..540930c8d1 100644 --- a/libmamba/CMakeLists.txt +++ b/libmamba/CMakeLists.txt @@ -457,6 +457,7 @@ find_package(reproc CONFIG REQUIRED) find_package(reproc++ CONFIG REQUIRED) find_package(Libsolv MODULE REQUIRED) find_package(Resolvo CONFIG REQUIRED) +include(FixResolvoWindows) find_package(msgpack-c CONFIG REQUIRED) add_subdirectory(ext/solv-cpp) From 871951cb6372df2d4584a84282f5692b24541195 Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Fri, 29 May 2026 13:30:21 +0200 Subject: [PATCH 15/20] Guard for absence of `ConditionId` and `ConditionalRequirement` Signed-off-by: Julien Jerphanion --- cmake/modules/CheckResolvoConditions.cmake | 39 +++++++++++++++++++ libmamba/CMakeLists.txt | 5 +++ .../include/mamba/solver/resolvo/database.hpp | 6 +++ libmamba/src/solver/resolvo/database.cpp | 8 ++++ libmamba/src/solver/resolvo/solver.cpp | 26 ++++++++++--- 5 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 cmake/modules/CheckResolvoConditions.cmake diff --git a/cmake/modules/CheckResolvoConditions.cmake b/cmake/modules/CheckResolvoConditions.cmake new file mode 100644 index 0000000000..476e22cad9 --- /dev/null +++ b/cmake/modules/CheckResolvoConditions.cmake @@ -0,0 +1,39 @@ +# Detect resolvo-cpp >= 0.3.0 builds that export ConditionId / ConditionalRequirement. win-64 0.3.0 +# (h0a879f1_1) predates this API; linux-64 0.3.0 (hf9e0b3e_1) includes it. Temporary until +# conda-forge republishes a consistent resolvo-cpp 0.3.0 on all platforms. + +set(LIBMAMBA_RESOLVO_HAS_CONDITIONS FALSE) + +find_file( + _libmamba_resolvo_dep_header + NAMES resolvo/resolvo_dependency_provider.h + PATHS ${CMAKE_PREFIX_PATH} + PATH_SUFFIXES include Library/include + NO_DEFAULT_PATH +) +if(NOT _libmamba_resolvo_dep_header) + find_file( + _libmamba_resolvo_dep_header + NAMES resolvo/resolvo_dependency_provider.h + PATH_SUFFIXES include Library/include + ) +endif() + +if(_libmamba_resolvo_dep_header) + file(READ "${_libmamba_resolvo_dep_header}" _libmamba_resolvo_dep_header_content) + if(_libmamba_resolvo_dep_header_content MATCHES "using cbindgen_private::ConditionId;") + set(LIBMAMBA_RESOLVO_HAS_CONDITIONS TRUE) + endif() +endif() + +if(LIBMAMBA_RESOLVO_HAS_CONDITIONS) + message(STATUS "resolvo-cpp: conditional requirements API available") +else() + message( + STATUS + "resolvo-cpp: legacy API without conditional requirements (temporary platform workaround)" + ) +endif() + +unset(_libmamba_resolvo_dep_header) +unset(_libmamba_resolvo_dep_header_content) diff --git a/libmamba/CMakeLists.txt b/libmamba/CMakeLists.txt index 540930c8d1..daf18da712 100644 --- a/libmamba/CMakeLists.txt +++ b/libmamba/CMakeLists.txt @@ -458,6 +458,7 @@ find_package(reproc++ CONFIG REQUIRED) find_package(Libsolv MODULE REQUIRED) find_package(Resolvo CONFIG REQUIRED) include(FixResolvoWindows) +include(CheckResolvoConditions) find_package(msgpack-c CONFIG REQUIRED) add_subdirectory(ext/solv-cpp) @@ -753,6 +754,10 @@ macro(libmamba_create_target target_name linkage output_name) target_link_libraries(${target_name} PUBLIC Threads::Threads) endif() + if(LIBMAMBA_RESOLVO_HAS_CONDITIONS) + target_compile_definitions(${target_name} PUBLIC LIBMAMBA_RESOLVO_HAS_CONDITIONS) + endif() + list(APPEND libmamba_targets ${target_name}) add_library(mamba::${target_name} ALIAS ${target_name}) endmacro() diff --git a/libmamba/include/mamba/solver/resolvo/database.hpp b/libmamba/include/mamba/solver/resolvo/database.hpp index 3a32b79cce..e34694191a 100644 --- a/libmamba/include/mamba/solver/resolvo/database.hpp +++ b/libmamba/include/mamba/solver/resolvo/database.hpp @@ -69,6 +69,7 @@ namespace std } }; +#ifdef LIBMAMBA_RESOLVO_HAS_CONDITIONS template <> struct hash<::resolvo::ConditionId> { @@ -77,6 +78,7 @@ namespace std return static_cast(id.id); } }; +#endif } namespace mamba::solver::resolvo @@ -213,7 +215,9 @@ namespace mamba::solver::resolvo ::resolvo::NameId solvable_name(::resolvo::SolvableId solvable_id) override; ::resolvo::Slice<::resolvo::VersionSetId> version_sets_in_union(::resolvo::VersionSetUnionId version_set_union_id) override; +#ifdef LIBMAMBA_RESOLVO_HAS_CONDITIONS ::resolvo::Condition resolve_condition(::resolvo::ConditionId condition) override; +#endif ::resolvo::Candidates get_candidates(::resolvo::NameId package) override; void sort_candidates(::resolvo::Slice<::resolvo::SolvableId> solvables) override; ::resolvo::Vector<::resolvo::SolvableId> filter_candidates( @@ -252,7 +256,9 @@ namespace mamba::solver::resolvo solvable_to_dependency_version_sets; std::unordered_map<::resolvo::VersionSetUnionId, ::resolvo::Vector<::resolvo::VersionSetId>> version_set_unions; +#ifdef LIBMAMBA_RESOLVO_HAS_CONDITIONS std::unordered_map<::resolvo::ConditionId, ::resolvo::Condition> conditions; +#endif std::unordered_map<::resolvo::VersionSetId, std::pair> version_set_to_max_version_and_track_features_numbers; std::unordered_map normalized_matchspec_to_version_set_id; diff --git a/libmamba/src/solver/resolvo/database.cpp b/libmamba/src/solver/resolvo/database.cpp index c191afffbd..2059766470 100644 --- a/libmamba/src/solver/resolvo/database.cpp +++ b/libmamba/src/solver/resolvo/database.cpp @@ -344,9 +344,15 @@ namespace mamba::solver::resolvo const auto dep_name_id = name_pool.alloc( ::resolvo::String{ match_spec.name().to_string() } ); +#ifdef LIBMAMBA_RESOLVO_HAS_CONDITIONS dependencies.requirements.push_back( ::resolvo::ConditionalRequirement(::resolvo::Requirement(version_set_id)) ); +#else + dependencies.requirements.push_back( + ::resolvo::cbindgen_private::resolvo_requirement_single(version_set_id) + ); +#endif dependency_version_sets[dep_name_id] = version_set_id; } for (const auto& constr : package_info.constrains) @@ -485,6 +491,7 @@ namespace mamba::solver::resolvo return empty; } +#ifdef LIBMAMBA_RESOLVO_HAS_CONDITIONS ::resolvo::Condition Database::resolve_condition(::resolvo::ConditionId condition_id) { if (auto it = conditions.find(condition_id); it != conditions.end()) @@ -493,6 +500,7 @@ namespace mamba::solver::resolvo } return ::resolvo::Condition(::resolvo::VersionSetId{ 0 }); } +#endif /** * Obtains a list of solvables that should be considered when a package diff --git a/libmamba/src/solver/resolvo/solver.cpp b/libmamba/src/solver/resolvo/solver.cpp index 75ad24e1cf..68aa8536d3 100644 --- a/libmamba/src/solver/resolvo/solver.cpp +++ b/libmamba/src/solver/resolvo/solver.cpp @@ -117,18 +117,19 @@ namespace mamba::solver::resolvo 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); } +#ifdef LIBMAMBA_RESOLVO_HAS_CONDITIONS + ::resolvo::Vector<::resolvo::VersionSetId> req_vec; + for (const auto& req : requirements) + { + req_vec.push_back(req); + } + ::resolvo::Vector<::resolvo::ConditionalRequirement> req_conditional; for (const auto& req : req_vec) { @@ -140,6 +141,19 @@ namespace mamba::solver::resolvo .constraints = constr_vec, .soft_requirements = {}, }; +#else + ::resolvo::Vector<::resolvo::Requirement> req_vec; + for (const auto& req : requirements) + { + req_vec.push_back(::resolvo::cbindgen_private::resolvo_requirement_single(req)); + } + + ::resolvo::Problem problem = { + .requirements = req_vec, + .constraints = constr_vec, + .soft_requirements = {}, + }; +#endif ::resolvo::String reason = ::resolvo::solve(database, problem, result); From 57c0f800b82c328d821003b8844b3bfcb5225e55 Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Fri, 29 May 2026 13:48:30 +0200 Subject: [PATCH 16/20] config: Allow `libmamba` as an alias of `libsolv` for `solver` Signed-off-by: Julien Jerphanion --- libmamba/src/api/configuration.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/libmamba/src/api/configuration.cpp b/libmamba/src/api/configuration.cpp index 274d1e9732..cf8df9e081 100644 --- a/libmamba/src/api/configuration.cpp +++ b/libmamba/src/api/configuration.cpp @@ -1397,7 +1397,14 @@ namespace mamba insert(Configurable("solver", std::string("libsolv")) .group("Basic") - .description("Solver backend to use (`libsolv` or `resolvo`).") + .description( + "Solver backend to use (`libsolv`, `resolvo`, or `libmamba` as an alias for `libsolv`)." + ) + .long_description(unindent(R"( + `libsolv` uses the Libsolv backend (default). + `resolvo` uses the experimental Resolvo backend. + `libmamba` is accepted as an alias for `libsolv` for compatibility with Conda, + which sets `solver: libmamba` in `.condarc` to select Libsolv via libmamba.)")) .set_rc_configurable() .set_env_var_names() .set_post_merge_hook( @@ -1411,10 +1418,10 @@ namespace mamba [](unsigned char c) { return static_cast(std::tolower(c)); } ); - if (normalized == "libsolv") + if (normalized == "libsolv" || normalized == "libmamba") { m_context.experimental_resolvo_solver = false; - value = normalized; + value = "libsolv"; return; } if (normalized == "resolvo") @@ -1424,8 +1431,9 @@ namespace mamba return; } - LOG_ERROR << "Invalid value for `solver`: " << value - << ". Expected `libsolv` or `resolvo`."; + LOG_ERROR + << "Invalid value for `solver`: " << value + << ". Expected `libsolv`, `resolvo`, or `libmamba` (alias for `libsolv`)."; throw std::runtime_error("Aborting."); } )); From 8c8de81bceede03d72b82e2a162177209936bf98 Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Wed, 27 May 2026 17:15:17 +0200 Subject: [PATCH 17/20] Add non-regression test for #4272. Signed-off-by: Julien Jerphanion --- libmamba/tests/data/env_file/omni.env.txt | 2 ++ .../test_sharded_repodata_integration.cpp | 21 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/libmamba/tests/data/env_file/omni.env.txt b/libmamba/tests/data/env_file/omni.env.txt index a14080859e..bfc9bf3931 100644 --- a/libmamba/tests/data/env_file/omni.env.txt +++ b/libmamba/tests/data/env_file/omni.env.txt @@ -1,3 +1,5 @@ +# Non-regression environment derived from omni.env.yaml (classic conda env format). +# Used to exercise sharded repodata with a large multi-domain spec set. anndata autopep8 bcftools diff --git a/libmamba/tests/src/core/test_sharded_repodata_integration.cpp b/libmamba/tests/src/core/test_sharded_repodata_integration.cpp index bc3aae196b..69807b8506 100644 --- a/libmamba/tests/src/core/test_sharded_repodata_integration.cpp +++ b/libmamba/tests/src/core/test_sharded_repodata_integration.cpp @@ -207,6 +207,23 @@ namespace return result; } + /** + * Read classic conda environment specs (one MatchSpec per non-empty, non-comment line). + */ + std::vector read_classic_env_specs(const fs::u8path& path) + { + std::vector specs; + for (const auto& line : read_lines(path)) + { + if (line.empty() || line.front() == '#') + { + continue; + } + specs.push_back(line); + } + return specs; + } + expected_t solve_common( Context& ctx, ChannelContext& channel_context, @@ -864,6 +881,8 @@ TEST_CASE("Sharded repodata - solve pyjs-obspy env specs on emscripten", "[mamba TEST_CASE("Sharded repodata - solve omni env specs", "[mamba::core][sharded][.integration]") { // Non-regression for https://github.com/mamba-org/mamba/issues/4277 + // Large classic env file (omni.env.txt): sharded loading and root expansion must remain + // solvable when mixing flat bioconda subdirs with sharded conda-forge. auto& ctx = mambatests::context(); mambatests::ScopedContextChange context_change{ ctx }; context_change.set_channels({ "conda-forge", "bioconda" }) @@ -877,7 +896,7 @@ TEST_CASE("Sharded repodata - solve omni env specs", "[mamba::core][sharded][.in auto channel_context = ChannelContext::make_conda_compatible(ctx); init_channels(ctx, channel_context); - const auto specs = read_lines(mambatests::test_data_dir / "env_file/omni.env.txt"); + const auto specs = read_classic_env_specs(mambatests::test_data_dir / "env_file/omni.env.txt"); REQUIRE(specs.size() >= 80); auto sharded_solution = solve_environment(ctx, channel_context, specs, true, cache_dir); From a818ce9de4c6c7fa0024e7bffdd3cd5b26e83bf9 Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Wed, 27 May 2026 17:19:07 +0200 Subject: [PATCH 18/20] Adapt non-regression test Signed-off-by: Julien Jerphanion --- libmamba/tests/data/env_file/omni.env.txt | 2 -- .../test_sharded_repodata_integration.cpp | 19 +------------------ 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/libmamba/tests/data/env_file/omni.env.txt b/libmamba/tests/data/env_file/omni.env.txt index bfc9bf3931..a14080859e 100644 --- a/libmamba/tests/data/env_file/omni.env.txt +++ b/libmamba/tests/data/env_file/omni.env.txt @@ -1,5 +1,3 @@ -# Non-regression environment derived from omni.env.yaml (classic conda env format). -# Used to exercise sharded repodata with a large multi-domain spec set. anndata autopep8 bcftools diff --git a/libmamba/tests/src/core/test_sharded_repodata_integration.cpp b/libmamba/tests/src/core/test_sharded_repodata_integration.cpp index 69807b8506..d1e0d815d5 100644 --- a/libmamba/tests/src/core/test_sharded_repodata_integration.cpp +++ b/libmamba/tests/src/core/test_sharded_repodata_integration.cpp @@ -207,23 +207,6 @@ namespace return result; } - /** - * Read classic conda environment specs (one MatchSpec per non-empty, non-comment line). - */ - std::vector read_classic_env_specs(const fs::u8path& path) - { - std::vector specs; - for (const auto& line : read_lines(path)) - { - if (line.empty() || line.front() == '#') - { - continue; - } - specs.push_back(line); - } - return specs; - } - expected_t solve_common( Context& ctx, ChannelContext& channel_context, @@ -896,7 +879,7 @@ TEST_CASE("Sharded repodata - solve omni env specs", "[mamba::core][sharded][.in auto channel_context = ChannelContext::make_conda_compatible(ctx); init_channels(ctx, channel_context); - const auto specs = read_classic_env_specs(mambatests::test_data_dir / "env_file/omni.env.txt"); + const auto specs = read_lines(mambatests::test_data_dir / "env_file/omni.env.txt"); REQUIRE(specs.size() >= 80); auto sharded_solution = solve_environment(ctx, channel_context, specs, true, cache_dir); From 7b7a61d9edeecf31c4d0e1994cc107385243ff1e Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Thu, 28 May 2026 09:54:32 +0200 Subject: [PATCH 19/20] Add non-regression test for repo2docker downgrade See: https://gist.github.com/minrk/5fdabeb7ab8cd2c69cbc27588fed1903 Signed-off-by: Julien Jerphanion --- .../src/core/test_sharded_repodata_integration.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/libmamba/tests/src/core/test_sharded_repodata_integration.cpp b/libmamba/tests/src/core/test_sharded_repodata_integration.cpp index d1e0d815d5..27b7ab64c1 100644 --- a/libmamba/tests/src/core/test_sharded_repodata_integration.cpp +++ b/libmamba/tests/src/core/test_sharded_repodata_integration.cpp @@ -449,6 +449,19 @@ namespace } } + auto read_explicit_urls(const fs::u8path& path) -> std::vector + { + std::vector urls; + for (const auto& line : read_lines(path)) + { + if (line.starts_with("http://") || line.starts_with("https://")) + { + urls.push_back(line); + } + } + return urls; + } + } TEST_CASE("Sharded repodata - load_channels accepts root_packages", "[mamba::core][sharded][.integration]") From 6ae8ff0d1c25e942dce97434d38d4e1452746754 Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Thu, 28 May 2026 12:22:04 +0200 Subject: [PATCH 20/20] Address review comments Signed-off-by: Julien Jerphanion Co-authored-by: Hind Montassif --- .../core/test_sharded_repodata_integration.cpp | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/libmamba/tests/src/core/test_sharded_repodata_integration.cpp b/libmamba/tests/src/core/test_sharded_repodata_integration.cpp index 27b7ab64c1..13bb34226c 100644 --- a/libmamba/tests/src/core/test_sharded_repodata_integration.cpp +++ b/libmamba/tests/src/core/test_sharded_repodata_integration.cpp @@ -44,7 +44,6 @@ namespace mamba { std::vector extract_package_names_from_specs(const std::vector& specs); - std::vector read_explicit_urls(const fs::u8path& path); void add_python_related_roots_if_python_requested(std::vector& root_packages); std::pair prepare_solver_context( Context& ctx, @@ -449,19 +448,6 @@ namespace } } - auto read_explicit_urls(const fs::u8path& path) -> std::vector - { - std::vector urls; - for (const auto& line : read_lines(path)) - { - if (line.starts_with("http://") || line.starts_with("https://")) - { - urls.push_back(line); - } - } - return urls; - } - } TEST_CASE("Sharded repodata - load_channels accepts root_packages", "[mamba::core][sharded][.integration]") @@ -1002,7 +988,7 @@ TEST_CASE("Sharded repodata - minrk gist downgrade non-regression", "[mamba::cor auto channel_context = ChannelContext::make_conda_compatible(ctx); init_channels(ctx, channel_context); - const auto explicit_urls = read_explicit_urls( + const auto explicit_urls = mambatests::read_explicit_urls( mambatests::test_data_dir / "env_file/minrk_environment.py-3.9-linux-64.lock" ); REQUIRE(explicit_urls.size() >= 150);