Versioning binary crates independently from the library crate in the same package

My project has two binaries (command line utilities) which make use of common functionality from a library crate. Unless there is a reason I shouldn't, I figure that I might as well publish the library on crates.io.

In this case, I might argue that there are three different public APIs in my package: one for each of the command line interfaces of the binary crates (e.g., if they are used in shell scripts), and one for the library crate that developers interact with in Rust. I could conceivably break the API of the library crate and bump the major version, but adapt both of the binary crates so that this is merely an implementation detail to them, and their command line interfaces stay the same. Therefore, I would want to keep the major version number the same for them.

I don't see a way in Cargo.toml to specify different versions for the library crate and each of the binary crates. Does that mean the best solution is to publish these crates as three separate packages? If this is the case, then how is the ability to have multiple crates in one package not moot in practice?

Yes.

A package is a different level of abstraction than you need. The source code of all crates in a package is meant to be developed together. That's why I can use lib::whatever; from my binary crates, without having to declare the library as an explicit dependency for them. If you need to version your crates independently, a workspace with a package per crate would be the way to go.

I got my original idea to export my project's library crate from this section of the Rust book:

We mentioned that a package can contain both a src/main.rs binary crate root as well as a src/lib.rs library crate root, and both crates will have the package name by default. Typically, packages with this pattern of containing both a library and a binary crate will have just enough code in the binary crate to start an executable that calls code within the library crate. This lets other projects benefit from most of the functionality that the package provides because the library crate’s code can be shared.

The module tree should be defined in src/lib.rs. Then, any public items can be used in the binary crate by starting paths with the name of the package. The binary crate becomes a user of the library crate just like a completely external crate would use the library crate: it can only use the public API. This helps you design a good API; not only are you the author, you’re also a client!

This is why I am thinking this way. But it appears this pattern of putting my library crate in the same package doesn't fit my use case, because I could conceivably change its API independently from the CLI tool.

So what use case does this pattern fit? Do packages that use this pattern make the binary crate(s) expose the exact same API as the library crate, so that when one changes the other automatically does as well? Or do they only use SemVer to describe the library crate, and ignore the API presented by the binary crate(s)?

The Cargo docs leave the question of whether API changes to a binary crate are considered breaking open to the package developer (my emphasis):

Cargo projects may also include executable binaries which have their own interfaces (such as a CLI interface, OS-level interaction, etc.). Since these are part of the Cargo package, they often use and share the same version as the package. You will need to decide if and how you want to employ a SemVer contract with your users in the changes you make to your application. The potential breaking and compatible changes to an application are too numerous to list, so you are encouraged to use the spirit of the SemVer spec to guide your decisions on how to apply versioning to your application, or at least document what your commitments are.

I personally think it depends on the project. What I can say from my experience is that when I have to change the API of a binary crate in my package, it usually goes hand-in-hand with a change to the library and/or another binary as well (barring bug fixes). For example, I like to group my http servers in packages containing a library, the http server binary and a utility CLI binary that I can use to quickly query a server instance. When I make changes to the CLI, it is almost always because I added an endpoint to the server or updated the data model, which means I have to bump the package version anyway, as to not break library and server users (which are mostly me, too; I like doing myself a solid).

1 Like