Crate equivalent of Edition

A discussion concerning SemVer popped up on Reddit last week over a blog article. It had the usual suspects as far as complaints are concerned.

I keep coming back to the idea that maybe crates need their own "edition" in addition to a "version", allowing them to separate intention from actual contract in versioning?

I don't mean to say that a crate should treat its "edition" in the same way that Rust does. Crate editions could give authors that arbitrary numbering system that they're looking for so that they don't break or bend the SemVer contract regularly.

Has there been any previous discussion on this? I didn't see anything in my search.

I think that before expanding Rust's already massive footprint, it's worth exploring first why those authors can't stick to semver. So could you provide data on that?

3 Likes

Rust needs editions so that it can evolve without breaking any existing crates, so that people never need to run an old version of Rust to run their code.

Crates don't need editions; they have semver major versions, and old major versions continue to work.

And if you want the behavior of "old and new editions still interoperate", that's possible too with a lot of care; for instance, you can have two different major versions of a crate export the same types (e.g. by having the older version re-export the types from the newer version), while providing different methods on them.

6 Likes

SemVer conditionally gives authors an exception to latch on to:

What seems to be happening, and has been discussed before, is that many projects on crates.io end up sitting on major version 0. In doing so, they're arguably not using SemVer in the way that it was intended.

We can explore reasons. Some of them might be:

  • Rapid development
  • No need to assure that its production ready
  • Misplaced fear of bumping the major version

SemVer's website brings this up:

If a crate is sitting on crates.io with any semblance of popularity, it's safe to assume that it has been opted into production by end-users (whether the author says that's okay or not).

It's not necessarily a new problem or unique to crates.io. For example: Steam, the video game distribution platform ran by Valve, has recently dealt with a similar problem where games sit in "Early Access" for years in some cases. Finally, Valve began setting limitations in order to curb this behavior.

We can pop over to the front page of crates.io and quickly take a poll of how this plays out. The top 10 "Most Downloaded" category (a section that's likely being used in production by now) lists the following today:

  1. syn
  2. proc-macro2
  3. quote
  4. libc
  5. bitflags
  6. rand_core
  7. cfg-if
  8. rand
  9. hashbrown
  10. serde

40% of those are in major version 0, despite some being as old 5 years in some cases. I'm not throwing anyone under the bus on that, I'm just pointing out a trend. That's just stats from grabbing a handful off of the front page.

So, we absolutely do have SemVer and it is awesome when used properly ... but I would argue that it isn't being used properly.

We have editions in the wild, though. We're just not capturing that information in a crate's metadata. Any app, suite, or library that slaps an edition number in its name somewhere along its release cycle, separate from its actual version, is using the concept of an edition.

Again, I'm not promoting Rust's idea of editions. It doesn't even need to use the same term. I'm referring to the generic concept: Edition -> Version

One big reason I've encountered for crates not wanting to go to 1.x, but to stick to 0.x, is that it's unclear what the "right" way is in SemVer to have a pre-2.x version series, where the API is unstable because you're trying to get the 2.0.0 API right.

I know of 4 variants that people have used, and I'm probably missing something:

  1. Go back and resume 0.x releases after 1.x, where 0.r is the last release before 1.x, and then 0.r+n onwards are 2.x pre-releases. This is unpopular, because it means that the version number goes backwards, and it's hard to explain that 0.7 (say) is a 1.0 prerelease, while 0.11 is a 2.0 prerelease.
  2. Add some sort of compile-time flag - checked via #[cfg] directives - to hide the major changes in 1.x releases. The advantage is that if you find a way to stabilise the changed API as an extension to existing 1.x APIs, not as a breaking change, it's trivial to integrate the APIs into 1.x; the disadvantage is that your code base can fill up with conditional compilation flags.
  3. Use a textual version tag, which indicates no SemVer compliance, and have 2.0.0-pre1, 2.0.0-pre2 and so on until you're happy to release 2.0.0. Disadvantage is that tooling can't look at the text tags and tell if it's safe to upgrade, so humans have to do every bump manually.
  4. Temporarily use another name for a "new" 0.x branch, and merge back in when you do the release. This means having a crate like "mycrate9" version 0.x.y, where when you're stable again, you don't release "mycrate9" ever again, but rather release "mycrate" version 9.0.0. Disadvantage is that tooling doesn't understand that, while you're on the last release of "mycrate9", you really want to be on "mycrate" 9.3.1 instead.
5 Likes

For myself, I use an unstable-vN and unstable-<feature> features in Cargo, wherever possible, to expose breaking changes earlier. We have an issue so users can tell Cargo that a feature is unstable and the "feature metadata" RFC has been approved, unblocking progress on a design for this.

Granted, this doesn't work well for invasive changes and we are stuck with pre-release or git dependencies and both have their downsides.

1 Like

Using features is a variant of option 2, where the feature is your compile-time flag. I've also seen --cfg compiler options used for the same purpose.

And I'd forgotten git dependencies, which is a 5th option - just don't make a formal release until the next major version is stable, and ask users to depend on git tags or hashes instead of SemVer releases until 2.0.0 is stable. This also has the problem that tooling doesn't know when you can safely update, and when it's a potentially breaking update.

Which leads nicely to the 5th option that people hate - burning huge swathes of major numbers for experimentation. If you declare that 1.0.0, 101.0.0, 201.0.0 etc are "long term stable" releases, then you can keep to SemVer by bumping major release versions every time you break something in your API evolution.

1 Like

I'd argue that Cargo/crates-io ecosystem doesn't use SemVer as specified in its official spec, but rather a Cargo fork of SemVer where 0.x is not unstable/experimental. In the Cargo-SemVer, x in 0.x is the major version with the same stability guarantees as x.0.0 for x >= 1. This is evidenced by the behavior of the tooling (version unification and update logic), and expectations of the community. The top 0.x crates are stable and behave like crates with stability guarantees.

Either way, prerelease versions are the way to test the next 0.x and the next x.0.0 major release. If the problem is in auto-updating of pre-release versions, then Cargo could fork SemVer further to define compatibility between prerelease versions (e.g. -beta.x and -rc.x auto update x, others don't. Or -pre.x.y updates y but not x).

4 Likes

Atm pre-releases have several flaws

  • You can only opt-in to a pre-release by changing your version requirement (experimenting with cargo update <foo> --precise <prerelease> to override this)
  • We consider all pre-releases compatible with each other, which requires people switching to = operators during pre-release development of multiple packages, and then switching back (but only switching back the right ones, as proc-macro exporters tend to use = always)

I feel like there are a couple more but I can't remember them off the top of my head. The feeling I get from the Cargo team is that pre-releases are viewed as a broken workflow and we need to slowly untangle and define what the workflow should be. cargo update --precise <prerelease> is the first step in this.

8 Likes