Custom resolve complex MatchSpec in Solver (#3233)

* Add ObjPool::current_error

* Support reference_wrapper in MatchSpec::contains

* Rename subdir > platform in solv-cpp

* Add Matcher as namespace callback

* Add MatcherFlags

* Redefine MatchSpec::is_simple

* Add VersionSpec::from_predicate

* Refactor reinstall jobs

* Fix MatchSpec::is_simple

* Handle complex MatchSpec in solver

* Add version cache in Matcher

* Add more pool namespace tests

* Handle exception in ObjPool callback

* Try removing SOLVABLE_PROVIDES

* Add unsolvable complex spec test

* Fix Database callback exceptions

* Add channel in Matcher

* Adapt channel_specific tests

* Fix updates
This commit is contained in:
Antoine Prouvost 2024-03-19 19:52:56 +01:00 committed by GitHub
parent 25431a6d38
commit 43e38efb18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1182 additions and 420 deletions

View File

@ -181,6 +181,7 @@ set(
# 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

View File

@ -7,6 +7,7 @@
#ifndef MAMBA_SOLV_POOL_HPP
#define MAMBA_SOLV_POOL_HPP
#include <functional>
#include <memory>
#include <optional>
#include <stdexcept>
@ -44,6 +45,11 @@ namespace solv
auto raw() -> raw_ptr;
auto raw() const -> const_raw_ptr;
auto current_error() const -> std::string_view;
void set_current_error(raw_str_view msg);
void set_current_error(const std::string& msg);
/**
* Get the current distribution type of the pool.
*
@ -285,6 +291,22 @@ namespace solv
template <typename UnaryFunc>
void for_each_installed_solvable(UnaryFunc&& func);
/** Rethrow exception thrown in callback. */
void rethrow_potential_callback_exception() const;
protected:
using UserCallback = std::function<OffsetId(ObjPoolView, StringId, StringId)>;
/**
* A wrapper around user callback to handle exceptions.
*
* This cannot be set in this class since this is only a view type but can be checked
* for errors.
*/
struct NamespaceCallbackWrapper;
private:
raw_ptr m_pool;
@ -302,6 +324,8 @@ namespace solv
~ObjPool();
using ObjPoolView::raw;
using ObjPoolView::current_error;
using ObjPoolView::set_current_error;
using ObjPoolView::disttype;
using ObjPoolView::set_disttype;
using ObjPoolView::find_string;
@ -337,6 +361,7 @@ namespace solv
using ObjPoolView::for_each_solvable;
using ObjPoolView::for_each_installed_solvable_id;
using ObjPoolView::for_each_installed_solvable;
using ObjPoolView::rethrow_potential_callback_exception;
/** Set the callback to handle libsolv messages.
*
@ -347,8 +372,9 @@ namespace solv
template <typename Func>
void set_debug_callback(Func&& callback);
template <typename Func>
void set_namespace_callback(Func&& callback);
using UserCallback = ObjPoolView::UserCallback;
void set_namespace_callback(UserCallback&& callback);
private:
@ -358,7 +384,7 @@ namespace solv
};
std::unique_ptr<void, void (*)(void*)> m_user_debug_callback;
std::unique_ptr<void, void (*)(void*)> m_user_namespace_callback;
std::unique_ptr<NamespaceCallbackWrapper> m_user_namespace_callback;
// Must be deleted before the debug callback
std::unique_ptr<::Pool, ObjPool::PoolDeleter> m_pool = nullptr;
};
@ -531,28 +557,5 @@ namespace solv
::pool_setdebugcallback(raw(), debug_callback, m_user_debug_callback.get());
}
template <typename Func>
void ObjPool::set_namespace_callback(Func&& callback)
{
static_assert(
std::is_nothrow_invocable_v<Func, ObjPoolView, StringId, StringId>,
"User callback must be marked noexcept."
);
m_user_namespace_callback.reset(new Func(std::forward<Func>(callback)));
m_user_namespace_callback.get_deleter() = [](void* ptr)
{ delete reinterpret_cast<Func*>(ptr); };
// Wrap the user callback in the libsolv function type that must cast the callback ptr
auto namespace_callback = [](::Pool* pool, void* user_data, StringId name, StringId ver
) noexcept -> OffsetId
{
auto* user_namespace_callback = reinterpret_cast<Func*>(user_data);
return (*user_namespace_callback)(ObjPoolView(pool), name, ver); // noexcept
};
::pool_setnamespacecallback(raw(), namespace_callback, m_user_namespace_callback.get());
}
}
#endif

View File

@ -75,11 +75,11 @@ namespace solv
auto channel() const -> std::string_view;
/**
* The sub-directory of the solvable.
* The platform of the solvable.
*
* @see ObjSolvableView::set_subdir
**/
auto subdir() const -> std::string_view;
auto platform() const -> std::string_view;
/**
* Queue of ``DependencyId``.
@ -254,7 +254,7 @@ namespace solv
void set_channel(const std::string& str) const;
/**
* Set the sub-directory of the solvable.
* Set the platform of the solvable.
*
* This has no effect for libsolv and is purely for data storing.
* This may not be the same as @ref ObjRepoViewConst::channel, for instance the install
@ -263,8 +263,8 @@ namespace solv
* @note A call to @ref ObjRepoView::internalize is required for this attribute to
* be available for lookup.
*/
void set_subdir(raw_str_view str) const;
void set_subdir(const std::string& str) const;
void set_platform(raw_str_view str) const;
void set_platform(const std::string& str) const;
/** Set the dependencies of the solvable. */
void set_dependencies(const ObjQueue& q, DependencyMarker marker = 0) const;

View File

@ -5,7 +5,9 @@
// The full license is in the file LICENSE, distributed with this software.
#include <cassert>
#include <exception>
#include <limits>
#include <memory>
#include <stdexcept>
#include <solv/conda.h>
@ -38,6 +40,21 @@ namespace solv
return m_pool;
}
auto ObjPoolView::current_error() const -> std::string_view
{
return ::pool_errstr(m_pool);
}
void ObjPoolView::set_current_error(raw_str_view msg)
{
::pool_error(m_pool, -1, "%s", msg);
}
void ObjPoolView::set_current_error(const std::string& msg)
{
return set_current_error(msg.c_str());
}
auto ObjPoolView::disttype() const -> DistType
{
return raw()->disttype;
@ -323,6 +340,24 @@ namespace solv
return std::nullopt;
}
struct ObjPoolView::NamespaceCallbackWrapper
{
UserCallback callback;
std::exception_ptr error = nullptr;
};
void ObjPoolView::rethrow_potential_callback_exception() const
{
if (auto callback = reinterpret_cast<NamespaceCallbackWrapper*>(raw()->nscallbackdata))
{
if (auto error = callback->error)
{
callback->error = nullptr;
std::rethrow_exception(error);
}
}
}
/*******************************
* Implementation of ObjPool *
*******************************/
@ -335,11 +370,45 @@ namespace solv
ObjPool::ObjPool()
: ObjPoolView(nullptr)
, m_user_debug_callback(nullptr, [](void* /*ptr*/) {})
, m_user_namespace_callback(nullptr, [](void* /*ptr*/) {})
, m_user_namespace_callback(nullptr)
, m_pool(::pool_create())
{
ObjPoolView::m_pool = m_pool.get();
}
ObjPool::~ObjPool() = default;
void ObjPool::set_namespace_callback(UserCallback&& callback)
{
m_user_namespace_callback = std::make_unique<NamespaceCallbackWrapper>();
// Set the callback
m_user_namespace_callback->callback = [wrapper = m_user_namespace_callback.get(),
callback = std::move(callback
)](ObjPoolView pool, StringId name, StringId ver
) mutable noexcept -> OffsetId
{
auto error = std::exception_ptr(nullptr);
try
{
std::swap(error, wrapper->error);
return callback(pool, name, ver);
}
catch (...)
{
wrapper->error = std::current_exception();
return 0;
}
};
// Wrap the user callback in the libsolv function type that must cast the callback ptr
auto libsolv_callback = +[](::Pool* pool, void* user_data, StringId name, StringId ver
) noexcept -> OffsetId
{
auto* user_namespace_callback = reinterpret_cast<NamespaceCallbackWrapper*>(user_data);
return user_namespace_callback->callback(ObjPoolView(pool), name, ver); // noexcept
};
::pool_setnamespacecallback(raw(), libsolv_callback, m_user_namespace_callback.get());
}
}

View File

@ -352,20 +352,20 @@ namespace solv
return set_channel(str.c_str());
}
auto ObjSolvableViewConst::subdir() const -> std::string_view
auto ObjSolvableViewConst::platform() const -> std::string_view
{
return ptr_to_strview(::solvable_lookup_str(const_cast<::Solvable*>(raw()), SOLVABLE_MEDIADIR)
);
}
void ObjSolvableView::set_subdir(raw_str_view str) const
void ObjSolvableView::set_platform(raw_str_view str) const
{
::solvable_set_str(raw(), SOLVABLE_MEDIADIR, str);
}
void ObjSolvableView::set_subdir(const std::string& str) const
void ObjSolvableView::set_platform(const std::string& str) const
{
return set_subdir(str.c_str());
return set_platform(str.c_str());
}
auto ObjSolvableViewConst::dependencies(DependencyMarker marker) const -> ObjQueue

View File

@ -180,10 +180,11 @@ namespace solv
return val != 0;
}
auto ObjSolver::solve(const ObjPool& /* pool */, const ObjQueue& jobs) -> bool
auto ObjSolver::solve(const ObjPool& pool, const ObjQueue& jobs) -> bool
{
// pool is captured inside solver so we take it as a parameter to be explicit.
const auto n_pbs = ::solver_solve(raw(), const_cast<::Queue*>(jobs.raw()));
pool.rethrow_potential_callback_exception();
return n_pbs == 0;
}

View File

@ -30,6 +30,12 @@ TEST_SUITE("solv::ObjPool")
CHECK_EQ(pool.disttype(), DISTTYPE_CONDA);
}
SUBCASE("Error")
{
pool.set_current_error("Some failure");
CHECK_EQ(pool.current_error(), "Some failure");
}
SUBCASE("Add strings")
{
const auto id_hello = pool.add_string("Hello");

View File

@ -151,26 +151,130 @@ TEST_SUITE("solv::scenariso")
const auto dep_id = pool.add_dependency(dep_name_id, REL_NAMESPACE, dep_ver_id);
auto [repo_id, repo] = pool.add_repo("forge");
auto [solv_id, solv] = repo.add_solvable();
solv.set_name("a");
solv.set_version("1.0");
const auto a_solv_id = add_simple_package(pool, repo, SimplePkg{ "a", "1.0" });
repo.internalize();
pool.set_namespace_callback(
[&pool,
name_id = dep_name_id,
ver_id = dep_ver_id,
solv_id = solv_id](ObjPoolView, StringId name, StringId ver) noexcept -> OffsetId
SUBCASE("Direct job namespace dependency")
{
SUBCASE("Which resolves to some packages")
{
CHECK_EQ(name, name_id);
CHECK_EQ(ver, ver_id);
return pool.add_to_whatprovides_data({ solv_id });
}
);
bool called = false;
pool.set_namespace_callback(
[&, a_solv_id = a_solv_id](ObjPoolView, StringId name, StringId ver) noexcept -> OffsetId
{
called = true;
CHECK_EQ(name, dep_name_id);
CHECK_EQ(ver, dep_ver_id);
return pool.add_to_whatprovides_data({ a_solv_id });
}
);
auto solver = ObjSolver(pool);
auto jobs = ObjQueue{ SOLVER_INSTALL, dep_id };
auto solved = solver.solve(pool, jobs);
CHECK(solved);
auto solver = ObjSolver(pool);
auto solved = solver.solve(pool, { SOLVER_INSTALL, dep_id });
CHECK(solved);
CHECK(called);
}
SUBCASE("Which is unsatisfyable")
{
bool called = false;
pool.set_namespace_callback(
[&](ObjPoolView, StringId, StringId) noexcept -> OffsetId
{
called = true;
return 0; // 0 means "not-found"
}
);
auto solver = ObjSolver(pool);
auto solved = solver.solve(pool, { SOLVER_INSTALL, dep_id });
CHECK(called);
CHECK_FALSE(solved);
}
SUBCASE("Callback throws")
{
pool.set_namespace_callback(
[](ObjPoolView, StringId, StringId) -> OffsetId
{ throw std::runtime_error("Error!"); }
);
auto solver = ObjSolver(pool);
CHECK_THROWS_AS(
[&] {
return solver.solve(pool, { SOLVER_INSTALL, dep_id });
}(),
std::runtime_error
);
}
}
SUBCASE("transitive job dependency")
{
// Add a dependency ``job==3.0``
const auto job_name_id = pool.add_string("job");
const auto job_ver_id = pool.add_string("3.0");
const auto job_id = pool.add_dependency(job_name_id, REL_EQ, job_ver_id);
// Add a package ``{name=job, version=3.0}`` with dependency in namespace dep.
auto [job_solv_id, job_solv] = repo.add_solvable();
job_solv.set_name(job_name_id);
job_solv.set_version(job_ver_id);
job_solv.set_dependencies({ dep_id });
job_solv.add_self_provide();
repo.internalize();
SUBCASE("Which resolves to some packages")
{
bool called = false;
pool.set_namespace_callback(
[&, a_solv_id = a_solv_id](ObjPoolView, StringId name, StringId ver) noexcept -> OffsetId
{
called = true;
CHECK_EQ(name, dep_name_id);
CHECK_EQ(ver, dep_ver_id);
return pool.add_to_whatprovides_data({ a_solv_id });
}
);
auto solver = ObjSolver(pool);
auto solved = solver.solve(pool, { SOLVER_INSTALL, job_id });
CHECK(called);
CHECK(solved);
}
SUBCASE("Which is unsatisfyable")
{
bool called = false;
pool.set_namespace_callback(
[&](ObjPoolView, StringId, StringId) noexcept -> OffsetId
{
called = true;
return 0; // 0 means "not-found"
}
);
auto solver = ObjSolver(pool);
auto solved = solver.solve(pool, { SOLVER_INSTALL, job_id });
CHECK(called);
CHECK_FALSE(solved);
}
SUBCASE("Callback throws")
{
pool.set_namespace_callback(
[](ObjPoolView, StringId, StringId) -> OffsetId
{ throw std::runtime_error("Error!"); }
);
auto solver = ObjSolver(pool);
CHECK_THROWS_AS(
[&] {
return solver.solve(pool, { SOLVER_INSTALL, job_id });
}(),
std::runtime_error
);
}
}
}
}

View File

@ -53,7 +53,7 @@ TEST_SUITE("solv::ObjSolvable")
solv.set_timestamp(4110596167);
solv.set_url("https://conda.anaconda.org/conda-forge/linux-64");
solv.set_channel("conda-forge");
solv.set_subdir("linux-64");
solv.set_platform("linux-64");
solv.set_type(SolvableType::Virtualpackage);
SUBCASE("Empty without internalize")
@ -70,7 +70,7 @@ TEST_SUITE("solv::ObjSolvable")
CHECK_EQ(solv.timestamp(), 0);
CHECK_EQ(solv.url(), "");
CHECK_EQ(solv.channel(), "");
CHECK_EQ(solv.subdir(), "");
CHECK_EQ(solv.platform(), "");
CHECK_EQ(solv.type(), SolvableType::Package);
}
@ -97,7 +97,7 @@ TEST_SUITE("solv::ObjSolvable")
CHECK_EQ(solv.timestamp(), 4110596167);
CHECK_EQ(solv.url(), "https://conda.anaconda.org/conda-forge/linux-64");
CHECK_EQ(solv.channel(), "conda-forge");
CHECK_EQ(solv.subdir(), "linux-64");
CHECK_EQ(solv.platform(), "linux-64");
CHECK_EQ(solv.type(), SolvableType::Virtualpackage);
SUBCASE("Override attribute")
@ -126,7 +126,7 @@ TEST_SUITE("solv::ObjSolvable")
CHECK_EQ(solv.timestamp(), 0);
CHECK_EQ(solv.url(), "");
CHECK_EQ(solv.channel(), "");
CHECK_EQ(solv.subdir(), "");
CHECK_EQ(solv.platform(), "");
CHECK_EQ(solv.type(), SolvableType::Package);
}

View File

@ -105,8 +105,16 @@ namespace mamba::specs
[[nodiscard]] auto conda_build_form() const -> std::string;
[[nodiscard]] auto str() const -> std::string;
/**
* Return true if the MatchSpec can be written as ``<name> <version> <build_string>``.
*/
[[nodiscard]] auto is_simple() const -> bool;
/**
* Return true if the MatchSpec contains an exact package name and nothing else.
*/
[[nodiscard]] auto is_only_package_name() const -> bool;
/**
* Check if the MatchSpec matches the given package.
*
@ -184,31 +192,61 @@ struct fmt::formatter<::mamba::specs::MatchSpec>
namespace mamba::specs
{
namespace detail
{
template <typename Return>
struct Deref
{
template <typename T>
static auto deref(T&& x) -> decltype(auto)
{
return x;
}
};
template <typename Inner>
struct Deref<std::reference_wrapper<Inner>>
{
template <typename T>
static auto deref(T&& x) -> decltype(auto)
{
return std::forward<T>(x).get();
}
};
template <typename Attr, typename Pkg>
auto invoke_pkg(Attr&& attr, Pkg&& pkg) -> decltype(auto)
{
using Return = std::decay_t<std::invoke_result_t<Attr&&, Pkg&&>>;
return Deref<Return>::deref(std::invoke(std::forward<Attr>(attr), std::forward<Pkg>(pkg)));
}
}
template <typename Pkg>
auto MatchSpec::contains_except_channel(const Pkg& pkg) const -> bool
{
if ( //
!name().contains(std::invoke(&Pkg::name, pkg)) //
|| !version().contains(std::invoke(&Pkg::version, pkg)) //
|| !build_string().contains(std::invoke(&Pkg::build_string, pkg)) //
|| !build_number().contains(std::invoke(&Pkg::build_number, pkg)) //
|| (!md5().empty() && (md5() != std::invoke(&Pkg::md5, pkg))) //
|| (!sha256().empty() && (sha256() != std::invoke(&Pkg::sha256, pkg))) //
|| (!license().empty() && (license() != std::invoke(&Pkg::license, pkg))) //
if ( //
!name().contains(detail::invoke_pkg(&Pkg::name, pkg)) //
|| !version().contains(detail::invoke_pkg(&Pkg::version, pkg)) //
|| !build_string().contains(detail::invoke_pkg(&Pkg::build_string, pkg)) //
|| !build_number().contains(detail::invoke_pkg(&Pkg::build_number, pkg)) //
|| (!md5().empty() && (md5() != detail::invoke_pkg(&Pkg::md5, pkg))) //
|| (!sha256().empty() && (sha256() != detail::invoke_pkg(&Pkg::sha256, pkg))) //
|| (!license().empty() && (license() != detail::invoke_pkg(&Pkg::license, pkg))) //
)
{
return false;
}
if (const auto& plats = platforms();
plats.has_value() && !plats->get().contains(std::invoke(&Pkg::platform, pkg)))
plats.has_value() && !plats->get().contains(detail::invoke_pkg(&Pkg::platform, pkg)))
{
return false;
}
if (const auto& tfeats = track_features();
tfeats.has_value()
&& !util::set_is_subset_of(tfeats->get(), std::invoke(&Pkg::track_features, pkg)))
&& !util::set_is_subset_of(tfeats->get(), detail::invoke_pkg(&Pkg::track_features, pkg)))
{
return false;
}

View File

@ -152,6 +152,11 @@ namespace mamba::specs
[[nodiscard]] static auto parse(std::string_view str) -> expected_parse_t<VersionSpec>;
/**
* Create a Version spec with a single predicate in the expression.
*/
[[nodiscard]] static auto from_predicate(VersionPredicate pred) -> VersionSpec;
/** Construct VersionSpec that match all versions. */
VersionSpec() = default;
explicit VersionSpec(tree_type&& tree) noexcept;

View File

@ -24,18 +24,19 @@
#include "solv-cpp/queue.hpp"
#include "solver/libsolv/helpers.hpp"
#include "solver/libsolv/matcher.hpp"
namespace mamba::solver::libsolv
{
struct Database::DatabaseImpl
{
DatabaseImpl(specs::ChannelResolveParams p_channel_params)
: channel_params(std::move(p_channel_params))
explicit DatabaseImpl(specs::ChannelResolveParams p_channel_params)
: matcher(std::move(p_channel_params))
{
}
specs::ChannelResolveParams channel_params;
solv::ObjPool pool = {};
Matcher matcher;
};
Database::Database(specs::ChannelResolveParams channel_params)
@ -45,6 +46,14 @@ namespace mamba::solver::libsolv
// Ensure that debug logging never goes to stdout as to not interfere json output
pool().raw()->debugmask |= SOLV_DEBUG_TO_STDERR;
::pool_setdebuglevel(pool().raw(), -1); // Off
pool().set_namespace_callback(
[&data = (*m_data
)](solv::ObjPoolView pool, solv::StringId first, solv::StringId second) -> solv::OffsetId
{
auto [dep, flags] = get_abused_namespace_callback_args(pool, first, second);
return data.matcher.get_matching_packages(pool, dep, flags);
}
);
}
Database::~Database() = default;
@ -75,7 +84,7 @@ namespace mamba::solver::libsolv
auto Database::channel_params() const -> const specs::ChannelResolveParams&
{
return m_data->channel_params;
return m_data->matcher.channel_params();
}
namespace
@ -305,13 +314,9 @@ namespace mamba::solver::libsolv
namespace
{
auto matchspec2id(
solv::ObjPool& pool,
const specs::ChannelResolveParams& channel_params,
const specs::MatchSpec& ms
) -> solv::DependencyId
auto matchspec2id(solv::ObjPool& pool, const specs::MatchSpec& ms) -> solv::DependencyId
{
return pool_add_matchspec(pool, ms, channel_params)
return pool_add_matchspec(pool, ms)
.or_else([](mamba_error&& error) { throw std::move(error); })
.value_or(0);
}
@ -322,7 +327,7 @@ namespace mamba::solver::libsolv
static_assert(std::is_same_v<std::underlying_type_t<PackageId>, solv::SolvableId>);
pool().ensure_whatprovides();
const auto ms_id = matchspec2id(pool(), channel_params(), ms);
const auto ms_id = matchspec2id(pool(), ms);
auto solvables = pool().select_solvables({ SOLVER_SOLVABLE_PROVIDES, ms_id });
auto out = std::vector<PackageId>(solvables.size());
std::transform(
@ -339,7 +344,7 @@ namespace mamba::solver::libsolv
static_assert(std::is_same_v<std::underlying_type_t<PackageId>, solv::SolvableId>);
pool().ensure_whatprovides();
const auto ms_id = matchspec2id(pool(), channel_params(), ms);
const auto ms_id = matchspec2id(pool(), ms);
auto solvables = pool().what_matches_dep(SOLVABLE_REQUIRES, ms_id);
auto out = std::vector<PackageId>(solvables.size());
std::transform(

View File

@ -28,6 +28,7 @@
#include "solver/helpers.hpp"
#include "solver/libsolv/helpers.hpp"
#include "solver/libsolv/matcher.hpp"
#define MAMBA_TOOL_VERSION "2.0"
@ -51,8 +52,11 @@ namespace mamba::solver::libsolv
}
solv.set_build_number(pkg.build_number);
solv.set_channel(pkg.channel);
// TODO In the case of a repo with all similar subdir (which is not the case in the
// install repo) we could also not set this (to save the strings stored in libsolv)
// and recreate it by concatenating filename and repo URL.
solv.set_url(pkg.package_url);
solv.set_subdir(pkg.platform);
solv.set_platform(pkg.platform);
solv.set_file_name(pkg.filename);
solv.set_license(pkg.license);
solv.set_size(pkg.size);
@ -98,7 +102,7 @@ namespace mamba::solver::libsolv
out.build_number = s.build_number();
out.channel = s.channel();
out.package_url = s.url();
out.platform = s.subdir();
out.platform = s.platform();
out.filename = s.file_name();
out.license = s.license();
out.size = s.size();
@ -251,11 +255,11 @@ namespace mamba::solver::libsolv
if (auto subdir = pkg["subdir"].get_c_str(); !subdir.error())
{
solv.set_subdir(subdir.value_unsafe());
solv.set_platform(subdir.value_unsafe());
}
else
{
solv.set_subdir(default_subdir);
solv.set_platform(default_subdir);
}
if (auto size = pkg["size"].get_uint64(); !size.error())
@ -706,143 +710,31 @@ namespace mamba::solver::libsolv
repo.set_pip_added(true);
}
namespace
auto
make_abused_namespace_dep_args(solv::ObjPool& pool, std::string_view dependency, const MatchFlags& flags)
-> std::pair<solv::StringId, solv::StringId>
{
auto
channel_match(const std::vector<specs::Channel>& ms_channels, const specs::CondaURL& pkg_url)
-> specs::Channel::Match
{
auto match = specs::Channel::Match::No;
// More than one element means the channel spec was a custom_multi_channel
for (const auto& chan : ms_channels)
{
switch (chan.contains_package(pkg_url))
{
case specs::Channel::Match::Full:
return specs::Channel::Match::Full;
case specs::Channel::Match::InOtherPlatform:
// Keep looking for full matches
match = specs::Channel::Match::InOtherPlatform;
break;
case specs::Channel::Match::No:
// No overriding potential InOtherPlatform match
break;
}
}
return match;
}
return {
pool.add_string(dependency),
pool.add_string(flags.internal_serialize()),
};
}
/**
* Add function to handle matchspec while parsing is done by libsolv.
*/
auto add_channel_specific_matchspec(
solv::ObjPool& pool,
const specs::MatchSpec& ms,
const specs::ChannelResolveParams& params
) -> expected_t<solv::DependencyId>
{
assert(ms.channel().has_value());
const std::string repr = ms.str();
// Already added, return that id
if (const auto maybe_id = pool.find_string(repr))
{
return maybe_id.value();
}
// conda_build_form does **NOT** contain the channel info
const solv::DependencyId match_id = pool_conda_matchspec(
pool.raw(),
ms.conda_build_form().c_str()
);
auto maybe_ms_channels = specs::Channel::resolve(*ms.channel(), params);
if (!maybe_ms_channels)
{
return make_unexpected(
fmt::format(R"(Failed to resolve channels in "{}")", ms.channel().value()),
mamba_error_code::invalid_spec
);
}
const auto& ms_channels = maybe_ms_channels.value();
solv::ObjQueue selected_pkgs = {};
auto other_subdir_match = std::string();
pool.for_each_whatprovides(
match_id,
[&](solv::ObjSolvableViewConst s)
{
if (s.installed())
{
// This will have the effect that channel-specific MatchSpec will always be
// reinstalled.
// This is not the intended behaviour but an historical artifact on which
// ``--force-reinstall`` currently rely.
return;
}
assert(ms.channel().has_value());
if (auto pkg_url = specs::CondaURL::parse(s.url()))
{
const auto match = channel_match(ms_channels, *pkg_url);
switch (match)
{
case (specs::Channel::Match::Full):
{
selected_pkgs.push_back(s.id());
break;
}
case (specs::Channel::Match::InOtherPlatform):
{
other_subdir_match = s.subdir();
break;
}
case (specs::Channel::Match::No):
{
break;
}
}
}
}
);
if (selected_pkgs.empty())
{
if (!other_subdir_match.empty())
{
const auto& filters = ms.channel()->platform_filters();
throw std::runtime_error(fmt::format(
R"(The package "{}" is not available for the specified platform{} ({}))"
R"( but is available on {}.)",
ms.str(),
filters.size() > 1 ? "s" : "",
fmt::join(filters, ", "),
other_subdir_match
));
}
else
{
throw std::runtime_error(fmt::format(
R"(The package "{}" is not found in any loaded channels.)"
R"( Try adding more channels or subdirs.)",
ms.str()
));
}
}
const solv::StringId repr_id = pool.add_string(repr);
// FRAGILE This get deleted when calling ``pool_createwhatprovides`` so care
// must be taken to do it before
// TODO investigate namespace providers
pool.add_to_whatprovides(repr_id, pool.add_to_whatprovides_data(selected_pkgs));
return repr_id;
}
auto get_abused_namespace_callback_args( //
solv::ObjPoolView& pool,
solv::StringId name,
solv::StringId ver
) -> std::pair<std::string_view, MatchFlags>
{
return {
pool.get_string(name),
MatchFlags::internal_deserialize(pool.get_string(ver)),
};
}
[[nodiscard]] auto pool_add_matchspec( //
solv::ObjPool& pool,
const specs::MatchSpec& ms,
const specs::ChannelResolveParams& params
const specs::MatchSpec& ms
) -> expected_t<solv::DependencyId>
{
auto check_not_zero = [&](solv::DependencyId id) -> expected_t<solv::DependencyId>
@ -857,21 +749,17 @@ namespace mamba::solver::libsolv
return id;
};
if (!ms.channel().has_value())
if (ms.is_simple())
{
return check_not_zero(pool.add_conda_dependency(ms.conda_build_form()));
}
// Working around shortcomings of ``pool_conda_matchspec``
// The channels are not processed.
// TODO Fragile! Installing this matchspec will always trigger a reinstall
return add_channel_specific_matchspec(pool, ms, params).and_then(check_not_zero);
const auto [first, second] = make_abused_namespace_dep_args(pool, ms.str());
return check_not_zero(pool.add_dependency(first, REL_NAMESPACE, second));
}
auto pool_add_pin( //
solv::ObjPool& pool,
const specs::MatchSpec& pin,
const specs::ChannelResolveParams& params
const specs::MatchSpec& pin
) -> expected_t<solv::ObjSolvableView>
{
// In libsolv, locking means that a package keeps the same state: if it is installed,
@ -924,37 +812,36 @@ namespace mamba::solver::libsolv
return repo;
}();
return pool_add_matchspec(pool, pin, params)
.transform(
[&](solv::DependencyId cons)
{
// Add dummy solvable with a constraint on the pin (not installed if not
// present)
auto [cons_solv_id, cons_solv] = installed.add_solvable();
const std::string cons_solv_name = fmt::format(
"pin-{}",
util::generate_random_alphanumeric_string(10)
);
cons_solv.set_name(cons_solv_name);
cons_solv.set_version("1");
return pool_add_matchspec(pool, pin).transform(
[&](solv::DependencyId cons)
{
// Add dummy solvable with a constraint on the pin (not installed if not
// present)
auto [cons_solv_id, cons_solv] = installed.add_solvable();
const std::string cons_solv_name = fmt::format(
"pin-{}",
util::generate_random_alphanumeric_string(10)
);
cons_solv.set_name(cons_solv_name);
cons_solv.set_version("1");
cons_solv.add_constraint(cons);
cons_solv.add_constraint(cons);
// Solvable need to provide itself
cons_solv.add_self_provide();
// Solvable need to provide itself
cons_solv.add_self_provide();
// Even if we lock it, libsolv may still try to remove it with
// `SOLVER_FLAG_ALLOW_UNINSTALL`, so we flag it as not a real package to filter
// it out in the transaction
cons_solv.set_type(solv::SolvableType::Pin);
// Even if we lock it, libsolv may still try to remove it with
// `SOLVER_FLAG_ALLOW_UNINSTALL`, so we flag it as not a real package to filter
// it out in the transaction
cons_solv.set_type(solv::SolvableType::Pin);
// Necessary for attributes to be properly stored
// TODO move this at the end of all job requests
installed.internalize();
// Necessary for attributes to be properly stored
// TODO move this at the end of all job requests
installed.internalize();
return cons_solv;
}
);
return cons_solv;
}
);
}
namespace
@ -1270,15 +1157,34 @@ namespace mamba::solver::libsolv
namespace
{
[[nodiscard]] auto add_reinstall_job(
solv::ObjQueue& jobs,
solv::ObjPool& pool,
const specs::MatchSpec& ms,
const specs::ChannelResolveParams& params
) -> expected_t<void>
[[nodiscard]] auto match_as_closely(solv::ObjSolvableViewConst s) -> specs::MatchSpec
{
static constexpr int install_flag = SOLVER_INSTALL | SOLVER_SOLVABLE_PROVIDES;
auto ms = specs::MatchSpec();
ms.set_name(specs::MatchSpec::NameSpec(std::string(s.name())));
// Ignoring version error, the point is to find a close match
specs::Version::parse(s.version())
.transform(
[&](specs::Version&& ver)
{
ms.set_version(specs::VersionSpec::from_predicate(
specs::VersionPredicate::make_equal_to(std::move(ver))
));
}
);
ms.set_build_string(specs::MatchSpec::BuildStringSpec(std::string(s.build_string())));
ms.set_build_number(
specs::BuildNumberSpec(specs::BuildNumberPredicate::make_equal_to(s.build_number()))
);
ms.set_md5(std::string(s.md5()));
ms.set_sha256(std::string(s.sha256()));
return ms;
}
[[nodiscard]] auto
add_reinstall_job(solv::ObjQueue& jobs, solv::ObjPool& pool, const specs::MatchSpec& ms)
-> expected_t<void>
{
auto solvable = std::optional<solv::ObjSolvableViewConst>{};
// the data about the channel is only in the prefix_data unfortunately
@ -1294,93 +1200,70 @@ namespace mamba::solver::libsolv
}
);
if (!solvable.has_value() || solvable->channel().empty())
if (solvable.has_value())
{
// We are not reinstalling but simply installing.
// Right now, using `--force-reinstall` will send all specs (whether they have
// been previously installed or not) down this path, so we need to handle specs
// that are not installed.
return pool_add_matchspec(pool, ms, params)
.transform([&](auto id) { jobs.push_back(install_flag, id); });
}
if (ms.channel().has_value() || !ms.version().is_explicitly_free()
|| !ms.build_string().is_free())
{
Console::stream() << ms.conda_build_form()
<< ": overriding channel, version and build from "
"installed packages due to --force-reinstall.";
}
auto ms_modified = ms;
auto unresolved_chan = specs::UnresolvedChannel::parse(solvable->channel());
if (unresolved_chan.has_value())
{
ms_modified.set_channel(std::move(unresolved_chan).value());
}
else
{
return make_unexpected(
std::move(unresolved_chan).error().what(),
mamba_error_code::invalid_spec
);
}
auto version_spec = specs::VersionSpec::parse(solvable->version());
if (version_spec.has_value())
{
ms_modified.set_version(std::move(version_spec).value());
}
else
{
return make_unexpected(
std::move(version_spec).error().what(),
mamba_error_code::invalid_spec
// To Reinstall, we add a install job with our custom namespace matcher,
// passing a flag to exclude matching installed packages.
// This has the effect of reinstalling in libsolv.
const auto [first, second] = make_abused_namespace_dep_args(
pool,
match_as_closely(solvable.value()).str(),
{ /* .skip_installed= */ true }
);
const auto job_id = pool.add_dependency(first, REL_NAMESPACE, second);
jobs.push_back(SOLVER_INSTALL, job_id);
return {};
}
ms_modified.set_build_string(specs::GlobSpec(std::string(solvable->build_string())));
// We are not reinstalling but simply installing.
return pool_add_matchspec(pool, ms).transform([&](auto id)
{ jobs.push_back(SOLVER_INSTALL, id); });
}
LOG_INFO << "Reinstall " << ms_modified.conda_build_form() << " from channel "
<< ms_modified.channel()->str();
// TODO Fragile! The only reason why this works is that with a channel specific
// matchspec the job will always be reinstalled.
return pool_add_matchspec(pool, ms_modified, params)
.transform([&](auto id) { jobs.push_back(install_flag, id); });
[[nodiscard]] auto has_installed_package( //
const solv::ObjPool& pool,
const specs::MatchSpec::NameSpec& name_spec
) -> bool
{
bool found = false;
pool.for_each_installed_solvable(
[&](solv::ObjSolvableViewConst s)
{
if (name_spec.contains(s.name()))
{
found = true;
return solv::LoopControl::Break;
}
return solv::LoopControl::Continue;
}
);
return found;
}
template <typename Job>
[[nodiscard]] auto add_job(
const Job& job,
solv::ObjQueue& raw_jobs,
solv::ObjPool& pool,
const specs::ChannelResolveParams& params,
bool force_reinstall
) -> expected_t<void>
[[nodiscard]] auto
add_job(const Job& job, solv::ObjQueue& raw_jobs, solv::ObjPool& pool, bool force_reinstall)
-> expected_t<void>
{
if constexpr (std::is_same_v<Job, Request::Install>)
{
if (force_reinstall)
{
return add_reinstall_job(raw_jobs, pool, job.spec, params);
return add_reinstall_job(raw_jobs, pool, job.spec);
}
else
{
return pool_add_matchspec(pool, job.spec, params)
.transform(
[&](auto id)
{ raw_jobs.push_back(SOLVER_INSTALL | SOLVER_SOLVABLE_PROVIDES, id); }
);
return pool_add_matchspec(pool, job.spec)
.transform([&](auto id) { raw_jobs.push_back(SOLVER_INSTALL, id); });
}
}
if constexpr (std::is_same_v<Job, Request::Remove>)
{
return pool_add_matchspec(pool, job.spec, params)
return pool_add_matchspec(pool, job.spec)
.transform(
[&](auto id)
{
[&](auto id) {
raw_jobs.push_back(
SOLVER_ERASE | SOLVER_SOLVABLE_PROVIDES
| (job.clean_dependencies ? SOLVER_CLEANDEPS : 0),
SOLVER_ERASE | (job.clean_dependencies ? SOLVER_CLEANDEPS : 0),
id
);
}
@ -1388,23 +1271,43 @@ namespace mamba::solver::libsolv
}
if constexpr (std::is_same_v<Job, Request::Update>)
{
return pool_add_matchspec(pool, job.spec, params)
return pool_add_matchspec(pool, job.spec)
.transform(
[&](auto id)
{
// In libsolv update specs apply to installed packages, not available
// ones, as opposed to mamba.
// With ``numpy=0.5`` installed, update ``numpy>=1.0`` means update
// numpy if a ``numpy>=1.0`` is installed, which would be false.
// In Mamba, it means update any installed numpy to a new
// ``numpy>=1.0``, leading to an update.
// This is especially tricky with channel-specific MatchSpec.
auto const clean_deps = job.clean_dependencies ? SOLVER_CLEANDEPS : 0;
// TODO: ignoring update specs here for now
if (!job.spec.is_simple())
// In this case, libsolv and mamba meanings are the same.
if (job.spec.is_only_package_name())
{
raw_jobs.push_back(
SOLVER_INSTALL | SOLVER_SOLVABLE_PROVIDES | clean_deps,
id
);
raw_jobs.push_back(SOLVER_UPDATE | clean_deps, id);
}
raw_jobs.push_back(
SOLVER_UPDATE | SOLVER_SOLVABLE_PROVIDES | clean_deps,
id
);
// Otherwise, we try our ad-hoc solution
else if (has_installed_package(pool, job.spec.name()))
{
// We still need to issue an update command to libsolv, otherwise
// the package won't be changed, but we apply it only to the
// package name, not the full spec.
if (job.spec.name().is_exact())
{
auto name_id = pool.add_string(job.spec.name().str());
raw_jobs.push_back(SOLVER_UPDATE | clean_deps, name_id);
}
// And we add an install statement to be sure the full spec is
// respected.
// Unfortunately this breaks ``clean_deps``.
raw_jobs.push_back(SOLVER_INSTALL, id);
}
// Finally there is no such package installed so we simply don't do
// anything.
}
);
}
@ -1419,20 +1322,17 @@ namespace mamba::solver::libsolv
}
if constexpr (std::is_same_v<Job, Request::Freeze>)
{
return pool_add_matchspec(pool, job.spec, params)
return pool_add_matchspec(pool, job.spec)
.transform([&](auto id) { raw_jobs.push_back(SOLVER_LOCK, id); });
}
if constexpr (std::is_same_v<Job, Request::Keep>)
{
raw_jobs.push_back(
SOLVER_USERINSTALLED,
pool_add_matchspec(pool, job.spec, params).value()
);
raw_jobs.push_back(SOLVER_USERINSTALLED, pool_add_matchspec(pool, job.spec).value());
return {};
}
if constexpr (std::is_same_v<Job, Request::Pin>)
{
return pool_add_pin(pool, job.spec, params)
return pool_add_pin(pool, job.spec)
.transform(
[&](solv::ObjSolvableView pin_solv)
{
@ -1451,10 +1351,9 @@ namespace mamba::solver::libsolv
}
}
auto request_to_decision_queue(
auto request_to_decision_queue( //
const Request& request,
solv::ObjPool& pool,
const specs::ChannelResolveParams& chan_params,
bool force_reinstall
) -> expected_t<solv::ObjQueue>
{
@ -1468,7 +1367,7 @@ namespace mamba::solver::libsolv
{
if constexpr (std::is_same_v<std::decay_t<decltype(job)>, Request::Pin>)
{
return add_job(job, solv_jobs, pool, chan_params, force_reinstall);
return add_job(job, solv_jobs, pool, force_reinstall);
}
return {};
},
@ -1479,9 +1378,8 @@ namespace mamba::solver::libsolv
return forward_error(std::move(xpt));
}
}
// Fragile: Pins add solvables to Pol and hence require a call to create_whatprovides.
// Channel specific MatchSpec write to whatprovides and hence require it is not modified
// afterwards.
// Pins add solvables to Pol and hence require a call to create_whatprovides.
// For some reason we need to add them first.
pool.create_whatprovides();
for (const auto& unkown_job : request.jobs)
{
@ -1490,7 +1388,7 @@ namespace mamba::solver::libsolv
{
if constexpr (!std::is_same_v<std::decay_t<decltype(job)>, Request::Pin>)
{
return add_job(job, solv_jobs, pool, chan_params, force_reinstall);
return add_job(job, solv_jobs, pool, force_reinstall);
}
return {};
},

View File

@ -4,8 +4,8 @@
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef MAMBA_SOLVER_LIBSOLV_HERLPERS
#define MAMBA_SOLVER_LIBSOLV_HERLPERS
#ifndef MAMBA_SOLVER_LIBSOLV_HELPERS
#define MAMBA_SOLVER_LIBSOLV_HELPERS
#include <optional>
#include <string>
@ -23,6 +23,8 @@
#include "solv-cpp/solvable.hpp"
#include "solv-cpp/transaction.hpp"
#include "solver/libsolv/matcher.hpp"
/**
* Solver, repo, and solvable helpers dependent on specifi libsolv logic and objects.
*/
@ -75,16 +77,38 @@ namespace mamba::solver::libsolv
void add_pip_as_python_dependency(solv::ObjPool& pool, solv::ObjRepoView repo);
/**
* Make parameters to use as a namespace dependency.
*
* We use these proxy function since we are abusing the two string parameters of namespace
* callback to pass our own information.
*/
[[nodiscard]] auto make_abused_namespace_dep_args(
solv::ObjPool& pool,
std::string_view dependency,
const MatchFlags& flags = {}
) -> std::pair<solv::StringId, solv::StringId>;
/**
* Retrieved parameters used in a namespace callback.
*
* We use these proxy function since we are abusing the two string parameters of namespace
* callback to pass our own information.
*/
[[nodiscard]] auto get_abused_namespace_callback_args( //
solv::ObjPoolView& pool,
solv::StringId first,
solv::StringId second
) -> std::pair<std::string_view, MatchFlags>;
[[nodiscard]] auto pool_add_matchspec( //
solv::ObjPool& pool,
const specs::MatchSpec& ms,
const specs::ChannelResolveParams& params
const specs::MatchSpec& ms
) -> expected_t<solv::DependencyId>;
[[nodiscard]] auto pool_add_pin( //
solv::ObjPool& pool,
const specs::MatchSpec& pin_ms,
const specs::ChannelResolveParams& params
const specs::MatchSpec& pin_ms
) -> expected_t<solv::ObjSolvableView>;
[[nodiscard]] auto transaction_to_solution_all( //
@ -125,11 +149,8 @@ namespace mamba::solver::libsolv
std::string_view noarch_type
) -> Solution;
[[nodiscard]] auto request_to_decision_queue(
const Request& request,
solv::ObjPool& pool,
const specs::ChannelResolveParams& chan_params,
bool force_reinstall
) -> expected_t<solv::ObjQueue>;
[[nodiscard]] auto
request_to_decision_queue(const Request& request, solv::ObjPool& pool, bool force_reinstall)
-> expected_t<solv::ObjQueue>;
}
#endif

View File

@ -0,0 +1,277 @@
// 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 <fmt/format.h>
#include "solver/libsolv/matcher.hpp"
namespace mamba::solver::libsolv
{
/**********************************
* Implementation of MatchFlags *
**********************************/
auto MatchFlags::internal_deserialize(std::string_view in) -> MatchFlags
{
auto out = MatchFlags{};
if (in.size() >= 1)
{
out.skip_installed = in[0] == '1';
}
return out;
}
void MatchFlags::internal_serialize_to(std::string& out) const
{
// We simply write a bitset for flags
out.push_back(skip_installed ? '1' : '0');
}
[[nodiscard]] auto MatchFlags::internal_serialize() const -> std::string
{
auto out = std::string();
internal_serialize_to(out);
return out;
}
/*******************************
* Implementation of Matcher *
*******************************/
Matcher::Matcher(specs::ChannelResolveParams channel_params)
: m_channel_params(std::move(channel_params))
{
}
auto Matcher::channel_params() const -> const specs::ChannelResolveParams&
{
return m_channel_params;
}
auto Matcher::get_matching_packages( //
solv::ObjPoolView pool,
const specs::MatchSpec& ms,
const MatchFlags& flags
) -> solv::OffsetId
{
m_packages_buffer.clear(); // Reuse the buffer
auto add_pkg_if_matching = [&](solv::ObjSolvableViewConst s)
{
if (flags.skip_installed && s.installed())
{
return;
}
if (pkg_match_except_channel(pool, s, ms) && pkg_match_channels(s, ms))
{
m_packages_buffer.push_back(s.id());
}
};
if (ms.name().is_exact())
{
// Name does not have glob so we can use it as index into packages with exact name.
auto name_id = pool.add_string(ms.name().str());
pool.for_each_whatprovides(name_id, add_pkg_if_matching);
}
else
{
// Name is a Glob (e.g. ``py*``) so we have to loop through all packages.
pool.for_each_solvable(add_pkg_if_matching);
}
if (m_packages_buffer.empty())
{
return 0; // Means not found
}
return pool.add_to_whatprovides_data(m_packages_buffer);
}
auto Matcher::get_matching_packages( //
solv::ObjPoolView pool,
std::string_view dep,
const MatchFlags& flags
) -> solv::OffsetId
{
return specs::MatchSpec::parse(dep)
.transform([&](const specs::MatchSpec& ms)
{ return get_matching_packages(pool, ms, flags); })
.or_else(
[&](const auto& error) -> specs::expected_parse_t<solv::OffsetId>
{
pool.set_current_error(error.what());
return pool.add_to_whatprovides_data({});
}
)
.value();
}
namespace
{
template <typename Map>
[[nodiscard]] auto make_cached_version(Map& cache, std::string version)
-> specs::expected_parse_t<std::reference_wrapper<const specs::Version>>
{
if (auto it = cache.find(version); it != cache.cend())
{
return { std::cref(it->second) };
}
if (version.empty())
{
auto [it, inserted] = cache.emplace(std::move(version), specs::Version());
assert(inserted);
return { std::cref(it->second) };
}
return specs::Version::parse(version).transform(
[&](specs::Version&& ver) -> std::reference_wrapper<const specs::Version>
{
auto [it, inserted] = cache.emplace(std::move(version), std::move(ver));
assert(inserted);
return { std::cref(it->second) };
}
);
}
}
auto Matcher::get_pkg_attributes(solv::ObjPoolView pool, solv::ObjSolvableViewConst solv)
-> expected_t<Pkg>
{
auto track_features = specs::MatchSpec::string_set();
for (solv::StringId id : solv.track_features())
{
track_features.insert(std::string(pool.get_string(id)));
}
return make_cached_version(m_version_cache, std::string(solv.version()))
.transform(
[&](auto ver_ref)
{
return Pkg{
/* .name= */ solv.name(),
/* .version= */ ver_ref,
/* .build_string= */ solv.build_string(),
/* .build_number= */ solv.build_number(),
/* .md5= */ solv.md5(),
/* .sha256= */ solv.sha256(),
/* .license= */ solv.license(),
/* .platform= */ std::string(solv.platform()),
/* .track_features= */ std::move(track_features),
};
}
)
.transform_error( //
[](specs::ParseError&& err)
{ return mamba_error(err.what(), mamba_error_code::invalid_spec); }
);
}
auto Matcher::pkg_match_except_channel( //
solv::ObjPoolView pool,
solv::ObjSolvableViewConst solv,
const specs::MatchSpec& ms
) -> bool
{
return get_pkg_attributes(pool, solv)
.transform([&](const Pkg& pkg) -> bool { return ms.contains_except_channel(pkg); })
.or_else([](const auto&) -> expected_t<bool> { return false; })
.value();
}
auto Matcher::get_channels(const specs::UnresolvedChannel& uc)
-> expected_t<channel_list_const_ref>
{
// Channel maps require converting channel to string because unresolved channels are
// akward to compare.
auto str = uc.str();
if (const auto it = m_channel_cache.find(str); it != m_channel_cache.end())
{
return { std::cref(it->second) };
}
return specs::Channel::resolve(std::move(uc), channel_params())
.transform(
[&](channel_list&& chan)
{
auto [it, inserted] = m_channel_cache.emplace(std::move(str), std::move(chan));
assert(inserted);
return std::cref(it->second);
}
)
.transform_error( //
[](specs::ParseError&& err)
{ return mamba_error(err.what(), mamba_error_code::invalid_spec); }
);
}
auto Matcher::get_channels(std::string_view chan) -> expected_t<channel_list_const_ref>
{
if (const auto it = m_channel_cache.find(std::string(chan)); it != m_channel_cache.end())
{
return { std::cref(it->second) };
}
return specs::UnresolvedChannel::parse(chan)
.transform_error( //
[](specs::ParseError&& err)
{ return mamba_error(err.what(), mamba_error_code::invalid_spec); }
)
.and_then([&](specs::UnresolvedChannel&& uc) { return get_channels(uc); });
}
auto Matcher::pkg_match_channels( //
solv::ObjSolvableViewConst solv,
const channel_list& channels
) -> bool
{
// First check the package url
if (auto pkg_url = specs::CondaURL::parse(solv.url()))
{
for (const auto& chan : channels)
{
if (chan.contains_package(pkg_url.value()) == specs::Channel::Match::Full)
{
return true;
}
}
}
// Fallback to package channel attribute
else if (auto pkg_channels = get_channels(solv.channel()))
{
for (const auto& ms_chan : channels)
{
// There should really be only one here.
for (const auto& pkg_chan : pkg_channels.value().get())
{
if (ms_chan.contains_equivalent(pkg_chan))
{
return true;
}
}
}
}
return false;
}
auto Matcher::pkg_match_channels( //
solv::ObjSolvableViewConst solv,
const specs::MatchSpec& ms
) -> bool
{
if (auto uc = ms.channel())
{
if (auto channels = get_channels(uc.value()))
{
return pkg_match_channels(solv, channels.value());
}
return false;
}
return true;
}
}

View File

@ -0,0 +1,110 @@
// Copyright (c) 2024, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef MAMBA_SOLVER_LIBSOLV_MATCHER
#define MAMBA_SOLVER_LIBSOLV_MATCHER
#include <functional>
#include <string>
#include <string_view>
#include <unordered_map>
#include "mamba/core/error_handling.hpp"
#include "mamba/specs/channel.hpp"
#include "mamba/specs/match_spec.hpp"
#include "mamba/specs/version.hpp"
#include "solv-cpp/pool.hpp"
#include "solv-cpp/solvable.hpp"
namespace mamba::solver::libsolv
{
struct MatchFlags
{
bool skip_installed = false;
/**
* Deserialization for internal use mamba use, should not be loaded from disk.
*/
static auto internal_deserialize(std::string_view) -> MatchFlags;
/**
* Serialization for internal use mamba use, should not be saved to disk.
*/
void internal_serialize_to(std::string& out) const;
[[nodiscard]] auto internal_serialize() const -> std::string;
};
class Matcher
{
public:
explicit Matcher(specs::ChannelResolveParams channel_params);
[[nodiscard]] auto channel_params() const -> const specs::ChannelResolveParams&;
auto get_matching_packages( //
solv::ObjPoolView pool,
const specs::MatchSpec& ms,
const MatchFlags& flags = {}
) -> solv::OffsetId;
auto get_matching_packages( //
solv::ObjPoolView pool,
std::string_view dep,
const MatchFlags& flags = {}
) -> solv::OffsetId;
private:
using channel_list = specs::ChannelResolveParams::channel_list;
using channel_list_const_ref = std::reference_wrapper<const channel_list>;
struct Pkg
{
std::string_view name;
std::reference_wrapper<const specs::Version> version;
std::string_view build_string;
std::size_t build_number;
std::string_view md5;
std::string_view sha256;
std::string_view license;
std::string platform;
specs::MatchSpec::string_set track_features;
};
auto get_pkg_attributes( //
solv::ObjPoolView pool,
solv::ObjSolvableViewConst solv
) -> expected_t<Pkg>;
auto pkg_match_except_channel( //
solv::ObjPoolView pool,
solv::ObjSolvableViewConst solv,
const specs::MatchSpec& ms
) -> bool;
auto get_channels(const specs::UnresolvedChannel& uc) -> expected_t<channel_list_const_ref>;
auto get_channels(std::string_view chan) -> expected_t<channel_list_const_ref>;
auto pkg_match_channels( //
solv::ObjSolvableViewConst solv,
const channel_list& channels
) -> bool;
auto pkg_match_channels( //
solv::ObjSolvableViewConst solv,
const specs::MatchSpec& ms
) -> bool;
specs::ChannelResolveParams m_channel_params;
solv::ObjQueue m_packages_buffer = {};
// No need for matchspec cache since they have the same string id they should be handled
// by libsolv.
std::unordered_map<std::string, specs::Version> m_version_cache = {};
std::unordered_map<std::string, channel_list> m_channel_cache = {};
};
}
#endif

View File

@ -53,10 +53,9 @@ namespace mamba::solver::libsolv
auto Solver::solve_impl(Database& mpool, const Request& request) -> expected_t<Outcome>
{
auto& pool = Database::Impl::get(mpool);
const auto& chan_params = mpool.channel_params();
const auto& flags = request.flags;
return solver::libsolv::request_to_decision_queue(request, pool, chan_params, flags.force_reinstall)
return solver::libsolv::request_to_decision_queue(request, pool, flags.force_reinstall)
.transform(
[&](auto&& jobs) -> Outcome
{

View File

@ -375,6 +375,7 @@ namespace mamba::solver::libsolv
}
case SOLVER_RULE_JOB_NOTHING_PROVIDES_DEP:
case SOLVER_RULE_JOB_UNKNOWN_PACKAGE:
case SOLVER_RULE_JOB_UNSUPPORTED:
{
// A top level dependency does not exist.
// Could be a wrong name or missing channel.

View File

@ -941,8 +941,28 @@ namespace mamba::specs
auto MatchSpec::is_simple() const -> bool
{
return m_version.is_explicitly_free() && m_build_string.is_free()
&& m_build_number.is_explicitly_free();
// Based on what libsolv and conda_build_form can handle.
// Glob in names and build_string are fine
return (version().expression_size() <= 3) // includes op so e.g. ``>3,<4``
&& build_number().is_explicitly_free() //
&& !channel().has_value() //
&& filename().empty() //
&& !platforms().has_value() //
&& name_space().empty() //
&& md5().empty() //
&& sha256().empty() //
&& license().empty() //
&& license_family().empty() //
&& features().empty() //
&& !track_features().has_value();
}
[[nodiscard]] auto MatchSpec::is_only_package_name() const -> bool
{
return name().is_exact() //
&& version().is_explicitly_free() //
&& build_string().is_free() //
&& is_simple();
}
auto MatchSpec::contains_except_channel(const PackageInfo& pkg) const -> bool

View File

@ -284,6 +284,13 @@ namespace mamba::specs
* VersionSpec Implementation *
********************************/
auto VersionSpec::from_predicate(VersionPredicate pred) -> VersionSpec
{
auto inner_tree = tree_type::tree_type();
inner_tree.add_leaf(std::move(pred));
return VersionSpec{ tree_type(std::move(inner_tree)) };
}
VersionSpec::VersionSpec(tree_type&& tree) noexcept
: m_tree(std::move(tree))
{

View File

@ -103,6 +103,31 @@ TEST_SUITE("solver::libsolv::solver")
CHECK(std::holds_alternative<Solution::Install>(python_actions.front()));
}
SUBCASE("Force reinstall not installed numpy")
{
auto flags = Request::Flags();
flags.force_reinstall = true;
const auto request = Request{
/* .flags= */ std::move(flags),
/* .jobs= */ { Request::Install{ "numpy"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
const auto& solution = std::get<Solution>(outcome.value());
REQUIRE_FALSE(solution.actions.empty());
// Numpy is last because of topological sort
CHECK(std::holds_alternative<Solution::Install>(solution.actions.back()));
CHECK_EQ(std::get<Solution::Install>(solution.actions.back()).install.name, "numpy");
REQUIRE_EQ(find_actions_with_name(solution, "numpy").size(), 1);
const auto python_actions = find_actions_with_name(solution, "python");
REQUIRE_EQ(python_actions.size(), 1);
CHECK(std::holds_alternative<Solution::Install>(python_actions.front()));
}
SUBCASE("Install numpy without dependencies")
{
const auto request = Request{
@ -281,6 +306,45 @@ TEST_SUITE("solver::libsolv::solver")
}
}
TEST_CASE("Reinstall packages")
{
auto db = libsolv::Database({});
// A conda-forge/linux-64 subsample with one version of numpy and pip and their dependencies
const auto repo_installed = db.add_repo_from_repodata_json(
mambatests::test_data_dir / "repodata/conda-forge-numpy-linux-64.json",
"installed",
"installed"
);
REQUIRE(repo_installed.has_value());
db.set_installed_repo(repo_installed.value());
const auto repo = db.add_repo_from_repodata_json(
mambatests::test_data_dir / "repodata/conda-forge-numpy-linux-64.json",
"https://conda.anaconda.org/conda-forge/linux-64",
"conda-forge"
);
REQUIRE(repo.has_value());
SUBCASE("Force reinstall numpy resinstalls it")
{
auto flags = Request::Flags();
flags.force_reinstall = true;
const auto request = Request{
/* .flags= */ std::move(flags),
/* .jobs= */ { Request::Install{ "numpy"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
const auto& solution = std::get<Solution>(outcome.value());
REQUIRE_EQ(solution.actions.size(), 1);
CHECK(std::holds_alternative<Solution::Reinstall>(solution.actions.front()));
CHECK_EQ(std::get<Solution::Reinstall>(solution.actions.front()).what.name, "numpy");
}
}
TEST_CASE("Solve a existing environment with one repository")
{
auto db = libsolv::Database({});
@ -738,7 +802,7 @@ TEST_SUITE("solver::libsolv::solver")
pkg2.package_url = "https://conda.anaconda.org/mamba-forge/linux-64/foo-1.0.0-phony.conda";
db.add_repo_from_packages(std::array{ pkg2 });
SUBCASE("conda-forge")
SUBCASE("conda-forge::foo")
{
auto request = Request{
/* .flags= */ {},
@ -756,7 +820,7 @@ TEST_SUITE("solver::libsolv::solver")
CHECK_EQ(std::get<Solution::Install>(actions.front()).install.build_string, "conda");
}
SUBCASE("mamba-forge")
SUBCASE("mamba-forge::foo")
{
auto request = Request{
/* .flags= */ {},
@ -774,18 +838,20 @@ TEST_SUITE("solver::libsolv::solver")
CHECK_EQ(std::get<Solution::Install>(actions.front()).install.build_string, "mamba");
}
SUBCASE("pixi-forge")
SUBCASE("pixi-forge::foo")
{
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "pixi-forge::foo"_ms } },
};
// TODO should really be an unsolvable state
CHECK_THROWS(libsolv::Solver().solve(db, request));
const auto outcome = libsolv::Solver().solve(db, request);
REQUIRE(outcome.has_value());
CHECK(std::holds_alternative<libsolv::UnSolvable>(outcome.value()));
}
SUBCASE("https://conda.anaconda.org/mamba-forge/")
SUBCASE("https://conda.anaconda.org/mamba-forge::foo")
{
auto request = Request{
/* .flags= */ {},
@ -814,36 +880,30 @@ TEST_SUITE("solver::libsolv::solver")
);
REQUIRE(repo_linux.has_value());
const auto repo_win = db.add_repo_from_repodata_json(
// FIXME the subdir is not overriden here so it is still linux-64 because that's what
// is in the json file.
// We'de want to pass option to the database to override channel and subsir.
const auto repo_noarch = db.add_repo_from_repodata_json(
mambatests::test_data_dir / "repodata/conda-forge-numpy-linux-64.json",
"https://conda.anaconda.org/conda-forge/noarch",
"conda-forge",
libsolv::PipAsPythonDependency::No
);
REQUIRE(repo_win.has_value());
REQUIRE(repo_noarch.has_value());
SUBCASE("conda-forge/noarch")
SUBCASE("conda-forge/win-64::numpy")
{
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "conda-forge/noarch::numpy"_ms } },
/* .jobs= */ { Request::Install{ "conda-forge/win-64::numpy"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
const auto& solution = std::get<Solution>(outcome.value());
const auto actions = find_actions_with_name(solution, "numpy");
REQUIRE_EQ(actions.size(), 1);
CHECK(std::holds_alternative<Solution::Install>(actions.front()));
CHECK(util::contains(
std::get<Solution::Install>(actions.front()).install.package_url,
"noarch"
));
REQUIRE(std::holds_alternative<libsolv::UnSolvable>(outcome.value()));
}
SUBCASE("conda-forge[subdir=linux-64]")
SUBCASE("conda-forge::numpy[subdir=linux-64]")
{
auto request = Request{
/* .flags= */ {},
@ -865,4 +925,109 @@ TEST_SUITE("solver::libsolv::solver")
}
}
}
TEST_CASE("Handle complex matchspecs")
{
using PackageInfo = specs::PackageInfo;
auto db = libsolv::Database({});
SUBCASE("*[md5=0bab699354cbd66959550eb9b9866620]")
{
auto pkg1 = PackageInfo("foo");
pkg1.md5 = "0bab699354cbd66959550eb9b9866620";
auto pkg2 = PackageInfo("foo");
pkg2.md5 = "bad";
db.add_repo_from_packages(std::array{ pkg1, pkg2 });
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "*[md5=0bab699354cbd66959550eb9b9866620]"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
const auto& solution = std::get<Solution>(outcome.value());
REQUIRE_EQ(solution.actions.size(), 1);
CHECK(std::holds_alternative<Solution::Install>(solution.actions.front()));
CHECK_EQ(
std::get<Solution::Install>(solution.actions.front()).install.md5,
"0bab699354cbd66959550eb9b9866620"
);
}
SUBCASE("foo[md5=notreallymd5]")
{
auto pkg1 = PackageInfo("foo");
pkg1.md5 = "0bab699354cbd66959550eb9b9866620";
db.add_repo_from_packages(std::array{ pkg1 });
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "foo[md5=notreallymd5]"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<libsolv::UnSolvable>(outcome.value()));
}
SUBCASE("foo[build_string=bld]")
{
auto pkg1 = PackageInfo("foo");
pkg1.build_string = "bad";
auto pkg2 = PackageInfo("foo");
pkg2.build_string = "bld";
db.add_repo_from_packages(std::array{ pkg1, pkg2 });
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "foo[build=bld]"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
const auto& solution = std::get<Solution>(outcome.value());
REQUIRE_EQ(solution.actions.size(), 1);
CHECK(std::holds_alternative<Solution::Install>(solution.actions.front()));
CHECK_EQ(std::get<Solution::Install>(solution.actions.front()).install.build_string, "bld");
}
SUBCASE("foo[build_string=bld, build_number='>2']")
{
auto pkg1 = PackageInfo("foo");
pkg1.build_string = "bad";
pkg1.build_number = 3;
auto pkg2 = PackageInfo("foo");
pkg2.build_string = "bld";
pkg2.build_number = 2;
auto pkg3 = PackageInfo("foo");
pkg3.build_string = "bld";
pkg3.build_number = 4;
db.add_repo_from_packages(std::array{ pkg1, pkg2, pkg3 });
auto request = Request{
/* .flags= */ {},
/* .jobs= */ { Request::Install{ "foo[build=bld]"_ms } },
};
const auto outcome = libsolv::Solver().solve(db, request);
REQUIRE(outcome.has_value());
REQUIRE(std::holds_alternative<Solution>(outcome.value()));
const auto& solution = std::get<Solution>(outcome.value());
REQUIRE_EQ(solution.actions.size(), 1);
CHECK(std::holds_alternative<Solution::Install>(solution.actions.front()));
CHECK_EQ(std::get<Solution::Install>(solution.actions.front()).install.build_string, "bld");
CHECK_EQ(std::get<Solution::Install>(solution.actions.front()).install.build_number, 4);
}
}
}

View File

@ -467,34 +467,39 @@ TEST_SUITE("specs::match_spec")
TEST_CASE("is_simple")
{
SUBCASE("libblas")
SUBCASE("Positive")
{
auto ms = MatchSpec::parse("libblas").value();
CHECK(ms.is_simple());
for (std::string_view str : {
"libblas",
"libblas=12.9=abcdef",
"libblas=0.15*",
"libblas[version=12.2]",
"xtensor =0.15*",
})
{
CAPTURE(str);
const auto ms = MatchSpec::parse(str).value();
CHECK(ms.is_simple());
}
}
SUBCASE("libblas=12.9=abcdef")
SUBCASE("Negative")
{
auto ms = MatchSpec::parse("libblas=12.9=abcdef").value();
CHECK_FALSE(ms.is_simple());
}
SUBCASE("libblas=0.15*")
{
auto ms = MatchSpec::parse("libblas=0.15*").value();
CHECK_FALSE(ms.is_simple());
}
SUBCASE("libblas[version=12.2]")
{
auto ms = MatchSpec::parse("libblas[version=12.2]").value();
CHECK_FALSE(ms.is_simple());
}
SUBCASE("xtensor =0.15*")
{
auto ms = MatchSpec::parse("xtensor =0.15*").value();
CHECK_FALSE(ms.is_simple());
for (std::string_view str : {
"pkg[build_number=3]",
"pkg[md5=85094328554u9543215123]",
"pkg[sha256=0320104934325453]",
"pkg[license=MIT]",
"pkg[track_features=mkl]",
"pkg[version='(>2,<3)|=4']",
"conda-forge::pkg",
"pypi:pkg",
})
{
CAPTURE(str);
const auto ms = MatchSpec::parse(str).value();
CHECK_FALSE(ms.is_simple());
}
}
}

View File

@ -128,6 +128,16 @@ TEST_SUITE("specs::version_spec")
CHECK_EQ(spec.str(), "=*");
}
SUBCASE("from_predicate")
{
const auto v1 = "1.0"_v;
const auto v2 = "2.0"_v;
auto spec = VersionSpec::from_predicate(VersionPredicate::make_equal_to(v1));
CHECK(spec.contains(v1));
CHECK_FALSE(spec.contains(v2));
CHECK_EQ(spec.str(), "==1.0");
}
SUBCASE("<2.0|(>2.3,<=2.8.0)")
{
using namespace mamba::util;

View File

@ -587,7 +587,12 @@ namespace mambapy
.def_readonly_static("glob_suffix_str", &VersionSpec::glob_suffix_str)
.def_readonly_static("glob_suffix_token", &VersionSpec::glob_suffix_token)
.def_static("parse", &VersionSpec::parse, py::arg("str"))
.def_static("from_predicate", &VersionSpec::from_predicate, py::arg("pred"))
.def(py::init<>())
.def("contains", &VersionSpec::contains, py::arg("point"))
.def("is_explicitly_free", &VersionSpec::is_explicitly_free)
.def("expression_size", &VersionSpec::expression_size)
.def("str_conda_build", &VersionSpec::str_conda_build)
.def("__str__", &VersionSpec::str)
.def("__copy__", &copy<VersionSpec>)
.def("__deepcopy__", &deepcopy<VersionSpec>, py::arg("memo"));
@ -768,7 +773,7 @@ namespace mambapy
std::string_view sha256,
std::string_view license,
std::string& platform,
MatchSpec::string_set track_features)
const MatchSpec::string_set& track_features)
{
struct Pkg
{
@ -780,7 +785,7 @@ namespace mambapy
std::string_view sha256;
std::string_view license;
std::reference_wrapper<const std::string> platform;
const MatchSpec::string_set track_features;
std::reference_wrapper<const MatchSpec::string_set> track_features;
};
return ms.contains_except_channel(Pkg{
@ -792,7 +797,7 @@ namespace mambapy
/* .sha256= */ sha256,
/* .license= */ license,
/* .platform= */ platform,
/* .track_features= */ std::move(track_features),
/* .track_features= */ track_features,
});
},
py::arg("name") = "",
@ -807,6 +812,7 @@ namespace mambapy
)
.def("is_file", &MatchSpec::is_file)
.def("is_simple", &MatchSpec::is_simple)
.def("is_only_package_name", &MatchSpec::is_only_package_name)
.def("conda_build_form", &MatchSpec::conda_build_form)
.def("__str__", &MatchSpec::str)
.def("__copy__", &copy<MatchSpec>)

View File

@ -257,9 +257,9 @@ def test_Solver_UnSolvable():
outcome = solver.solve(db, request)
assert isinstance(outcome, libsolv.UnSolvable)
assert "nothing provides" in "\n".join(outcome.problems(db))
assert "nothing provides" in outcome.problems_to_str(db)
assert "nothing provides" in outcome.all_problems_to_str(db)
assert len(outcome.problems(db)) > 0
assert isinstance(outcome.problems_to_str(db), str)
assert isinstance(outcome.all_problems_to_str(db), str)
assert "The following package could not be installed" in outcome.explain_problems(
db, libmambapy.Palette.no_color()
)

View File

@ -696,7 +696,15 @@ def test_VersionSpec():
assert isinstance(VersionSpec.glob_suffix_str, str)
assert isinstance(VersionSpec.glob_suffix_token, str)
# Constructor
vs = VersionSpec()
assert vs.is_explicitly_free()
assert vs.expression_size() == 0
# Parse
vs = VersionSpec.parse(">2.0,<3.0")
assert not vs.is_explicitly_free()
assert vs.expression_size() == 3 # including operator
# Errors
with pytest.raises(libmambapy.specs.ParseError):
@ -857,6 +865,7 @@ def test_MatchSpec():
assert ms.optional
assert not ms.is_file()
assert not ms.is_simple()
assert not ms.is_only_package_name()
# str
assert str(ms) == (

View File

@ -691,10 +691,11 @@ def test_spec_with_channel_and_subdir():
try:
helpers.create("-n", env_name, "conda-forge/noarch::xtensor", "--dry-run")
except subprocess.CalledProcessError as e:
assert (
'critical libmamba The package "conda-forge[noarch]::xtensor" is '
"not available for the specified platform (noarch) but is available on"
) in e.stderr.decode()
# The error message we are getting today is not the most informative but
# was needed to unify the solver interface.
msg = e.stderr.decode()
assert "The following package could not be installed" in msg
assert "xtensor" in msg
@pytest.mark.parametrize("shared_pkgs_dirs", [True], indirect=True)
@ -708,10 +709,11 @@ def test_spec_with_slash_in_channel(tmp_home, tmp_root_prefix):
with pytest.raises(subprocess.CalledProcessError) as info:
helpers.create("-n", "env1", "pkgs/main/noarch::python", "--dry-run")
assert info.value.stderr.decode() == (
'critical libmamba The package "pkgs/main[noarch]::python" is '
"not found in any loaded channels. Try adding more channels or subdirs.\n"
)
# The error message we are getting today is not the most informative but
# was needed to unify the solver interface.
msg = info.value.stderr.decode()
assert "The following package could not be installed" in msg
assert "python" in msg
os.environ["CONDA_SUBDIR"] = "linux-64"
helpers.create("-n", "env2", "pkgs/main/linux-64::python", "--dry-run")