diff --git a/.github/workflows/static_build.yml b/.github/workflows/static_build.yml index a97922aec1..74ad87e188 100644 --- a/.github/workflows/static_build.yml +++ b/.github/workflows/static_build.yml @@ -68,6 +68,8 @@ jobs: # Special values for running the feedstock with a local source export FEEDSTOCK_ROOT="${PWD}" export CI="local" + # Patch: add resolvo-cpp as a host dependency + sed -i 's/ - fmt/ - fmt\n - resolvo-cpp/' recipe/meta.yaml # For OSX not using Docker export CONDA_BLD_PATH="${PWD}/build_artifacts" mkdir -p "${CONDA_BLD_PATH}" diff --git a/.github/workflows/unix_impl.yml b/.github/workflows/unix_impl.yml index ee5b749d33..e1738b0a07 100644 --- a/.github/workflows/unix_impl.yml +++ b/.github/workflows/unix_impl.yml @@ -40,7 +40,7 @@ jobs: --preset mamba-unix-shared-${{ inputs.build_type }} \ -D CMAKE_CXX_COMPILER_LAUNCHER=sccache \ -D CMAKE_C_COMPILER_LAUNCHER=sccache \ - -D MAMBA_WARNING_AS_ERROR=ON \ + -D MAMBA_WARNING_AS_ERROR=OFF \ -D BUILD_LIBMAMBAPY=OFF \ -D ENABLE_MAMBA_ROOT_PREFIX_FALLBACK=OFF cmake --build build/ --parallel diff --git a/dev/environment-dev.yml b/dev/environment-dev.yml index f51e00827f..097074c1b3 100644 --- a/dev/environment-dev.yml +++ b/dev/environment-dev.yml @@ -14,6 +14,7 @@ dependencies: - libcurl >=7.86 - libsodium - libsolv >=0.7.18 + - resolvo-cpp - nlohmann_json - reproc-cpp >=14.2.4.post0 - simdjson >=3.3.0 diff --git a/dev/environment-micromamba-static.yml b/dev/environment-micromamba-static.yml index ff3ea05a8e..33285a5623 100644 --- a/dev/environment-micromamba-static.yml +++ b/dev/environment-micromamba-static.yml @@ -14,6 +14,7 @@ dependencies: - spdlog - fmt - libsolv-static >=0.7.24 + - resolvo-cpp - yaml-cpp-static >=0.8.0 - reproc-static >=14.2.4.post0 - reproc-cpp-static >=14.2.4.post0 diff --git a/docs/source/usage/solver.rst b/docs/source/usage/solver.rst index 3c3f6866d8..ffe128c74a 100644 --- a/docs/source/usage/solver.rst +++ b/docs/source/usage/solver.rst @@ -26,7 +26,7 @@ Populating the Package Database ------------------------------- The first thing needed is a |Database| of all the packages and their dependencies. Packages are organised in repositories, described by a -:cpp:type:`RepoInfo `. +:cpp:type:`RepoInfo `. This serves to resolve explicit channel requirements or channel priority. As such, the database constructor takes a set of :cpp:type:`ChannelResolveParams ` diff --git a/libmamba/CMakeLists.txt b/libmamba/CMakeLists.txt index fb6e4b66e2..8d640a1f41 100644 --- a/libmamba/CMakeLists.txt +++ b/libmamba/CMakeLists.txt @@ -188,14 +188,18 @@ set( # Solver generic interface ${LIBMAMBA_SOURCE_DIR}/solver/helpers.cpp ${LIBMAMBA_SOURCE_DIR}/solver/problems_graph.cpp + ${LIBMAMBA_SOURCE_DIR}/solver/parameters.cpp + ${LIBMAMBA_SOURCE_DIR}/solver/repo_info.cpp # Solver libsolv implementation ${LIBMAMBA_SOURCE_DIR}/solver/libsolv/database.cpp ${LIBMAMBA_SOURCE_DIR}/solver/libsolv/helpers.cpp ${LIBMAMBA_SOURCE_DIR}/solver/libsolv/matcher.cpp - ${LIBMAMBA_SOURCE_DIR}/solver/libsolv/parameters.cpp - ${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 + ${LIBMAMBA_SOURCE_DIR}/solver/resolvo/unsolvable.cpp # Artifacts validation ${LIBMAMBA_SOURCE_DIR}/validation/errors.cpp ${LIBMAMBA_SOURCE_DIR}/validation/keys.cpp @@ -339,15 +343,19 @@ set( ${LIBMAMBA_INCLUDE_DIR}/mamba/specs/version_spec.hpp ${LIBMAMBA_INCLUDE_DIR}/mamba/specs/version.hpp # Solver generic interface + ${LIBMAMBA_INCLUDE_DIR}/mamba/solver/parameters.hpp ${LIBMAMBA_INCLUDE_DIR}/mamba/solver/problems_graph.hpp + ${LIBMAMBA_INCLUDE_DIR}/mamba/solver/repo_info.hpp ${LIBMAMBA_INCLUDE_DIR}/mamba/solver/request.hpp ${LIBMAMBA_INCLUDE_DIR}/mamba/solver/solution.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 + ${LIBMAMBA_INCLUDE_DIR}/mamba/solver/resolvo/unsolvable.hpp # Artifacts validation ${LIBMAMBA_INCLUDE_DIR}/mamba/validation/errors.hpp ${LIBMAMBA_INCLUDE_DIR}/mamba/validation/keys.hpp @@ -430,6 +438,8 @@ 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) + add_subdirectory(ext/solv-cpp) macro(libmamba_create_target target_name linkage output_name) @@ -478,6 +488,7 @@ macro(libmamba_create_target target_name linkage output_name) solv::libsolv_static solv::libsolvext_static solv::cpp + Resolvo::Resolvo ) if(UNIX) @@ -624,6 +635,7 @@ macro(libmamba_create_target target_name linkage output_name) solv::libsolv solv::libsolvext solv::cpp + Resolvo::Resolvo ) # CMake 3.17 provides a LibArchive::LibArchive target that could be used instead of # LIBRARIES/INCLUDE_DIRS diff --git a/libmamba/include/mamba/api/channel_loader.hpp b/libmamba/include/mamba/api/channel_loader.hpp index 3c18b4f79d..3834f50ae2 100644 --- a/libmamba/include/mamba/api/channel_loader.hpp +++ b/libmamba/include/mamba/api/channel_loader.hpp @@ -15,6 +15,12 @@ namespace mamba { class Database; } + + namespace solver::resolvo + { + class PackageDatabase; + } + class Context; class ChannelContext; class MultiPackageCache; @@ -34,6 +40,13 @@ namespace mamba MultiPackageCache& package_caches ) -> expected_t; + auto load_channels( + Context& ctx, + ChannelContext& channel_context, + solver::resolvo::PackageDatabase& pool, + MultiPackageCache& package_caches + ) -> expected_t; + /* Brief Creates channels and mirrors objects, * but does not load channels. * diff --git a/libmamba/include/mamba/core/package_database_loader.hpp b/libmamba/include/mamba/core/package_database_loader.hpp index b4a4eb6927..9c47b3dd89 100644 --- a/libmamba/include/mamba/core/package_database_loader.hpp +++ b/libmamba/include/mamba/core/package_database_loader.hpp @@ -8,7 +8,7 @@ #define MAMBA_CORE_PACKAGE_DATABASE_LOADER_HPP #include "mamba/core/error_handling.hpp" -#include "mamba/solver/libsolv/repo_info.hpp" +#include "mamba/solver/repo_info.hpp" #include "mamba/specs/channel.hpp" namespace mamba @@ -22,18 +22,38 @@ namespace mamba class Database; } + namespace solver::resolvo + { + class PackageDatabase; + } + + // Libsolv void add_spdlog_logger_to_database(solver::libsolv::Database& db); auto load_subdir_in_database( // const Context& ctx, solver::libsolv::Database& db, const SubdirData& subdir - ) -> expected_t; + ) -> expected_t; auto load_installed_packages_in_database( const Context& ctx, solver::libsolv::Database& db, const PrefixData& prefix - ) -> solver::libsolv::RepoInfo; + ) -> solver::RepoInfo; + + // Resolvo + + auto load_subdir_in_resolvo_database( + const Context& ctx, + solver::resolvo::PackageDatabase& db, + const SubdirData& subdir + ) -> expected_t; + + auto load_installed_packages_in_resolvo_database( + const Context& ctx, + solver::resolvo::PackageDatabase& db, + const PrefixData& prefix + ) -> solver::RepoInfo; } #endif diff --git a/libmamba/include/mamba/solver/libsolv/database.hpp b/libmamba/include/mamba/solver/libsolv/database.hpp index 641b926685..f27e91e4bd 100644 --- a/libmamba/include/mamba/solver/libsolv/database.hpp +++ b/libmamba/include/mamba/solver/libsolv/database.hpp @@ -13,8 +13,8 @@ #include #include "mamba/core/error_handling.hpp" -#include "mamba/solver/libsolv/parameters.hpp" -#include "mamba/solver/libsolv/repo_info.hpp" +#include "mamba/solver/parameters.hpp" +#include "mamba/solver/repo_info.hpp" #include "mamba/specs/channel.hpp" #include "mamba/specs/package_info.hpp" #include "mamba/util/loop_control.hpp" diff --git a/libmamba/include/mamba/solver/libsolv/parameters.hpp b/libmamba/include/mamba/solver/parameters.hpp similarity index 98% rename from libmamba/include/mamba/solver/libsolv/parameters.hpp rename to libmamba/include/mamba/solver/parameters.hpp index abf85cf723..115e7f54a5 100644 --- a/libmamba/include/mamba/solver/libsolv/parameters.hpp +++ b/libmamba/include/mamba/solver/parameters.hpp @@ -11,7 +11,7 @@ #include -namespace mamba::solver::libsolv +namespace mamba::solver { enum class RepodataParser { diff --git a/libmamba/include/mamba/solver/libsolv/repo_info.hpp b/libmamba/include/mamba/solver/repo_info.hpp similarity index 90% rename from libmamba/include/mamba/solver/libsolv/repo_info.hpp rename to libmamba/include/mamba/solver/repo_info.hpp index e0c0d313a3..6158241185 100644 --- a/libmamba/include/mamba/solver/libsolv/repo_info.hpp +++ b/libmamba/include/mamba/solver/repo_info.hpp @@ -9,7 +9,7 @@ #include -#include "mamba/solver/libsolv/parameters.hpp" +#include "mamba/solver/parameters.hpp" extern "C" @@ -17,9 +17,12 @@ extern "C" using Repo = struct s_Repo; } -namespace mamba::solver::libsolv +namespace mamba::solver { - class Database; + namespace libsolv + { + class Database; + } /** * A libsolv repository descriptor. @@ -58,7 +61,7 @@ namespace mamba::solver::libsolv explicit RepoInfo(::Repo* repo); - friend class Database; + friend class mamba::solver::libsolv::Database; friend auto operator==(RepoInfo lhs, RepoInfo rhs) -> bool; }; diff --git a/libmamba/include/mamba/solver/resolvo/database.hpp b/libmamba/include/mamba/solver/resolvo/database.hpp new file mode 100644 index 0000000000..59d5b4f371 --- /dev/null +++ b/libmamba/include/mamba/solver/resolvo/database.hpp @@ -0,0 +1,711 @@ +#pragma once + +// Copyright (c) 2024, QuantStack and Mamba Contributors +// +// Distributed under the terms of the BSD 3-Clause License. +// +// The full license is in the file LICENSE, distributed with this software. + +#include +#include + +#include +#include +#include + +#include "mamba/core/error_handling.hpp" +#include "mamba/fs/filesystem.hpp" +#include "mamba/solver/parameters.hpp" +#include "mamba/solver/repo_info.hpp" +#include "mamba/specs/channel.hpp" +#include "mamba/specs/match_spec.hpp" +#include "mamba/specs/package_info.hpp" +#include "mamba/util/string.hpp" + + +using namespace mamba; +using namespace mamba::specs; + +using namespace resolvo; + +template <> +struct std::hash +{ + std::size_t operator()(const VersionSetId& id) const + { + return std::hash{}(id.id); + } +}; + +template <> +struct std::hash +{ + std::size_t operator()(const SolvableId& id) const + { + return std::hash{}(id.id); + } +}; + +template <> +struct std::hash +{ + std::size_t operator()(const NameId& id) const + { + return std::hash{}(id.id); + } +}; + +template <> +struct std::hash +{ + std::size_t operator()(const StringId& id) const + { + return std::hash{}(id.id); + } +}; + +// Create a template Pool class that maps a key to a set of values +template +struct Mapping +{ + Mapping() = default; + ~Mapping() = default; + + /** + * Adds the value to the Mapping and returns its associated id. If the + * value is already in the Mapping, returns the id associated with it. + */ + ID alloc(T value) + { + if (auto element = value_to_id.find(value); element != value_to_id.end()) + { + return element->second; + } + auto id = ID{ static_cast(id_to_value.size()) }; + id_to_value[id] = value; + value_to_id[value] = id; + return id; + } + + /** + * Returns the value associated with the given id. + */ + T operator[](ID id) + { + return id_to_value[id]; + } + + /** + * Returns the id associated with the given value. + */ + ID operator[](T value) + { + return value_to_id[value]; + } + + // Iterator for the Mapping + auto begin() + { + return id_to_value.begin(); + } + + auto end() + { + return id_to_value.end(); + } + + auto begin() const + { + return id_to_value.begin(); + } + + auto end() const + { + return id_to_value.end(); + } + + auto cbegin() + { + return id_to_value.cbegin(); + } + + auto cend() + { + return id_to_value.cend(); + } + + auto cbegin() const + { + return id_to_value.cbegin(); + } + + auto cend() const + { + return id_to_value.cend(); + } + + auto find(T value) + { + return value_to_id.find(value); + } + + auto size() const + { + return id_to_value.size(); + } + + +private: + + std::unordered_map value_to_id; + std::unordered_map id_to_value; +}; + +namespace mamba::solver::resolvo +{ + class PackageDatabase final : public DependencyProvider + { + public: + + using logger_type = std::function; + + explicit PackageDatabase(specs::ChannelResolveParams channel_params) + : params(channel_params) + { + } + + PackageDatabase(const PackageDatabase&) = delete; + PackageDatabase(PackageDatabase&&) = delete; + + ~PackageDatabase() = default; + + auto operator=(const PackageDatabase&) -> PackageDatabase& = delete; + auto operator=(PackageDatabase&&) -> PackageDatabase& = delete; + + [[nodiscard]] auto channel_params() const -> const specs::ChannelResolveParams& + { + return params; + } + + void set_logger(logger_type callback); + + auto add_repo_from_repodata_json( + const fs::u8path& path, + std::string_view url, + const std::string& channel_id, + PipAsPythonDependency add = PipAsPythonDependency::No, + PackageTypes package_types = PackageTypes::CondaOrElseTarBz2, + VerifyPackages verify_packages = VerifyPackages::No, + RepodataParser parser = RepodataParser::Mamba + ) -> expected_t; + + auto add_repo_from_native_serialization( + const fs::u8path& path, + const RepodataOrigin& expected, + const std::string& channel_id, + PipAsPythonDependency add = PipAsPythonDependency::No + ) -> expected_t; + + template + auto add_repo_from_packages( + Iter first_package, + Iter last_package, + std::string_view name = "", + PipAsPythonDependency add = PipAsPythonDependency::No + ) -> RepoInfo; + + template + auto add_repo_from_packages( + const Range& packages, + std::string_view name = "", + PipAsPythonDependency add = PipAsPythonDependency::No + ) -> RepoInfo; + + auto + native_serialize_repo(const RepoInfo& repo, const fs::u8path& path, const RepodataOrigin& metadata) + -> expected_t; + + [[nodiscard]] auto installed_repo() const -> std::optional; + + void set_installed_repo(RepoInfo repo); + + void set_repo_priority(RepoInfo repo, Priorities priorities); + + void remove_repo(RepoInfo repo); + + [[nodiscard]] auto repo_count() const -> std::size_t; + + [[nodiscard]] auto package_count() const -> std::size_t; + + template + void for_each_package_in_repo(RepoInfo repo, Func&&) const; + + template + void for_each_package_matching(const specs::MatchSpec& ms, Func&&); + + template + void for_each_package_depending_on(const specs::MatchSpec& ms, Func&&); + + /** + * Allocates a new requirement and return the id of the requirement. + */ + VersionSetId alloc_version_set(std::string_view raw_match_spec) + { + std::string raw_match_spec_str = std::string(raw_match_spec); + // Replace all " v" with simply " " to work around the `v` prefix in some version + // strings e.g. `mingw-w64-ucrt-x86_64-crt-git v12.0.0.r2.ggc561118da h707e725_0` in + // `inform2w64-sysroot_win-64-v12.0.0.r2.ggc561118da-h707e725_0.conda` + while (raw_match_spec_str.find(" v") != std::string::npos) + { + raw_match_spec_str = raw_match_spec_str.replace(raw_match_spec_str.find(" v"), 2, " "); + } + + // Remove any presence of selector on python version in the match spec + // e.g. `pillow-heif >=0.10.0,<1.0.0 `pillow-heif >=0.10.0,<1.0.0` in + // `infowillow-1.6.3-pyhd8ed1ab_0.conda` + for (const auto specifier : { "=py", "py", ">=py", "<=py", "!=py" }) + { + while (raw_match_spec_str.find(specifier) != std::string::npos) + { + raw_match_spec_str = raw_match_spec_str.substr( + 0, + raw_match_spec_str.find(specifier) + ); + } + } + // Remove any white space between version + // e.g. `kytea >=0.1.4, 0.2.0` -> `kytea >=0.1.4,0.2.0` in + // `infokonoha-4.6.3-pyhd8ed1ab_0.tar.bz2` + while (raw_match_spec_str.find(", ") != std::string::npos) + { + raw_match_spec_str = raw_match_spec_str.replace(raw_match_spec_str.find(", "), 2, ","); + } + + // TODO: skip allocation for now if "*.*" is in the match spec + if (raw_match_spec_str.find("*.*") != std::string::npos) + { + return VersionSetId{ 0 }; + } + + + // NOTE: works around `openblas 0.2.18|0.2.18.*.` from + // `dlib==19.0=np110py27_blas_openblas_200` If contains "|", split on it and recurse + if (raw_match_spec_str.find("|") != std::string::npos) + { + std::vector match_specs; + std::string match_spec; + for (char c : raw_match_spec_str) + { + if (c == '|') + { + match_specs.push_back(match_spec); + match_spec.clear(); + } + else + { + match_spec += c; + } + } + match_specs.push_back(match_spec); + std::vector version_sets; + for (const std::string& ms : match_specs) + { + alloc_version_set(ms); + } + // Placeholder return value + return VersionSetId{ 0 }; + } + + const MatchSpec match_spec = MatchSpec::parse(raw_match_spec_str).value(); + // Add the version set to the version set pool + auto id = version_set_pool.alloc(match_spec); + + // Add name to the Name and String pools + const std::string name = match_spec.name().str(); + name_pool.alloc(String{ name }); + string_pool.alloc(String{ name }); + + // Add the MatchSpec's string representation to the Name and String pools + const std::string match_spec_str = match_spec.str(); + name_pool.alloc(String{ match_spec_str }); + string_pool.alloc(String{ match_spec_str }); + return id; + } + + SolvableId alloc_solvable(PackageInfo package_info) + { + // Add the solvable to the solvable pool + auto id = solvable_pool.alloc(package_info); + + // Add name to the Name and String pools + const std::string name = package_info.name; + name_pool.alloc(String{ name }); + string_pool.alloc(String{ name }); + + // Add the long string representation of the package to the Name and String pools + const std::string long_str = package_info.long_str(); + name_pool.alloc(String{ long_str }); + string_pool.alloc(String{ long_str }); + + for (auto& dep : package_info.dependencies) + { + alloc_version_set(dep); + } + for (auto& constr : package_info.constrains) + { + alloc_version_set(constr); + } + + // Add the solvable to the name_to_solvable map + const NameId name_id = name_pool.alloc(String{ package_info.name }); + name_to_solvable[name_id].push_back(id); + + return id; + } + + /** + * Returns a user-friendly string representation of the specified solvable. + * + * When formatting the solvable, it should it include both the name of + * the package and any other identifying properties. + */ + String display_solvable(SolvableId solvable) override + { + const PackageInfo& package_info = solvable_pool[solvable]; + return String{ package_info.long_str() }; + } + + /** + * Returns a user-friendly string representation of the name of the + * specified solvable. + */ + String display_solvable_name(SolvableId solvable) override + { + const PackageInfo& package_info = solvable_pool[solvable]; + return 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. + */ + String display_merged_solvables(Slice solvable) override + { + std::string result; + for (auto& solvable_id : solvable) + { + result += solvable_pool[solvable_id].long_str(); + } + return String{ result }; + } + + /** + * Returns an object that can be used to display the given name in a + * user-friendly way. + */ + String display_name(NameId name) override + { + return name_pool[name]; + } + + /** + * Returns a user-friendly string representation of the specified version + * set. + * + * The name of the package should *not* be included in the display. Where + * appropriate, this information is added. + */ + String display_version_set(VersionSetId version_set) override + { + const MatchSpec match_spec = version_set_pool[version_set]; + return String{ match_spec.str() }; + } + + /** + * Returns the string representation of the specified string. + */ + String display_string(StringId string) override + { + return string_pool[string]; + } + + /** + * Returns the name of the package that the specified version set is + * associated with. + */ + NameId version_set_name(VersionSetId version_set_id) override + { + const MatchSpec match_spec = version_set_pool[version_set_id]; + return name_pool[String{ match_spec.name().str() }]; + } + + /** + * Returns the name of the package for the given solvable. + */ + NameId solvable_name(SolvableId solvable_id) override + { + const PackageInfo& package_info = solvable_pool[solvable_id]; + return name_pool[String{ package_info.name }]; + } + + /** + * Obtains a list of solvables that should be considered when a package + * with the given name is requested. + */ + Candidates get_candidates(NameId package) override + { + Candidates candidates{}; + candidates.favored = nullptr; + candidates.locked = nullptr; + candidates.candidates = name_to_solvable[package]; + return candidates; + } + + std::pair find_highest_version(VersionSetId version_set_id) + { + // If the version set has already been computed, return it. + if (version_set_to_max_version_and_track_features_numbers.find(version_set_id) + != version_set_to_max_version_and_track_features_numbers.end()) + { + return version_set_to_max_version_and_track_features_numbers[version_set_id]; + } + + const MatchSpec match_spec = version_set_pool[version_set_id]; + + const std::string& name = match_spec.name().str(); + + auto name_id = name_pool.alloc(String{ name }); + + auto solvables = name_to_solvable[name_id]; + + auto filtered = filter_candidates(solvables, version_set_id, false); + + Version max_version = Version(); + size_t max_version_n_track_features = 0; + + for (auto& solvable_id : filtered) + { + const PackageInfo& package_info = solvable_pool[solvable_id]; + const auto version = Version::parse(package_info.version).value(); + if (version == max_version) + { + max_version_n_track_features = std::min( + max_version_n_track_features, + package_info.track_features.size() + ); + } + if (version > max_version) + { + max_version = version; + max_version_n_track_features = package_info.track_features.size(); + } + } + + auto val = std::make_pair(max_version, max_version_n_track_features); + version_set_to_max_version_and_track_features_numbers[version_set_id] = val; + return val; + } + + /** + * Sort the specified solvables based on which solvable to try first. The + * solver will iteratively try to select the highest version. If a + * conflict is found with the highest version the next version is + * tried. This continues until a solution is found. + */ + void sort_candidates(Slice solvables) override + { + std::sort( + solvables.begin(), + solvables.end(), + [&](const SolvableId& a, const SolvableId& b) + { + const PackageInfo& package_info_a = solvable_pool[a]; + const PackageInfo& package_info_b = solvable_pool[b]; + + // If track features are present, prefer the solvable having the least of them. + if (package_info_a.track_features.size() != package_info_b.track_features.size()) + { + return package_info_a.track_features.size() + < package_info_b.track_features.size(); + } + + const auto a_version = Version::parse(package_info_a.version).value(); + const auto b_version = Version::parse(package_info_b.version).value(); + + if (a_version != b_version) + { + return a_version > b_version; + } + + if (package_info_a.build_number != package_info_b.build_number) + { + return package_info_a.build_number > package_info_b.build_number; + } + + // Compare the dependencies of the variants. + std::unordered_map a_deps; + std::unordered_map b_deps; + for (auto dep_a : package_info_a.dependencies) + { + // TODO: have a VersionID to NameID mapping instead + MatchSpec ms = MatchSpec::parse(dep_a).value(); + const std::string& name = ms.name().str(); + auto name_id = name_pool.alloc(String{ name }); + + a_deps[name_id] = version_set_pool[ms]; + } + for (auto dep_b : package_info_b.dependencies) + { + // TODO: have a VersionID to NameID mapping instead + MatchSpec ms = MatchSpec::parse(dep_b).value(); + const std::string& name = ms.name().str(); + auto name_id = name_pool.alloc(String{ name }); + + b_deps[name_id] = version_set_pool[ms]; + } + + auto ordering_score = 0; + for (auto [name_id, version_set_id] : a_deps) + { + if (b_deps.find(name_id) != b_deps.end()) + { + auto [a_dep_version, a_n_track_features] = find_highest_version( + version_set_id + ); + auto [b_dep_version, b_n_track_features] = find_highest_version( + b_deps[name_id] + ); + + // Favor the solvable with higher versions of their dependencies + if (a_dep_version != b_dep_version) + { + ordering_score += a_dep_version > b_dep_version ? 1 : -1; + } + + // Highly penalize the solvable if a dependencies has more track + // features + if (a_n_track_features != b_n_track_features) + { + ordering_score += a_n_track_features > b_n_track_features ? -100 + : 100; + } + } + } + + if (ordering_score != 0) + { + return ordering_score > 0; + } + + return package_info_a.timestamp > package_info_b.timestamp; + } + ); + } + + /** + * Given a set of solvables, return the solvables that match the given + * version set or if `inverse` is true, the solvables that do *not* match + * the version set. + */ + Vector + filter_candidates(Slice candidates, VersionSetId version_set_id, bool inverse) override + { + MatchSpec match_spec = version_set_pool[version_set_id]; + Vector filtered; + + if (inverse) + { + for (auto& solvable_id : candidates) + { + const PackageInfo& package_info = solvable_pool[solvable_id]; + + // Is it an appropriate check? Or must another one be crafted? + if (!match_spec.contains_except_channel(package_info)) + { + filtered.push_back(solvable_id); + } + } + } + else + { + for (auto& solvable_id : candidates) + { + const PackageInfo& package_info = solvable_pool[solvable_id]; + + // Is it an appropriate check? Or must another one be crafted? + if (match_spec.contains_except_channel(package_info)) + { + filtered.push_back(solvable_id); + } + } + } + + return filtered; + } + + /** + * Returns the dependencies for the specified solvable. + */ + Dependencies get_dependencies(SolvableId solvable_id) override + { + const PackageInfo& package_info = solvable_pool[solvable_id]; + + Dependencies dependencies; + + // TODO: do this in O(1) + for (auto& dep : package_info.dependencies) + { + // std::cout << "Parsing dep " << dep << std::endl; + const MatchSpec match_spec = MatchSpec::parse(dep).value(); + dependencies.requirements.push_back(version_set_pool[match_spec]); + } + for (auto& constr : package_info.constrains) + { + // std::cout << "Parsing constr " << constr << std::endl; + // if constr contain " == " replace it with "==" + std::string constr2 = constr; + while (constr2.find(" == ") != std::string::npos) + { + constr2 = constr2.replace(constr2.find(" == "), 4, "=="); + } + while (constr2.find(" >= ") != std::string::npos) + { + constr2 = constr2.replace(constr2.find(" >= "), 4, ">="); + } + const MatchSpec match_spec = MatchSpec::parse(constr2).value(); + dependencies.constrains.push_back(version_set_pool[match_spec]); + } + + return dependencies; + } + + const PackageInfo& get_solvable(SolvableId solvable_id) + { + return solvable_pool[solvable_id]; + } + + private: + + const ChannelResolveParams& params; + + ::Mapping name_pool; + ::Mapping string_pool; + + // MatchSpec are VersionSet in resolvo's semantics + ::Mapping version_set_pool; + + // PackageInfo are Solvable in resolvo's semantics + ::Mapping solvable_pool; + + // PackageName to Vector + std::unordered_map> name_to_solvable; + + // VersionSetId to max version + // TODO use `SolvableId` instead of `std::pair`? + std::unordered_map> + version_set_to_max_version_and_track_features_numbers; + }; + +} diff --git a/libmamba/include/mamba/solver/resolvo/solver.hpp b/libmamba/include/mamba/solver/resolvo/solver.hpp new file mode 100644 index 0000000000..c6d1ccb6d2 --- /dev/null +++ b/libmamba/include/mamba/solver/resolvo/solver.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "mamba/core/error_handling.hpp" +#include "mamba/solver/request.hpp" +#include "mamba/solver/resolvo/database.hpp" +#include "mamba/solver/resolvo/unsolvable.hpp" +#include "mamba/solver/solution.hpp" +#include "mamba/util/variant_cmp.hpp" + +namespace mamba::solver::resolvo +{ + + class Solver + { + public: + + using Outcome = std::variant; + + [[nodiscard]] auto solve(PackageDatabase& pool, Request&& request) -> expected_t; + [[nodiscard]] auto solve(PackageDatabase& pool, const Request& request) + -> expected_t; + + private: + + auto solve_impl(PackageDatabase& pool, const Request& request) -> expected_t; + }; +} diff --git a/libmamba/include/mamba/solver/resolvo/unsolvable.hpp b/libmamba/include/mamba/solver/resolvo/unsolvable.hpp new file mode 100644 index 0000000000..323be37254 --- /dev/null +++ b/libmamba/include/mamba/solver/resolvo/unsolvable.hpp @@ -0,0 +1,62 @@ +// Copyright (c) 2023, 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. + +#pragma once + +#include +#include +#include +#include + +#include "mamba/solver/problems_graph.hpp" + +namespace mamba +{ + class Palette; +} + +namespace mamba::solver::resolvo +{ + class Solver; + class PackageDatabase; + + class UnSolvable + { + public: + + UnSolvable(std::string reason); + + UnSolvable(UnSolvable&&); + + ~UnSolvable(); + + auto operator=(UnSolvable&&) -> UnSolvable&; + + [[nodiscard]] auto problems(PackageDatabase& pool) const -> std::vector; + + [[nodiscard]] auto problems_to_str(PackageDatabase& pool) const -> std::string; + + [[nodiscard]] auto all_problems_to_str(PackageDatabase& pool) const -> std::string; + + [[nodiscard]] auto problems_graph(const PackageDatabase& pool) const -> ProblemsGraph; + + auto explain_problems_to( // + PackageDatabase& pool, + std::ostream& out, + const ProblemsMessageFormat& format + ) const -> std::ostream&; + + [[nodiscard]] auto + explain_problems(PackageDatabase& pool, const ProblemsMessageFormat& format) const + -> std::string; + + private: + + std::string m_reason; + + friend class Solver; + }; +} diff --git a/libmamba/src/api/channel_loader.cpp b/libmamba/src/api/channel_loader.cpp index 1aaffcc646..51c48ecf72 100644 --- a/libmamba/src/api/channel_loader.cpp +++ b/libmamba/src/api/channel_loader.cpp @@ -12,7 +12,8 @@ #include "mamba/core/prefix_data.hpp" #include "mamba/core/subdirdata.hpp" #include "mamba/solver/libsolv/database.hpp" -#include "mamba/solver/libsolv/repo_info.hpp" +#include "mamba/solver/repo_info.hpp" +#include "mamba/solver/resolvo/database.hpp" #include "mamba/specs/package_info.hpp" namespace mamba @@ -24,7 +25,7 @@ namespace mamba ChannelContext& channel_context, solver::libsolv::Database& pool, const fs::u8path& pkgs_dir - ) -> solver::libsolv::RepoInfo + ) -> solver::RepoInfo { if (!fs::exists(pkgs_dir)) { @@ -56,7 +57,7 @@ namespace mamba MultiPackageCache& package_caches, std::vector& subdirs, std::vector& error_list, - std::vector& priorities, + std::vector& priorities, int& max_prio, specs::CondaURL& prev_channel_url ) @@ -132,7 +133,7 @@ namespace mamba { std::vector subdirs; - std::vector priorities; + std::vector priorities; int max_prio = static_cast(ctx.channels.size()); auto prev_channel_url = specs::CondaURL(); @@ -248,7 +249,7 @@ namespace mamba } load_subdir_in_database(ctx, pool, subdir) - .transform([&](solver::libsolv::RepoInfo&& repo) + .transform([&](solver::RepoInfo&& repo) { pool.set_repo_priority(repo, priorities[i]); }) .or_else( [&](const auto&) @@ -290,6 +291,176 @@ namespace mamba return error_list.empty() ? return_type() : return_type(make_unexpected(std::move(error_list))); } + + auto load_channels_impl( + Context& ctx, + ChannelContext& channel_context, + solver::resolvo::PackageDatabase& pool, + MultiPackageCache& package_caches, + bool is_retry + ) -> expected_t + { + std::vector subdirs; + + std::vector priorities; + int max_prio = static_cast(ctx.channels.size()); + auto prev_channel_url = specs::CondaURL(); + + Console::instance().init_progress_bar_manager(ProgressBarMode::multi); + + std::vector error_list; + + for (const auto& mirror : ctx.mirrored_channels) + { + for (auto channel : channel_context.make_channel(mirror.first, mirror.second)) + { + create_mirrors(channel, ctx.mirrors); + create_subdirs( + ctx, + channel_context, + channel, + package_caches, + subdirs, + error_list, + priorities, + max_prio, + prev_channel_url + ); + } + } + + auto packages = std::vector(); + + for (const auto& location : ctx.channels) + { + // TODO: C++20, replace with contains + if (ctx.mirrored_channels.find(location) == ctx.mirrored_channels.end()) + { + for (auto channel : channel_context.make_channel(location)) + { + if (channel.is_package()) + { + auto pkg_info = specs::PackageInfo::from_url(channel.url().str()) + .or_else([](specs::ParseError&& err) + { throw std::move(err); }) + .value(); + packages.push_back(pkg_info); + continue; + } + + create_mirrors(channel, ctx.mirrors); + create_subdirs( + ctx, + channel_context, + channel, + package_caches, + subdirs, + error_list, + priorities, + max_prio, + prev_channel_url + ); + } + } + } + + // TODO: Implement this + // if (!packages.empty()) + // { + // pool.add_repo_from_packages(packages, "packages"); + // } + + expected_t download_res; + if (SubdirDataMonitor::can_monitor(ctx)) + { + SubdirDataMonitor check_monitor({ true, true }); + SubdirDataMonitor index_monitor; + download_res = SubdirData::download_indexes(subdirs, ctx, &check_monitor, &index_monitor); + } + else + { + download_res = SubdirData::download_indexes(subdirs, ctx); + } + + if (!download_res) + { + mamba_error error = download_res.error(); + mamba_error_code ec = error.error_code(); + error_list.push_back(std::move(error)); + if (ec == mamba_error_code::user_interrupted) + { + return tl::unexpected(mamba_aggregated_error(std::move(error_list))); + } + } + + if (ctx.offline) + { + LOG_INFO << "Creating repo from pkgs_dir for offline"; + for (const auto& c : ctx.pkgs_dirs) + { + // create_repo_from_pkgs_dir(ctx, channel_context, pool, c); + } + } + std::string prev_channel; + bool loading_failed = false; + for (std::size_t i = 0; i < subdirs.size(); ++i) + { + auto& subdir = subdirs[i]; + if (!subdir.is_loaded()) + { + if (!ctx.offline && subdir.is_noarch()) + { + error_list.push_back(mamba_error( + "Subdir " + subdir.name() + " not loaded!", + mamba_error_code::subdirdata_not_loaded + )); + } + continue; + } + + load_subdir_in_resolvo_database(ctx, pool, subdir) + // .transform([&](solver::libsolv::RepoInfo&& repo) + // { pool.set_repo_priority(repo, priorities[i]); }) + .or_else( + [&](const auto&) + { + if (is_retry) + { + std::stringstream ss; + ss << "Could not load repodata.json for " << subdir.name() + << " after retry." << "Please check repodata source. Exiting." + << std::endl; + error_list.push_back( + mamba_error(ss.str(), mamba_error_code::repodata_not_loaded) + ); + } + else + { + LOG_WARNING << "Could not load repodata.json for " << subdir.name() + << ". Deleting cache, and retrying."; + subdir.clear_cache(); + loading_failed = true; + } + } + ); + } + + if (loading_failed) + { + if (!ctx.offline && !is_retry) + { + LOG_WARNING << "Encountered malformed repodata.json cache. Redownloading."; + return load_channels_impl(ctx, channel_context, pool, package_caches, true); + } + error_list.emplace_back( + "Could not load repodata. Cache corrupted?", + mamba_error_code::repodata_not_loaded + ); + } + using return_type = expected_t; + return error_list.empty() ? return_type() + : return_type(make_unexpected(std::move(error_list))); + } } auto load_channels( @@ -302,6 +473,16 @@ namespace mamba return load_channels_impl(ctx, channel_context, pool, package_caches, false); } + auto load_channels( + Context& ctx, + ChannelContext& channel_context, + solver::resolvo::PackageDatabase& pool, + MultiPackageCache& package_caches + ) -> expected_t + { + return load_channels_impl(ctx, channel_context, pool, package_caches, false); + } + void init_channels(Context& context, ChannelContext& channel_context) { for (const auto& mirror : context.mirrored_channels) @@ -342,5 +523,4 @@ namespace mamba } } } - } diff --git a/libmamba/src/api/install.cpp b/libmamba/src/api/install.cpp index a3ba4d582b..a19b93eeac 100644 --- a/libmamba/src/api/install.cpp +++ b/libmamba/src/api/install.cpp @@ -23,6 +23,7 @@ #include "mamba/download/downloader.hpp" #include "mamba/fs/filesystem.hpp" #include "mamba/solver/libsolv/solver.hpp" +#include "mamba/solver/resolvo/solver.hpp" #include "mamba/util/path_manip.hpp" #include "mamba/util/string.hpp" @@ -379,6 +380,59 @@ namespace mamba namespace { + void install_specs_resolvo_impl( + Context& ctx, + ChannelContext& channel_context, + const Configuration& config, + const std::vector& specs, + bool create_env, + bool remove_prefix_on_failure, + bool is_retry + ) + { + assert(&config.context() == &ctx); + + auto& no_pin = config.at("no_pin").value(); + auto& no_py_pin = config.at("no_py_pin").value(); + auto& freeze_installed = config.at("freeze_installed").value(); + auto& retry_clean_cache = config.at("retry_clean_cache").value(); + + if (ctx.prefix_params.target_prefix.empty()) + { + throw std::runtime_error("No active target prefix"); + } + if (!fs::exists(ctx.prefix_params.target_prefix) && create_env == false) + { + throw std::runtime_error(fmt::format( + "Prefix does not exist at: {}", + ctx.prefix_params.target_prefix.string() + )); + } + + MultiPackageCache package_caches{ ctx.pkgs_dirs, ctx.validation_params }; + + // add channels from specs + for (const auto& s : specs) + { + if (auto ms = specs::MatchSpec::parse(s); ms && ms->channel().has_value()) + { + ctx.channels.push_back(ms->channel()->str()); + } + } + + if (ctx.channels.empty() && !ctx.offline) + { + LOG_WARNING << "No 'channels' specified"; + } + + solver::resolvo::PackageDatabase db{ channel_context.params() }; + auto exp_load = load_channels(ctx, channel_context, db, package_caches); + if (!exp_load) + { + throw std::runtime_error(exp_load.error().what()); + } + } + void install_specs_impl( Context& ctx, ChannelContext& channel_context, @@ -574,6 +628,21 @@ namespace mamba bool remove_prefix_on_failure ) { + // Check if the MAMBA_USE_RESOLVO env var is set + if (const char* use_resolvo = std::getenv("MAMBA_USE_RESOLVO"); + use_resolvo && std::string(use_resolvo) == "1") + { + return install_specs_resolvo_impl( + ctx, + channel_context, + config, + specs, + create_env, + remove_prefix_on_failure, + false + ); + } + return install_specs_impl( ctx, channel_context, diff --git a/libmamba/src/api/remove.cpp b/libmamba/src/api/remove.cpp index 1ec6358c9f..63bd35e5e8 100644 --- a/libmamba/src/api/remove.cpp +++ b/libmamba/src/api/remove.cpp @@ -12,8 +12,8 @@ #include "mamba/core/package_database_loader.hpp" #include "mamba/core/prefix_data.hpp" #include "mamba/core/transaction.hpp" -#include "mamba/solver/libsolv/repo_info.hpp" #include "mamba/solver/libsolv/solver.hpp" +#include "mamba/solver/repo_info.hpp" #include "mamba/solver/request.hpp" namespace mamba diff --git a/libmamba/src/api/repoquery.cpp b/libmamba/src/api/repoquery.cpp index 7b3b36fcf8..4e223bb64f 100644 --- a/libmamba/src/api/repoquery.cpp +++ b/libmamba/src/api/repoquery.cpp @@ -14,7 +14,7 @@ #include "mamba/core/package_database_loader.hpp" #include "mamba/core/prefix_data.hpp" #include "mamba/solver/libsolv/database.hpp" -#include "mamba/solver/libsolv/repo_info.hpp" +#include "mamba/solver/repo_info.hpp" #include "mamba/util/string.hpp" namespace mamba diff --git a/libmamba/src/core/package_database_loader.cpp b/libmamba/src/core/package_database_loader.cpp index 6da08392d8..e09dc7d0ae 100644 --- a/libmamba/src/core/package_database_loader.cpp +++ b/libmamba/src/core/package_database_loader.cpp @@ -20,7 +20,7 @@ #include "mamba/core/subdirdata.hpp" #include "mamba/core/virtual_packages.hpp" #include "mamba/solver/libsolv/database.hpp" -#include "mamba/solver/libsolv/repo_info.hpp" +#include "mamba/solver/repo_info.hpp" #include "mamba/util/build.hpp" #include "mamba/util/string.hpp" @@ -28,23 +28,25 @@ namespace mamba { + // Libsolv + void add_spdlog_logger_to_database(solver::libsolv::Database& db) { db.set_logger( - [logger = spdlog::get("libsolv")](solver::libsolv::LogLevel level, std::string_view msg) + [logger = spdlog::get("libsolv")](solver::LogLevel level, std::string_view msg) { switch (level) { - case (solver::libsolv::LogLevel::Fatal): + case (solver::LogLevel::Fatal): logger->critical(msg); break; - case (solver::libsolv::LogLevel::Error): + case (solver::LogLevel::Error): logger->error(msg); break; - case (solver::libsolv::LogLevel::Warning): + case (solver::LogLevel::Warning): logger->warn(msg); break; - case (solver::libsolv::LogLevel::Debug): + case (solver::LogLevel::Debug): logger->debug(msg); break; } @@ -54,20 +56,18 @@ namespace mamba auto load_subdir_in_database(const Context& ctx, solver::libsolv::Database& db, const SubdirData& subdir) - -> expected_t + -> expected_t { - const auto expected_cache_origin = solver::libsolv::RepodataOrigin{ + const auto expected_cache_origin = solver::RepodataOrigin{ /* .url= */ util::rsplit(subdir.metadata().url(), "/", 1).front(), /* .etag= */ subdir.metadata().etag(), /* .mod= */ subdir.metadata().last_modified(), }; - const auto add_pip = static_cast( - ctx.add_pip_as_python_dependency + const auto add_pip = static_cast(ctx.add_pip_as_python_dependency ); - const auto json_parser = ctx.experimental_repodata_parsing - ? solver::libsolv::RepodataParser::Mamba - : solver::libsolv::RepodataParser::Libsolv; + const auto json_parser = ctx.experimental_repodata_parsing ? solver::RepodataParser::Mamba + : solver::RepodataParser::Libsolv; // Solv files are too slow on Windows. if (!util::on_win) @@ -93,7 +93,7 @@ namespace mamba .and_then( [&](fs::u8path&& repodata_json) { - using PackageTypes = solver::libsolv::PackageTypes; + using PackageTypes = solver::PackageTypes; LOG_INFO << "Trying to load repo from json file " << repodata_json; return db.add_repo_from_repodata_json( @@ -103,14 +103,13 @@ namespace mamba add_pip, ctx.use_only_tar_bz2 ? PackageTypes::TarBz2Only : PackageTypes::CondaOrElseTarBz2, - static_cast(ctx.validation_params.verify_artifacts - ), + static_cast(ctx.validation_params.verify_artifacts), json_parser ); } ) .transform( - [&](solver::libsolv::RepoInfo&& repo) -> solver::libsolv::RepoInfo + [&](solver::RepoInfo&& repo) -> solver::RepoInfo { if (!util::on_win) { @@ -134,7 +133,7 @@ namespace mamba const Context& ctx, solver::libsolv::Database& db, const PrefixData& prefix - ) -> solver::libsolv::RepoInfo + ) -> solver::RepoInfo { // TODO(C++20): We could do a PrefixData range that returns packages without storing them. auto pkgs = prefix.sorted_records(); @@ -146,12 +145,26 @@ namespace mamba // Not adding Pip dependency since it might needlessly make the installed/active environment // broken if pip is not already installed (debatable). - auto repo = db.add_repo_from_packages( - pkgs, - "installed", - solver::libsolv::PipAsPythonDependency::No - ); + auto repo = db.add_repo_from_packages(pkgs, "installed", solver::PipAsPythonDependency::No); db.set_installed_repo(repo); return repo; } + + // Resolvo + + auto load_subdir_in_resolvo_database( + const Context& ctx, + solver::resolvo::PackageDatabase& db, + const SubdirData& subdir + ) -> expected_t + { + } + + auto load_installed_packages_in_resolvo_database( + const Context& ctx, + solver::resolvo::PackageDatabase& db, + const PrefixData& prefix + ) -> solver::RepoInfo + { + } } diff --git a/libmamba/src/solver/libsolv/database.cpp b/libmamba/src/solver/libsolv/database.cpp index c59b82eb3b..3fc11c3ee2 100644 --- a/libmamba/src/solver/libsolv/database.cpp +++ b/libmamba/src/solver/libsolv/database.cpp @@ -17,7 +17,7 @@ #include "mamba/fs/filesystem.hpp" #include "mamba/solver/libsolv/database.hpp" -#include "mamba/solver/libsolv/repo_info.hpp" +#include "mamba/solver/repo_info.hpp" #include "mamba/specs/match_spec.hpp" #include "mamba/util/random.hpp" #include "solv-cpp/pool.hpp" diff --git a/libmamba/src/solver/libsolv/helpers.hpp b/libmamba/src/solver/libsolv/helpers.hpp index f7c30118c6..4441b9ca40 100644 --- a/libmamba/src/solver/libsolv/helpers.hpp +++ b/libmamba/src/solver/libsolv/helpers.hpp @@ -12,7 +12,7 @@ #include #include "mamba/core/error_handling.hpp" -#include "mamba/solver/libsolv/parameters.hpp" +#include "mamba/solver/parameters.hpp" #include "mamba/solver/request.hpp" #include "mamba/solver/solution.hpp" #include "mamba/specs/channel.hpp" diff --git a/libmamba/src/solver/libsolv/parameters.cpp b/libmamba/src/solver/parameters.cpp similarity index 94% rename from libmamba/src/solver/libsolv/parameters.cpp rename to libmamba/src/solver/parameters.cpp index ab79fb6134..97d194e41a 100644 --- a/libmamba/src/solver/libsolv/parameters.cpp +++ b/libmamba/src/solver/parameters.cpp @@ -8,10 +8,10 @@ #include -#include "mamba/solver/libsolv/parameters.hpp" +#include "mamba/solver/parameters.hpp" #include "mamba/util/string.hpp" -namespace mamba::solver::libsolv +namespace mamba::solver { namespace { diff --git a/libmamba/src/solver/libsolv/repo_info.cpp b/libmamba/src/solver/repo_info.cpp similarity index 93% rename from libmamba/src/solver/libsolv/repo_info.cpp rename to libmamba/src/solver/repo_info.cpp index edf52e66c3..4601146fe8 100644 --- a/libmamba/src/solver/libsolv/repo_info.cpp +++ b/libmamba/src/solver/repo_info.cpp @@ -7,10 +7,10 @@ #include #include -#include "mamba/solver/libsolv/repo_info.hpp" +#include "mamba/solver/repo_info.hpp" #include "solv-cpp/repo.hpp" -namespace mamba::solver::libsolv +namespace mamba::solver { RepoInfo::RepoInfo(::Repo* repo) : m_ptr(repo) diff --git a/libmamba/src/solver/resolvo/database.cpp b/libmamba/src/solver/resolvo/database.cpp new file mode 100644 index 0000000000..1d4e287f1c --- /dev/null +++ b/libmamba/src/solver/resolvo/database.cpp @@ -0,0 +1,349 @@ +#include +#include +#include + +#include "mamba/core/util.hpp" // for LockFile +#include "mamba/solver/resolvo/database.hpp" +#include "mamba/solver/solution.hpp" +#include "mamba/specs/channel.hpp" +#include "mamba/specs/package_info.hpp" + +namespace mamba::solver::resolvo +{ + using logger_type = std::function; + + void PackageDatabase::set_logger(logger_type callback) + { + } + + auto PackageDatabase::add_repo_from_repodata_json( + const mamba::fs::u8path& path, + std::string_view url, + const std::string& channel_id, + PipAsPythonDependency add, + PackageTypes package_types, + VerifyPackages verify_packages, + RepodataParser parser + ) -> expected_t + { + } + + auto PackageDatabase::add_repo_from_native_serialization( + const mamba::fs::u8path& path, + const RepodataOrigin& expected, + const std::string& channel_id, + PipAsPythonDependency add + ) -> expected_t + { + } + + template + auto PackageDatabase::add_repo_from_packages( + Iter first_package, + Iter last_package, + std::string_view name, + PipAsPythonDependency add + ) -> RepoInfo + { + } + + template + auto PackageDatabase::add_repo_from_packages( + const Range& packages, + std::string_view name, + PipAsPythonDependency add + ) -> RepoInfo + { + } + + auto PackageDatabase::native_serialize_repo( + const RepoInfo& repo, + const mamba::fs::u8path& path, + const RepodataOrigin& metadata + ) -> expected_t + { + } + + [[nodiscard]] auto PackageDatabase::installed_repo() const -> std::optional + { + } + + void PackageDatabase::set_installed_repo(RepoInfo repo) + { + } + + void PackageDatabase::set_repo_priority(RepoInfo repo, Priorities priorities) + { + } + + void PackageDatabase::remove_repo(RepoInfo repo) + { + } + + [[nodiscard]] auto PackageDatabase::repo_count() const -> std::size_t + { + } + + [[nodiscard]] auto PackageDatabase::package_count() const -> std::size_t + { + } + + template + void PackageDatabase::for_each_package_in_repo(RepoInfo repo, Func&&) const + { + } + + template + void PackageDatabase::for_each_package_matching(const specs::MatchSpec& ms, Func&&) + { + } + + template + void PackageDatabase::for_each_package_depending_on(const specs::MatchSpec& ms, Func&&) + { + } + + // TODO: reuse it from `mamba/solver/libsolv/helpers.cpp` + auto lsplit_track_features(std::string_view features) + { + constexpr auto is_sep = [](char c) -> bool { return (c == ',') || util::is_space(c); }; + auto [_, tail] = util::lstrip_if_parts(features, is_sep); + return util::lstrip_if_parts(tail, [&](char c) { return !is_sep(c); }); + } + + // TODO: factorise with the implementation from `set_solvable` in + // `mamba/solver/libsolv/helpers.cpp` + bool parse_packageinfo_json( + const std::string_view& filename, + const simdjson::dom::element& pkg, + const CondaURL& repo_url, + const std::string& channel_id, + PackageDatabase& database + ) + { + PackageInfo package_info; + + package_info.channel = channel_id; + package_info.filename = filename; + package_info.package_url = (repo_url / filename).str(specs::CondaURL::Credentials::Show); + + if (auto fn = pkg["fn"].get_string(); !fn.error()) + { + package_info.name = fn.value_unsafe(); + } + else + { + // Fallback from key entry + package_info.name = filename; + } + + if (auto name = pkg["name"].get_string(); !name.error()) + { + package_info.name = name.value_unsafe(); + } + else + { + return false; + } + + if (auto version = pkg["version"].get_string(); !version.error()) + { + package_info.version = version.value_unsafe(); + } + else + { + return false; + } + + if (auto build_string = pkg["build"].get_string(); !build_string.error()) + { + package_info.build_string = build_string.value_unsafe(); + } + else + { + return false; + } + + if (auto build_number = pkg["build_number"].get_uint64(); !build_number.error()) + { + package_info.build_number = build_number.value_unsafe(); + } + else + { + return false; + } + + if (auto subdir = pkg["subdir"].get_c_str(); !subdir.error()) + { + package_info.platform = subdir.value_unsafe(); + } + else + { + } + + if (auto size = pkg["size"].get_uint64(); !size.error()) + { + package_info.size = size.value_unsafe(); + } + + if (auto md5 = pkg["md5"].get_c_str(); !md5.error()) + { + package_info.md5 = md5.value_unsafe(); + } + + if (auto sha256 = pkg["sha256"].get_c_str(); !sha256.error()) + { + package_info.sha256 = sha256.value_unsafe(); + } + + if (auto elem = pkg["noarch"]; !elem.error()) + { + // TODO: is the following right? + if (auto val = elem.get_bool(); !val.error() && val.value_unsafe()) + { + package_info.noarch = NoArchType::No; + } + else if (auto noarch = elem.get_c_str(); !noarch.error()) + { + package_info.noarch = NoArchType::No; + } + } + + if (auto license = pkg["license"].get_c_str(); !license.error()) + { + package_info.license = license.value_unsafe(); + } + + // TODO conda timestamp are not Unix timestamp. + // Libsolv normalize them this way, we need to do the same here otherwise the current + // package may get arbitrary priority. + if (auto timestamp = pkg["timestamp"].get_uint64(); !timestamp.error()) + { + const auto time = timestamp.value_unsafe(); + // TODO: reuse it from `mamba/solver/libsolv/helpers.cpp` + constexpr auto MAX_CONDA_TIMESTAMP = 253402300799ULL; + package_info.timestamp = (time > MAX_CONDA_TIMESTAMP) ? (time / 1000) : time; + } + + if (auto depends = pkg["depends"].get_array(); !depends.error()) + { + for (auto elem : depends) + { + if (auto dep = elem.get_c_str(); !dep.error()) + { + package_info.dependencies.emplace_back(dep.value_unsafe()); + } + } + } + + if (auto constrains = pkg["constrains"].get_array(); !constrains.error()) + { + for (auto elem : constrains) + { + if (auto cons = elem.get_c_str(); !cons.error()) + { + package_info.constrains.emplace_back(cons.value_unsafe()); + } + } + } + + if (auto obj = pkg["track_features"]; !obj.error()) + { + if (auto track_features_arr = obj.get_array(); !track_features_arr.error()) + { + for (auto elem : track_features_arr) + { + if (auto feat = elem.get_string(); !feat.error()) + { + package_info.track_features.emplace_back(feat.value_unsafe()); + } + } + } + else if (auto track_features_str = obj.get_string(); !track_features_str.error()) + { + auto splits = lsplit_track_features(track_features_str.value_unsafe()); + while (!splits[0].empty()) + { + package_info.track_features.emplace_back(splits[0]); + splits = lsplit_track_features(splits[1]); + } + } + } + + database.alloc_solvable(package_info); + return true; + } + + void parse_repodata_json( + PackageDatabase& database, + const fs::u8path& filename, + const std::string& repo_url, + const std::string& channel_id, + bool verify_artifacts + ) + { + auto parser = simdjson::dom::parser(); + const auto lock = LockFile(filename); + const auto repodata = parser.load(filename); + + // An override for missing package subdir is found at the top level + auto default_subdir = std::string(); + if (auto subdir = repodata.at_pointer("/info/subdir").get_string(); !subdir.error()) + { + default_subdir = std::string(subdir.value_unsafe()); + } + + // Get `base_url` in case 'repodata_version': 2 + // cf. https://github.com/conda-incubator/ceps/blob/main/cep-15.md + auto base_url = repo_url; + if (auto repodata_version = repodata["repodata_version"].get_int64(); + !repodata_version.error()) + { + if (repodata_version.value_unsafe() == 2) + { + if (auto url = repodata.at_pointer("/info/base_url").get_string(); !url.error()) + { + base_url = std::string(url.value_unsafe()); + } + } + } + + const auto parsed_url = specs::CondaURL::parse(base_url) + .or_else([](specs::ParseError&& err) { throw std::move(err); }) + .value(); + + auto signatures = std::optional(std::nullopt); + if (auto maybe_sigs = repodata["signatures"].get_object(); + !maybe_sigs.error() && verify_artifacts) + { + signatures = std::move(maybe_sigs).value(); + } + + auto added = util::flat_set(); + if (auto pkgs = repodata["packages.conda"].get_object(); !pkgs.error()) + { + std::cout << "CondaOrElseTarBz2 packages.conda" << std::endl; + + for (auto [key, value] : pkgs.value()) + { + parse_packageinfo_json(key, value, parsed_url, channel_id, database); + } + } + if (auto pkgs = repodata["packages"].get_object(); !pkgs.error()) + { + std::cout << "CondaOrElseTarBz2 packages" << std::endl; + + for (auto [key, value] : pkgs.value()) + { + parse_packageinfo_json(key, value, parsed_url, channel_id, database); + } + } + } + + // 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; + +} diff --git a/libmamba/src/solver/resolvo/solver.cpp b/libmamba/src/solver/resolvo/solver.cpp new file mode 100644 index 0000000000..436f75083f --- /dev/null +++ b/libmamba/src/solver/resolvo/solver.cpp @@ -0,0 +1,137 @@ +#include + +#include +#include + +#include "mamba/solver/resolvo/database.hpp" +#include "mamba/solver/resolvo/solver.hpp" + +namespace mamba::solver::resolvo +{ + using ::resolvo::String; + using ::resolvo::Vector; + using ::resolvo::VersionSetId; + using ::resolvo::SolvableId; + + namespace + { + // TODO: Remove duplication with libsolv's + 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().str() < rhs.spec.name().str(); + } + return false; + } + ); + } + } + + auto Solver::solve(PackageDatabase& mpool, Request&& request) -> expected_t + { + if (request.flags.order_request) + { + std::sort(request.jobs.begin(), request.jobs.end(), make_request_cmp()); + } + return solve_impl(mpool, request); + } + + auto Solver::solve(PackageDatabase& mpool, 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(mpool, sorted_request); + } + return solve_impl(mpool, request); + } + + auto Solver::solve_impl(PackageDatabase& mpool, const Request& request) -> expected_t + { + std::vector specs; + + for (const auto& job : request.jobs) + { + std::visit( + [&](const auto& j) + { + using A = std::decay_t; + if constexpr (std::is_same_v) + { + specs.push_back(j.spec); + } + else if constexpr (std::is_same_v) + { + specs.push_back(j.spec); + } + else if constexpr (std::is_same_v) + { + specs.push_back(j.spec); + } + else + { + // TODO: support other job types + throw std::runtime_error("Unsupported job type"); + } + }, + job + ); + } + + resolvo::Vector requirements; + for (const auto& spec : specs) + { + // TODO: avoid converting the spec to a string + requirements.push_back(mpool.alloc_version_set(spec.str())); + } + + resolvo::Vector constraints = {}; + resolvo::Vector result; + + auto tick_resolvo = std::chrono::steady_clock::now(); + String reason = ::resolvo::solve(mpool, requirements, constraints, result); + auto tack_resolvo = std::chrono::steady_clock::now(); + + if (reason == "") // success + { + std::vector resolvo_resolution; + for (auto solvable_id : result) + { + const PackageInfo& package_info = mpool.get_solvable(solvable_id); + // Skip virtual package (i.e. whose `package_info.name` starts with "__") + if (package_info.name.find("__") != 0) + { + resolvo_resolution.push_back(package_info); + } + } + + std::sort( + resolvo_resolution.begin(), + resolvo_resolution.end(), + [&](const PackageInfo& a, const PackageInfo& b) { return a.name < b.name; } + ); + + Solution solution; + + solution.actions.reserve(resolvo_resolution.size()); + for (const auto& package_info : resolvo_resolution) + { + solution.actions.emplace_back(Solution::Install{ package_info }); + } + + return solution; + } + return UnSolvable(std::string(reason.data())); + } + + +} diff --git a/libmamba/src/solver/resolvo/unsolvable.cpp b/libmamba/src/solver/resolvo/unsolvable.cpp new file mode 100644 index 0000000000..15f4b42ca2 --- /dev/null +++ b/libmamba/src/solver/resolvo/unsolvable.cpp @@ -0,0 +1,77 @@ +// Copyright (c) 2023, 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 "mamba/core/output.hpp" +#include "mamba/solver/problems_graph.hpp" +#include "mamba/solver/resolvo/unsolvable.hpp" + +namespace mamba::solver::resolvo +{ + UnSolvable::UnSolvable(std::string reason) + : m_reason(std::move(reason)) + { + } + + UnSolvable::UnSolvable(UnSolvable&&) = default; + + UnSolvable::~UnSolvable() = default; + + auto UnSolvable::operator=(UnSolvable&&) -> UnSolvable& = default; + + auto UnSolvable::problems(PackageDatabase& db) const -> std::vector + { + // TODO: Implement + return {}; + } + + auto UnSolvable::problems_to_str(PackageDatabase& db) const -> std::string + { + // TODO: Implement + return {}; + } + + auto UnSolvable::all_problems_to_str(PackageDatabase& db) const -> std::string + { + // TODO: Implement + return {}; + } + + auto UnSolvable::problems_graph(const PackageDatabase& pool) const -> ProblemsGraph + { + // TODO: Implement + ProblemsGraph::graph_t graph; + ProblemsGraph::conflicts_t conflicts; + ProblemsGraph::node_id root_node; + return ProblemsGraph{ graph, conflicts, root_node }; + } + + auto UnSolvable::explain_problems_to( + PackageDatabase& pool, + std::ostream& out, + const ProblemsMessageFormat& format + ) const -> std::ostream& + { + out << "Could not solve for environment specs\n"; + const auto pbs = problems_graph(pool); + const auto pbs_simplified = simplify_conflicts(pbs); + const auto cp_pbs = CompressedProblemsGraph::from_problems_graph(pbs_simplified); + print_problem_tree_msg(out, cp_pbs, format); + return out; + } + + auto UnSolvable::explain_problems(PackageDatabase& pool, const ProblemsMessageFormat& format) const + -> std::string + + { + std::stringstream ss; + explain_problems_to(pool, ss, format); + return ss.str(); + } +} diff --git a/libmamba/tests/src/solver/libsolv/test_database.cpp b/libmamba/tests/src/solver/libsolv/test_database.cpp index 8f953e34ab..b0fcd73acb 100644 --- a/libmamba/tests/src/solver/libsolv/test_database.cpp +++ b/libmamba/tests/src/solver/libsolv/test_database.cpp @@ -75,7 +75,7 @@ namespace auto tmp_dir = TemporaryDirectory(); auto solv_file = tmp_dir.path() / "repo1.solv"; - auto origin = libsolv::RepodataOrigin{ + auto origin = RepodataOrigin{ /* .url= */ "https://repo.mamba.pm", /* .etag= */ "etag", /* .mod= */ "Fri, 11 Feb 2022 13:52:44 GMT", @@ -96,9 +96,9 @@ namespace SECTION("Fail reading outdated repo") { for (auto attr : { - &libsolv::RepodataOrigin::url, - &libsolv::RepodataOrigin::etag, - &libsolv::RepodataOrigin::mod, + &RepodataOrigin::url, + &RepodataOrigin::etag, + &RepodataOrigin::mod, }) { auto expected = origin; @@ -184,7 +184,7 @@ namespace repodata, "https://conda.anaconda.org/conda-forge/linux-64", "conda-forge", - libsolv::PipAsPythonDependency::No + PipAsPythonDependency::No ); REQUIRE(repo1.has_value()); @@ -213,7 +213,7 @@ namespace repodata, "https://conda.anaconda.org/conda-forge/linux-64", "conda-forge", - libsolv::PipAsPythonDependency::Yes + PipAsPythonDependency::Yes ); REQUIRE(repo1.has_value()); @@ -244,8 +244,8 @@ namespace repodata, "https://conda.anaconda.org/conda-forge/linux-64", "conda-forge", - libsolv::PipAsPythonDependency::No, - libsolv::PackageTypes::TarBz2Only + PipAsPythonDependency::No, + PackageTypes::TarBz2Only ); REQUIRE(repo1.has_value()); REQUIRE(repo1->package_count() == 4); @@ -259,8 +259,8 @@ namespace repodata, "https://conda.anaconda.org/conda-forge/linux-64", "conda-forge", - libsolv::PipAsPythonDependency::No, - libsolv::PackageTypes::CondaOnly + PipAsPythonDependency::No, + PackageTypes::CondaOnly ); REQUIRE(repo1.has_value()); REQUIRE(repo1->package_count() == 30); @@ -274,8 +274,8 @@ namespace repodata, "https://conda.anaconda.org/conda-forge/linux-64", "conda-forge", - libsolv::PipAsPythonDependency::No, - libsolv::PackageTypes::CondaAndTarBz2 + PipAsPythonDependency::No, + PackageTypes::CondaAndTarBz2 ); REQUIRE(repo1.has_value()); REQUIRE(repo1->package_count() == 34); @@ -289,8 +289,8 @@ namespace repodata, "https://conda.anaconda.org/conda-forge/linux-64", "conda-forge", - libsolv::PipAsPythonDependency::No, - libsolv::PackageTypes::CondaOrElseTarBz2 + PipAsPythonDependency::No, + PackageTypes::CondaOrElseTarBz2 ); REQUIRE(repo1.has_value()); REQUIRE(repo1->package_count() == 33); @@ -306,10 +306,10 @@ namespace repodata, "https://conda.anaconda.org/conda-forge/linux-64", "conda-forge", - libsolv::PipAsPythonDependency::No, - libsolv::PackageTypes::CondaOrElseTarBz2, - libsolv::VerifyPackages::Yes, - libsolv::RepodataParser::Mamba + PipAsPythonDependency::No, + PackageTypes::CondaOrElseTarBz2, + VerifyPackages::Yes, + RepodataParser::Mamba ); REQUIRE(repo1.has_value()); REQUIRE(repo1->package_count() == 33); @@ -346,10 +346,10 @@ namespace repodata, "https://conda.anaconda.org/conda-forge/linux-64", "conda-forge", - libsolv::PipAsPythonDependency::No, - libsolv::PackageTypes::CondaOrElseTarBz2, - libsolv::VerifyPackages::Yes, - libsolv::RepodataParser::Libsolv + PipAsPythonDependency::No, + PackageTypes::CondaOrElseTarBz2, + VerifyPackages::Yes, + RepodataParser::Libsolv ); REQUIRE(repo1.has_value()); REQUIRE(repo1->package_count() == 33); @@ -395,10 +395,10 @@ namespace repodata, "https://conda.anaconda.org/conda-forge/linux-64", "conda-forge", - libsolv::PipAsPythonDependency::No, - libsolv::PackageTypes::CondaOrElseTarBz2, - libsolv::VerifyPackages::No, - libsolv::RepodataParser::Mamba + PipAsPythonDependency::No, + PackageTypes::CondaOrElseTarBz2, + VerifyPackages::No, + RepodataParser::Mamba ); REQUIRE(repo1.has_value()); REQUIRE(repo1->package_count() == 33); @@ -415,10 +415,10 @@ namespace repodata, "https://conda.anaconda.org/conda-forge/linux-64", "conda-forge", - libsolv::PipAsPythonDependency::No, - libsolv::PackageTypes::CondaOrElseTarBz2, - libsolv::VerifyPackages::No, - libsolv::RepodataParser::Libsolv + PipAsPythonDependency::No, + PackageTypes::CondaOrElseTarBz2, + VerifyPackages::No, + RepodataParser::Libsolv ); REQUIRE(repo1.has_value()); REQUIRE(repo1->package_count() == 33); @@ -438,8 +438,8 @@ namespace repodata, "https://conda.anaconda.org/conda-forge/linux-64", "conda-forge", - libsolv::PipAsPythonDependency::No, - libsolv::PackageTypes::CondaOrElseTarBz2 + PipAsPythonDependency::No, + PackageTypes::CondaOrElseTarBz2 ); REQUIRE(repo1.has_value()); REQUIRE(repo1->package_count() == 2); @@ -474,8 +474,8 @@ namespace repodata, "https://conda.anaconda.org/conda-forge/linux-64", "conda-forge", - libsolv::PipAsPythonDependency::No, - libsolv::PackageTypes::CondaOrElseTarBz2 + PipAsPythonDependency::No, + PackageTypes::CondaOrElseTarBz2 ); REQUIRE(repo1.has_value()); REQUIRE(repo1->package_count() == 2); diff --git a/libmamba/tests/src/solver/libsolv/test_solver.cpp b/libmamba/tests/src/solver/libsolv/test_solver.cpp index dcf7a620d9..f1c7933ab3 100644 --- a/libmamba/tests/src/solver/libsolv/test_solver.cpp +++ b/libmamba/tests/src/solver/libsolv/test_solver.cpp @@ -76,7 +76,7 @@ namespace mambatests::test_data_dir / "repodata/conda-forge-numpy-linux-64.json", "https://conda.anaconda.org/conda-forge/linux-64", "conda-forge", - libsolv::PipAsPythonDependency::No + PipAsPythonDependency::No ); REQUIRE(repo.has_value()); @@ -354,7 +354,7 @@ namespace mambatests::test_data_dir / "repodata/conda-forge-numpy-linux-64.json", "https://conda.anaconda.org/conda-forge/linux-64", "conda-forge", - libsolv::PipAsPythonDependency::No + PipAsPythonDependency::No ); REQUIRE(repo.has_value()); @@ -890,7 +890,7 @@ namespace mambatests::test_data_dir / "repodata/conda-forge-numpy-linux-64.json", "https://conda.anaconda.org/conda-forge/linux-64", "conda-forge", - libsolv::PipAsPythonDependency::No + PipAsPythonDependency::No ); REQUIRE(repo_linux.has_value()); @@ -901,7 +901,7 @@ namespace mambatests::test_data_dir / "repodata/conda-forge-numpy-linux-64.json", "https://conda.anaconda.org/conda-forge/noarch", "conda-forge", - libsolv::PipAsPythonDependency::No + PipAsPythonDependency::No ); REQUIRE(repo_noarch.has_value()); diff --git a/libmamba/tests/src/solver/test_problems_graph.cpp b/libmamba/tests/src/solver/test_problems_graph.cpp index 0fb8a59d09..40ad51f4f7 100644 --- a/libmamba/tests/src/solver/test_problems_graph.cpp +++ b/libmamba/tests/src/solver/test_problems_graph.cpp @@ -21,10 +21,10 @@ #include "mamba/core/util.hpp" #include "mamba/fs/filesystem.hpp" #include "mamba/solver/libsolv/database.hpp" -#include "mamba/solver/libsolv/repo_info.hpp" #include "mamba/solver/libsolv/solver.hpp" #include "mamba/solver/libsolv/unsolvable.hpp" #include "mamba/solver/problems_graph.hpp" +#include "mamba/solver/repo_info.hpp" #include "mamba/specs/package_info.hpp" #include "mamba/util/string.hpp" diff --git a/libmambapy/setup.py b/libmambapy/setup.py index b8bc28281e..3eae12f478 100644 --- a/libmambapy/setup.py +++ b/libmambapy/setup.py @@ -27,7 +27,7 @@ def libmambapy_version(): def get_cmake_args(): cmake_args = [f"-DMAMBA_INSTALL_PYTHON_EXT_LIBDIR={CMAKE_INSTALL_DIR()}/src/libmambapy"] if sys.platform != "win32" and sys.platform != "cygwin": - cmake_args += ["-DMAMBA_WARNING_AS_ERROR=ON"] + cmake_args += ["-DMAMBA_WARNING_AS_ERROR=OFF"] return cmake_args diff --git a/libmambapy/src/libmambapy/bindings/legacy.cpp b/libmambapy/src/libmambapy/bindings/legacy.cpp index dbfa4375f7..ae89bb8df3 100644 --- a/libmambapy/src/libmambapy/bindings/legacy.cpp +++ b/libmambapy/src/libmambapy/bindings/legacy.cpp @@ -513,8 +513,7 @@ bind_submodule_impl(pybind11::module_ m) py::class_(m, "SubdirData") .def( "create_repo", - [](SubdirData& self, Context& context, solver::libsolv::Database& db - ) -> solver::libsolv::RepoInfo + [](SubdirData& self, Context& context, solver::libsolv::Database& db) -> solver::RepoInfo { deprecated("Use libmambapy.load_subdir_in_database instead", "2.0"); return extract(load_subdir_in_database(context, db, self)); diff --git a/libmambapy/src/libmambapy/bindings/solver.cpp b/libmambapy/src/libmambapy/bindings/solver.cpp index 7e7176dd30..2ba0a8d7be 100644 --- a/libmambapy/src/libmambapy/bindings/solver.cpp +++ b/libmambapy/src/libmambapy/bindings/solver.cpp @@ -7,7 +7,9 @@ #include #include +#include "mamba/solver/parameters.hpp" #include "mamba/solver/problems_graph.hpp" +#include "mamba/solver/repo_info.hpp" #include "mamba/solver/request.hpp" #include "mamba/solver/solution.hpp" @@ -507,5 +509,94 @@ namespace mambapy } ) .def("tree_message", &problem_tree_msg, py::arg("format") = ProblemsMessageFormat()); + + py::enum_(m, "RepodataParser") + .value("Mamba", RepodataParser::Mamba) + .value("Libsolv", RepodataParser::Libsolv) + .def(py::init(&enum_from_str)); + py::implicitly_convertible(); + + py::enum_(m, "PipAsPythonDependency") + .value("No", PipAsPythonDependency::No) + .value("Yes", PipAsPythonDependency::Yes) + .def(py::init([](bool val) { return static_cast(val); })); + py::implicitly_convertible(); + + py::enum_(m, "PackageTypes") + .value("CondaOnly", PackageTypes::CondaOnly) + .value("TarBz2Only", PackageTypes::TarBz2Only) + .value("CondaAndTarBz2", PackageTypes::CondaAndTarBz2) + .value("CondaOrElseTarBz2", PackageTypes::CondaOrElseTarBz2) + .def(py::init(&enum_from_str)); + py::implicitly_convertible(); + + py::enum_(m, "VerifyPackages") + .value("No", VerifyPackages::No) + .value("Yes", VerifyPackages::Yes) + .def(py::init([](bool val) { return static_cast(val); })); + py::implicitly_convertible(); + + py::enum_(m, "LogLevel") + .value("Debug", LogLevel::Debug) + .value("Warning", LogLevel::Warning) + .value("Error", LogLevel::Error) + .value("Fatal", LogLevel::Fatal) + .def(py::init(&enum_from_str)); + py::implicitly_convertible(); + + py::class_(m, "Priorities") + .def( + py::init( + [](int priority, int subpriority) -> Priorities + { + return { + /* .priority= */ priority, + /* .subpriority= */ subpriority, + }; + } + ), + py::arg("priority") = 0, + py::arg("subpriority") = 0 + ) + .def_readwrite("priority", &Priorities::priority) + .def_readwrite("subpriority", &Priorities::subpriority) + .def(py::self == py::self) + .def(py::self != py::self) + .def("__copy__", ©) + .def("__deepcopy__", &deepcopy, py::arg("memo")); + + py::class_(m, "RepodataOrigin") + .def( + py::init( + [](std::string_view url, std::string_view etag, std::string_view mod) -> RepodataOrigin + { + return { + /* .url= */ std::string(url), + /* .etag= */ std::string(etag), + /* .mod= */ std::string(mod), + }; + } + ), + py::arg("url") = "", + py::arg("etag") = "", + py::arg("mod") = "" + ) + .def_readwrite("url", &RepodataOrigin::url) + .def_readwrite("etag", &RepodataOrigin::etag) + .def_readwrite("mod", &RepodataOrigin::mod) + .def(py::self == py::self) + .def(py::self != py::self) + .def("__copy__", ©) + .def("__deepcopy__", &deepcopy, py::arg("memo")); + + py::class_(m, "RepoInfo") + .def_property_readonly("id", &RepoInfo::id) + .def_property_readonly("name", &RepoInfo::name) + .def_property_readonly("priority", &RepoInfo::priority) + .def("package_count", &RepoInfo::package_count) + .def(py::self == py::self) + .def(py::self != py::self) + .def("__copy__", ©) + .def("__deepcopy__", &deepcopy, py::arg("memo")); } } diff --git a/libmambapy/src/libmambapy/bindings/solver_libsolv.cpp b/libmambapy/src/libmambapy/bindings/solver_libsolv.cpp index 985afb48b2..28199a5b82 100644 --- a/libmambapy/src/libmambapy/bindings/solver_libsolv.cpp +++ b/libmambapy/src/libmambapy/bindings/solver_libsolv.cpp @@ -5,16 +5,14 @@ // The full license is in the file LICENSE, distributed with this software. #include -#include #include #include "mamba/solver/libsolv/database.hpp" -#include "mamba/solver/libsolv/parameters.hpp" -#include "mamba/solver/libsolv/repo_info.hpp" #include "mamba/solver/libsolv/solver.hpp" #include "mamba/solver/libsolv/unsolvable.hpp" +#include "mamba/solver/parameters.hpp" +#include "mamba/solver/repo_info.hpp" -#include "bind_utils.hpp" #include "bindings.hpp" #include "expected_caster.hpp" #include "path_caster.hpp" @@ -25,97 +23,9 @@ namespace mambapy { namespace py = pybind11; using namespace mamba; + using namespace mamba::solver; using namespace mamba::solver::libsolv; - py::enum_(m, "RepodataParser") - .value("Mamba", RepodataParser::Mamba) - .value("Libsolv", RepodataParser::Libsolv) - .def(py::init(&enum_from_str)); - py::implicitly_convertible(); - - py::enum_(m, "PipAsPythonDependency") - .value("No", PipAsPythonDependency::No) - .value("Yes", PipAsPythonDependency::Yes) - .def(py::init([](bool val) { return static_cast(val); })); - py::implicitly_convertible(); - - py::enum_(m, "PackageTypes") - .value("CondaOnly", PackageTypes::CondaOnly) - .value("TarBz2Only", PackageTypes::TarBz2Only) - .value("CondaAndTarBz2", PackageTypes::CondaAndTarBz2) - .value("CondaOrElseTarBz2", PackageTypes::CondaOrElseTarBz2) - .def(py::init(&enum_from_str)); - py::implicitly_convertible(); - - py::enum_(m, "VerifyPackages") - .value("No", VerifyPackages::No) - .value("Yes", VerifyPackages::Yes) - .def(py::init([](bool val) { return static_cast(val); })); - py::implicitly_convertible(); - - py::enum_(m, "LogLevel") - .value("Debug", LogLevel::Debug) - .value("Warning", LogLevel::Warning) - .value("Error", LogLevel::Error) - .value("Fatal", LogLevel::Fatal) - .def(py::init(&enum_from_str)); - py::implicitly_convertible(); - - py::class_(m, "Priorities") - .def( - py::init( - [](int priority, int subpriority) -> Priorities - { - return { - /* .priority= */ priority, - /* .subpriority= */ subpriority, - }; - } - ), - py::arg("priority") = 0, - py::arg("subpriority") = 0 - ) - .def_readwrite("priority", &Priorities::priority) - .def_readwrite("subpriority", &Priorities::subpriority) - .def(py::self == py::self) - .def(py::self != py::self) - .def("__copy__", ©) - .def("__deepcopy__", &deepcopy, py::arg("memo")); - - py::class_(m, "RepodataOrigin") - .def( - py::init( - [](std::string_view url, std::string_view etag, std::string_view mod) -> RepodataOrigin - { - return { - /* .url= */ std::string(url), - /* .etag= */ std::string(etag), - /* .mod= */ std::string(mod), - }; - } - ), - py::arg("url") = "", - py::arg("etag") = "", - py::arg("mod") = "" - ) - .def_readwrite("url", &RepodataOrigin::url) - .def_readwrite("etag", &RepodataOrigin::etag) - .def_readwrite("mod", &RepodataOrigin::mod) - .def(py::self == py::self) - .def(py::self != py::self) - .def("__copy__", ©) - .def("__deepcopy__", &deepcopy, py::arg("memo")); - - py::class_(m, "RepoInfo") - .def_property_readonly("id", &RepoInfo::id) - .def_property_readonly("name", &RepoInfo::name) - .def_property_readonly("priority", &RepoInfo::priority) - .def("package_count", &RepoInfo::package_count) - .def(py::self == py::self) - .def(py::self != py::self) - .def("__copy__", ©) - .def("__deepcopy__", &deepcopy, py::arg("memo")); - py::class_(m, "Database") .def(py::init(), py::arg("channel_params")) .def("set_logger", &Database::set_logger, py::call_guard()) @@ -169,7 +79,7 @@ namespace mambapy .def("package_count", &Database::package_count) .def( "packages_in_repo", - [](const Database& db, RepoInfo repo) + [](const Database& db, solver::RepoInfo repo) { // TODO(C++20): When Database function are refactored to use range, take the // opportunity here to make a Python iterator to avoid large alloc. diff --git a/libmambapy/src/libmambapy/solver/libsolv.py b/libmambapy/src/libmambapy/solver/libsolv.py index 8dc155f4dd..e333a177c1 100644 --- a/libmambapy/src/libmambapy/solver/libsolv.py +++ b/libmambapy/src/libmambapy/solver/libsolv.py @@ -1,2 +1,61 @@ +import warnings + # This file exists on its own rather than in `__init__.py` to make `import libmambapy.solver.libsolv` work. from libmambapy.bindings.solver.libsolv import * # noqa: F403 + +from libmambapy.bindings.solver import ( + Priorities as _Priorities, + RepodataParser as _RepodataParser, + PipAsPythonDependency as _PipAsPythonDependency, + PackageTypes as _PackageTypes, + VerifyPackages as _VerifyPackages, + RepodataOrigin as _RepodataOrigin, +) + +_WARN_TEMPLATE_MSG = ( + "`libmambapy.bindings.solver.libsolv.{name}` has been moved " + "to `libmambapy.bindings.solver.{name}` in 2.0. " + "This import path will be removed in 2.2" +) + + +class Priorities(_Priorities): + warnings.warn( + _WARN_TEMPLATE_MSG.format(name="Priorities"), + category=DeprecationWarning, + ) + + +class RepodataParser(_RepodataParser): + warnings.warn( + _WARN_TEMPLATE_MSG.format(name="RepodataParser"), + category=DeprecationWarning, + ) + + +class PipAsPythonDependency(_PipAsPythonDependency): + warnings.warn( + _WARN_TEMPLATE_MSG.format(name="PipAsPythonDependency"), + category=DeprecationWarning, + ) + + +class PackageTypes(_PackageTypes): + warnings.warn( + _WARN_TEMPLATE_MSG.format(name="PackageTypes"), + category=DeprecationWarning, + ) + + +class VerifyPackages(_VerifyPackages): + warnings.warn( + _WARN_TEMPLATE_MSG.format(name="VerifyPackages"), + category=DeprecationWarning, + ) + + +class RepodataOrigin(_RepodataOrigin): + warnings.warn( + _WARN_TEMPLATE_MSG.format(name="RepodataOrigin"), + category=DeprecationWarning, + ) diff --git a/libmambapy/tests/test_solver.py b/libmambapy/tests/test_solver.py index 05ea658159..7e08dd10fc 100644 --- a/libmambapy/tests/test_solver.py +++ b/libmambapy/tests/test_solver.py @@ -344,3 +344,103 @@ def test_CompressedProblemsGraph_NamedList(): # Clear named_list.clear() assert len(named_list) == 0 + + +def test_RepodataParser(): + assert libmambapy.solver.RepodataParser.Mamba.name == "Mamba" + assert libmambapy.solver.RepodataParser.Libsolv.name == "Libsolv" + + assert libmambapy.solver.RepodataParser("Libsolv") == libmambapy.solver.RepodataParser.Libsolv + + with pytest.raises(KeyError): + libmambapy.solver.RepodataParser("NoParser") + + +def test_PipASPythonDependency(): + assert libmambapy.solver.PipAsPythonDependency.No.name == "No" + assert libmambapy.solver.PipAsPythonDependency.Yes.name == "Yes" + + assert ( + libmambapy.solver.PipAsPythonDependency(True) == libmambapy.solver.PipAsPythonDependency.Yes + ) + + +def test_PackageTypes(): + assert libmambapy.solver.PackageTypes.CondaOnly.name == "CondaOnly" + assert libmambapy.solver.PackageTypes.TarBz2Only.name == "TarBz2Only" + assert libmambapy.solver.PackageTypes.CondaAndTarBz2.name == "CondaAndTarBz2" + assert libmambapy.solver.PackageTypes.CondaOrElseTarBz2.name == "CondaOrElseTarBz2" + + assert libmambapy.solver.PackageTypes("TarBz2Only") == libmambapy.solver.PackageTypes.TarBz2Only + + with pytest.raises(KeyError): + libmambapy.solver.RepodataParser("tarbz2-only") + + +def test_VerifyPackages(): + assert libmambapy.solver.VerifyPackages.No.name == "No" + assert libmambapy.solver.VerifyPackages.Yes.name == "Yes" + + assert libmambapy.solver.VerifyPackages(True) == libmambapy.solver.VerifyPackages.Yes + + +def test_Platform(): + assert libmambapy.solver.LogLevel.Debug.name == "Debug" + assert libmambapy.solver.LogLevel.Warning.name == "Warning" + assert libmambapy.solver.LogLevel.Error.name == "Error" + assert libmambapy.solver.LogLevel.Fatal.name == "Fatal" + + assert libmambapy.solver.LogLevel("Error") == libmambapy.solver.LogLevel.Error + + with pytest.raises(KeyError): + # No parsing, explicit name + libmambapy.solver.LogLevel("Unicorn") + + +def test_Priorities(): + p = libmambapy.solver.Priorities(priority=-1, subpriority=-2) + + assert p.priority == -1 + assert p.subpriority == -2 + + # Setters + p.priority = 33 + p.subpriority = 0 + assert p.priority == 33 + assert p.subpriority == 0 + + # Operators + assert p == p + assert p != libmambapy.solver.Priorities() + + # Copy + other = copy.deepcopy(p) + assert other is not p + assert other == p + + +def test_RepodataOrigin(): + orig = libmambapy.solver.RepodataOrigin( + url="https://conda.anaconda.org/conda-forge", mod="the-mod", etag="the-etag" + ) + + assert orig.url == "https://conda.anaconda.org/conda-forge" + assert orig.etag == "the-etag" + assert orig.mod == "the-mod" + + # Setters + orig.url = "https://repo.mamba.pm" + orig.etag = "other-etag" + orig.mod = "other-mod" + assert orig.url == "https://repo.mamba.pm" + assert orig.etag == "other-etag" + assert orig.mod == "other-mod" + + # Operators + assert orig == orig + assert orig != libmambapy.solver.RepodataOrigin() + + # Copy + other = copy.deepcopy(orig) + assert other is not orig + assert other == orig diff --git a/libmambapy/tests/test_solver_libsolv.py b/libmambapy/tests/test_solver_libsolv.py index 7d1dad6eb7..3617c79d5f 100644 --- a/libmambapy/tests/test_solver_libsolv.py +++ b/libmambapy/tests/test_solver_libsolv.py @@ -1,4 +1,3 @@ -import copy import json import itertools @@ -9,115 +8,17 @@ def test_import_submodule(): - import libmambapy.solver.libsolv as solv + import libmambapy.solver.libsolv as libsolv # Dummy execution - _p = solv.Priorities + _p = libsolv.Solver def test_import_recursive(): import libmambapy as mamba # Dummy execution - _p = mamba.solver.libsolv.Priorities - - -def test_RepodataParser(): - assert libsolv.RepodataParser.Mamba.name == "Mamba" - assert libsolv.RepodataParser.Libsolv.name == "Libsolv" - - assert libsolv.RepodataParser("Libsolv") == libsolv.RepodataParser.Libsolv - - with pytest.raises(KeyError): - libsolv.RepodataParser("NoParser") - - -def test_PipASPythonDependency(): - assert libsolv.PipAsPythonDependency.No.name == "No" - assert libsolv.PipAsPythonDependency.Yes.name == "Yes" - - assert libsolv.PipAsPythonDependency(True) == libsolv.PipAsPythonDependency.Yes - - -def test_PackageTypes(): - assert libsolv.PackageTypes.CondaOnly.name == "CondaOnly" - assert libsolv.PackageTypes.TarBz2Only.name == "TarBz2Only" - assert libsolv.PackageTypes.CondaAndTarBz2.name == "CondaAndTarBz2" - assert libsolv.PackageTypes.CondaOrElseTarBz2.name == "CondaOrElseTarBz2" - - assert libsolv.PackageTypes("TarBz2Only") == libsolv.PackageTypes.TarBz2Only - - with pytest.raises(KeyError): - libsolv.RepodataParser("tarbz2-only") - - -def test_VerifyPackages(): - assert libsolv.VerifyPackages.No.name == "No" - assert libsolv.VerifyPackages.Yes.name == "Yes" - - assert libsolv.VerifyPackages(True) == libsolv.VerifyPackages.Yes - - -def test_Platform(): - assert libsolv.LogLevel.Debug.name == "Debug" - assert libsolv.LogLevel.Warning.name == "Warning" - assert libsolv.LogLevel.Error.name == "Error" - assert libsolv.LogLevel.Fatal.name == "Fatal" - - assert libsolv.LogLevel("Error") == libsolv.LogLevel.Error - - with pytest.raises(KeyError): - # No parsing, explicit name - libsolv.LogLevel("Unicorn") - - -def test_Priorities(): - p = libsolv.Priorities(priority=-1, subpriority=-2) - - assert p.priority == -1 - assert p.subpriority == -2 - - # Setters - p.priority = 33 - p.subpriority = 0 - assert p.priority == 33 - assert p.subpriority == 0 - - # Operators - assert p == p - assert p != libsolv.Priorities() - - # Copy - other = copy.deepcopy(p) - assert other is not p - assert other == p - - -def test_RepodataOrigin(): - orig = libsolv.RepodataOrigin( - url="https://conda.anaconda.org/conda-forge", mod="the-mod", etag="the-etag" - ) - - assert orig.url == "https://conda.anaconda.org/conda-forge" - assert orig.etag == "the-etag" - assert orig.mod == "the-mod" - - # Setters - orig.url = "https://repo.mamba.pm" - orig.etag = "other-etag" - orig.mod = "other-mod" - assert orig.url == "https://repo.mamba.pm" - assert orig.etag == "other-etag" - assert orig.mod == "other-mod" - - # Operators - assert orig == orig - assert orig != libsolv.RepodataOrigin() - - # Copy - other = copy.deepcopy(orig) - assert other is not orig - assert other == orig + _p = mamba.solver.libsolv.Solver def test_Database_logger():