Introduction
rx is a fast local-CI and task runner for Rust workspaces. Define your pipeline once, run it the same way locally and in CI.
The problem
CI failures you could have caught locally waste round-trips, and every project reinvents the same fmt/clippy/test/build pipeline in shell scripts and YAML. Workspace-aware concerns — which packages did my change affect? what order do tasks run in? — get solved ad hoc, per repo.
What rx does
- One-command CI —
rx ciruns your full pipeline locally - Task runner — define tasks once in
[tasks]withdepends-onpipelines, run them anywhere withrx run - Affected-only testing —
rx test --affectedtests only packages changed since a base ref - Workspace orchestration — dependency-aware parallel execution across members
- Unified commands —
rx testpicks nextest when available,rx lintruns clippy strict,rx fmtruns rustfmt - Fast builds — auto-detected
mold/lldlinkers - CI generation —
rx init --ciwrites a workflow that mirrors your local pipeline
rx deliberately does not replace Cargo, rustup, or the cargo plugin ecosystem. Compilation scheduling and target/ belong to Cargo; compiler caching belongs to tools like sccache; releases, audits, and scaffolding belong to their dedicated tools. See PRODUCT.md for the full list of non-goals.
Getting started
curl -fsSL https://raw.githubusercontent.com/iPeluwa/rx/master/install.sh | sh
cd my-rust-project
rx init --ci
rx ci
Installation
One-liner
curl -fsSL https://raw.githubusercontent.com/iPeluwa/rx/master/install.sh | sh
Downloads a prebuilt binary for your platform (Linux, macOS, Windows/MSYS), or falls back to cargo install from source.
From source
cargo install --path .
GitHub Action
- uses: iPeluwa/rx@v1
with:
command: ci
Shell completions
rx completions bash >> ~/.bashrc
rx completions zsh >> ~/.zshrc
rx completions fish > ~/.config/fish/completions/rx.fish
rx completions powershell >> $PROFILE
Completions are context-aware — they include workspace members, installed targets, and tasks.
Quick Start
Set up a project
cd my-rust-project
rx init # generate rx.toml with smart defaults
rx init --ci # also generate .github/workflows/ci.yml
rx init --migrate # detect existing tools and configure
rx config # show resolved configuration
Daily workflow
rx check # fast type-check feedback
rx build # build with fast linker
rx run # build and run
rx test # run tests (nextest if available)
rx lint # clippy with strict defaults
rx fmt # rustfmt
rx fix # auto-fix everything in one pass
rx ci # run full CI pipeline locally
Workspaces
rx graph # see the dependency graph
rx ws run build # build all members in dependency order
rx test --affected # only test packages your change touched
Tasks
Define project tasks once in rx.toml:
[tasks]
bench = "cargo bench"
[tasks.ci]
depends-on = ["fmt", "lint", "test", "build"]
Then run them locally or in CI:
rx run ci # runs fmt, lint, test, build through the task graph
rx run bench
rx run # list all available tasks
fmt, lint, test, build, and check are built-in tasks, so a ci pipeline needs no shell commands at all. Independent tasks run concurrently.
Configuration
rx uses a TOML configuration file called rx.toml to control build, test, lint, format, and watch behavior. Configuration is optional – rx works out of the box with sensible defaults.
Generating rx.toml
rx init # generate rx.toml with smart defaults
rx init --migrate # detect existing tools and generate tailored config
rx init --ci # also generate .github/workflows/ci.yml
rx init inspects your project and applies smart defaults:
- A default
citask pipeline (fmt, lint, test, build) is defined - If
moldorlldis detected, it is set as the default linker - If
cargo-nextestis installed, the test runner is set to"auto"
rx init --migrate goes further by detecting Makefiles, benchmarks, error handling crates, and other project-specific patterns.
File structure
[build]
linker = "auto" # "auto", "mold", "lld", or "system"
rustflags = [] # extra RUSTFLAGS
cache = false # opt-in global artifact cache (see PRODUCT.md)
jobs = 0 # parallel jobs (0 = auto-detect CPU count)
incremental_link = true # split-debuginfo and --as-needed
[test]
runner = "auto" # "auto", "nextest", or "cargo"
extra_args = []
[lint]
severity = "deny" # "deny", "warn", or "allow"
extra_lints = [] # e.g. ["clippy::pedantic"]
[fmt]
extra_args = []
[tasks]
bench = "cargo bench"
[tasks.ci]
depends-on = ["fmt", "lint", "test", "build"]
[env]
RUST_BACKTRACE = "1"
Global vs project config
rx resolves configuration by merging two files:
- Global config at
~/.rx/config.toml– applies to all projects - Project config at
./rx.toml– applies to the current project
Project values override global values. This lets you set personal defaults (like a preferred linker) globally while allowing each project to customize as needed.
rx config # show the fully resolved configuration
Unknown key warnings
rx validates every key in rx.toml. If you mistype a key name, rx prints a warning rather than silently ignoring it:
warning: unknown key `buld` in rx.toml (did you mean `build`?)
This catches typos that would otherwise lead to confusing behavior.
Environment variables
The [env] section sets environment variables for all rx commands:
[env]
RUST_BACKTRACE = "1"
RUST_LOG = "info"
Profiles
Override any configuration per context with [profile.<name>]. See the Profiles chapter for details.
See also
- rx.toml Reference – full field reference
- Profiles – profile-based overrides
rx.toml Reference
[build]
| Key | Type | Default | Description |
|---|---|---|---|
linker | string | "auto" | Linker to use: "auto", "mold", "lld", or "system" |
rustflags | string[] | [] | Extra RUSTFLAGS to append |
cache | bool | false | Enable the global artifact cache (opt-in; fingerprint does not yet cover target triple, features, toolchain, or build scripts — see PRODUCT.md) |
jobs | u32 | 0 | Parallel jobs (0 = auto-detect CPU count) |
incremental_link | bool | true | Enable incremental linking (split-debuginfo, –as-needed) |
[test]
| Key | Type | Default | Description |
|---|---|---|---|
runner | string | "auto" | Test runner: "auto", "nextest", or "cargo" |
extra_args | string[] | [] | Extra arguments always passed to the test runner |
[lint]
| Key | Type | Default | Description |
|---|---|---|---|
severity | string | "deny" | Clippy severity: "deny", "warn", or "allow" |
extra_lints | string[] | [] | Extra clippy lints (e.g. "clippy::pedantic") |
[fmt]
| Key | Type | Default | Description |
|---|---|---|---|
extra_args | string[] | [] | Extra rustfmt arguments |
[tasks]
Named tasks for rx run. A task is a shell command (string shorthand), or a table with a command and/or depends-on list. Built-in tasks (fmt, lint, test, build, check, ci) are used for names you don’t define; defining the name overrides the built-in.
[tasks]
deploy = "cargo build --release && scp target/release/myapp server:/opt/"
[tasks.ci]
depends-on = ["fmt", "lint", "test", "build"]
[scripts] (deprecated)
Legacy alias for [tasks]: entries are plain name = "command" pairs and are merged into the task table ([tasks] wins on name collisions).
[env]
Key-value pairs of environment variables set for all rx commands:
[env]
RUST_BACKTRACE = "1"
DATABASE_URL = "postgres://localhost/dev"
[profile.<name>]
Override settings per context. See Profiles.
Profiles
Profiles let you override rx configuration for different contexts – CI, development, release builds, or any custom scenario.
Defining a profile
Add a [profile.<name>] section to your rx.toml. Each profile can override build, lint, test, and env settings:
[profile.ci]
build = { cache = false, jobs = 2 }
lint = { severity = "deny" }
test = { runner = "nextest" }
env = { CI = "true" }
Using a profile
Pass --profile <name> to any rx command:
rx --profile ci build
rx --profile ci test
rx --profile ci lint
The profile flag is a global flag, so it must appear before the subcommand.
How merging works
When a profile is active, rx starts with the base configuration (global + project) and then applies the profile overrides on top. Only the fields explicitly set in the profile are changed; everything else keeps its base value.
For example, given this configuration:
[build]
linker = "mold"
cache = true
jobs = 0
[profile.ci]
build = { cache = false, jobs = 2 }
Running rx --profile ci build uses:
| Field | Value | Source |
|---|---|---|
linker | "mold" | base config |
cache | false | profile override |
jobs | 2 | profile override |
Common profiles
CI profile
Disable caching (CI runners are ephemeral) and enforce strict linting:
[profile.ci]
build = { cache = false, jobs = 2 }
lint = { severity = "deny" }
test = { runner = "nextest" }
env = { CI = "true", RUST_BACKTRACE = "1" }
Release profile
Use the system linker for maximum compatibility:
[profile.release]
build = { cache = true }
env = { RUSTFLAGS = "-C target-cpu=native" }
Minimal profile
Fast iteration during development:
[profile.quick]
build = { cache = true }
lint = { severity = "warn" }
Profile environment variables
The env field in a profile merges with (and overrides) the base [env] section:
[env]
RUST_BACKTRACE = "1"
RUST_LOG = "info"
[profile.ci]
env = { RUST_LOG = "warn", CI = "true" }
With --profile ci, RUST_BACKTRACE remains "1", RUST_LOG becomes "warn", and CI is set to "true".
Overridable fields
| Section | Fields |
|---|---|
build | cache, jobs |
lint | severity |
test | runner |
env | Any key-value pair |
Commands
rx has a deliberately small command surface. All commands support the global flags --quiet (-q), --verbose (-v), and --profile <name>.
Project setup
| Command | Description |
|---|---|
rx init | Generate rx.toml with smart defaults |
rx init --migrate | Auto-detect project settings from existing tools |
rx init --ci | Also generate .github/workflows/ci.yml |
rx config | Show resolved configuration |
Build and run
| Command | Description |
|---|---|
rx build | Build with fast linker |
rx build --release | Release build |
rx build --target <triple> | Cross-compile for a target triple |
rx run [-- args...] | Build and run the binary |
rx check | Type-check without codegen (fast feedback) |
rx clean | Clean build artifacts |
rx clean --gc | Clean local target/ and GC global cache |
rx clean --all | Clean all workspace members |
Testing and code quality
| Command | Description |
|---|---|
rx test | Run tests (nextest if available) |
rx test --affected | Only test changed packages + their dependents (single invocation, repeated -p) |
rx test --affected --base main | Changed since a specific branch |
rx lint | Lint with clippy (strict defaults) |
rx fmt | Format code with rustfmt |
rx fix | Auto-fix everything (compiler + clippy + fmt) |
rx ci | Run full pipeline: fmt, clippy, test, build |
rx ci --affected | Run the pipeline against affected packages only |
Tasks
| Command | Description |
|---|---|
rx run <task> | Run a task and its depends-on graph (built-ins: fmt, lint, test, build, check, ci) |
rx run <task> --affected | Scope the task to affected packages (RX_AFFECTED_PACKAGES for shell tasks) |
rx run <task> -- <args> | Append extra args to the task’s shell command |
rx run | List available tasks |
Workspace
| Command | Description |
|---|---|
rx graph | Show the workspace dependency graph |
rx ws list | List all workspace members |
rx ws run <cmd> | Run one cargo <cmd> --workspace from the root |
rx ws exec <cmd> | Run a shell command in each member directory |
Maintenance
| Command | Description |
|---|---|
rx cache status/gc/purge | Manage the opt-in global artifact cache |
rx doctor | Check your development environment |
rx stats show/clear | View or clear build time statistics |
rx completions <shell> | Generate shell completions |
Removed commands
The 0.2 scope reset removed the toolchain-manager surface (new, pkg, toolchain, release, publish, audit, outdated, tree, deps, doc, size, bloat, coverage, bench, expand, compat, upgrade, self-update, sbom, telemetry, plugin, registry, lockfile, env, insights, explain, manpage, sandbox, watch, daemon, worker, test-smart, test-advanced). rx script was folded into rx run, and rx run no longer builds-and-executes your binary — use cargo run or define a task. Use the dedicated tools directly (rustup, cargo and its plugins, sccache, cargo-release, …) or invoke them from a configured rx task. See PRODUCT.md for the reasoning.
Build & Run
The rx build and rx run commands compile your project with automatic fast linker detection, artifact caching, and cross-compilation support.
Basic usage
rx build # debug build
rx build --release # release build
rx run # build and run the binary
rx run -- --port 8080 # pass arguments to the binary
rx check # type-check only (no codegen)
Fast linker
rx auto-detects the fastest available linker on your system:
- mold – fastest, preferred when available
- lld – fast, widely available
- system – default fallback (cc/ld)
The detected linker is cached at ~/.rx/env.lock so detection only runs once. Override it in rx.toml:
[build]
linker = "mold" # or "lld", "system", "auto"
Cross-compilation
rx build --target aarch64-unknown-linux-gnu
rx build --target wasm32-unknown-unknown
rx build --target x86_64-pc-windows-msvc
rx passes the --target flag through to cargo. Ensure the target is installed:
rustup target add aarch64-unknown-linux-gnu
Note: cross-compiled builds always invoke cargo directly — the artifact cache never answers --target or --package builds.
Caching
When cache = true (opt-in, off by default — see PRODUCT.md), rx maintains a global content-addressed cache at ~/.rx/cache:
- Mtime check – if no source file has changed, skip everything
- Fingerprint – compute xxh3-128 hash of sources, Cargo.toml, Cargo.lock, profile, and RUSTFLAGS
- Cache hit – restore artifacts via reflink/hardlink into
target/ - Cache miss – build normally, then store artifacts for next time
Disable caching per-command or in config:
rx build --no-cache # skip cache for this build
[build]
cache = false
Incremental linking
When incremental_link = true (the default), rx enables:
- split-debuginfo – keeps debug info separate from the binary
--as-needed– only links referenced libraries
This significantly reduces link times for iterative debug builds.
RUSTFLAGS
Add extra RUSTFLAGS in rx.toml:
[build]
rustflags = ["-C", "target-cpu=native"]
These are appended to any existing RUSTFLAGS environment variable.
Parallel jobs
[build]
jobs = 0 # auto-detect (default)
jobs = 4 # use exactly 4 parallel jobs
Flags reference
| Flag | Description |
|---|---|
--release | Build in release mode |
--target <triple> | Cross-compile for a target |
--no-cache | Skip artifact cache |
--verbose | Show cache paths and timing |
--quiet | Suppress non-error output |
--profile <name> | Use a config profile |
Related commands
rx check– type-check without building (faster feedback loop)rx clean– remove build artifactsrx clean --gc– clean and garbage-collect the global cache
Test
rx test runs your tests with the best available runner (nextest when installed) and can restrict the run to packages affected by a change.
Basic usage
rx test # run all tests
rx test --release # test in release mode
rx test -- --nocapture # pass flags to the test harness
rx test -- test_name # run a specific test
Test runner selection
rx selects the test runner based on your rx.toml configuration:
[test]
runner = "auto" # use nextest if installed, else cargo test
runner = "nextest" # always use cargo-nextest
runner = "cargo" # always use cargo test
extra_args = [] # extra args passed to every test run
With "auto", rx checks for cargo-nextest on the PATH and uses it when available. nextest provides better output formatting, per-test timeouts, and parallel execution.
Affected-only testing
Only test packages that have changed since a base ref:
rx test --affected # changed since HEAD~1
rx test --affected --base main # changed since main branch
rx test --affected --base v1.0 # changed since a tag
rx maps changed files from git diff to workspace members, expands the set to transitive dependents (a change in core also affects everything that depends on it), and runs one test invocation with repeated -p selections. If nothing relevant changed, the run is skipped.
--affected also works on rx ci and any task: rx ci --affected, rx run lint --affected. Shell tasks receive the selection as RX_AFFECTED_PACKAGES.
Related commands
rx ci– run the full pipeline (includes tests)
Lint & Format
rx wraps clippy and rustfmt into unified rx lint and rx fmt commands with configurable severity and a combined rx fix that applies all auto-fixes in one step.
Linting
rx lint # lint with clippy
rx lint --release # lint in release mode
Severity configuration
Control how clippy warnings are treated:
[lint]
severity = "deny" # treat all warnings as errors (default)
severity = "warn" # show warnings but don't fail
severity = "allow" # suppress warnings entirely
Extra lints
Add additional clippy lint groups or specific lints:
[lint]
extra_lints = ["clippy::pedantic", "clippy::nursery"]
These are passed as additional -W flags to clippy.
Formatting
rx fmt # format all code
rx fmt --check # check formatting without modifying files
Extra arguments
Pass additional arguments to rustfmt:
[fmt]
extra_args = ["--edition", "2021"]
Auto-fix everything
rx fix combines all auto-fix capabilities in a single command:
rx fix
This runs, in order:
- Compiler suggestions – applies
rustcfix suggestions - Clippy fixes – applies clippy auto-fix suggestions
- Formatting – runs rustfmt on all files
After running rx fix, your code should be free of all auto-fixable issues.
CI pipeline
rx ci runs the full quality pipeline in order:
rx ci
This executes:
rx fmt --check– verify formattingrx lint– run clippyrx test– run testsrx build– verify build succeeds
Use rx ci locally before pushing to catch issues before CI runs.
Profile overrides
Override lint severity per profile:
[lint]
severity = "warn" # relaxed defaults for local dev
[profile.ci]
lint = { severity = "deny" } # strict in CI
rx --profile ci lint # fails on any warning
Flags reference
rx lint
| Flag | Description |
|---|---|
--release | Lint in release mode |
--verbose | Show detailed clippy output |
--quiet | Suppress non-error output |
rx fmt
| Flag | Description |
|---|---|
--check | Check formatting without modifying files |
--verbose | Show formatted file paths |
--quiet | Suppress non-error output |
Workspace
The rx ws commands provide workspace-level orchestration for Cargo workspaces. Cargo commands run as a single --workspace invocation; shell commands run per member directory.
Listing members
rx ws list
Lists all workspace members with their paths and versions.
Dependency graph
rx ws graph
Displays the inter-package dependency graph of the workspace, showing which packages depend on which.
Running commands
rx ws run build # build all members
rx ws run test # test all members
rx ws run test --release # test all members in release mode
rx ws run check # check all members
rx ws run <cmd> runs one cargo <cmd> --workspace from the workspace root. Cargo already builds independent crates in parallel — rx does not spawn one Cargo process per member.
Parallel waves
rx uses Kahn’s algorithm for topological sorting to group packages into parallel “waves”:
Wave 1: [core, utils] # no inter-dependencies
Wave 2: [api, cli] # depend on core/utils
Wave 3: [integration-tests] # depends on api/cli
All packages in a wave run concurrently. The next wave starts only after the current wave completes.
Running shell commands
rx ws exec "wc -l src/*.rs" # count lines in each member
rx ws exec "cargo doc" # run arbitrary cargo commands
rx ws exec "echo \$PWD" # show each member's directory
rx ws exec runs an arbitrary shell command in each member’s directory, in dependency order (topological sort).
Example workflow
A typical CI setup for a workspace:
# Check all members in dependency order
rx ws run check
# Test only affected packages
rx test --affected --base main
# Build everything
rx ws run build --release
Or use the rx ci command which handles the full pipeline automatically.
Cycle detection
rx detects dependency cycles in the workspace graph and reports them as errors. Cargo workspaces should not have cycles, but rx provides a clear error message if one is found.
Cache
rx maintains a global content-addressed artifact cache at ~/.rx/cache. The cache stores compiled artifacts keyed by a fingerprint of your source code, configuration, and build profile.
Commands
rx cache status # show cache size and artifact count
rx cache gc # remove artifacts older than 30 days
rx cache purge # delete the entire cache
How the cache works
Every rx build follows this flow:
1. Mtime fast-path
rx records the modification time of every source file after each build. On the next build, it checks whether any file has a newer mtime. If nothing changed, the cached fingerprint is reused instantly without reading any file contents.
2. Content fingerprinting
If an mtime mismatch is detected, rx computes a full xxHash (xxh3-128) fingerprint from:
Cargo.tomlcontentsCargo.lockcontents- All source files
- Build profile (debug or release)
- Active RUSTFLAGS
3. Cache hit
If a cached build matches the fingerprint, artifacts are restored into target/ using the fastest available method:
- Reflink (copy-on-write) – instant on APFS and btrfs
- Hardlink – instant, shares the inode
- Regular copy – fallback, always works
The build is skipped entirely.
4. Cache miss
The build runs normally via cargo. After completion, artifacts are stored in the cache for future use.
Integrity guarantees
The cache is designed for correctness under concurrent access:
- Atomic writes – the cache index and mtime snapshots are written to a temp file and atomically renamed
- File locking – a lock file at
~/.rx/cache/lockprevents concurrent rx processes from racing on the index - Staging directory – new artifacts are written to a staging directory, then renamed into place once complete
- Parallel I/O –
rayonis used for parallel file copies during store and restore
Cache layout
~/.rx/cache/
├── index.json # fingerprint -> artifact metadata
├── mtime-snapshot.json # per-file modification times
├── lock # file lock for concurrent access
└── artifacts/
└── <fingerprint>/ # one directory per unique build
├── deps/
├── build/
└── ...
Garbage collection
rx cache gc # remove entries older than 30 days
Garbage collection removes cache entries that have not been accessed within 30 days. The cache index is updated atomically.
Cleaning
rx clean # clean local target/ directory
rx clean --gc # clean target/ and GC global cache
rx clean --all # clean all workspace member target/ dirs
rx cache purge # delete the entire global cache
Disabling the cache
Disable caching globally or per-profile:
[build]
cache = false
# or per-profile:
[profile.ci]
build = { cache = false }
CI Integration
rx is designed to work seamlessly in continuous integration environments. This section covers the official GitHub Action, VS Code extension, and general CI best practices.
One-command CI
The simplest way to run your full CI pipeline is:
rx ci
This executes, in order:
rx fmt --check– verify formattingrx lint– run clippy with configured severityrx test– run the test suiterx build– verify the build succeeds
CI profiles
Use a profile to customize behavior for CI:
[profile.ci]
build = { cache = false, jobs = 2 }
lint = { severity = "deny" }
test = { runner = "nextest" }
env = { CI = "true", RUST_BACKTRACE = "1" }
rx --profile ci ci
Affected-only execution
In large workspaces, only check what changed (plus its dependents):
rx ci --affected --base main
rx test --affected --base main
Recommended CI pipeline
rx --profile ci ci
Or as separate steps for better CI reporting:
steps:
- run: rx fmt --check
- run: rx lint
- run: rx test --affected --base main
- run: rx build --release
Dedicated tools slot in as extra steps where you need them (e.g. cargo audit, cargo deny).
Integrations
- GitHub Action – official action for GitHub Actions
- VS Code Extension – editor integration for local development
GitHub Action
rx provides an official GitHub Action at iPeluwa/rx@v1 for easy CI integration.
Basic usage
- uses: iPeluwa/rx@v1
with:
command: ci
This installs rx, sets up the Rust toolchain, caches build artifacts, and runs rx ci.
Inputs
| Input | Default | Description |
|---|---|---|
version | latest | rx version to install. Use latest for the newest release or a specific version like 0.5.0. |
command | ci | The rx command to run. Can be any valid rx command, e.g. build --release, test, lint. |
working-directory | . | Working directory for rx commands. Set this if your Rust project is in a subdirectory. |
profile | (empty) | rx config profile to use. Maps to the --profile flag. |
cache | true | Enable build artifact caching. Caches the Cargo registry, git checkouts, rx cache, and target/ directory. |
rust-toolchain | stable | Rust toolchain to install via rustup. Examples: stable, nightly, 1.75.0. |
Examples
Full CI pipeline
name: CI
on: [push, pull_request]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: iPeluwa/rx@v1
with:
command: ci
Multiple steps
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: iPeluwa/rx@v1
with:
command: fmt --check
- uses: iPeluwa/rx@v1
with:
command: lint
- uses: iPeluwa/rx@v1
with:
command: test
With CI profile
- uses: iPeluwa/rx@v1
with:
command: ci
profile: ci
Cross-platform matrix
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: iPeluwa/rx@v1
with:
command: test
MSRV verification
- uses: iPeluwa/rx@v1
with:
command: compat
rust-toolchain: stable
Release build
- uses: iPeluwa/rx@v1
with:
command: build --release
cache: true
Subdirectory project
- uses: iPeluwa/rx@v1
with:
command: ci
working-directory: backend/
How it works
The action performs these steps:
- Install Rust toolchain – uses
dtolnay/rust-toolchainwith the specified toolchain and addsclippyandrustfmtcomponents - Cache artifacts – uses
actions/cache@v4to cache~/.cargo/registry,~/.cargo/git,~/.rx/cache, andtarget/ - Install rx – downloads the prebuilt binary for the runner’s platform, or falls back to
cargo installfrom source - Run command – executes
rx [--profile <profile>] <command>in the specified working directory
Cache key
The cache key is based on the runner OS and Cargo.lock hash:
rx-<os>-<hash of Cargo.lock>
A restore key of rx-<os>- ensures partial cache hits when dependencies change.
VS Code Extension
rx includes a VS Code extension located in the editors/vscode/ directory of the repository. It provides editor integration for all major rx commands.
Installation
Build and install the extension from source:
cd editors/vscode
npm install
npm run compile
# Then install the .vsix file, or use "Developer: Install Extension from Location"
The extension activates automatically when a workspace contains Cargo.toml or rx.toml.
Commands
The extension provides commands accessible from the Command Palette (Ctrl+Shift+P / Cmd+Shift+P):
| Command | Description |
|---|---|
rx: Build | Run rx build |
rx: Build (Release) | Run rx build --release |
rx: Test | Run rx test |
rx: Format | Run rx fmt |
rx: Lint | Run rx lint |
rx: Check | Run rx check |
rx: Fix | Run rx fix |
rx: Run CI | Run rx ci |
rx: Clean | Run rx clean |
rx: Doctor | Run rx doctor |
rx: Workspace Graph | Run rx graph |
rx: List Tasks | Run rx run |
Task provider
The extension includes a VS Code task provider. Define tasks in .vscode/tasks.json:
{
"version": "2.0.0",
"tasks": [
{
"type": "rx",
"command": "build --release",
"label": "rx: Release Build"
},
{
"type": "rx",
"command": "test",
"profile": "ci",
"label": "rx: Test (CI profile)"
}
]
}
Task properties:
| Property | Required | Description |
|---|---|---|
command | Yes | The rx command to run |
profile | No | Config profile to use |
Auto-check on save
When rx.autoCheck is enabled (the default), the extension runs rx check automatically every time you save a Rust file. Errors and warnings appear in the Problems panel.
Disable it in VS Code settings:
{
"rx.autoCheck": false
}
Problem matchers
The extension includes a problem matcher that parses Rust compiler errors and warnings from rx output. Errors appear as squiggly underlines in the editor and in the Problems panel, with clickable file/line references.
Settings
| Setting | Default | Description |
|---|---|---|
rx.path | "rx" | Path to the rx binary. Set this if rx is not on your PATH. |
rx.autoCheck | true | Run rx check automatically on save. |
rx.profile | "" | Default config profile. If set, all commands use this profile. |
Tips
- Use
rx: Watchto start continuous rebuilding in a terminal panel - The
rx: Fixcommand is useful as a keyboard shortcut for quick auto-fixing - Set
rx.profileto switch between development and CI configurations without editingrx.toml - Task definitions support the
profileproperty, so you can create tasks for different profiles
Performance
rx is designed to minimize build and iteration time at every layer. This page describes each performance feature and how it works.
xxHash fingerprinting
rx uses xxHash (xxh3-128) for all content hashing – fingerprinting source files, cache keys, and artifact identity. xxh3-128 is approximately 10x faster than SHA-256 while providing sufficient collision resistance for build caching.
The fingerprint covers:
Cargo.tomlandCargo.lockcontents- All source files in the crate
- The build profile (debug/release)
- Active RUSTFLAGS
Mtime fast-path
Before computing any content hash, rx checks the mtime (modification time) of every source file against a stored snapshot. If no file has a newer mtime than the last recorded build, the cached fingerprint is reused instantly without reading any file contents.
This makes repeated rx build invocations with no changes effectively free.
Reflink copy-on-write
When restoring cached artifacts into target/, rx uses reflink (copy-on-write) on filesystems that support it (APFS on macOS, btrfs on Linux). A reflink creates an instant zero-copy clone that only allocates new disk blocks when modified.
Fallback order:
- Reflink (instant, zero-copy)
- Hardlink (instant, shares inode)
- Regular copy (byte-by-byte, always works)
Parallel cache operations
Cache store and restore operations use rayon for parallel file copies. When storing or restoring many artifacts, multiple files are processed concurrently across all available CPU cores.
Pipelined builds
In workspace builds, rx overlaps type-checking of downstream crates with code generation of upstream crates. Instead of waiting for a full build of a dependency before starting the dependent crate, downstream cargo check begins as soon as metadata is available.
This is especially effective in large workspaces where independent packages can make progress in parallel.
Fast linker detection
rx auto-detects mold and lld linkers at first run and caches the detection result persistently at ~/.rx/env.lock. Subsequent runs skip the detection entirely. Run rx doctor to refresh the cached environment.
The linker priority is:
mold(fastest)lld(fast)- System linker (default fallback)
Incremental linking
When incremental_link = true (the default), rx configures:
- split-debuginfo – separates debug info from the binary, reducing link input size
--as-needed– only links libraries actually referenced by the binary
These reduce link times for iterative debug builds.
PGO release builds
The rx binary itself is built with Profile-Guided Optimization (PGO) in CI:
- Build an instrumented binary
- Run the test suite to generate profile data
- Rebuild with the profile data for optimized branch prediction and inlining
Optimized release binary
The distributed rx binary is compiled with:
- Thin LTO (link-time optimization across crates)
- Single codegen unit (maximum optimization)
- Stripped symbols (smaller binary)
panic = abort(no unwind tables)
Lazy config loading
Commands that do not need project configuration (like rx doctor or rx completions) skip loading and parsing rx.toml entirely, reducing startup latency.
Persistent env cache
Linker detection, toolchain paths, and other environment probes are cached at ~/.rx/env.lock. This avoids repeated which mold, which lld, and similar subprocess calls on every invocation.
Architecture
rx is a single Rust binary (MSRV 1.85.0) organized into focused modules. This page describes the high-level design.
Module layout
rx
├── cli/ CLI definition (clap derive), lazy config loading, profiles
├── config/ rx.toml parsing, global/project merge, profile resolution, validation
├── build/ cargo build orchestration, fast linker, cross-compilation
├── cache/ Opt-in content-addressed artifact store (xxHash, atomic writes, reflink)
├── cargo_output/ Cargo JSON output parser with error hints
├── workspace/ Dependency graph via cargo metadata, topological sort (Kahn's)
├── affected/ Git-diff-based affected package detection
├── ci/ + ci_gen/ Local CI pipeline and CI workflow generation
├── task/ task graph + runner: [tasks], depends-on, wave concurrency
├── completions/ Shell completions with context-aware dynamic values
├── output/ Colored output, timing, verbosity
├── stats/ Build time tracking and statistics
├── hints/ Error code hints surfaced next to cargo output
├── migrate/ Auto-detection of existing project settings
├── doctor/ Development environment checks
└── install.sh Self-installer script
How a command runs
- CLI parsing –
clapparses arguments and flags. The--profileflag is captured as a global option. - Config loading (lazy) – if the command needs configuration, rx loads
~/.rx/config.tomland./rx.toml, merges them (project overrides global), and applies the active profile. - Environment setup – environment variables from
[env]are set. The cached env at~/.rx/env.lockprovides linker paths and toolchain info. - Command execution – pipelines (
rx run,rx ci) resolve a task graph and execute it wave by wave; each task is a shell command or a built-in module (fmt/lint/test/build/check) that calls cargo as a subprocess. Output is parsed from cargo’s JSON stream for error hints and progress display. - Cache update – if the opt-in artifact cache is enabled, the fingerprint and artifacts are stored in
~/.rx/cache.
Config resolution
Configuration is resolved in layers:
defaults < ~/.rx/config.toml < ./rx.toml < [profile.<name>] < CLI flags
Each layer overrides the previous. Unknown keys produce warnings.
Cache design
The global cache at ~/.rx/cache is content-addressed:
~/.rx/cache/
├── index.json # fingerprint -> artifact metadata
├── mtime-snapshot.json # per-file modification times
├── lock # file lock for concurrent access
└── artifacts/
└── <fingerprint>/ # one directory per unique build
├── deps/
├── build/
└── ...
Correctness guarantees:
- Atomic writes – the index and mtime snapshots are written to a temp file and atomically renamed
- File locking – a lock file prevents concurrent rx processes from corrupting the index
- Staging directory – new artifacts are written to a staging directory, then renamed into place
- Parallel I/O – rayon is used for parallel file copies during store/restore
Workspace execution model
rx reads the workspace graph from cargo metadata. Cargo commands run as a single invocation (--workspace, or repeated -p selections for --affected) — Cargo schedules independent crates in parallel itself. rx’s own wave concurrency applies at the task level ([tasks] with depends-on), not inside Cargo’s compilation graph. ws exec uses a topological sort (Kahn’s algorithm) to visit member directories in dependency order.
For --affected, changed files are mapped to members, expanded to transitive dependents, and resolved once into a -p selection.
Error handling
rx wraps all errors with context using anyhow. Every user-facing error includes:
- A clear description of what went wrong
- The underlying cause (if available)
- A hint on how to fix it (common error codes get one-line practical hints next to cargo output)
Comparison
How rx compares to other Rust build tools and task runners.
rx vs raw Cargo
| Feature | Cargo | rx |
|---|---|---|
| Build, test, fmt, clippy | Yes (separate commands) | Yes (unified + rx ci) |
| One-command local CI | No | rx ci |
| Affected-package detection | No | rx test --affected |
| Workspace task orchestration | Basic (--workspace) | Task graphs + affected-package selection |
| Fast linker detection | No | Yes (auto-detects mold/lld) |
| Config file | Cargo.toml only | rx.toml with profiles, tasks, env |
| Shell completions | No | Yes (bash, zsh, fish, PowerShell) |
rx wraps Cargo — it doesn’t replace it. Every rx command runs standard Cargo under the hood, and Cargo owns compilation scheduling and target/.
rx vs cargo-make
cargo-make is a task runner with a Makefile.toml format.
| Feature | cargo-make | rx |
|---|---|---|
| Task definitions | Makefile.toml (verbose) | rx.toml [tasks] with depends-on (concise) |
| Built-in Rust commands | No (shell tasks) | Yes (build, test, lint, fmt, ci) |
| Workspace awareness | Plugin-based | Built-in (cargo metadata) |
| Affected-package detection | No | Built-in |
rx vs just
just is a command runner (like make but simpler).
| Feature | just | rx |
|---|---|---|
| Purpose | General task runner | Rust-specific local CI / task runner |
| Rust integration | None (runs shell commands) | Deep (understands Cargo workspaces) |
| Affected-package detection | No | Built-in |
| Config | justfile | rx.toml |
rx vs cargo-xtask
cargo-xtask is a pattern for writing build scripts in Rust.
| Feature | cargo-xtask | rx |
|---|---|---|
| Setup | Write Rust code per-project | Zero config, works out of the box |
| Maintenance | You maintain the xtask crate | rx maintains the tooling |
| Cross-project reuse | Copy-paste | Same rx binary everywhere |
rx vs sccache
sccache is a shared compilation cache. They are complementary, not competing: rx orchestrates tasks; sccache caches compilation. The Cargo documentation recommends sccache for sharing compiled dependencies, and rx defers compiler-level caching to it.
Summary
Use rx if you want your CI pipeline to be defined once and runnable identically on your machine and in CI, with workspace-aware task execution and affected-package detection. Use a general-purpose task runner (just, cargo-make) if your tasks aren’t Rust-shaped, and sccache for compilation caching either way.