Solving Package Environments#
The libmambapy.solver
submodule contains a generic API for solving
requirements (MatchSpec
) into a list of packages (PackageInfo
) with no conflicting dependencies.
Note
Solving Package Environments can be cast as a Boolean satisfiability problem (SAT).
Mamba currently only uses one SAT solver:
LibSolv. For this reason, the generic
interface has not been fully completed and users need to access the submodule
libmambapy.solver.libsolv
for certain types.
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
RepoInfo
.
This serves to resolve explicit channel requirements or channel priority.
As such, the database constructor takes a set of
ChannelResolveParams
to work with Channel
data
internally (see the usage section on Channels for more
information).
The first way to add a repository is from a list of PackageInfo
using
DataBase.add_repo_from_packages
:
import libmambapy
channel_alias = libmambapy.specs.CondaURL.parse("https://conda.anaconda.org")
db = libmambapy.solver.libsolv.Database(
libmambapy.specs.ChannelResolveParams(channel_alias=channel_alias)
)
repo1 = db.add_repo_from_packages(
packages=[
libmambapy.specs.PackageInfo(name="python", version="3.8", ...),
libmambapy.specs.PackageInfo(name="pip", version="3.9", ...),
...,
],
name="myrepo",
)
The second way of loading packages is through Conda’s repository index format repodata.json
using
DataBase.add_repo_from_repodata_json
.
This is meant for convenience, and is not a performant alternative to the former method, since these files
grow large.
repo2 = db.add_repo_from_repodata_json(
path="path/to/repodata.json",
url="htts://conda.anaconda.org/conda-forge/linux-64",
channel_id="conda-forge",
)
One of the repositories can be set to have a special meaning of “installed repository”.
It is used as a reference point in the solver to compute changes.
For instance, if a package is required but is already available in the installed repo, the solving
result will not mention it.
The function
DataBase.set_installed_repo
is
used for that purpose.
db.set_installed_repo(repo1)
Binary serialization of the database (Advanced)#
The Database
reporitories can be serialized in binary format for faster reloading.
To ensure integrity and freshness of the serialized file, metadata about the packages,
such as source url and
RepodataOrigin
, are stored inside the
file when calling
DataBase.native_serialize_repo
.
Upon reading, similar parameters are expected as inputs to
DataBase.add_repo_from_native_serialization
.
If they mismatch, the loading results in an error.
A typical wokflow first tries to load a repository from such binary cache, and then quietly
fallbacks to repodata.json
on failure.
Creating a solving request#
All jobs that need to be resolved are added as part of a Request
.
This includes installing, updating, removing packages, as well as solving cutomization parameters.
Request = libmambapy.solver.Request
MatchSpec = libmambapy.specs.MatchSpec
request = Request(
jobs=[
Request.Install(MatchSpec.parse("python>=3.9")),
Request.Update(MatchSpec.parse("numpy")),
Request.Remove(MatchSpec.parse("pandas"), clean_dependencies=False),
],
flags=Request.Flags(
allow_downgrade=True,
allow_uninstall=True,
),
)
Solving the request#
The Request
and the Database
are the two input parameters needed to solve an environment.
This task is achieved with the Solver.solve
method.
solver = libmambapy.solver.libsolv.Solver()
outcome = solver.solve(db, request)
The outcome can be of two types, either a Solution
listing packages (PackageInfo
) and the
action to take on them (install, remove…), or an UnSolvable
type when no solution exists
(because of conflict, missing packages…).
Examine the solution#
We can test if a valid solution exists by checking the type of the outcome.
The attribute Solution.actions
contains the actions
to take on the installed repository so that it satisfies the Request
requirements.
Solution = libmambapy.solver.Solution
if isinstance(outcome, Solution):
for action in outcome.actions:
if isinstance(action, Solution.Upgrade):
my_upgrade(from_pkg=action.remove, to_pkg=action.install)
if isinstance(action, Solution.Reinstall):
...
...
Alternatively, an easy way to compute the update to the environment is to check for install
and
remove
members, since they will populate the relevant fields for all actions:
Solution = libmambapy.solver.Solution
if isinstance(outcome, Solution):
for action in outcome.actions:
if hasattr(action, "install"):
my_download_and_install(action.install)
# WARN: Do not use `elif` since actions like `Upgrade`
# are represented as an `install` and `remove` pair.
if hasattr(action, "remove"):
my_delete(action.remove)
Understand unsolvable problems#
When a problem has no Solution
, it is inherenty hard to come up with an explanation.
In the easiest case, a required package is missing from the Database
.
In the most complex, many package dependencies are incompatible without a single culprit.
In this case, packages should be rebuilt with weaker requirements, or with more build variants.
The UnSolvable
class attempts to build an explanation.
The UnSolvable.problems
is a list
of problems, as defined by the solver.
It is not easy to understand without linking it to specific MatchSpec
and PackageInfo
.
The method
UnSolvable.problems_graph
gives a more structured graph of package dependencies and incompatibilities.
This graph is the underlying mechanism used in
UnSolvable.explain_problems
to build a detail unsolvability message.