Skip to content

crates.io and semver

Sven Nilsen edited this page Jan 11, 2016 · 2 revisions

The Piston project has a lot of libraries and naturally there are a lot of dependencies between them. The libraries are published on crates.io so the versions changes over time. This page is about how to update dependencies and use semver versioning.

For making updating dependencies easier, use Eco.

Ownership

crates.io supports adding ownership teams. The PistonDevelopers team is added for all libraries that are published on crates.io. In addition several people are added as owners in case there is a problem.

cargo owner -a github:pistondevelopers:pistoncollaborator

Summary

  • To debug a dependency conflict, look in Cargo.lock for multiple versions of the same library.
  • To figure out how to upgrade a library, use the dependency graph in README for the various projects. Start at the bottom and work your way up.
  • Increment the first non-zero number in a library if a dependency increases in the first non-zero number.

crates.io also lists the dependencies of a library. If these versions are old, then the library needs to be updated.

Semver

Semver treats the first non-zero number as incompatible with another. For example, 0.3.0 and 0.4.0 are considered incompatible. 0.3.0 and 0.3.1 are considered compatible. When you put 0.3.2 in the Cargo.toml, it means "versions compatible with 0.3, but at least as new as 0.3.2".

When you update a dependency, you should use all numbers x.y.z. This is important to get bug fixes or minor updates.

When there is a breaking change, we increase the first non-zero version to keep existing projects running. This leads to more work on updating libraries, but at least that is incremental progress. If we broke libraries silently, it would lead to regression of working code, which is worse.

Dependency conflicts

When two incompatible libraries are linked, it causes an error message in the Rust compiler of the form "expected X, found X" or "the trait X is not implemented for Y" when Y certainly implements X.

This happens when there are multiple dependency paths to the same library. If library A depends on B and C, where B depends on C, then upgrading C without upgrading B causes the upgrade of A to fail.

It does not matter if the code in the incompatible libraries is almost the same. Cargo sees only the version number. All dependency paths should be upgraded before upgrading the library.

Libraries with many dependencies

Libraries with lots of dependencies increases more rapidly in the first non-zero number. This is because any breaking change in the dependencies is a potential conflict for the composition of that dependency tree with any other dependency path in a minor update. While this might seem counter intuitive because high abstractions tend to stay the same, the conflict is caused by Cargo using the semver version to decide whether two libraries are equal or not. We prefer to keep existing code working, so if a dependency increases in the first non-zero number, we update the first non-zero number in that library. If you don't do this, then it can break libraries depending on your library.

When things go wrong, minimize the damage

When a dependency breaks silently without increasing the first non-zero number, the damage has already happened. Since it breaks anyway, one can minimize the damage.

The way to fix this is to set the minimum version x.y.z and increase a minor version in the updated library, so cargo update has a chance to compile for that dependency path. People using your library will have to do the same, by setting the minimum version and fix the breaking change.

When people use cargo update, it will get the latest version of your library which fixes the problem with the breaking change. It will break downstream, but at least there is a way to fix it.