Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an upgrade section #6

Open
wants to merge 5 commits into
base: v03
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
- [Strategical decision making in a DependencyProvider](./pubgrub_crate/strategy.md)
- [Solution and error reporting](./pubgrub_crate/solution.md)
- [Writing your own error reporting logic](./pubgrub_crate/custom_report.md)
- [Upgrading from a previous version](./upgrades/intro.md)
- [Upgrading from v0.2 to v0.3](./upgrades/v02_v03.md)
- [Advanced usage and limitations](./limitations/intro.md)
- [Optional dependencies](./limitations/optional_deps.md)
- [Allowing multiple versions of a package](./limitations/multiple_versions.md)
Expand Down
5 changes: 5 additions & 0 deletions src/upgrades/intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Upgrading from a previous version

Dealing with dependency management, we know that dependency upgrades are painful, especially with breaking changes.
We aim to provide meaningful breaking changes, and to help as much as possible people who want to upgrade their usage of PubGrub.
So we will try to provide a dedicated section for breaking changes with upgrades explanations and instructions.
137 changes: 137 additions & 0 deletions src/upgrades/v02_v03.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Upgrading from v0.2 to v0.3

If you encounter any issue upgrading, don't hesitate to reach out on our [Zulip stream](https://rust-lang.zulipchat.com/#narrow/stream/260232-t-cargo.2FPubGrub).

Version 0.2 was the first publicly advertised version of our pubgrub crate so it was developped as a minimum viable solution with sound foundations for people to start building on it.
For the sake of simplicity, it was however overly constraining the types for versions and version sets, making it impossible to correctly represent complex version types such as semantic versions with pre-release tags.
The goal with version 0.3 is to give more flexibility to implementors of dependency providers, by defining a `VersionSet` trait with minimum requirements to be compatible with the PubGrub version solving algorithm.
Our previous `Range` type is now one possible implementation of that trait when more constraints are imposed, but the new `VersionSet` trait enables more flexible implementations.
For comparison, let remind us how the v0.2 `Version` trait was defined:

```rust
/// Versions in pubgrub v0.2 have a minimal version (a "0" version)
/// and are ordered such that every version has a next one.
pub trait Version: Clone + Ord + Debug + Display {
/// Returns the lowest version.
fn lowest() -> Self;
/// Returns the next version, the smallest strictly higher version.
fn bump(&self) -> Self;
}
```

In v0.3, the new trait `VersionSet` is similar to v0.2 `Range` type.
It now has an associated `V` type for the version type, which still requires `Ord`, but we got rid of the `lowest()` and `bump()` requirements!
In the code and documentation, we often alias `VersionSet` to `VS` so if you see `VS::V`, it refers to the associated version type.

```rust
/// Trait describing sets of versions.
pub trait VersionSet: Debug + Display + Clone + Eq {
/// Version type associated with the sets manipulated.
type V: Debug + Display + Clone + Ord;

// Constructors
/// Constructor for an empty set containing no version.
fn empty() -> Self;
/// Constructor for a set containing exactly one version.
fn singleton(v: Self::V) -> Self;

// Operations
/// Compute the complement of this set.
fn complement(&self) -> Self;
/// Compute the intersection with another set.
fn intersection(&self, other: &Self) -> Self;

// Membership
/// Evaluate membership of a version in this set.
fn contains(&self, v: &Self::V) -> bool;

// Automatically implemented functions ###########################

/// Constructor for the set containing all versions.
fn full() -> Self { ... }

/// Compute the union with another set.
fn union(&self, other: &Self) -> Self { ... }
}
```

This combined with the implementation freedom to handle sets how you want, instead of relying on our previous `Range` type gives a lot of possibilities.
But with great power comes great responsability, meaning users have the responsability to implement their set operations correctly!

## The new BoundedRange to replace the old Range

Previously, the `Range<V: Version>` type was our implementation handling sets of version with efficient set operations for computing complements, intersections, and evaluating membership of versions.
One of the main limitations of that type was the constraint that versions live in a discrete space, where one need to know exactly which version comes after any given version.
This makes handling of pre-release tags not ergonomic since the `bump()` function had to produce the next possible pre-release.
In cases where any character is allowed in tags, it could consist in adding the `\x00` character to the utf8 string, but as you can see that's not convenient and it does not feel right.

The new `VersionSet` trait helps solve this particular issue, but also makes the pubgrub API more complex.
However, there are still users who do not need the added complexity, such as for Elm versions.
We care about complexity, so we also now provide the `BoundedRange<V: Ord>` type, which is our new default implementation of the `VersionSet` trait.
At its heart, `BoundedRange` now stores an ordered collection of bounded intervals `( Bound<V>, Bound<V> )`.
Our performance evaluation of this change indicates that the performance cost is negligeable when the version type is complex enough, like semantic versions, and is quite small (less than 10%) when the version type is minimalist like `u32`.
This change brings two main advantages compared to previously:

1. There is no need anymore to provide a smallest version, or a bump function for your version type.
2. It is much more ergonomic to build pairs of inclusive bounds, or pairs of exclusive bounds.

To illustrate (2), defining the `[v1, v2]` range was previously done with the need of a `bump()` call, since `[v1, v2]` is theoretically equivalent to `[v1, v2.bump()[` (right bound is excluded) under the v0.2 `Version` trait constraints.

```rust
Range::between(v1, v2.bump())
```

In v0.3, you can now leverage the official [`core::ops::RangeBounds`](https://doc.rust-lang.org/core/ops/trait.RangeBounds.html) trait.

```rust
BoundedRange::from_range_bounds(v1..=v2)
```

To update the usage of the `Range` type in your code should be quite straightforward.
There has been few naming changes for consistency, such as `Range::none()` becoming `BoundedRange::empty()` but every constructor previously available is still available now.
Of course, if for any reason the provided `BoundedRange` type does not fit your constraints for handling sets of versions, you can provide your own implementation of the `VersionSet` trait thanks to the new v0.3 API.
If you are especially interested in v0.3 for dealing with pre-releases, we strongly suggest you also read the [dedicated section of the guide for pre-releases](/limitations/prerelease_versions.md).

## Changes in the DependencyProvider to choose package versions

One fundamental piece of flexibility of the pubgrub crate is that it delegates all decision making to the caller in the `DependencyProvider` trait.

```rust
/// v0.2 dependency provider trait
pub trait DependencyProvider<P: Package, V: Version> {
/// Given candidates, choose the next package version attempt
/// to continue building the solution.
fn choose_package_version(&self, potential_packages) -> // (package, version)

/// Retrieve the dependencies of the given package version.
fn get_dependencies(&self, package, version) -> // dependencies
}
```

Previously, when choosing the next package to be added to the partial solution being built, the caller would be given a list of candidates, and it has the responsability to pick one package and version satisfying the provided candidates.
That list of candidates typically grows everytime we encounter a new package and add its dependencies to candidates for the next choices.
So depending on the resolution path chosen, it is possible to build a list of candidates that grows fast.
Then, every round, the caller has to work again on the whole list of candidates to pick one package version.
There is a smarter way to tackle this problem, consisting in puting candidates in a priority queue.
Everytime the solver encounters a new candidate, it will ask the caller to rate the priority to give for the choice of that candidate.
Then everytime there is a choice to be made, the algorithm picks the first candidate in the priority list, and simply asks the caller to pick one version in that version set.

```rust
/// v0.3 dependency provider trait
pub trait DependencyProvider<P: Package, V: Version> {
/// Compute the priority of a given package and range of versions for the solver.
/// When deciding which package to pick next, the solver will choose the highest priority one.
fn prioritize(&self, package, version_set) -> // priority

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could help to include an example of the types that might be commonly used for Priority.


/// Given a package and version constraints, pick the next version to choose.
/// Can be the newest, oldest, or any thing you want depending on context.
fn choose_version(&self, package, version_set) -> // version

/// Retrieve the dependencies of the given package version.
fn get_dependencies(&self, package, version) -> // dependencies
}
```

This new API is slightly more constraining than the old one, but it can give a significant performance boost.
It's indeed turning a problem with complexity `N^2` (evaluating all potential packages at every choice) into one with complexity `N * log(N)` (inserting in a prioritized sorted list).
If you have a situation where this API change prevents you from using pubgrub, please let us know!