Writing your own dependency provider
The OfflineDependencyProvider
is very useful for testing and playing with the
API, but would not be usable in more complex settings like Cargo for example. In
such cases, a dependency provider may need to retrieve package information from
caches, from the disk or from network requests. Then, you might want to
implement DependencyProvider
for your own type. The DependencyProvider
trait
is defined as follows.
#![allow(unused)] fn main() { /// Trait that allows the algorithm to retrieve available packages and their dependencies. /// An implementor needs to be supplied to the [resolve] function. pub trait DependencyProvider<P: Package, V: Version> { /// Decision making is the process of choosing the next package /// and version that will be appended to the partial solution. /// Every time such a decision must be made, /// potential valid packages and version ranges are preselected by the resolver, /// and the dependency provider must choose. /// /// Note: the type `T` ensures that this returns an item from the `packages` argument. fn choose_package_version<T: Borrow<P>, U: Borrow<Range<V>>>( &self, potential_packages: impl Iterator<Item = (T, U)>, ) -> Result<(T, Option<V>), Box<dyn Error>>; /// Retrieves the package dependencies. /// Return [Dependencies::Unknown] if its dependencies are unknown. fn get_dependencies( &self, package: &P, version: &V, ) -> Result<Dependencies<P, V>, Box<dyn Error>>; /// This is called fairly regularly during the resolution, /// if it returns an Err then resolution will be terminated. /// This is helpful if you want to add some form of early termination like a timeout, /// or you want to add some form of user feedback if things are taking a while. /// If not provided the resolver will run as long as needed. fn should_cancel(&self) -> Result<(), Box<dyn Error>> { Ok(()) } } }
As you can see, implementing the DependencyProvider
trait requires you to
implement two functions, choose_package_version
and get_dependencies
. The
first one, choose_package_version
is called by the resolver when a new package
has to be tried. At that point, the resolver call choose_package_version
with
a list of potential packages and their associated acceptable version ranges.
It's then the role of the dependency retriever to pick a package and a suitable
version in that range. The simplest decision strategy would be to just pick the
first package, and first compatible version. Provided there exists a method
fn available_versions(package: &P) -> impl Iterator<Item = &V>
for your type,
it could be implemented as follows. We discuss advanced
decision making strategies later.
#![allow(unused)] fn main() { fn choose_package_version<T: Borrow<P>, U: Borrow<Range<V>>>( &self, potential_packages: impl Iterator<Item = (T, U)>, ) -> Result<(T, Option<V>), Box<dyn Error>> { let (package, range) = potential_packages.next().unwrap(); let version = self .available_versions(package.borrow()) .filter(|v| range.borrow().contains(v)) .next(); Ok((package, version.cloned())) } }
The second required method is the get_dependencies
method. For a given package
version, this method should return the corresponding dependencies. Retrieving
those dependencies may fail due to IO or other issues, and in such cases the
function should return an error. Even if it does not fail, we want to
distinguish the cases where the dependency provider does not know the answer and
the cases where the package has no dependencies. For this reason, the return
type in case of a success is the Dependencies
enum, defined as follows.
#![allow(unused)] fn main() { pub enum Dependencies<P: Package, V: Version> { Unknown, Known(DependencyConstraints<P, V>), } pub type DependencyConstraints<P, V> = Map<P, Range<V>>; }
Finally, there is an optional should_cancel
method. As described in its
documentation, this method is regularly called in the solver loop, and defaults
to doing nothing. But if needed, you can override it to provide custom behavior,
such as giving some feedback, or stopping early to prevent ddos. Any useful
behavior would require mutability of self
, and that is possible thanks to
interior mutability. Read on the next section for more info on
that!