How do you organize Rust projects with multiple binaries so that the build
output winds up in a common subdirectory? Should you be looking for a solution
other than cargo? Regardless of whether you are using nested crates within a
workspace or simply a mixture of .rs
files under src/bin/
, you absolutely
should be looking for something other than cargo. What you need is a proper
task runner and the most portable task runner ships with every unix
flavored operating system; sh
.
People seem to conflate task runners with build tools. Build tools generate
artifacts such as binaries or libraries whereas task runners act as the glue for
teams to share ways to achieve particular chores. Some people use tools like
make
to do both jobs and the crossed responsibility brings a lot of pain and
maintenance burden. People need to be aware of the many nuances of make
such
as the fact that tabs for indenting are semantic, rules for tasks need to be
marked as .PHONY
if there is a target they relate to, and so on. Others end up
using scripting languages such as python or javascript or they may use some
hybrid domain specific language that mixes a bit of programming and
configuration to specify how tasks are run, e.g. gulp
. You don't need any of
these options.
I'll call this script bin/build
. We will assume there are several crates in a
workspace for this example and that we use git
since cargo bootstraps projects
with it by default.
#!/bin/sh -eux
ROOT=$(git rev-parse --show-toplevel)
cd "$ROOT"
mkdir -p dist/bin
for crate in crate1 crate2 crate3; do
cd "$crate"
cargo build --release
cp target/release/$crate "$ROOT/dist/bin/"
cd "$ROOT"
done
This script is dead-simple. It shoots to the root of the project, makes the
directories dist
and its subdirectory bin
. We have a list of crates in a
loop we iterate across but we could make this dynamic, as well. Then, in each
crate we create a release build and copy the binary from the project up to the
common subdirectory. Then, we shoot back to the root directory again and repeat.
All we have to do now to do now is make the script executable and call it:
$ chmod +x bin/build
$ bin/build
You don't need to let scripts grow out of control, either. What's awesome about keeping scripts, and, more generally, programs small means you can compose things like this:
bin/init
bin/run
Where init
might do some stubbing or setup work and run
might launch a
service, whatever those tasks may be.
sh
is POSIX compliant, which means it allows us to write highly portable, and
therefore shareable, scripts. Like anything there are ways things can go wrong
but you can address this by using the linter
shellcheck. Every shell script you
write should have the following
#!/bin/sh -eux
Which says to use sh
instead of, say, bash
. shellcheck will actually
recommend things intelligently based on which shell you specify. bash
is not
ideal here because support for particular features differs between versions and
we are aiming to have something pretty much anyone on a team can use at a
moment's notice so long as they are using linux, bsd, darwin, or any other *nix
flavor. This prelude also turns on some common flags.
- e to stop on the first error
- u to stop if a variable is unset
- x to print tracing output of each executed statement
(3) can be optionally dropped if you don't want to expose details or want cleaner output.
The last convention is to keep scripts in a common bin
directory at the root
of your project which enhances discoverability of scripts for others. Allowing
people to make less guesses about which directory is the single source of truth
for automation scripts helps people move faster. If they want a chore done, they
can see what's present under bin
, or if they need to add a chore they know
exactly where it's added for every project. The reason for why its called bin
is that they are executables!
In summary, for shell script success all you need is:
- A common prelude that uses
sh
and some options set - Using shellcheck to ensure you're writing sensible and POSIX compliant scripts
- A common directory for scripts that is the same for all projects