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!