Getting important crates to version 1.0

There are still many widely used crates that haven't made it to version 1.x.x yet. This creates collateral damage in a way that hasn't been widely realized.

In the graphics/game dev community, everybody needs a 2D/3D vector and matrix crate. The most widely used one is "glam". Types from glam appear in crate interfaces. So all the graphics crates have to use the same version of glam. They have to advance in lockstep. This slows down bug fixes and updates of other crates. Discussion on Reddit.

One workaround is for everybody to stay with an old version of the crate and ignore upgrades. Some of the graphics crates do that, staying with an old version of glam. Nobody can upgrade separately.

We need some process which pushes crates at the bottom of dependencies to reach 1.x.x. Users should just be able to write

cratename = "1"

and have it Just Work, with a stable crate API.

Over in web land, crates url and uuid have reached version 1, so those basics can be used everywhere. In what other areas of Rust usage is this a problem? What widely used crates not yet at version 1 export types which are used in the interfaces of other crates?

10 Likes

What advantage do you believe 1.0 gives over say, 0.3? Breaking changes would still come just as often, as merely calling it something else doesn't reduce what needs to change.

12 Likes

This looks like an XY problem. You aren't asking for 1.0. You're asking for API stability. Those are two different things. The former just looks like setting a version number and, is in some sense, trivial. The latter demonstrates what you seemingly actually want. When you phrase it as API stability, the problem doesn't look as simple as "just releasing 1.0."

This is something to bring up with specific crates. Go talk to them. Do they have a plan? Are they still iterating? Are they volunteers? How much pressure can you realistically put on the free time of others?

36 Likes

Ref: Rust semantic version policy

The whole point of semantic versioning is to distinguish non-breaking ("minor") changes from breaking changes. Packages which are still "initial development releases", 0.x.x, version numbers, get more slack on this. At some point, things need to be firmed up. That point comes when the published crates of others rely on your crate.

4 Likes

How so? 0.3 → 0.4 is just as much a breaking change as 1.0 to 2.0, yet there is no reason to believe that simply publishing an existing version as 1.0 would reduce the frequency of breaking changes. That is, of course, barring actual stability which is a very different problem.

8 Likes

Just because a crate releases version 1.0.0 doesn't mean they won't release version 2.0.0 in the following month. You aren't asking for 1.0.0. You are asking for stability. You are asking for longer release cycles.

18 Likes

I think the only way to create the kind of stability that's being asked for is to create consensus among the consumers of any given crate under consideration (eg glam in OP). Specifically, consensus about what functionality is needed, and what the APIs to that functionality should look like.

Get that right, and API stability will follow pretty much automatically, in that actively breaking changes (as opposed to API additions) should be quite rare.

I think this also hints at why so many crates (including some of my own) are 0.x.y: they're written with the set of use cases of the author in mind, who themselves may not be entirely clear yet on what that set will actually turn out to be, let alone what the use cases of others might be.

6 Likes

I'm talking about crates with download counts in seven figures. That's way past being 'not entirely clear yet" on the use cases of others. This is an issue for major crates with major dependencies, because they force other crates into lockstep updates, which causes breakage because some other crate has to be kept at an old version.

2 Likes

Any crate that reëxports some type defined by a dependency will do this, if only because the same type in the same module in a different version of the same crate is considered a different type as far as dependency resolution and imports concerned.

So that lockstep problem won't be solved by stability for the crates that reëxport n-dimensional vectors alone.
It sounds like some decoupling might be in order there, perhaps by putting the types behind a (set of) trait(s) defined in a separate (extra-stable) crate. If it's true that the use cases for such crates are clear, then that should be doable.

Which is why crates with types commonly re-exported need stability.

Adding another layer of indirection just moves the problem; it doesn't fix it.

Didn't you have roughly the same topic last year?

You're getting mostly the same answers.

7 Likes

I do agree that crates in the Rust ecosystem currently tend to stay on 0.* after they have reached a reasonable level of stability, and would be better off moving to 1.x.y.

  • It removes the conflation of feature bumps and non-feature-changing patches
  • It signals "we've figured out the general shape of things and aren't going to change things all the time"
    • Or alternatively put, 0.x to 0.{x+1} might change a function argument or rewrite the entire world and you don't know which, but x.y to {x+1}.0 is rarely just the former

Though I recognize the latter point is somewhat psychological and doesn't have to be adhered to.


Something that tends to happen when projects in the wider world resist a major version bump is that the minor version becomes the major version and the actual major version is eventually deleted. How's everyone getting along with the update to Java 1.19? Or Elasticsearch 2.8.6.2?

This is how Cargo works within version 0.* already, but I still don't think it's a good model. If bangwhiz 0.5 is massively different from bangwhiz 0.7, I'd rather they be versions 5.8 and 7.0 to better signal my primate brain.


Not sure how to encourage the ecosystem to do this though. @BurntSushi if I can be so imposing, do you have an opinion on if staying in 0.* is ever a bad choice? I'm asking because your load-bearing crates do tend to reach 1.* eventually, and rather intentionally (or at least so it seems to me).

16 Likes

Yes, unfortunately.

This time, I'm outlining why this gradually pollutes other crates. It's a recognized problem. See the Rust Roadmap for 2024:

"We encourage people to experiment and explore in the library ecosystem, building new functionality for people to use. Sometimes, that new functionality becomes a foundation for others to build on, and standardizing it simplifies further development atop it, letting the cycle continue at another level. However, some aspects of the Rust language (notably coherence) make it harder to extend the Rust standard library or well-established crates from separate libraries, discouraging experimentation. Other features (such as aspects of method resolution) make it hard to promote best-in-class functionality into the standard library or into well-established crates without breaking users of the crates that first developed that functionality. For Rust 2024, we want to pursue changes that enable more exploration in the ecosystem, and enable stable migration of code from the ecosystem into the standard library."

1 Like

Not quite. It's true that then the interface would be trait based, but if those traits are in their own crate, such a trait crate barely ever have to change (though when it does, that's a fairly massive shift).

What you get back for that is that the functionality in crates implementing the traits becomes mostly an implementation detail, as opposed to those crates defining the actual APIs themselves.
This alone has a stabilizing effect, all else equal, at the cost of needing to define the correct traits and associated social communication overhead.

We also need some process which pushes people to use turn signals correctly and not throw cigarette butts on the sidewalk.

There's (thankfully) nothing with sufficient coercive power to get arbitrary people on the internet to do what you want. You'll need to talk to each of the relevant maintainers and ask them, then move to other libraries if you're unhappy with the answer.

12 Likes

I consistently use X.Y.Z names to refer to the numbers as shorthand independent of semantics.

Some of this is a misuse of zerover. (Fun site! Documents some huge name "offenders" which are still version 0.Y despite being widely used.) A number of crates (but thankfully, primarily smaller ones) tend to bump Y for new minor, API-compatible features the way you would for X≥1, despite still being on X=0. Semantically, they're doing 0.MAJOR.PATCH versioning, without a minor version number.

This is problematic (and especially so for public dependencies), because you're creating an API incompatibility where none exists. The proper practice is 0.MAJOR.MINOR, where additions are made compatibly when they are.

It's also true that the semver specification states that once you're concerning yourself with backwards compatibility, you should be on version 1.0. There's some truth to the value of 1.0 to advertise stability, but it's only actually beneficial if your timeframe of stability pretty closely matches the library's idea, and it's worth noting that Rust has a much stricter idea/definition of what's API stable than a lot of other languages (especially dynamic ones like JavaScript).

For the specific case of glam (or other math libraries), mint exists to address this problem. Crates which want to be more stable than their algebra crate of choice can define their interface in terms of mint's types. For nearly the same ergonomics, define your function inputs as generic, taking impl IntoMint (and probably monomorphize the actual impl to the specific type, if that would make it non-generic).

The tradeoff is that mint provides no functionality on its types, just structure; if you're going to work with the types directly, it needs to be via functions, not methods or (upstream) traits. (But hey, if you're using something like glm in C++, using functions for most things is standard practice anyway.) This is because despite algebra being fairly fundamental and stable[^1], the best API vocabulary for working with algebraic types in Rust is not.

And FWIW, it's not really any better in other languages' ecosystems. While you may have major algebra libraries that have been around and stable longer than the Rust gamedev scene has existed (e.g. glm in C++), it's extremely common for libraries/engines/etc to define their own types (especially in C++).

If you're a library for use with bevy, you use the bevy_math types, and if you don't update when bevy_math updates, you become legacy and lose users. Same for glam, nalgebra, or any other upstream; you either track upstream or get left behind. When you decided to publish your code for that ecosystem you took on that responsibility.

Yeah, if a crate has upwards of 1,000,000 downloads, it's probably time to be discussing what's required to be confident in committing to a 1.0 release, or at least pulling out a "lib-core" which can be 1.0. (Requiring less stable things to be defined in extension traits is unfortunate, though; I do hope we can eventually gain the ability to have "friend" crates, unstable crate APIs, or similar to ease this a bit.) By the original semver specification, if you're concerned about stability/backwards-compatibility and choosing not to make changes because they would cause breakage, it makes sense to claim that by having version 1.0.

Culturally, though, there's significant pressure that 1.0 is stable stable, and that there won't be a need for a 2.0. That you're even asking for crates to "be 1.0" illustrates this. What you actually want is that implied promise of longer-term stability.

Perhaps what's needed is tooling around "partially compatible" API revisions. This can't fix unmaintained dependencies not updating, but it could potentially be used as a lightweight way to patch them into using a later version where the API subset they actually utilize is compatible. (This is kinda possible already, but requires making a local version of the crate to patch with, utilizing the semver trick and reexporting the later version. It also only works for apps, not for publishing a library, but also, maybe upgrade your libraries off of unmaintained deps. If it's unmaintained, you forking it and only maintaining it enough to update public deps for your own usage is an improvement over the status quo, and relatively low effort.)

So yes, there's significant value to vocabulary crates getting to a properly stable 1.Y release. But it's not worth anything without the "properly." And it's also more difficult for non-std crates to do the whole "stability without stagnation" thing, because there's no standard way to handle experimental/unstable API additions. So, in short, it's complicated.

If a crate isn't 1.0 and you think it should be,

  • Check their issue tracker to see if there's an issue/project/similar tracking what's desired before 1.0 (or even just for the next breaking release). If they're at the point of "should be 1.0," this should probably be publicly tracked, and if it isn't, asking about tracking it probably isn't completely unwarranted.
  • The library maintainers' idea of what constitutes a 1.0 release probably disagrees with yours. You may be fine with a 1 year stability commitment; they may want a 5 year commitment, or even want an "essentially finished" level. Asking about and tracking what's desired for 1.0 will help clarify what the library is looking for.
5 Likes

For what it's worth, I find that there's a significant advantage to being 1.x.y in Tokio: we have the ability to backport bugfixes.

For example, 1.18.x is an LTS release of Tokio, and 1.18.5 was released after 1.24.0.

16 Likes

There's nothing stopping you from doing the same with 0.18.5 and 0.24.0. The difference only matters if you have an LTS line which is still API compatible with the latest version. For 99% of crates, downstream is just expected to update within the compatible range, and their scope is small enough that there isn't really significant risk in new features that would be mitigated by LTS support of an older version without the feature additions.

If you want to be overly cute, I think you can abuse a prerelease version as a "postrelease" version (e.g. 0.1.18-lts.5); last I checked, cargo treats prerelease versions as compatible for version resolution (at least for when the first dotted part of the prerelease matches, but I think always, somewhat annoyingly), meaning that depending on 0.1.18-lts.3 would have the same effect as depending on ~1.18.3 (except that it wouldn't prevent 0.1.24/1.24.0 from being included in the tree or get dependencies on a looser 0.1.18/1.18.0 dependency to pick the LTS version instead of the latest... so it's rather limited, in comparison).

(Actually, it'd be interesting to see metrics from Tokio on the usage of the LTS minor version compared to others. IIRC, Tokio's LTS has the same rolling MSRV that latest does, so the only reason to stay on the older version is new feature risk aversion and/or avoiding some behavior breaking quirk fix that didn't get backported.)

That's not the same, because those are not semver-compatible. What I am comparing to is 0.1.18 and 0.1.24, and for those you cannot do it. (Okay, maybe there's an overly cute way to do it.)

No, our MSRV policy is stronger than just "rolling 6 months". The MSRV bumps don't happen automatically, and the 6 month figure is just a limit on how far forward we will bump.

Additionally, we only make MSRV bumps in minor releases, so an LTS will never have an MSRV bump since they can only receive patch releases. I generally try to make the latest release before an MSRV bump into an LTS release to avoid having a situation where you have to downgrade Tokio if you can't upgrade past an MSRV bump and want to stay on an LTS release until you can upgrade rustc.

4 Likes

"Friend" crates is something I would love to discuss more with those interested (primarily how it would look and whether it's feasible in the compiler). With regard to stability, I hope to have a blog post out in a few weeks laying out my vision.