Utilizing Rust in a monorepo is a trivial task, thanks to Cargo, and also moon. With this handbook, we'll help guide you through this process.
moon is not a build system and does not replace Cargo. Instead, moon runs
cargo commands, and
efficiently orchestrates those tasks within the workspace.
For this part of the handbook, we'll be focusing on moon, our task runner. To start, languages in moon act like plugins, where their functionality and support is not enabled unless explicitly configured. We follow this approach to avoid unnecessary overhead.
Enabling the language
To enable Rust, define the
rust setting in
.moon/toolchain.yml, even if an empty object.
# Enable Rust
# Enable Rust and override default settings
Or by pinning a
rust version in
.prototools in the workspace root.
rust = "1.69.0"
This will enable the Rust platform and provide the following automations around its ecosystem:
- Manifests and lockfiles are parsed for accurate dependency versions for hashing purposes.
- Cargo binaries (in
~/.cargo/bin) are properly located and executed.
- Automatically sync
- For non-workspaces, will inherit
Cargo.tomlas a project alias.
- And more to come!
Utilizing the toolchain
When a language is enabled, moon by default will assume that the language's binary is available
within the current environment (typically on
PATH). This has the downside of requiring all
developers and machines to manually install the correct version of the language, and to stay in
Instead, you can utilize moon's toolchain, which will download and install the language in the background, and ensure every task is executed using the exact version across all machines.
Enabling the toolchain is as simple as defining the
# Enable Rust toolchain with an explicit version
Versions can also be defined with
rustup to exist in the environment, and will use this to install the necessary Rust
toolchains. This requires Rust to be manually installed on the machine, as moon does not
auto-install the language, just the toolchains.
Rust/Cargo repositories come in two flavors: a single crate with one
Cargo.toml, or multiple
crates with many
Cargo workspaces. The latter is
highly preferred as it enables Cargo incremental caching.
Regardless of which flavor your repository uses, in moon, both flavors are a single
moon project. This means that all Rust crates are grouped together into a
single moon project, and the
moon.yml file is located at the root relative
Cargo.lock and the
An example of this layout is demonstrated below:
│ ├── client/
| │ ├── ...
│ │ └── Cargo.toml
│ ├── server/
| │ ├── ...
│ │ └── Cargo.toml
│ └── utils/
| ├── ...
│ └── Cargo.toml
│ └── lib.rs
│ └── ...
The following configuration represents a base that covers most Rust projects.
command: 'cargo build'
command: 'cargo check --workspace'
command: 'cargo fmt --all --check'
command: 'cargo clippy --workspace'
command: 'cargo test --workspace'
command: 'cargo build'
command: 'cargo check'
command: 'cargo fmt --check'
command: 'cargo clippy'
command: 'cargo test'
You can't use Rust without Cargo -- well you could but why would you do that? With moon, we're doing our best to integrate with Cargo as much as possible. Here's a few of the benefits we currently provide.
Cargo supports global binaries through the
cargo install command, which
installs a crate to
~/.cargo/bin, or makes it available through the
cargo <crate> command. These
are extremely beneficial for development, but they do require every developer to manually install
the crate (and appropriate version) to their machine.
With moon, this is no longer an issue with the
This setting requires a list of crates (with optional versions) to install, and moon will install
them as part of the task runner
InstallRustDeps action. Furthermore, binaries will be installed
cargo-binstall in an effort to reduce build and
At this point, tasks can be configured to run this binary as a command. The
cargo prefix is
optional, as we'll inject it when necessary.
command: 'nextest run --workspace'
To expand our integration even further, we also take
Cargo.lock into account, and apply the
following automations when a target is being ran:
- If the lockfile does not exist, we generate one with
- We parse and extract the resolved checksums and versions for more accurate hashing.
Should we cache the
target directory as an output?
No, we don't believe so. Both moon and Cargo support incremental caching, but they're not entirely compatible, and will most likely cause problems when used together.
The biggest factor is that moon's caching and hydration uses a tarball strategy, where each task would unpack a tarball on cache hit, and archive a tarball on cache miss. The Cargo target directory is extremely large (moon's is around 50gb), and coupling this with our tarball strategy is not viable. This would cause massive performance degradation.
However, at maximum, you could cache the compiled binary itself as an output, instead of the entire target directory. Example:
command: 'cargo build --release'
How can we improve CI times?
Rust is known for slow build times and CI is no exception. With that being said, there are a few patterns to help alleviate this, both on the moon side and outside of it.
To start, you can cache Rust builds in CI. This is a non-moon solution to the
- If you use GitHub Actions, feel free to use our moonrepo/setup-rust action, which has built-in caching.
- A more integrated solution is sccache, which stores build artifacts in a cloud storage provider.
For moon, if you're looking to persist task results across CI runs, you can utilize the
runner.archivableTargets setting. This is useful for
cargo clippy, and other Cargo tasks that do not produce direct outputs.