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/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/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/environment-dev.yml b/dev/environment-dev.yml index 3fe56f778b..049b59da40 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.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 703333834d..03bde080d9 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.3.0 - libsolv-static >=0.7.24 - yaml-cpp-static >=0.8.0 - reproc-static >=14.2.4.post0 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 2f10878112..daf18da712 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,9 @@ 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) +include(FixResolvoWindows) +include(CheckResolvoConditions) find_package(msgpack-c CONFIG REQUIRED) add_subdirectory(ext/solv-cpp) @@ -496,14 +508,15 @@ 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 + Resolvo::Resolvo ) if(UNIX) @@ -675,7 +688,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} @@ -683,11 +701,11 @@ macro(libmamba_create_target target_name linkage output_name) BZip2::BZip2 reproc reproc++ - simdjson::simdjson zstd::libzstd_shared 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 @@ -736,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/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..e34694191a --- /dev/null +++ b/libmamba/include/mamba/solver/resolvo/database.hpp @@ -0,0 +1,270 @@ +// 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); + } + }; + +#ifdef LIBMAMBA_RESOLVO_HAS_CONDITIONS + template <> + struct hash<::resolvo::ConditionId> + { + size_t operator()(const ::resolvo::ConditionId& id) const noexcept + { + return static_cast(id.id); + } + }; +#endif +} + +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; +#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( + ::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) override + { + 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::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; +#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; + + 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/api/configuration.cpp b/libmamba/src/api/configuration.cpp index f1e2ae3251..cf8df9e081 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,49 @@ namespace mamba ) .set_env_var_names()); + insert(Configurable("solver", std::string("libsolv")) + .group("Basic") + .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( + [&](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" || normalized == "libmamba") + { + m_context.experimental_resolvo_solver = false; + value = "libsolv"; + return; + } + if (normalized == "resolvo") + { + m_context.experimental_resolvo_solver = true; + value = normalized; + return; + } + + LOG_ERROR + << "Invalid value for `solver`: " << value + << ". Expected `libsolv`, `resolvo`, or `libmamba` (alias for `libsolv`)."; + 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..e7a07dd279 100644 --- a/libmamba/src/api/install.cpp +++ b/libmamba/src/api/install.cpp @@ -608,7 +608,39 @@ namespace mamba // Console stream prints on destruction } - auto outcome = solve_request_with_status(ctx.experimental_matchspec_parsing, db, request); + 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 c8cdce8a73..80d96e6fbb 100644 --- a/libmamba/src/api/update.cpp +++ b/libmamba/src/api/update.cpp @@ -186,7 +186,27 @@ namespace mamba // Console stream prints on destruction } - auto outcome = solve_request_with_status(ctx.experimental_matchspec_parsing, db, request); + 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 98769076f4..a8d6a2ae62 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" @@ -485,37 +488,86 @@ 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, + 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()) { 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(); - auto outcome = solver::libsolv::Solver() - .solve( - db, - request, - experimental_matchspec_parsing - ? solver::libsolv::MatchSpecParser::Mamba - : solver::libsolv::MatchSpecParser::Mixed - ) - .value(); - if (Console::can_report_status()) + try { - Console::instance().print_in_place( - fmt::format( - "{:<85} {:>20}", - "Resolving Environment", - done_with_duration(std::chrono::steady_clock::now() - started_at) - ), - true - ); + 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 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 outcome; + } + catch (...) + { + if (Console::can_report_status()) + { + Console::instance().print_in_place( + fmt::format("{:<85} {:>20}", resolving_label, "✗ Failed"), + true + ); + } + throw; } - return outcome; } solver::libsolv::Database diff --git a/libmamba/src/api/utils.hpp b/libmamba/src/api/utils.hpp index ba61ad4a54..b1274131bc 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, + bool use_resolvo = false ); /** diff --git a/libmamba/src/solver/resolvo/database.cpp b/libmamba/src/solver/resolvo/database.cpp new file mode 100644 index 0000000000..2059766470 --- /dev/null +++ b/libmamba/src/solver/resolvo/database.cpp @@ -0,0 +1,692 @@ +// 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 }; + } + + 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 + // `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 }); + normalized_matchspec_to_version_set_id.emplace(raw_match_spec_str, id); + 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) + { + ::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() } + ); +#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) + { + // 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); + + // 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 }); + + // 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; + } + + /** + * 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; + } + +#ifdef LIBMAMBA_RESOLVO_HAS_CONDITIONS + ::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 }); + } +#endif + + /** + * 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. + 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 (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_it->second); + + // 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) + { + return solvable_to_dependencies.at(solvable_id); + } +} diff --git a/libmamba/src/solver/resolvo/solver.cpp b/libmamba/src/solver/resolvo/solver.cpp new file mode 100644 index 0000000000..68aa8536d3 --- /dev/null +++ b/libmamba/src/solver/resolvo/solver.cpp @@ -0,0 +1,190 @@ +// 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/string.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]; + if (util::starts_with(solvable.name, "__")) + { + continue; + } + // 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) }); + } + + 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> 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) + { + req_conditional.push_back(::resolvo::ConditionalRequirement(::resolvo::Requirement(req))); + } + + ::resolvo::Problem problem = { + .requirements = req_conditional, + .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); + + 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/CMakeLists.txt b/libmamba/tests/CMakeLists.txt index db0e38d240..7776540582 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 @@ -154,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) diff --git a/libmamba/tests/src/core/test_sharded_repodata_integration.cpp b/libmamba/tests/src/core/test_sharded_repodata_integration.cpp index bc3aae196b..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, @@ -864,6 +863,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" }) @@ -987,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); 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..fe243f89ec --- /dev/null +++ b/libmamba/tests/src/solver/resolvo/test_solver.cpp @@ -0,0 +1,771 @@ +// 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 "mamba/solver/resolvo/database.hpp" +#include "mamba/solver/resolvo/solver.hpp" + +#include "mambatests.hpp" + + +using namespace mamba; +using namespace mamba::specs; +using namespace mamba::solver; + +using namespace resolvo; + +// 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; +}; + +mamba::solver::libsolv::Database libsolv_db = create_libsolv_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 {}; +} + +mamba::solver::resolvo::Database +create_resolvo_libmamba_db() +{ + auto resolvo_db = mamba::solver::resolvo::Database( + { + /* .platforms= */ { "linux-64", "noarch" }, + /* .channel_alias= */ specs::CondaURL::parse("https://conda.anaconda.org/").value(), + } + ); + + 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; +} + +mamba::solver::resolvo::Database resolvo_db = create_resolvo_libmamba_db(); + +std::vector +resolvo_libmamba_resolve(const std::vector& specs) +{ + auto resolvo_db = create_resolvo_libmamba_db(); + + 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") +{ + using namespace specs::match_spec_literals; + + using PackageInfo = PackageInfo; + + SECTION("Addition of PackageInfo to PackageDatabase") + { + 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"); + 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") + { + 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); + + 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") + { + 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); + + 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)") + { + 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); + + 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") + { + 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); + 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") + { + mamba::solver::resolvo::Database database( + { + /* .platforms= */ { "linux-64", "noarch" }, + /* .channel_alias= */ specs::CondaURL::parse("https://conda.anaconda.org/").value(), + } + ); + + database.add_repo_from_repodata_json( + "/tmp/linux-64/repodata.json", + "https://conda.anaconda.org/conda-forge/linux-64", + "conda-forge", + false + ); + + std::cout << "Number of solvables: " << database.solvable_pool.size() << std::endl; + } + + SECTION("Parse noarch/repodata.json") + { + mamba::solver::resolvo::Database database( + { + /* .platforms= */ { "linux-64", "noarch" }, + /* .channel_alias= */ specs::CondaURL::parse("https://conda.anaconda.org/").value(), + } + ); + + database.add_repo_from_repodata_json( + "/tmp/noarch/repodata.json", + "https://conda.anaconda.org/conda-forge/noarch", + "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 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); + + 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 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); + + 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") + { + // 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; } + ); + + 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++) + { + 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_libmamba_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_libmamba_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); + } + } +} 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)); + } + } +} 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");