Versioning scheme of "adaptor" crates

Hello

I'm trying to figure out the best way to version an "adaptor" crate.

What's an adaptor crate

Best if I give the concrete example. I maintain the signal-hook. Currently, I'm getting a huge help with carving integrations into the async world into separate crates, so eventually there will be something like signal-hook-tokio and signal-hook-async-std and signal-hook-mio (unfortunately, there seems to be no obvious way to create a runtime-agnostic adaptation at this point yet, but eventually in some future time maybe even signal-hook-async). The signal-hook would contain only the blocking primitives, like the Signals iterator.

And all these signal-hook-* crates is what I call adaptor crates, they connect the signal hook with something else.

The problem with versioning

The usual major.minor.patch versioning scheme is somehow linear. One can support multiple version lines in parallel (having both 1.* and 2.* supported and keep releasing both for a time, for example), but there's still some reasonable notion of what is a higher version.

But it seems the version number of the adaptor is at least 2D, possibly 3D. I'd need to bump the version in "incompatible" way under these conditions:

  • There are incompatible API changes inside the adaptor.
  • signal-hook releases an incompatible version
  • The adapted-to crate (eg. tokio) releases an incompatible version

However, I'd still like to be able to support both older and newer adapted-to crate for an extended time, possibly cross a breaking release of the base (signal-hook) which makes the versioning all that harder.

Ideas

Take first free version number

So let's say I support tokio 0.1 and tokio 0.2. These are already released as signal-hook-tokio-0.3 and signal-hook-tokio-0.4 and I bump signal-hook to 0.4. Then I'd release two versions as:

  • signal-hook-tokio-0.5 (for tokio 0.1)
  • signal-hook-tokio-0.6 (for tokio 0.2)

This is simple, but quite a mess for crate users to follow. It'd probably need some kind of lookup table of the right version number to use.

Complex version scheme

Don't use the major.minor.patch, but something like base_semi_major-adapted_semi_major-major.minor.patch. So, adaptor for tokio-0.2.* for signal-hook-0.3.* would be signal-hook-tokio-0.3-0.2-0.1.3 (it would become signal-hook-tokio-0.3-1-1.0.0 once tokio releases a 1.0 version).

This expresses the semantics of the 3D versioning that it is in practice, so I kind of like this approach. Cargo seems fine accepting such version too. But I don't know what, if anything, will break if I use that ‒ cargo update, what will happen on crates.io, etc.

Does anyone have experience or theories about this?

Separate crates

In this scheme, adaptor for tokio is not one crate, but multiple, depending on the tokio version supported. So one would have eg. signal-hook-tokio_0_1-0.1.2 (the 0_1 is part of the name, not version).

This would work OK, though a never version would not be discovered by cargo outdated automatically. On the other hand, it would be easy to spot and guess that the number in the name needs to be updated.

This, however, sounds inelegant and a bit dangerous. What I fear is a squatter/attacker being faster in releasing eg signal-hook-tokio_0_3 once tokio-0.3 is released and injecting some vulnerability in that. But as the user would „only“ bump the version, they would not be aware of this operation potentially changing the author of the dependency being used, skipping on proper research of the quality or security of the dependency, assuming this is just another version of the same. I could probably pre-register bunch of future versions, but that seems somewhat wrong too.

The same as above, but with number-less "leader"

The idea here is that the newest version of the adapted to crate is supported by a crate without the version in the name and only once (or maybe in addition) it becomes obsolete, it is published as the separate crate with the version-in-name as above.

This has the advantage that if one wants to keep up to date on all dependencies, the versions don't need to change. But it's still only a variation of the above with all the disadvantages.

Suggestions?

So I'm trying to reach out and asking on opinions and experiences (from both sides ‒ as a maintainer and as a user) with something like this. I'm OK if someone suggests something better than all these, or if some of the ideas are torn to shreds (which'll make choosing from the left ones easier).

Another option would be to use features to enable or disable integration with each version of Tokio. For example, you might have a tokio01 and tokio02 feature for your library, and depending on which is enabled, the integration with that version of Tokio is enabled. You can use dependency renaming to depend on multiple versions of Tokio.

For example, you might have a Cargo.toml like this:

[package]
name = "signal-hook-tokio"
version = "0.1.0"
authors = ["Alice Ryhl <alice@ryhl.io>"]
edition = "2018"

[dependencies.tokio01-main]
version = "0.1"
default-features = false
optional = true
package = "tokio"

[dependencies.tokio01-reactor]
version = "0.1"
default-features = false
optional = true
package = "tokio-reactor"

[dependencies.tokio02]
version = "0.2"
default-features = false
features = ["rt-threaded"]
optional = true
package = "tokio"

[features]
tokio01 = ["tokio01-main", "tokio01-reactor"]

[dependencies]

Note that enabling both tokio01 and tokio02 at the same time is fine.

With this method, you only need to bump the version when the adapter or signal-hook has breaking changes. When Tokio v0.3 comes out, you just add a new optional tokio03 feature in a backwards compatible update.

The features approach is the thing we are migrating away from now, because it's not flexible enough. I'd really like all these adaptor crates to be more independent and not depend on the base crate, for others to be able to provide their own, etc.

Also, I want to be able to go 1.0 with signal-hook some time in following months and I can't reasonably do that while depending on non-1.0 crates.

You could still have separate crates, and have each runtime-specific crate have the features. Then signal-hook can be v1.0, while signal-hook-tokio is v0.x and depends the v1.0 version of signal-hook, plus old versions of Tokio?

That sounds like an interesting approach. It doesn't sound exactly elegant, but certainly workable.

Thinking more about it, I see a problem here. Right now the idea is to export something like signal_hook_tokio::Signals.

But if that adaptor crate has features to enable things, then this need to become signal_hook_tokio::v0_2::Signals, to avoid collisions with signal_hook_tokio::v1::Signals.

That is doable but feels a bit weird on the API side :frowning:

My plan for something similar is to give Tokio v1 the "main" namespace (and hope it doesn't go to v2 too quickly), though my case is slightly less weird given that I have non-tokio features as well so have tokio_O2 next to futures and eventually tokio when v1 releases (though maybe by that point the traits I'm wrapping will be in std and I won't need a tokio feature at all).

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.