| Crates.io | cargo-ferris-wheel |
| lib.rs | cargo-ferris-wheel |
| version | 1.0.7 |
| created_at | 2025-07-09 20:36:14.363363+00 |
| updated_at | 2025-07-16 18:14:32.870117+00 |
| description | 🎡 Detect workspace dependency cycles in Rust monorepos |
| homepage | https://github.com/Ellipsis-Labs/cargo-ferris-wheel |
| repository | https://github.com/Ellipsis-Labs/cargo-ferris-wheel |
| max_upload_size | |
| id | 1745473 |
| size | 580,797 |
🎪 Step right up! Step right up! 🎪
The greatest circular dependency detector in all the land!
Find and fix circular dependencies in your Rust monorepo before they take you for a spin!
🎡 Round and Round We Go - Find cycles between workspaces before they make you dizzy
🎯 Ring Toss Champions - Precisely target which tests to run in CI
🎪 The Amazing Affected Analysis - Watch as we reveal which crates are impacted by your changes!
🎨 House of Formats - Output for humans, robots, and everything in between (JSON, JUnit, GitHub Actions)
🎢 Thrill Ride Visualizations - Draw dependency graphs in ASCII, Mermaid, DOT, or D2
🚀 Lightning-Fast Carousel - Parallel processing with Rayon keeps the show moving
🎟️ CI-Friendly Admission - Proper exit codes and machine-readable output for your automation needs
📚 Take It Home - Use as a library in your own Rust carnival
Managing dependencies in a Rust monorepo is like running a carnival - one merry-go-round spinning the wrong way and suddenly you've got a three-ring circus of chaos! Without a skilled ringmaster, circular dependencies sneak in like carnival crashers and cause:
Here's what happens when your monorepo becomes a runaway Tilt-a-Whirl:
graph LR
subgraph "🎡 The Cursed Ferris Wheel"
A[workspace-a] --> B[workspace-b]
B --> C[workspace-c]
C --> A
style A fill:#ff8fab,stroke:#d1477a,stroke-width:3px,color:#000
style B fill:#ff8fab,stroke:#d1477a,stroke-width:3px,color:#000
style C fill:#ff8fab,stroke:#d1477a,stroke-width:3px,color:#000
end
subgraph "🎪 The Domino Sideshow"
D[workspace-d] --> B
E[workspace-e] --> C
F[workspace-f] --> D
style D fill:#9cf6f6,stroke:#4fb3d4,stroke-width:2px,color:#000
style E fill:#9cf6f6,stroke:#4fb3d4,stroke-width:2px,color:#000
style F fill:#9cf6f6,stroke:#4fb3d4,stroke-width:2px,color:#000
end
In this sideshow spectacular:
Ferris Wheel helps you spot the troublemakers before opening day, keeping your monorepo carnival running smoothly instead of becoming a house of cards!
Step right up and install your very own Ferris Wheel operator's license:
# If you have cargo-binstall installed:
cargo binstall cargo-ferris-wheel
# Otherwise, you can compile and install it from crates.io:
cargo install cargo-ferris-wheel
No carnival experience required! Operators standing by!
The inspect command is your primary tool for detecting circular dependencies in your Rust monorepo. It analyzes your workspace dependency graph and identifies cycles that can cause build failures and complicate dependency management.
What it does:
When to use it:
# Inspect current directory for inter-workspace cycles
cargo ferris-wheel inspect
# Inspect for cycles within workspaces (intra-workspace)
cargo ferris-wheel inspect --intra-workspace
# Inspect specific paths
cargo ferris-wheel inspect path/to/workspace
# Output in different formats
cargo ferris-wheel inspect --format json
cargo ferris-wheel inspect --format junit
cargo ferris-wheel inspect --format github
# Fail CI if cycles are found
cargo ferris-wheel inspect --error-on-cycles
# Limit number of cycles displayed
cargo ferris-wheel inspect --max-cycles 5
The lineup command reveals the dependency relationships between workspaces in your monorepo. Like skilled ring toss performers, it shows you exactly which workspaces connect to others, helping you understand your project's architecture.
What it does:
When to use it:
# Show all workspace dependencies
cargo ferris-wheel lineup
# Show dependencies for a specific workspace
cargo ferris-wheel lineup --workspace core
# Show reverse dependencies (what depends on this workspace)
cargo ferris-wheel lineup --workspace core --reverse
# Show transitive dependencies
cargo ferris-wheel lineup --workspace core --transitive
# Output as JSON for CI integration
cargo ferris-wheel lineup --format json
The ripples command is the star of our CI circus! This precision tool determines exactly which workspaces and crates are affected by file changes, enabling smart build and test strategies that save time and resources.
What it does:
When to use it:
Why it's powerful:
Unlike naive approaches that rebuild everything or guess based on directory names, ripples understands your actual dependency graph. It precisely identifies affected components, even when:
# Show what's affected by changed files
cargo ferris-wheel ripples src/lib.rs tests/integration.rs
# Include crate-level information in output
cargo ferris-wheel ripples src/lib.rs --show-crates
# Show only directly affected crates (no reverse dependencies)
cargo ferris-wheel ripples src/lib.rs --direct-only
# Output as JSON for CI integration
cargo ferris-wheel ripples src/lib.rs --format json
# Multiple files can be specified as positional arguments
cargo ferris-wheel ripples src/lib.rs src/main.rs Cargo.toml
# Exclude specific dependency types from analysis
cargo ferris-wheel ripples src/lib.rs --exclude-dev
cargo ferris-wheel ripples src/lib.rs --exclude-build --exclude-target
Example JSON output:
{
"affected_crates": [
{
"name": "my-lib",
"workspace": "core",
"is_directly_affected": true
},
{
"name": "my-app",
"workspace": "apps",
"is_directly_affected": false
}
],
"affected_workspaces": [
{
"name": "core",
"path": "/home/user/monorepo/core"
},
{
"name": "apps",
"path": "/home/user/monorepo/apps"
}
],
"directly_affected_crates": ["my-lib"],
"directly_affected_workspaces": [
{
"name": "core",
"path": "/home/user/monorepo/core"
}
]
}
The spotlight command zooms in on a specific crate, revealing all circular dependencies that involve it. When you need to understand why a particular crate is tangled in dependency cycles, this focused analysis provides clarity.
What it does:
When to use it:
# Find all cycles involving a specific crate
cargo ferris-wheel spotlight my-crate
# Analyze intra-workspace cycles for a crate
cargo ferris-wheel spotlight my-crate --intra-workspace
# Output in different formats
cargo ferris-wheel spotlight my-crate --format json
The spectacle command transforms your dependency graph into beautiful visualizations. Whether you need ASCII art for your terminal, Mermaid diagrams for documentation, or professional graphs for presentations, this command delivers stunning visual representations of your monorepo structure.
What it does:
Supported formats:
When to use it:
# Generate ASCII graph
cargo ferris-wheel spectacle
# Generate Mermaid diagram
cargo ferris-wheel spectacle --format mermaid -o deps.mmd
# Generate DOT file for Graphviz
cargo ferris-wheel spectacle --format dot -o deps.dot
# Highlight cycles in the graph
cargo ferris-wheel spectacle --highlight-cycles
Step right up and witness the spectacular Mermaid diagram performance, generated by our very own cargo ferris-wheel spectacle --format mermaid for a hypothetical Rust carnival grounds:
graph TD
subgraph app_group["app"*]
app_backend(("app-backend\n3 crates"))
click app_backend "Workspace: app-backend - Crates: backend-api, backend-service, backend-db - Total: 3"
style app_backend fill:#FFF3E0,stroke:#F57C00,stroke-width:3px
app_frontend["app-frontend\n2 crates"]
click app_frontend "Workspace: app-frontend - Crates: frontend-ui, frontend-state - Total: 2"
style app_frontend fill:#E3F2FD,stroke:#1976D2,stroke-width:2px
app_worker["app-worker\n2 crates"]
click app_worker "Workspace: app-worker - Crates: worker-jobs, worker-scheduler - Total: 2"
style app_worker fill:#E3F2FD,stroke:#1976D2,stroke-width:2px
end
subgraph core_group["core"*]
core_runtime(("core-runtime\n2 crates"))
click core_runtime "Workspace: core-runtime - Crates: runtime, runtime-types - Total: 2"
style core_runtime fill:#FFF3E0,stroke:#F57C00,stroke-width:3px
core_storage["core-storage\n2 crates"]
click core_storage "Workspace: core-storage - Crates: storage, storage-api - Total: 2"
style core_storage fill:#E3F2FD,stroke:#1976D2,stroke-width:2px
core_rpc["core-rpc\n2 crates"]
click core_rpc "Workspace: core-rpc - Crates: rpc-server, rpc-client - Total: 2"
style core_rpc fill:#E3F2FD,stroke:#1976D2,stroke-width:2px
end
subgraph tools_group["tools"*]
tools_cli(["tools-cli\n1 crates"])
click tools_cli "Workspace: tools-cli - Crates: cli - Total: 1"
style tools_cli fill:#E3F2FD,stroke:#1976D2,stroke-width:2px
tools_migrate(["tools-migrate\n1 crates"])
click tools_migrate "Workspace: tools-migrate - Crates: migrate - Total: 1"
style tools_migrate fill:#E3F2FD,stroke:#1976D2,stroke-width:2px
tools_test_utils["tools-test-utils\n2 crates"]
click tools_test_utils "Workspace: tools-test-utils - Crates: test-utils, test-fixtures - Total: 2"
style tools_test_utils fill:#E3F2FD,stroke:#1976D2,stroke-width:2px
end
core_storage -->|storage → runtime| core_runtime
linkStyle 0 stroke:#64B5F6,stroke-width:2px
app_frontend -.->|frontend-ui → test-fixtures| tools_test_utils
linkStyle 1 stroke:#90A4AE,stroke-width:2px
tools_cli -->|cli → backend-api| app_backend
linkStyle 2 stroke:#64B5F6,stroke-width:2px
app_worker -->|worker-jobs → storage| core_storage
linkStyle 3 stroke:#64B5F6,stroke-width:2px
app_backend -->|backend-service → runtime| core_runtime
linkStyle 4 stroke:#FF6500,stroke-width:3px
core_rpc -->|rpc-server → runtime| core_runtime
linkStyle 5 stroke:#64B5F6,stroke-width:2px
app_worker -->|worker-scheduler → backend-service| app_backend
linkStyle 6 stroke:#64B5F6,stroke-width:2px
app_backend -->|backend-db → storage| core_storage
linkStyle 7 stroke:#64B5F6,stroke-width:2px
app_backend -.->|backend-service → test-utils| tools_test_utils
linkStyle 8 stroke:#90A4AE,stroke-width:2px
tools_migrate -->|migrate → storage| core_storage
linkStyle 9 stroke:#64B5F6,stroke-width:2px
core_runtime -.->|runtime → backend-service| app_backend
linkStyle 10 stroke:#FF6500,stroke-width:3px
app_backend -->|backend-api → rpc-server| core_rpc
linkStyle 11 stroke:#64B5F6,stroke-width:2px
app_frontend -->|frontend-ui → rpc-client| core_rpc
linkStyle 12 stroke:#64B5F6,stroke-width:2px
subgraph Legend
L1[Normal Workspace]
L2[Workspace in Cycle]
style L1 fill:#E3F2FD,stroke:#1976D2,stroke-width:2px
style L2 fill:#FFF3E0,stroke:#F57C00,stroke-width:3px
style Legend fill:#FAFAFA,stroke:#ddd,stroke-width:1px
end
subgraph CycleSeverity["Cycle Severity"]
CS1["⚠️ Cycle 1: 2 workspaces<br/>app-backend → core-runtime"]
style CycleSeverity fill:#FAFAFA,stroke:#ddd,stroke-width:1px
end
In this example:
([]) for single-crate workspaces[] for large workspaces (>5 crates)(()) for workspaces in cycles--> for normal dependencies-.-> for dev dependencies===> for build dependenciesTake home a memento from your visit in any of these delightful formats:
Take a peek behind the curtain at our carnival machinery:
src/
├── main.rs, lib.rs, cli.rs # The ticket booth and main entrance
├── analyzer/ # The ride inspectors (parallel safety checks!)
├── commands/ # The control panels for each attraction
├── config/ # Ride configuration and safety settings
├── core/ # The steel framework holding it all together
├── detector/ # The dizzy-detector (Tarjan's spinning algorithm)
├── executors/ # The ride operators who make it all happen
├── graph/ # The carnival map artist's studio
├── reports/ # The souvenir stand (take home your results!)
├── utils/ # The toolbox for fixing loose bolts
└── workspace_discovery.rs # The carnival grounds surveyor
Skip the rides you're not interested in:
--exclude-dev - Skip the developer funhouse--exclude-build - Bypass the construction zone--exclude-target - Avoid platform-specific sideshowsAll settings can be configured using environment variables with the CARGO_FERRIS_WHEEL_ prefix. Perfect for CI/CD pipelines where you want consistent settings across multiple attractions!
# Set up the carnival grounds with environment variables
env:
CARGO_FERRIS_WHEEL_FORMAT: github # GitHub-friendly announcements
CARGO_FERRIS_WHEEL_ERROR_ON_CYCLES: true # Stop the show if cycles found
CARGO_FERRIS_WHEEL_EXCLUDE_DEV: true # Skip the developer attractions
jobs:
safety-inspection:
steps:
- name: 🎡 Inspect the production rides for safety
run: cargo ferris-wheel inspect
# Automatically uses all the environment settings above!
🎟️ Pro Tip: Command-line arguments always get VIP treatment over environment variables, so you can override any setting for special performances!
# Check for cycles in CI
- name: Check for workspace cycles
run: cargo ferris-wheel inspect --error-on-cycles --format github
# Determine which workspaces to build based on changed files
- name: Analyze affected workspaces
run: |
# Get list of changed files from git
CHANGED_FILES=$(git diff --name-only origin/main...HEAD)
# Use ferris-wheel to determine affected workspaces
AFFECTED=$(cargo ferris-wheel ripples $CHANGED_FILES --format json)
echo "$AFFECTED"
# Extract just the workspace names for your build matrix
WORKSPACES=$(echo "$AFFECTED" | jq -r '.affected_workspaces[].name')
echo "Affected workspaces: $WORKSPACES"
# Example: Only run if specific workspaces are affected
- name: Build affected workspaces
run: |
AFFECTED=$(cargo ferris-wheel ripples $CHANGED_FILES --format json)
if echo "$AFFECTED" | jq -e '.affected_workspaces[] | select(.name == "core")'; then
echo "Core workspace affected, running specialized tests..."
cargo test -p core
fi
cargo ferris-wheel inspect --error-on-cycles
These examples showcase how cargo-ferris-wheel powers production Rust monorepos, based on real usage patterns.
Create a dynamic build matrix that only tests affected workspaces:
name: Rust CI
on: [pull_request]
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
affected-workspaces: ${{ steps.ripples.outputs.affected }}
steps:
- uses: actions/checkout@v4
- uses: tj-actions/changed-files@v45
id: changed-files
with:
files: |
**/*.rs
**/Cargo.toml
**/Cargo.lock
- name: Determine affected workspaces
id: ripples
if: steps.changed-files.outputs.any_changed == 'true'
run: |
# Pass all changed files to ripples at once
AFFECTED_JSON=$(cargo ferris-wheel ripples ${{ steps.changed-files.outputs.all_changed_files }} --format json)
# Create matrix for GitHub Actions
MATRIX=$(echo "$AFFECTED_JSON" | jq -c '{
"workspace": [.affected_workspaces[].name],
"include": [.affected_workspaces[] | {
"workspace": .name,
"path": .path
}]
}')
echo "affected=$MATRIX" >> $GITHUB_OUTPUT
test:
needs: detect-changes
if: needs.detect-changes.outputs.affected-workspaces != ''
strategy:
matrix: ${{ fromJson(needs.detect-changes.outputs.affected-workspaces) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Test workspace
run: |
cd ${{ matrix.path }}
cargo test
Instead of hardcoding workspace paths, discover them dynamically:
#!/bin/bash
# Run clippy on all workspaces automatically
# Get all workspace paths using lineup
workspaces=$(cargo ferris-wheel lineup --format json | jq -r '.workspaces[].path')
# Run clippy in each workspace
for workspace_path in $workspaces; do
echo "Running clippy in $workspace_path"
pushd "$workspace_path" > /dev/null
cargo clippy --all-targets -- -D warnings
popd > /dev/null
done
Format only the workspaces affected by your changes:
#!/usr/bin/env python3
# run-rustfmt-with-ferris-wheel.py
import json
import subprocess
import sys
def main():
# Get changed files from git or command line
changed_files = sys.argv[1:] if len(sys.argv) > 1 else []
if not changed_files:
return
# Find affected workspaces
result = subprocess.run(
["cargo", "ferris-wheel", "ripples", "--format", "json"] + changed_files,
capture_output=True,
text=True
)
if result.returncode != 0:
print(f"Error running cargo ferris-wheel: {result.stderr}")
sys.exit(1)
data = json.loads(result.stdout)
# Use directly_affected_workspaces for more precise formatting
workspaces = [ws["name"] for ws in data.get("directly_affected_workspaces", [])]
# Get workspace paths
lineup_result = subprocess.run(
["cargo", "ferris-wheel", "lineup", "--format", "json"],
capture_output=True,
text=True
)
lineup_data = json.loads(lineup_result.stdout)
workspace_paths = {
ws["name"]: ws["path"]
for ws in lineup_data["workspaces"]
}
# Format each affected workspace
for workspace in workspaces:
if workspace in workspace_paths:
print(f"Formatting {workspace}...")
subprocess.run(
["cargo", "fmt"],
cwd=workspace_paths[workspace]
)
if __name__ == "__main__":
main()
Use cargo-ferris-wheel with hakari to manage unified dependencies:
#!/usr/bin/env python3
# hakari-update.py - Update workspace-hack in dependency order
import json
import subprocess
from collections import defaultdict, deque
def topological_sort(workspaces):
"""Sort workspaces in dependency order"""
# Build adjacency list
graph = defaultdict(list)
in_degree = defaultdict(int)
for ws in workspaces:
name = ws["name"]
in_degree[name] = 0
for ws in workspaces:
name = ws["name"]
for dep in ws.get("dependencies", []):
if dep in in_degree: # Only consider workspace dependencies
graph[dep].append(name)
in_degree[name] += 1
# Kahn's algorithm
queue = deque([ws for ws in in_degree if in_degree[ws] == 0])
result = []
while queue:
current = queue.popleft()
result.append(current)
for neighbor in graph[current]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
return result
def main():
# Get all workspaces with dependencies
result = subprocess.run(
["cargo", "ferris-wheel", "lineup", "--format", "json"],
capture_output=True,
text=True
)
data = json.loads(result.stdout)
workspaces = data["workspaces"]
# Sort in dependency order
sorted_names = topological_sort(workspaces)
# Create name to path mapping
ws_paths = {ws["name"]: ws["path"] for ws in workspaces}
# Update hakari in each workspace
for ws_name in sorted_names:
if ws_name in ws_paths:
ws_path = ws_paths[ws_name]
print(f"Updating hakari in {ws_name}...")
subprocess.run(
["cargo", "hakari", "generate"],
cwd=ws_path
)
if __name__ == "__main__":
main()
The ripples command provides rich information about affected components:
{
"affected_crates": [
{
"name": "my-lib",
"workspace": "core",
"is_directly_affected": true
},
{
"name": "my-app",
"workspace": "apps",
"is_directly_affected": false
}
],
"affected_workspaces": [
{
"name": "core",
"path": "/home/user/monorepo/core"
},
{
"name": "apps",
"path": "/home/user/monorepo/apps"
}
],
"directly_affected_crates": ["my-lib"],
"directly_affected_workspaces": [
{
"name": "core",
"path": "/home/user/monorepo/core"
}
]
}
Key fields:
directly_affected_workspaces: Workspaces containing changed filesaffected_workspaces: All workspaces impacted (including reverse dependencies)is_directly_affected: Whether a crate contains changed files or is only affected transitivelyThe --reverse flag is essential for tools that need to process workspaces in dependency order:
# Process workspaces from leaves to roots (useful for hakari)
cargo ferris-wheel lineup --reverse --format json | \
jq -r '.workspaces[] | "\(.name) depends on: \(.dependencies | join(", "))"'
# Get workspaces with no dependencies (leaf nodes)
cargo ferris-wheel lineup --format json | \
jq -r '.workspaces[] | select(.dependencies | length == 0) | .name'
# Find workspaces that depend on a specific workspace
WORKSPACE="core"
cargo ferris-wheel lineup --reverse --format json | \
jq -r --arg ws "$WORKSPACE" '.workspaces[] | select(.dependencies | contains([$ws])) | .name'
Complete example of integrating cargo-ferris-wheel with lefthook for git hooks:
# lefthook.yml
pre-commit:
parallel: true
commands:
rustfmt:
glob: "**/*.rs"
run: ./scripts/run-rustfmt-with-ferris-wheel.py {staged_files}
stage_fixed: true
hakari-update:
glob:
- "**/Cargo.toml"
- "**/Cargo.lock"
run: |
# Only run if workspace dependencies changed
if cargo ferris-wheel ripples {staged_files} --format json | jq -e '.affected_workspaces | length > 1'; then
./scripts/hakari-update.py
fi
stage_fixed: true
pre-push:
commands:
check-cycles:
run: cargo ferris-wheel inspect --error-on-cycles
Built to handle even the biggest carnival operations:
This carnival is open to all! Your admission ticket is a 🎠 MIT License (https://opensource.org/license/mit).
🎡 Thanks for visiting the Ferris Wheel! 🎡
Remember: Life's too short for circular dependencies. Keep your code spinning smoothly!