Dependency management in the Rust ecosystem is fairly mature from my perspective, with crates.io, Cargo, and some cultural norms around semantic versions, I feel safer with dependencies in Rust than I have in previous toolchains. It’s far from perfect however, and this question helps highlight one of the quirks of how Rust dependency management does or does not work, depending on your perspective:
What is it that makes Rust users want libraries to re-export stuff from other libraries?
I often get requests for axum to re-export stuff from hyper, time, or other common crates. Why? Just “cargo add hyper” and you’re good to go. Hyper is in your crate graph regardless.
I also often get feature requests for the few types axum does re-export so it does confuse some. That’s why I’m reluctant to just re-export everything.
I started writing up a reply in Mastodon but then I noticed that my words were approaching the 500 character limit and perhaps this topic wasn’t microbloggable! I help maintain the deltalake package for Rust and we do re-export a number of libraries, such as arrow of which I am a strong supporter.
The biggest motivation for re-exporting is to preserve ABI compatibility in our
interfaces. For some crates your transitive dependencies may be masked entirely
from the end-user, for example if I pull in the regex
crate I’m typically
just using it for regular expressions inside my crate and not exposing an
interface which takes a regex::Regex
. The ABI is safe from transitive version
changes of that crate. If however my crate exposes an API which is dependent on
a transitive dependency then I can have problems with version mismatches. Such
is the case with arrow
in delta-rs,
which exposes arrow_array::RecordBatch
. There is a much larger chance of ABI
incompatibilties between a transitive version of arrow needed by the
deltalake
crate and what the consuming project may specify. This is
exacerbated in our case because another transitive dependency of deltalake
specifies a dependency on arrow
: datafusion.
That means that the user, deltalake
, and datafusion
all have to agree on
the same version of arrow
for types to properly interoperate between API
calls.
But it gets worse!
The Rust community generally seems to follow semantic versioning, but that
doesn’t mean anything about the releases, just the version numbers used for
them. I can make major breaking API changes every one of my 0.x.x releases, or
in the case of arrow
and datafusion
I can just increment the major version
every release.
By re-exporting symbols from those two crates, downstream users of the
deltalake
package will have a stable RecordBatch
type ABI to work with for
every release, and can largely ignore non-API breaking changes such as
struct layout changes, etc.
I am still mixed on whether all types from other crates exposed in my APIs should be exported. I think there is benefit to doing so for faster moving dependencies. In essence:
arrow
, moving fast, better user experience to re-exporturl
, moves slow, very mature, not really needed to re-export.
The judgement call I am typically making is whether this would make my life
easier as a downstream consumer of the crate. It’s not that much effort of
maintenance burden to pub use
something in a crate if that’s convenient.