Skip to main content

Rust handbook

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.

moon setup

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.

.moon/toolchain.yml
# Enable Rust
rust: {}

# Enable Rust and override default settings
rust:
syncToolchainConfig: true

Or by pinning a rust version in .prototools in the workspace root.

.prototools
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 rust-toolchain.toml configuration files.
  • For non-workspaces, will inherit package.name from Cargo.toml as 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 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.

.moon/toolchain.yml
# 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. This requires Rust to be manually installed on the machine, as moon does not auto-install the language, just the toolchains.

Repository structure

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.

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 to Cargo.lock and the target folder.

An example of this layout is demonstrated below:

/
├── .moon/
├── crates/
│ ├── client/
| │ ├── ...
│ │ └── Cargo.toml
│ ├── server/
| │ ├── ...
│ │ └── Cargo.toml
│ └── utils/
| ├── ...
│ └── Cargo.toml
├── target/
├── Cargo.lock
├── Cargo.toml
└── moon.yml

Example moon.yml

The following configuration represents a base that covers most Rust projects.

<project>/moon.yml
language: 'rust'
type: '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)'

Cargo integration

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.

Global binaries

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 InstallRustDeps action. Furthermore, binaries will be installed with cargo-binstall in an effort to reduce build and compilation times.

.moon/toolchain.yml
rust:
bins:
- 'cargo-make@0.35.0'
- '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.

<project>/moon.yml
tasks:
test:
command: 'nextest run --workspace'
platform: '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.

Lockfile handling

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 cargo generate-lockfile.
  • We parse and extract the resolved checksums and versions for more accurate hashing.

FAQ

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:

moon.yml
tasks:
build:
command: 'cargo build --release'
outputs: ['target/release/moon']

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 target directory problem above.

  1. If you use GitHub Actions, feel free to use our moonrepo/setup-rust action, which has built-in caching.
  2. 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 caching cargo check, cargo clippy, and other Cargo tasks that do not produce direct outputs.

.moon/workspace.yml
runner:
archivableTargets:
- 'rust:check'
- 'rust:lint'
- 'rust:test'