website/docs/guides/rust/handbook.mdx
import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';
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.
:::info
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.
To enable Rust, define the rust setting in
.moon/toolchains.*, even if an empty object.
# Enable Rust
rust: {}
# Enable Rust and override default settings
rust:
syncToolchainConfig: true
Or by pinning a rust version in .prototools in the workspace root.
rust = "1.93.0"
This will enable the Rust toolchain and provide the following automations around its ecosystem:
~/.cargo/bin) are properly located and executed.rust-toolchain.toml configuration files.package.name from Cargo.toml as a project alias.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
sync.
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
rust.version setting.
# Enable Rust toolchain with an explicit version
rust:
version: '1.69.0'
Versions can also be defined with
.prototools.
:::caution
moon requires rustup to exist in the environment, and will use this to install the necessary Rust
toolchains. moon will attempt to auto-install rustup if it's not found, but this may fail in some
environments.
:::
Rust/Cargo repositories come in two flavors: a single crate with one Cargo.toml, or multiple
crates with many Cargo.tomls using
Cargo workspaces. The latter is
highly preferred as it enables Cargo incremental caching.
With moon, you can place moon.* in each crate, or once relative to
Cargo.lock. The choice is yours! But be aware, if you do per-crate, Cargo itself will "wait for
build lock" when multiple processes are running.
An example of this layout is demonstrated below:
<Tabs groupId="repo-layout" defaultValue="workspaces" values={[ { label: 'Workspaces', value: 'workspaces' }, { label: 'Workspaces (per-crate)', value: 'workspaces-many' }, { label: 'Non-workspaces', value: 'project' }, ]}
<TabItem value="workspaces">
/
├── .moon/
├── crates/
│ ├── client/
| │ ├── ...
│ │ └── Cargo.toml
│ ├── server/
| │ ├── ...
│ │ └── Cargo.toml
│ └── utils/
| ├── ...
│ └── Cargo.toml
├── target/
├── Cargo.lock
├── Cargo.toml
└── moon.yml
/
├── .moon/
├── crates/
│ ├── client/
| │ ├── ...
| │ ├── moon.yml
│ │ └── Cargo.toml
│ ├── server/
| │ ├── ...
| │ ├── moon.yml
│ │ └── Cargo.toml
│ └── utils/
| ├── ...
| │ ├── moon.yml
│ └── Cargo.toml
├── target/
├── Cargo.lock
└── Cargo.toml
/
├── .moon/
├── src/
│ └── lib.rs
├── tests/
│ └── ...
├── target/
├── Cargo.lock
├── Cargo.toml
└── moon.yml
moon.*The following configuration represents a base that covers most Rust projects.
<Tabs groupId="repo-layout" defaultValue="workspaces" values={[ { label: 'Workspaces', value: 'workspaces' }, { label: 'Workspaces (per-crate) / Non-workspaces', value: 'project' }, ]}
<TabItem value="workspaces">
language: 'rust'
layer: 'application'
env:
CARGO_TERM_COLOR: 'always'
fileGroups:
sources:
- 'crates/*/src/**/*'
- 'crates/*/Cargo.toml'
- 'Cargo.toml'
tests:
- 'crates/*/benches/**/*'
- 'crates/*/tests/**/*'
tasks:
build:
command: 'cargo build'
inputs:
- '@globs(sources)'
check:
command: 'cargo check --workspace'
inputs:
- '@globs(sources)'
format:
command: 'cargo fmt --all --check'
inputs:
- '@globs(sources)'
- '@globs(tests)'
lint:
command: 'cargo clippy --workspace'
inputs:
- '@globs(sources)'
- '@globs(tests)'
test:
command: 'cargo test --workspace'
inputs:
- '@globs(sources)'
- '@globs(tests)'
language: 'rust'
layer: 'application'
env:
CARGO_TERM_COLOR: 'always'
fileGroups:
sources:
- 'src/**/*'
- 'Cargo.toml'
tests:
- 'benches/**/*'
- 'tests/**/*'
tasks:
build:
command: 'cargo build'
inputs:
- '@globs(sources)'
check:
command: 'cargo check'
inputs:
- '@globs(sources)'
format:
command: 'cargo fmt --check'
inputs:
- '@globs(sources)'
- '@globs(tests)'
lint:
command: 'cargo clippy'
inputs:
- '@globs(sources)'
- '@globs(tests)'
test:
command: 'cargo test'
inputs:
- '@globs(sources)'
- '@globs(tests)'
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 rust.bins setting.
This setting requires a list of crates (with optional versions) to install, and moon will install
them as part of the task runner install dependencies action. Furthermore, binaries will be installed
with cargo-binstall in an effort to reduce build and
compilation times.
rust:
bins:
- '[email protected]'
- 'cargo-nextest'
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.
tasks:
test:
command: 'nextest run --workspace'
toolchain: 'rust'
:::tip
The cargo-binstall crate may require a GITHUB_TOKEN environment variable to make GitHub Releases
API requests, especially in CI. If you're being rate limited, or fail to find a download, try
creating a token with necessary permissions.
:::
To expand our integration even further, we also take Cargo.lock into account, and apply the
following automations when a target is being ran:
cargo generate-lockfile.target directory as an output?No, definitely not! 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:
tasks:
build:
command: 'cargo build --release'
outputs: ['target/release/moon']
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 target directory
problem above.