Getting important crates to version 1.0

When I see library crates that have only two v2.x.y releases or only five v5.x.y, I feel “Why do they have >=v1 versions? Are their APIs really STABLE?” And once a crate reached v1.0.0, they cannot move to "more stable" versions. Users will think like "v24.0.0 is released. Is this really stable? How many months or weeks do they took to reach v25.0.0?" forever... So I don't want crates to be v1.0.0 until the developers and users are really really confident that they can keep API compatible for years and/or many releases.
(I'm not blaming them... Oversight will happen to everyone.)

Specifically for graphics math, I like the idea of mint crate. Representation of data and APIs for calculation can be split, and the former will be more easy to stabilize than the latter.
(However mint does not reach v1.0.0 yet :-P)

I feel like maybe there is too much interpretation in what those numbers mean.

Beside the extra third number you get with Rust's flavor of semver when going to 1.0.0 (which is a real difference as @alice pointed out), these are just numbers.

What matters is code quality, maintenance, most important: LICENSE, and also how many dependencies a crate pulls in. And it can also be relevant who owns the crate (and all its dependencies) and if you trust them to not do something nasty (or stupid) in the future. All these aspects are so much more relevant than the interpretation of what "1" means (in my opinion).

2 Likes

I reckon OP is bringing up the exact same topic at least for the 3rd time now.

4 Likes

Ah yes "quality".

Part of my high quality experience might be that the thing does not change it's API every week thus causing me to tinker with my code all the time to keep up with changes. Part of my high quality experience might be that there are not ten different versions of the thing pulled into my program by other libraries I use that depend on it that all have different API's and types.

Arriving at the the magical version 1, or any other number, does not by itself, mysteriously fix such inconveniences happening.

I have experience these inconveniences a little bit here and there. Not enough to really annoy me.

6 Likes

I've also run into this with other libraries (lsp-types), one thing I guess i've wished for rather than putting the onus on crates to reach 1.0, is some form of a "crate set", where there is only one unique instance/version of each crate in a set, then the set could also act as a place to coordinate upgrading versions.

Whether that's a problem depends:

Do you use data types from the crate for interchanging data with a huge number of different crates? Then you really need stability. E.g. it would be bad if there were a lot of different Vecs floating around.

But generally, it's not a problem if an API changes, I think, as long as the old version of the crate is still available and critical bugfixes are backported to old versions.

A crate with a flawed design that never gets fixed can be pretty much of an annoyance too.

I'm generally a friend of fixing errors when they are found, but I also understand the wish for backward compatibility.


Of course, changing APIs get worse as more and more dependencies exist. Hence why I wrote:

1 Like

Luckily for me, so far not really.

I agree, I would rather mistakes get fixed or improvements made than weld myself to some ancient versions of something. It's usually not a big deal to make the appropriate tweaks to my projects. Might be an issue if I was working on something gigantic but I don't.

The thing that puzzled me recently was finding two or three different versions of some crate I'd never heard of getting pulled into my code after I had adopted new versions of a couple of crates I use. It did no harm but bugged me. Not sure how I fixed it, likely just deleting Cargo.toml and Cargo.lock and starting over.

Which reminds me. Whilst we are here what is Cargo.lock all about? I seem to recall having this problem a couple of times and blowing away Cargo.lock fixing it.

Cargo.lock makes sure that dependencies aren't updated, such that you will not experience surprises during developing and/or compiling. I think the canonical way to fix this is running cargo update.

Thanks. That was my guess. Ironically I found myself deleting Cargo.toml to remove surprises.

Yes. I first brought this up in 2021, and it hasn't been fixed yet.

According to the 2024 Rust roadmap, there is a "Rust library team" working on a roadmap for libraries. Those are just the standard libraries that ship with Rust. There's a "crates.io" team, responsible for crate polices. Their output appears here, on Zulip..

Things are slow over there. Their last monthly meeting was in December, and the only comment was that someone wasn't going to attend. There's the comment "I think the main issue was/is that few of the team members had the time/energy to really dig in on this RFC. unless we're in a big rush I'd defer this until the crates.io position at the Foundation is filled (~April), which should make things a lot easier for the rest of the team." So that may be why crate curation is languishing.

2 Likes

There's nothing here to be "fixed". Your expectations happen not to match the vision of the broader ecosystem. If you keep bringing up an overly generic "libraries need to be 1.0" argument, you'll predictably get the same answer every time. This is not productive, and you should probably not continue it.

5 Likes

My position, at its most fundamental layer, is that semver is a means of communicating whether a breaking change occurs in a particular release in a decentralized system of volunteers. That's pretty much it.

Now, I do have some opinions about version numbers beyond that. I don't mean for them to be prescriptive, because I know they aren't universal. But they're just what I try to do:

  • I do tend to ascribe some murky notion of "stability" to a 1.x.y release. That is, I see 1.0 as something to work towards, and something to do once you believe your API is able to stand the test of time. Where "time" is probably "N years."
  • In the absence of any explicit statement on the matter, if a crate has been at 1.x.y for N years, I will assume that it has reached some murky notion of "stable." If I need to count on it, I'll ask.
  • If a crate is at version 24.x.y, then I will generally assume that it is not stable. Similarly, if a crate is at version 0.x.y, I will also assume it is not stable. Either of these assumptions may be wrong, but it's a decent first approximation.
  • I do have some crates where I don't have any current plans to publish a 1.x.y release. regex-syntax comes to mind, because its primary function is an implementation detail of the regex crate. API stability is a secondary concern. (That doesn't mean it isn't considered at all, but API stability will always lose in the context of regex-syntax when a competing interest as an implementation detail of regex is also in play.) I do not know whether this is a good strategy or not, and occasionally reconsider it.

These are opinions of mine that held weakly, and are generally meant to describe an approximation of my own internal mental model of this corner of the world. I have no interest in evangelizing these opinions for others to follow them, but I do suspect many others have a similar mental model of the world.

18 Likes

It might be instructive to try to build a similar list for the crates.io ecosystem. Maybe zero-major version number + some combination of high downloads and high number of reverse dependencies? From https://crates.io/crates?sort=recent-downloads, you immediately find:

  1. libc
  2. rand_core
  3. base64
  4. getrandom
  5. hashbrown (which is alternatively exposed through std)
  6. parking_lot_core (there is
  7. time
  8. log
  9. parking_lot
  10. rand_chacha

I think there's some discussion about bringing a minimal random byte interface into core or std, which would cover those. parking_lot and hashbrown have alternatives in std so that makes them less pressing.

3 Likes

Of that list,

Opinions on "1.0 ability"

1.0 only really matters for "vocabulary" crates, where their intended use case is as a public dependency. Taking 1.0 to mean "we don't expect to ever want to make breaking changes,"

  • libc is essentially committed to staying 0.2 forever. The 0.1=>0.2 move isn't (sometimes) referred to as the libcpocalypse for no reason. I see one scenario where libc goes to 1.0:
    • it's api compatible and semver tricked,
    • we're confident that Rust has the capability to express all C API features with full accuracy and matching the forward compatibility of ABI-stable C[1] (this means at least opaque extern type support), and
    • we've developed some standard for LTS and MSRV of LTS[2].
  • getrandom: As you mentioned, getrandom is (slowly) on its way towards being included in std as a minimal[3] interface to the OS's "strong" randomness source
    [4].
  • rand_core: How best to implement or consume pseudorandomness (especially where cryptographic integrity is concerned) is still evolving, even outside of Rust. I expect committing to a stable CryptoRngCore to be undesirable still (and especially with upcastable traits coming "soon").
    • There's also SeedableRng::seed_from_u64; rand considers determinism a stability guarantee, and that provided trait method embeds a small rng to mix any u64 into a "reasonably good quality" rng seed. The choice of how this is done is both currently considered part of the stable ABI and is subject to wanting to be replaced with a different choice in the future.
    • But I could see at least considering RngCore as "1.0 stable," and it's certainly a downstream public interface member... except that the error type definition is not really obviously correct; it would kinda prefer being a no_std-compatible std::io::Error.
  • rand_chacha: As far as I understand it, not intended to be a public API member. Also blocked on the rand_core public dependency.
  • base64: Not intended as a public dependency, and still seeing breaking API updates. (Efficient decoding interfaces aren't trivial!)
  • hashbrown, parking_lot: have stable equivalents in std.
  • time: APIs for time are complicated. Has a std alternative in SystemTime, although it's a bit more clunky to use and doesn't support timezones.
  • log: Like libc, seems for-the-most-part frozen to 0.4. Currently working on support for structured logging (attaching arbitrary fields to logs), which probably blocks it from being "1.0 ready."
    • AIUI they're currently aiming towards making the structured logging backend production and consumption stable, but not the macros yet. Perhaps a log-core for just the sink interface could make sense and 1.0 earlier?
      • The global state would still live in the log crate; log-core would be just the Log trait and supporting cast.
      • Depending on how exactly it's done, this could even separate out "simple" unstructured logging and 1.0 it before structured logging.
      • But this very much ends up feeling like an exercise in making API subsets 1.0 just for the point of them being 1.0, not for actualizable benefit.
    • On the other hand, I don't know the comparison/tradeoff/choices between log's sval and tracing's valuable. (log is an established choice, but tracing (is a lot more complex and) I think has a bit more adopted velocity.)
  • tracing_core: (for comparison): has been 0.1 since it existed. tracing was designed for downstream extension, so this has been possible... but also has a long-running 0.2 branch with some (very useful) renaming for clarification and some pending possible breaking changes they'd like to make to the core abstraction. 1.0 isn't happening without 0.2 first, and 0.2 isn't happening without a reasonable migration plan.

From that I guess the best takeaway is that library design is difficult. In the cargo backlog[5] is a feature for differentiating between public and private dependencies. It's reasonably plausible that if/when cargo gets support for explicitly public/private dependencies, that libraries with separate public/private API subsets will more often split themselves to take advantage of such. Better tooling support for split crates will help this as well.

And to repeat the point: while "late 1.0" is common within the Rust community, it's increasingly common darn near everywhere in the software space. But truly, it's less of a consistent ecosystem choice to avoid 1.0, and more that people (especially in Open Source) are a lot more open to sharing code for reuse before it's "done." There's no large-scale "solution" to widely used crates being 0.Y a lot of the time; the solution is individually tracking the 1.0 blockers for the crates you care about, and helping them to clear those blockers and publish that 1.0 version.

on glam, specifically

I meant to say this in my earlier, but I don't think I did.

Linear algebra terms may be fairly stable, and you might even consider the desirable API for doing linear algebra w.r.t. games a fairly set beast. But neither are really fully true.

To the latter, simd in Rust is still cooking, and this has impacts on API choices, such as when it's preferable to pass by-copy or by-reference. This makes a much larger impact on SIMD-enabled types because of the additional cost of moving things into and out of SIMD registers.

To the former, game maths typically use vec3 for multiple different concepts, such as direction, offset, position, and cross product. I think glam's fairly bought into just being semanticless math shapes, but other libraries are proving out benefits of separating these concepts more clearly. There's an entire field of mathematics called geometric algebra essentially derived from the fact that the cross products don't really produce a vector, but rather what geometric algebra calls a bivector, which makes the idea of cross product properly scale to different dimensionality, and uses bivectors to build a different version of Quaternions called Rotors, which are arguably easier to conceptualize and work with.

Typically, in almost any field you perceive as stable and/or solved, you're just not aware of the complexity hidden under the surface if you know where to look for it. And too many times, that hidden complexity has enough of an impact on API design choices that makes it difficult to commit to fully stable interfaces.

Rust also has a really high bar for API stability. It really does seem unfortunate that it's not (reasonably[6]) possible to have multiple incompatible interfaces visible to different users of the same semantic type.

And additionally, glam wants to (optionally) provide a number of implementations of (presumably) upstream traits from crates which aren't themselves 1.0, including but not limited to the aforementioned rand, and the ever difficult-to-stabilize num-traits (but which to be fair seems essentially 0.2-frozen due to the inertia of being widely vocabularized).


  1. This means that directly adding fields to existing structs, while it can technically be API compatible in C, doesn't need to be supported for stable libc, since libc type definitions are ABI stable and can't add new fields. But we also have to consider if C is going to manage to break ABI, e.g. change the intmax_t typedef, e.g. by mandating _Alias support... (atm, seems unlikely but possible). ↩︎

  2. There's currently some "enthusiastic" discussion around raising the MSRV of libc. I don't see a 1.0 happening without an official rustc LTS scheme which libc can mirror. (This is absolutely not a thread to debate LTS schemes.) ↩︎

  3. "Minimal" is the hard part which is (partly) why it hasn't happened yet. Even fn(&mut [u8]) is a bit contentious, since it can't be used to read to uninitialized memory; should Read used instead, to enable use of the unstable read_buf for such? An error type of io::Error limits the API to essentially only ever being usable under std, but not using it still means needing to choose a different error type and define its API as well. ↩︎

  4. I saw elsewhere that a std getrandom source might be injectable like #[global_allocator]. If hookable at all, I think it'd be more like #[panic_handler], where it annotates a function, and std defines one, meaning you can only define one for no_std targets. Yes, that'd mean it's stuck as a stub for wasm32-unknown-unknown. (That target has jank for other reasons as well not limited to other stubbed std interfaces, such as an ABI mismatch with C; it's perhaps better understood as wasm32-rustwasm-bindgen. I have hope eventually host bindings will be standardized and we can get wasm32-web, and the component model's canonical ABI for wasm32-canon, so we can retire wasm32-unknown-unknown.) ↩︎

  5. Important to remember, though, that the cargo team unfortunately doesn't really have any bandwidth for new features at the current time. And things interacting with version resolution are by and large the most complicated and involved parts of cargo. Perhaps if/when pubgrub is used to replace the current resolver? Since that would make adding new constraints simpler, as well as explaining why constraint solving fails. ↩︎

  6. If ignoring trait impls, since they have coherence and soundness implications if the world doesn't agree on them, it's theoretically possible to expose two incompatible APIs on the same type by putting everything in extension traits instead of in inherent impls. Unfortunately, even ignoring other trait implementations, this degrades usability significantly, at least without the ability to export the type and extensions under one name. Spitball: pub use {crate_core::Type, crate::TypeExt as _} as Type; and using that name would be the equivalent of using both the type and the anonymized trait. ↩︎

5 Likes

Nit: SystemTime is very limited, so it's not really an alternative.

With regard to time 1.0, I'm waiting on multiple language features before I even consider putting out a 1.0 release, as my intent is to commit to supporting it for a minimum of three years. I don't have an exhaustive list anywhere, but there's a few features that are in varying levels of closeness to being stabilized. Some are very close, while others haven't even been formally proposed (some of those I intend to propose myself).

4 Likes

It might be instructive to try to build a similar list for the crates.io ecosystem. Maybe zero-major version number + some combination of high downloads and high number of reverse dependencies?

Now that's progress. The Rust 2024 roadmap says that policies that can be enforced by bots are preferred.

From that list, can you pull the ones where types the crate exports are re-exported by the crate's users? That's when two versions can't coexist, and lockstep update problems appear downstream. Those are probably the crates to look at first.

Maybe crates should have ability to mark version 1.0.0 as a continuation of some 0.x.y line?

[package]
name="mycrate"
version="1.0.3"
v1origin="0.4.1"

cargo update would treat as if 1.0.0 were a sort of 0.4.2 and update lockfiles to v1 even though Cargo.toml specifies mycrate = "0.4".

Is there a [pre]-RFC about it already somewhere?

There's already the well-publicized semver hack, which functionally does the same thing.

1 Like

Imho part of the problem is that Semver has no affordance for experimental version tracks other than 0.x.y. Once you release 1.0, you're done. All your other releases are either assumed backwards-comopatible (1.x.y), or a major version break with arbitrary consequences. Pre-release versions are practically unusable for anything other than pre-release testing proper, because there is no compatibility information between them, and the naming schemes suck.

What would be imho more useful is a split between "kinda-LTS" versions, and experimental tracks. The major versions would be something that the wider ecosystem can rely on, while arbitrary semi-stable version tracks could exist between any major versions. So we would have e.g. 1.3.0 version which people could rely on, but there would also be something like 1-0.2.0 version which would be part of the transition from 1.0 to 2.0. These versions would be assumed greater than all 1.x.y ones, but otherwise between themselves would behave similar to the 0.x.y version track.

Technically, you can do something similar by releasing 0.21.0 after 1.2.25, because zero-versions don't have any implied compatibility. But that would be very confusing both to users and to maintainers.

On the other hand, who would want to maintain several simultaneous versions? The few projects which already care about LTS versions probably have a working solution, while for the others it would just be headache.

1 Like

Pre-semver, some projects would do this by specifying odd major versions as stable and even major versions as experimental. Even though it's not what semver tooling expects, you might be able to get away with it if you advertise your policy well enough; in that case, anyone using a non-pinned even-major version is opting into dealing with breaking changes.