I have been regularly publishing crates over the last year. I only gradually discovered various ways of prepping a crate for release, and I have also found myself wanting all my crates to have similar conventions in a number of areas. When searching through the internets, I haven’t really come across a central resource with general tips and guidelines for publishing crates, so I decided to keep track of all the things I discovered piecemeal, as a reference/checklist for myself and others. If you are an experienced publisher, you probably won’t find many new ideas in here, but if you’re just getting started with Rust, be sure to check it out and save yourself time later.
Disclaimer: Some of these will be quite generally accepted, while others are completely opinionated tips. Follow your heart.
Disclaimer 2: This post will always be work in progress and will evolve over time (last update: 2021-06-20).
Table of contents
- Read the cargo docs
- Basic Security
- Brush up your semver
- Take a look at the API guidelines.
- Use cargo-generate.
- Cargo install is not a package manager
- Code formatting
- Warnings
- Use clippy
- Avoid unsafe
- Imports
- Exports
- Use cargo features
- Target support
- Bundle related crates
- Keep an eye on your maintenance burden
- Error handling
- Logging
- Test
- Document
- README.md
- LICENSE
- CHANGELOG.md
- CONTRIBUTING.md
- Cargo.toml
- Dependencies
- The RANT part
- Conclusion
Read the cargo docs
The first tip arguably should be to read the cargo book and if you have never published a crate before, it’s definitely worth reading through the cargo manifest reference. The official documentation for the actual publishing operation with cargo publish
can be found here.
Basic Security
Please use strong passwords for accounts on crates.io, github, gitlab and others that might allow pubishing crates under your identity. It is far to common for people to use low quality passwords and if you publish a crate that will be depended on by others, you bear a responsibility. Obviously there is much more to say about security than can fit into this post, but that’s the absolute minimum you should definitely be responsible for.
Brush up your semver
The rust crates ecosystem uses Semantic Versioning. If you don’t like semver, bear with me, I don’t either, but for libraries it enables things that would otherwise be impossible. Namely it allows cargo to automatically update dependencies to the latest version without manual intervention while guaranteeing things won’t break. It means you should bump a major version number when making breaking changes to your API. That means if a dependency might no longer compile against the newer version while it did against the older one. You can find a proposal as to what are breaking changes in Rust in the API evolution RFC.
On a more general level. AFAICT it’s a general practice to wait until your API is pretty stable before rolling out version 1.0
. With lower versions, 0.x.y
the left most non-zero number is considered indicating the breaking changes. You can indicate that your crate is not production ready by adding an extra annotation: 0.1.0-alpha.1
or 0.1.0-beta.1
. I think the consensus is that each alpha version can have breaking changes. Note that cargo will update to the next alpha version automatically though.
If your crate gets a lot of dependent crates and you have to make a breaking change, it’s probably good to be aware of the semver trick. For binary crates, eg. applications that other code won’t depend on, by all means, use sentimental versioning (know you will not be able to publish them on crates.io if they don’t have a format of x.y.z as cargo enforces it).
Take a look at the API guidelines.
In order to have the most rust idiomatic public API’s, there is a set of guidelines which you should at least have read, whether you end up adhering to them or not.
Use cargo-generate.
If you are going to publish multiple libraries, it’s probably useful to stick to a consistent format. Consistent sections in your README.md, the same CONTRIBUTING.md, the same license, the same starting fields in every Cargo.toml
, etc…
By making a crate template and using cargo generate
for new libraries, you make your life a bunch easier, skipping copy pasting all the boilerplate. You can even make several templates for different purposes. Maybe one for WASM specific stuff, or for CLI apps.
When you improve your template, don’t forget to backport to your already existing crates. If you are looking for inspiration, you can check out my crate template.
Cargo install is not a package manager
If you want to distribute software from source, it can be tempting to tell your users to use cargo-install to install your software. Just beware that this is meant for rust developers. For example things won’t work as intended if the user doesn’t have the cargo bin directory in their path. It won’t install man pages, default config files and desktop launchers either. The ideal situation would be to use the dedicated system’s package manager, but that is sometimes overkill or downright unattainable for a young program with few users.
An intermediate solution is to use something like cargo-make. This will allow you to put all files in the right places. You can tune it to do the right thing on all supported platforms.
The next step in convenience is to no longer require that users install cargo and rust by providing binary packages, potentially with an installer. I’m not aware of any general purpose, cross platform (GUI) installers for binary packages written in Rust, but many general solutions exist.
Code formatting
Before making code public, you might want to think about code formatting. Rust bundles a code formatting tool rustfmt that is available on a crate base with cargo fmt
. Beware, this command will modify your files. Be sure to commit first. You can configure it to your liking and if the result pleases you, having consistent and automated formatting facilitates others contributing to your code base.
You can configure rustfmt with a file in the repository so that others will be able to format with the same configuration before filing a PR.
Warnings
I would strongly suggest that you make sure that your code compiles without warnings. It will reduce the noise you and others have to wade through when things go wrong. You can disable specific warnings by using the corresponding allow statement, like #[ allow(dead_code) ]
where needed.
It’s also possible to enable extra warnings. You can find a full list of allowed by default lints that you might want to turn into warnings in the rustc book. This is in every one of my crates, in lib.rs
:
#![ warn
(
anonymous_parameters ,
missing_copy_implementations ,
missing_debug_implementations ,
missing_docs ,
nonstandard_style ,
rust_2018_idioms ,
single_use_lifetimes ,
trivial_casts ,
trivial_numeric_casts ,
unreachable_pub ,
unused_extern_crates ,
unused_qualifications ,
variant_size_differences ,
)]
Rustc has several levels of lints: allow
, warn
, deny
, forbid
, which allow you to change the default level.
Do not use #[ deny(warnings) ]
. See rust-unofficial/patterns for an explanation.
Use clippy
cargo clippy
is a linter for Rust. It catches quite some less than optimal code constructs. It’s probably a good idea to run it before you release, and even if you don’t want to adhere to all of the warnings, it’s good practice to silence the ones you are going to ignore so your code doesn’t create noise by generating warnings.
You can disable clippy lints the usual way, with attributes which apply either to the next item, or to the current module/crate: #![ allow(clippy::suspicious_else_formatting) ]
.
Warning: at the time of this writing (April 2020) clippy has an issue where it will not do any work unless it has to recompile your code. For that, run touch **.rs
before cargo clippy --tests --examples --benches --all-features
.
Avoid unsafe
Unfortunately unsafe
is necessary sometimes, but if you can at all avoid it, it’s appreciated. Don’t use it just because the compiler won’t accept your code. It sometimes takes some creativity in designing things so they don’t need unsafe
, but you can go a very long way without needing it. If you really think you do, justify with code comments and if it’s for performance, with benchmarks why it’s needed. Make sure you thoroughly test your unsafe blocks and ask for review so you are confident everything is correct.
You can guarantee the absence of unsafe
in your crate by adding #![ forbid(unsafe_code) ]
to lib.rs
.
In libraries it’s also good to avoid panics where you can. Before releasing, try to remove all unwrap
s and expect
s and justify the remaining ones with code comments.
Imports
It can quickly become tedious to rewrite use
statements at the top of every file. Often enough you will be using the same types from your dependencies all over the place. I prefer to keep them in lib.rs
:
mod import
{
pub(crate) use
{
std :: { fmt, error::Error as ErrorTrait, ops::Deref, any::type_name } ,
std :: { task::{ Poll, Context }, pin::Pin } ,
futures :: { Stream, Sink, ready } ,
futures::channel::mpsc::
{
self ,
Sender as FutSender ,
Receiver as FutReceiver ,
UnboundedSender as FutUnboundedSender ,
UnboundedReceiver as FutUnboundedReceiver ,
SendError as FutSendError ,
},
};
}
Now in each file you can just do:
use crate::import::*;
The downside is that for people reading your code, if they wonder where a type comes from, they can’t just scroll up the file and see. They must go over to your lib.rs
, but it saves hours of typing the same very boring boilerplate over and over.
It also breaks down if you have many cargo features that will require different imports in different situations, as unused imports will give you warnings. That’s a good thing, but it also means that when feature X is no longer active, all of a sudden an import isn’t used anymore, so you need to feature gate several import blocks and when the combinations become to much, I sometimes give up on this technique.
One advantage is that you make sure every import has one single unique name throughout your entire crate, which is nice for consistency. I never do *
imports here.
Exports
I usually export almost all public types at the top level. Each having a unique name. I think it’s a good thing to not use *
exports either and nicely write them out explicitly. I realize I have sinned here though. When there is a bunch of types that really make sense to put in a namespace, I will just make the module public. Otherwise I re-export all types at the top level and leave modules private.
Another common practice is to create a prelude
module and re-export the most used types in there. They are meant for your users to easily do use somecrate::prelude::*;
You can also re-export types from other crates with pub use
, but beware that by doing this you also tie yourself to their breaking change release cycle, even if they didn’t change the signature of the types you use.
Use cargo features
If your library supports three different back-ends of which users will generally choose one, make them optional features. By doing this you allow users to reduce their compile times and binary bloat.
Features should be additive. That is if several crates in a dependency tree enable different features of your crate, all should be swell. Two features shouldn’t be incompatible with one another. That’s why you can enable features on your dependencies, but you can’t disable them. If you really are going to sin here, which I think you shouldn’t, at least use the compile_error!
macro to detect incompatible configurations and report a decent error.
cargo-hack is a nice tool worth looking into if you want to run your tests with all combinations of features. It has a bunch of other use cases too.
Target support
Where you can, it’s nice to support as many targets as possible. Two prominent ones you might want to consider are no_std and wasm.
Many crates can be useful for several targets, but contain things that can’t compile on those targets. Please take a moment to consider putting the offending code behind cargo features so that your crate is more compatible. You might also want to mention target support in the README and in the crate categories, making it easier for users to figure out if your crate supports certain targets.
no_std
no_std crates can run on embedded devices and in other circumstances where libc is not available (like bootloaders, os kernels, …). I haven’t got much experience with no_std, so this section is a placeholder for now.
In order for a crate to be available at all on no_std, the no_std
configuration attribute must be set at the top level of your crate. Usually feature flagged a shown below, where std
is enabled by default.
#![ cfg_attr( not(feature = "std"), no_std ) ]
wasm
WebAssembly can be run in the browser as well as in WebAssembly containers, which might be used in applications to run third party code (think plugins) in a sandbox. The WebAssembly book has a chapter on how to add wasm support to a general purpose crate.
You can use cargo features to enable/disable wasm specific parts. The downside is that you can no longer run cargo commands with --all-features
. You can also use platform specific dependencies in Cargo.toml
, where the target_arch
would be wasm32
. You can put a feature condition at the top of your integration tests, or on modules to exclude them from being compiled when you are not targeting wasm:
#![ cfg( target_arch = "wasm32" ) ]
Unfortunately this doesn’t work for examples and when executing wasm-pack test
, it will insist on compiling examples and it will complain about a missing main. You can work around it by either including an empty main specific to wasm, or you can indicate in your Cargo.toml
that examples require certain features. I haven’t found it in the cargo manifest documentation, but you can see it here in a Cargo.toml from hyper. I usually make a default feature called notwasm
that is required for examples, so when I run wasm-pack test
with --no-default-features
, it will not break because of the examples.
CI integration can also be a bit flaky. You can see an example configuration for travis from my async_executors crate. As you can see I didn’t get it working on Windows, so I gave up on it.
Bundle related crates
You can keep several crates in the same git repository. You can look at futures or tokio to see that in action. You can then refer to related crates with a relative path in Cargo.toml
and it will just work, even when publishing them. Such projects often use a cargo workspace. I can’t advise much on workspaces because I haven’t used them in a long time.
I personally keep different crates in git submodules. They are definitely a bit unwieldy and create some mental overhead, but I like to keep track of the development history in each crate and it allows me to check out different versions of each crate, independent of the others. You can commit a combination of sub crate versions that work well together and users can get everything in one go with git clone --recursive
.
Keep an eye on your maintenance burden
Even though Rust tries to guarantee stability, maintaining crates still takes time. I think it’s good practice to go over your published crates every now and then to do a maintenance cycle. I will generally do:
- update dependencies
- have a look over the todo list for outstanding work and try to motivate myself to tick of a box
- re-read the readme for typos, things that are out of date, etc
- backport new discoveries from my crate template
- verify all tests still pass locally
- CI is still green
As you publish more and more crates, there is more and more maintenance. I think it’s better to publish one very good crate than ten mediocre ones, so keep an eye out for the upkeep costs and privilege polishing existing work over publishing new stuff.
Error handling
Unless you are really lucky, many things can fail for your code at runtime. Proper error handling is important and it’s a part of your public API. Always test your error handling code thoroughly. Create failures and write integration tests from the perspective of your users. Can they get the relevant information out of the errors you return?
Libraries usually have a custom error type, sometimes even one per module. There are many error frameworks available, but for libraries the most common advice is to stick to the error type from the standard library. It means you add a minimum of dependencies and compile time.
The author of sled wrote an interesting article about how they test for error conditions.
Logging
It is common for libraries to log certain events, mainly to help client developers with debugging. The three main logging frameworks currently are:
log is the most used. It’s convenience comes from letting you log from globally available macros. You don’t have to pass a logger around. It doesn’t officially support structured logging yet, but it’s being worked on. slog has structured logging and chooses to pass around a logger, to avoid using globals. tracing also has structured logging but is aimed at providing good features for logging from async code. Both frameworks have good interop infrastructure with the log crate if either libraries or applications choose to use log.
The important thing when publishing a crate is to think about your log statements before releasing. You probably added a bunch while developing and debugging. Are they useful for client code, or just noise? Are they at the correct log level. Consider that your logging, just as your error handling in general are public facing parts of your API.
Test
Please thoroughly test your code. Even for yourself. If you are going to write software that depends on your library you really don’t want to be debugging your library while you think you are debugging your application. And other users of your crate neither.
Debugging can be quite tedious, so when you are trying to move forward a project and you get interrupted by the need to go and debug a dependency, the forced context switch usually is really unwelcome. By thoroughly testing the crate before trying to use it, you give yourself peace of mind afterwards. You know you can count on it.
Ideally you cover every way to use your API with tests. Be sure to pass unusual values, like empty strings, or very long ones. Have at least an integration test for each main user story.
Fuzz test
If you take any input from outside your application (read files, db, user input, …) you should probably do fuzz testing. You still need proper sanitation and validation of course, but fuzz testing is a nice extra, and it’s pretty automated.
CI
I strongly suggest that you use continuous integration. There are many advantages, and several providers offer free services for open source projects.
- the CI builds your software and runs your tests on a fresh VM. Sometimes things will work on your machine but depend on something specific in your setup.
- test several platforms
- you have proof that you properly verified everything runs and builds when a user specific setup might be broken
- you can automate deployment
- you can upload code coverage reports to websites that will visualize them for you.
- you can set the CI up to run your test suite in a cron job, say every month, so you get alerted if something suddenly breaks for a crate you are not developing actively anymore.
Travis
I use travis for CI and can share a few tips that hopefully save you time. Github actions are also widely available for rust (just search) if you prefer Github. The Github API is probably a bit more convenient and powerful, but after Githubs last summer’s over zealous application of Trump’s delusions, I really feel I want to get less dependent on Github rather than more. They have had way to much centralizing power over the whole open source world for a long time and that was the drop.
So for travis, this is a sample configuration:
language: rust
# Need to cache the whole `.cargo` directory to keep .crates.toml for
# cargo-update to work
#
cache:
directories:
- /home/travis/.cargo
# But don't cache the cargo registry
# and remove wasm-pack binary to avoid the installer asking confirmation for overwriting it.
#
before_cache:
- rm -rf /home/travis/.cargo/git
- rm -rf /home/travis/.cargo/registry
- rm -rf /home/travis/.cargo/bin/wasm-pack
- rm -rf /home/travis/.cargo/bin/cargo-tarpaulin
- rm -rf target/debug/incremental/{{{crate_name}},build_script_build}-*
- rm -rf target/debug/.fingerprint/{{crate_name}}-*
- rm -rf target/debug/build/{{crate_name}}-*
- rm -rf target/debug/deps/lib{{crate_name}}-*
- rm -rf target/debug/deps/{{crate_name}}-*
- rm -rf target/debug/{{{crate_name}},lib{{crate_name}}}.d
- cargo clean -p {{crate_name}}
install:
# You only need this if you run wasm tests
#
- curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
branches:
only:
- master
- dev
jobs:
include:
- name: linux stable rust
os : linux
rust: stable
script:
- bash ci.bash
- bash ci_wasm.bash
- name: linux nightly rust
os : linux
dist: bionic # required for tarpaulin binary distribution to work.
rust: nightly
addons:
firefox: latest
apt:
packages:
- libssl-dev # for cargo-tarpaulin
- libgtk-3-dev # needed for headless (sic) firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1372998
- libdbus-glib-1-dev # firefox
script:
- bash ci.bash
- bash ci_wasm.bash
- bash coverage.bash
- name: osx stable rust
os : osx
rust: stable
addons:
firefox: latest
script:
- bash ci.bash
- bash ci_wasm.bash
- name: windows stable rust
os : windows
rust: stable
# for wasm tests
#
# addons:
# firefox: latest
script:
- bash ci.bash
# - bash ci_wasm.bash # can't find firefox binary, I'm giving up.
This is coverage.bash
, note above that this requires dist: bionic
and a dependency (libssl-dev
):
#!/usr/bin/bash
# fail fast
#
set -e
# print each command before it's executed
#
set -x
bash <(curl https://raw.githubusercontent.com/xd009642/tarpaulin/master/travis-install.sh)
# all your features but not docs as it requires nightly and no wasm specific stuff,
# so we can't use --all-features.
#
cargo tarpaulin --features "feature1 feature2" --exclude-files src/bindgen.rs --out Xml
bash <(curl -s https://codecov.io/bash)
By keeping the actual work in bash scripts, I keep the config simpler and I can also run the whole test suite at home.
Document
Good documentation is paramount. A library that isn’t documented might as well not exists. Good documentation is kind of hard and requires some work. I usually do it just before I release, that way I know that I don’t have to rewrite it if I change the API during development. In the very least it’s one of the steps to double check before releasing.
Users of your library should be able to tell exactly what they can expect from your API, including all corner cases without ever having to revert to dissecting the code. They shouldn’t have to write tests to figure out “What happens if I …?”.
If you take care that you understand your own code, and properly test all corner cases, you might as well jot down the conclusions while it’s fresh in your head. You’ll thank yourself later and so will your users.
Be sure to cover all errors your API returns, and what they mean. Be sure to mention if a function can panic and under what circumstances. Add a few examples in the docs and in a dedicated examples
folder. Be precise and verbose.
Here’s an introduction to documenting Rust crates, and the rustdoc book.
docs.rs builds are configurable from your Cargo.toml
. Two things I set are enable all features, so all of my code has documentation and setting targets to []
to only build with the default target as most of my crates don’t have platform specific functionality.
Unfortunately, rustdoc doesn’t automatically pick up on cargo features, but by enabling doc_cfg
, you can annotate parts of your API that are only available when certain features are enabled, or when compiling for a certain target. See this post for how to enable the feature:
#[ cfg( feature = "tokio_io" ) ]
//
#[ cfg_attr( nightly, doc(cfg( feature = "tokio_io" )) ) ]
//
impl<S> tokio::io::AsyncRead for WsStream<S>
{
fn poll_read( self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8] ) -> Poll< io::Result<usize> >
{
self.poll_read_impl( cx, buf )
}
}
Pro tip: When someone asks a question on your issue tracker, don’t answer them on the issue tracker. Complete your documentation instead (that is, unless the answer is RTFM of course).
README.md
There is a proposal for a standard readme format that you might find interesting. I have been using it. I don’t think it’s perfect, but it’s pretty good and consistent. The main interest is that it provides you with a checklist to avoid forgetting important things.
Keep it DRY
You can tell rustdoc to include your README.md at the top level of your crate without having to copy/paste the whole thing in lib.rs. It’s a little bit more complicated than you would hope. I wrote a separate blog post to explain how to work around some issues.
One downside of this is that doc tests won’t print correct line numbers for errors. They don’t keep track of the span in the README.md file just yet, but it is planned.
LICENSE
I don’t care for licensing much, but for some people and especially companies, being confident using your code is legal matters. By including a license, you won’t scare people away from your libraries. A common choice in the Rust ecosystem is dual licensing Apache + MIT, which is also used by the Rust project itself. Here is some unofficial explanation of why this combination was chosen. There are many alternatives out there for your choosing.
CHANGELOG.md
You should keep a changelog in which you describe at least the breaking changes between versions and what users have to do to update their crates. I put a link to the changelog in the readme telling people to read that when upgrading.
There are tools to generate a changelog automatically from the commit log, if you are rigorous with commit titles. Checkout jilu.
Checkout this manual changelog for a way to create links showing which commits are included in each version.
CONTRIBUTING.md
Adding contribution guidelines to your crate will allow you to specify a number of things people should consider before contributing and code hosting services will point people to them when they open pull requests and issues. You might want to specify :
- which branch to file pull requests to
- what license will apply to contributions
- a code of conduct if you want to protect your project from oppressive behaviors
- what formatting style contribution should adhere to
Cargo.toml
Note that I use cargo-yaml, so some of the examples are in yaml
format rather than toml
.
Metadata
Make sure you properly set the package section of Cargo.toml
. Give a nice description, set all categories and keywords that are relevant. You can find the categories on crates.io. Setting repository and documentation links means those links will be prominent on the crates.io page for your crate. Everything is nicely explained in the cargo manifest documentation.
These days you probably want to at least at edition = "2018"
and resolver = "2"
. Note that a new edition will come out in 2021 and that resolver won’t be needed anymore when you switch to the 2021 edition.
Keep a list handy of all steps leading to a release
I have it just above the version number:
# When releasing to crates.io:
#
# - last check for all TODO, FIXME, expect, unwrap, todo!, unreachable!.
# - recheck log statements (informative, none left that were just for development, ...)
# - `cargo +nightly doc --all-features --no-deps --open` and re-read and final polish of documentation.
#
# - Update CHANGELOG.md.
# - Update version numbers in Cargo.yml, Cargo.toml, install section of readme.
#
# - `touch **.rs && cargo clippy --tests --examples --benches --all-features`
# - `cargo update`
# - `cargo outdated --root-deps-only`
# - `cargo audit`
# - `cargo udeps --all-targets --all-features`
# - `cargo crev crate verify --show-all --recursive` and review.
# - 'cargo test --all-targets --all-features'
#
# - push dev and verify CI result
# - `cargo test` on dependent crates
#
# - cargo publish
# - `git checkout master && git merge dev --no-ff`
# - `git tag x.x.x` with version number.
# - `git push && git push --tags`
#
version: 0.1.0
It makes it way easier not to forget an important step. Before a first release you should re-read all the code. Yes, all of it. Make sure it’s very clear what’s happening and why, add code comments. Fix anything that is suboptimal. Here is another example of such a release checklist from ripgrep.
Exclude unnecessary files from packaging
It’s possible to use package.exclude
to avoid bundling tests, examples, benchmarks and so on in the packages that get’s deployed to crates.io. Just note that users now need internet access to access these resources in case they count on cargo having downloaded your crate for offline use. I definitely don’t recommend removing the README.md and LICENSE files.
In any case you should make sure that big files that aren’t needed for linking against your crate are not included.
After publishing you can check the lean crate initiative to see if you forgot to exclude anything.
Document cargo features
Cargo has a mechanism called features that let’s you compile part of your code conditionally. I advise documenting the features in both Cargo.toml
with comments as well as in the readme. This reduces friction for using your crate.
Indicate which dependencies are public
When you use types from other crates in your public API, you now have to bump a major version when they do. In order not to forget that and make things easier for other contributors, I suggest you mark it clearly in Cargo.toml
. This is from my crate template:
# Cargo.yml
#
dependencies:
# Public dependencies (bump major if changing any version number here)
# Means we use types from these crates in our public API.
#
futures-core: ^0.3
# Private dependencies
#
cargo yaml
I personally feel yaml both reads and writes much nicer than TOML. It’s a bit of a pity to have to keep 2 files, because cargo itself will not read the yaml file, but it’s so much more pleasant to work with. Yaml notably has the benefit of having much less boilerplate like quotes.
I wrap cargo in a little fish script in /usr/local/bin/cargo
to automatically convert Cargo.yml
so I don’t run into issues by forgetting it. Only moment to pay attention is when changing Cargo.yml
and committing the changes without ever running cargo.
#!/usr/bin/fish
if test -e Cargo.yaml # if it exists.
mv Cargo.yaml Cargo.yml
end
if test -e Cargo.yml # if Cargo.yml exists
if command test ! -e Cargo.toml -o Cargo.yml -nt Cargo.toml # and it's newer than Cargo.toml
/usr/bin/cargo yaml # run cargo-yaml
end
end
/usr/bin/cargo $argv # call actual cargo command passing arguments
There is a few downsides to this. Notably cargo-yaml is a bit old and unmaintained and it misses some polish. It will interpret version numbers as floating points if they are in the form x.y
, so I usually prefix them with ^
. Urls and other strings containing :
also need to be quoted.
Dependencies
Keep it modest
Dependencies are awesome, because they give you plenty of functionality you don’t have to write yourself, but… They also increase compile times and bloat binary sizes. However, don’t fixate on the number of dependencies like the a cow watching a passing train. You should generally prefer using crates that provide the functionality you need rather than hand-rolling things. Crates that are small, do one thing and do it well will not add much to your compile times. They will not create much mental overhead since probably they don’t contain many bugs/updates. They enable the whole ecosystem to scrutinize and benefit from the same code and make it best in class. Handrolling has none of these advantages and won’t compile any faster.
One major thing to to do is to opt out of default features: default-features: false
and then enable just the parts you need.
cargo-udeps can help you detect lingering dependencies in your Cargo.toml
that you no longer use in your code. You want to make sure you cover all targets and features before removing them:
# first test our main dependencies
#
cargo +nightly udeps --all-features
# now we can also test our dev dependencies
#
cargo +nightly udeps --all-features --tests --examples --benches
Scrutinize your dependencies
Your dependencies become part of your product. Bugs in your dependencies are as bad as bugs in your own code as far as your users are concerned. “It wasn’t my fault” doesn’t make your software more reliable.
You should make sure that you understand exactly what your dependencies do. That way you can reason about what your program/library does. If you call a function of which you aren’t really sure how it behaves, how do you know how your resulting code behaves?
One library I use was public for two years, had 23 dependent crates published on crates.io and the maintainers are quite active to react to issues, but I found and fixed 3 bugs and reported a fourth, before I would consider adding it as a dependency. I also corrected and completed the documentation. This makes me wonder how all these other dependent crates could have taken it as a dependency without understanding what it does? Had they understood what it did, they should have realized it was doing the wrong things.
It took me several weeks to scrutinize the code, write tests, find and fix bugs and engage with the developers until they understood what was going on in their own code, just to be able to add a dependency. That’s why this article stresses so much how important it is to have good documentation and tests. I’ll happily admit being a nitpick and a perfectionist, but software engineering is an exact science after all.
Your unit and integration tests are a good place to verify your assumptions about you dependencies behavior. Write your tests so they will fail if a dependency doesn’t do exactly what you expected from it.
Security
Dependencies are code that get’s compiled in any program that uses your library. As such you are responsible for the security implications of including those dependencies. There is two distinct risks involved:
- Security vulnerabilities in dependencies
- Malicious code in dependencies
I think it’s safe to say that almost nobody does a full security audit of all of the crates in their dependency tree. It’s simply to much work and requires expertise too. However people do security audits and when they find security issues these do (sometimes) get filed to the RustSec Advisory Database. You should at least run cargo-audit
before releasing a crate to verify if any of your dependencies have known vulnerabilities. Also if someone files an issue on one of your crates that can be exploited, you should file an advisory if they haven’t already. Ideally run cargo-audit
and other tools that can give you warnings like cargo-deny
and cargo-crev
after running cargo update
and before compiling (build.rs) or running (cargo test
) new versions of dependencies, because you are running arbitrary code coming from the internet on your dev machine.
When updating dependencies, some maintainer down the line might have introduced malicious code. Unfortunately looking at changelogs won’t help you here. Neither will semver. And it’s important to understand that in terms of security, the “Repository” link on crates.io has nothing to do with the code cargo uses to build software. cargo publish
allows uploading crates to crates.io that have never been in any for of source version control, let alone be published online. There is strictly no way to get a clean commit history of all changes that come between 2 versions of a crate automatically.
cargo-review-deps is a tool that let’s you check out the actual code used by cargo to a local directory and also see some diffs when updating. The best I can think of right now to scrutinize what has changed is to take the official repository of the project. Check it out at the version you are interested in. Now copy over the files checked out by cargo-review-deps. Now you can verify the difference between the two. After verifying these differences, you can now use the commit log to see changes between two versions.
Use cargo-deny in CI
cargo-deny is a very nice tool that allows you to put conditions on your dependencies. Conditions can be things like license compatibility or “no open security vulnerabilities”. You can also exclude a specific crate or version of a crate from your entire dependency graph. I suggest you look into it and possibly run it on CI.
Updating dependencies
I usually start with cargo update
and then run cargo outdated --root-deps-only
. That last one will show you all the breaking changes in your version numbers. Go over to the repositories for each of them, read the changelog and then update your Cargo.toml
. Run tests and update code as needed. Don’t forget to bump your major version number if you updated any dependencies that you publicly expose.
The RANT part
Do your own chores
Maybe you can relate to this story:
You are in need of functionality X. If by any chance you don’t have to write this yourself, it would save you quite some time, so you are delighted to find there are already 5 libraries out there that provide X.
You try to figure out which one is the least mediocre and go with that. After realizing the docs and tests are meager to non-existent, you have no choice but to wade through the somewhat complicated source code and sometimes write tests to figure out what the API actually does, so you can use it correctly and with confidence, as well as be precise in your own documentation, as the behavior of your dependencies will affect the behavior of your own product.
So you find one or several bugs, while reading or testing, so you head over to Github to file them, or to file a PR with a fix. I usually let git blame tell me who introduced the bug so I can ping them, as I imagine they’d want to know about it. So you get the inevitable (so it seems) hint that the right way to go about this is to file a bugfix PR with regression tests, and while you are at it, could you update our CI configuration too?
At this point I start to boil over. From the frustration with the poor average quality of open source libraries and from having lost several hours already just to consider using something. From the anticipation that someone might get cross, because no, really no, I’m not gonna do your chores, I have enough of my own [1].
So please, do your own chores. Take some responsibility for the code you publish (or for the modifications you make to other people’s code). Clarify in your readme when people should expect something to be for educational or personal use only. Write docs and tests, and if despite this, you do introduce a bug in code that others depend on, please be responsible and fix it.
If people submit feature requests, I find it entirely normal to ask them to file pull requests. You don’t have to create and maintain functionality on demand and for free, however own your bugs and your lack of tests and docs. Nobody likes writing tests and documentation, so don’t try to offload that onto others just because they dare report your bugs.
[1] If you are the kind of person that asks nicely and that gracefully accepts declination, know that this section is not about you!
Conclusion
Few, that’s more than I thought. I hope this can be a help to beginning Rust users and that it might help increase the quality of our eco-system.