Unless I'm missing some big piece in the Rust ecosystem (which could totally be true), is everyone just ignoring the pains of managing teams working in Rust, and the global dependencies were all asked to install? Is that really the right way to do things?
Hear me out.
Take the fictional scenario that I'm a new guy joining a team of five working on a Rust project. I'm introduced to cargo-awesometestrunner
as they all use it to run the test suite of the app. So I head over to the docs, and I see cargo install cargo-awesometestrunner --locked
, paste it in my terminal, and bam I'm ready to go.
But wait...
Jane on the team says, "wait a sec, let's be sure were using the same version so we can repro any problems together". Sure, that makes sense. So I run cargo install cargo-awesometestrunner --version 0.5.23 --locked
because that's what she's got installed.
Bob a desk over is like "Hang on Jane, why is your version so old? Didn't you upgrade to version X to fix the bug Y we saw last week?"
Jane is now confused. "What bug? Did I miss the Slack message sent to @channel to upgrade to latest?".
Bob looks, realizing he forgot to send it.
Lizzy at the end of the table heard the discussion through her headphones and brings up "We can't install the new version yet, project ABC isn't compatible. We gotta stay on the old version".
Jane is now frustrated, Lizzy is distracted, Bob is annoyed.
And then there's me, wondering why we all install things globally.
I've touched a bunch of languages and tool sets over the years. Professionally I've mainly done Typescript/Javascript, Python, and Go. In the JS world, npm+friends scopes all binary dependencies within node_modules
, allowing them to be accessed by running something like npm run eslint --help
. Python does something similar where all dependencies are inside a virtual environment (.venv
) that you activate to update your shells paths.
As part of the official tooling, Rust is missing that at the moment. That's totally fine, they're busy building the really important stuff. However, when I'm onboarding new people, or full teams on to the Rust language coming from ecosystems like JS and Python, this can be hard to manage. CI is another story. It's frustrating when something unique is breaking your CI workflows, only to find it's because some global dependency updated that you forgot to pin.
That's where cargo-run-bin
comes in. Instead of installing all the additional Rust CLIs and Cargo extensions globally, you just install cargo-run-bin, and it scopes all your tooling to each project.
It works by adding a block in your Cargo.toml specifying all the tooling and versions you'd like attached and available to the developers working on your project. For example, cargo-run-bin has the following tools:
[package.metadata.bin]
cargo-binstall = { version = "1.4.4" }
cargo-cmd = { version = "0.3.1" }
cargo-deny = { version = "0.13.5" }
cargo-insta = { version = "1.31.0", locked = true }
cargo-llvm-cov = { version = "0.5.25" }
cargo-nextest = { version = "0.9.57", locked = true }
cargo-release = { version = "0.24.11" }
cargo-watch = { version = "8.4.0" }
committed = { version = "1.0.20" }
dprint = { version = "0.40.2" }
git-cliff = { version = "1.3.1" }
Now, for something like dprint
, I can run cargo bin dprint
and it'll run the exact version that's specified in Cargo.toml. Any local or CI script that is executing dprint
, I can go and update with the new cargo bin
prefix, and I'll for sure have all versions are locked in.
If you don't want to wait to every dependency to build, run-bin can be paired with cargo-binstall
where it'll look to download precompiled releases before attempting to build from source simply by adding it to [package.metadata.bin]
.
The real killer feature that cargo-run-bin has is the capability to assigning Cargo aliases for any of the cargo-*
tools by running cargo bin --sync-aliases
, where it adds lines to .cargo/config.toml
. With this, the conversation between my fictional team and I never happen. By running cargo nextest run
, run-bin will download, cache, and execute version 0.9.57
.
This is not the first time I've solved this issue. Every time I pick up a new language, I look for what may be missing in my development experience, and build that first! Go has the same problem, and I solved for my own apps a couple of years back with gomodrun, which does close enough to the same thing by producing a tools.go file for all binaries, and updating the lock file accordingly.
If you're having the same kinda issues as my fictional team, or are preparing for the release of your new open source project, check out cargo-run-bin on GitHub! If you're looking to scope binaries that live outside the Rust ecosystem, the sister project cargo-gha
gets the job done.
Thanks for reading!