"Why would you build a C++ tool in Rust?" It's the first question we get, and it deserves a thoughtful answer.
cmod is a package manager and build orchestrator for C++20 modules, but its implementation is entirely in Rust. This isn't a philosophical statement about language superiority — it's a pragmatic engineering decision. Here's what led us here and what we've learned.
Why Not C++?
The obvious choice for a C++ tool would be C++ itself. But consider what cmod actually does:
- Parses TOML configuration files
- Resolves complex dependency graphs with semver constraints
- Invokes Git operations (clone, fetch, tag listing)
- Constructs DAGs and performs topological sorts
- Orchestrates parallel subprocess invocations
- Manages a content-addressed file cache
- Handles cryptographic hashing for verification
None of these operations require C++ features like templates, RTTI, or direct memory manipulation. They do require reliable error handling, safe concurrency, and fast string processing — areas where Rust excels.
Why Rust Specifically
Cargo as a role model
cmod is explicitly Cargo-inspired. Building it in Rust means we can study Cargo's implementation directly, borrow architectural patterns, and leverage the same ecosystem of libraries (serde, clap, toml, sha2) that Cargo uses. The Rust ecosystem has first-class libraries for every operation cmod needs.
Error handling
A package manager must handle errors gracefully: network failures, corrupt caches, invalid manifests, version conflicts, missing compilers. Rust's Result type and the ? operator make exhaustive error handling the path of least resistance. Every error in cmod is typed, contextual, and produces a clear message with an exit code.
pub enum CmodError {
ManifestParse { path: PathBuf, source: toml::de::Error },
ResolutionConflict { dep: String, constraints: Vec },
GitCloneFailed { url: String, source: git2::Error },
BuildFailed { module: String, exit_code: i32 },
SecurityViolation { reason: String },
// ...
}
Safe concurrency
cmod compiles modules in parallel, respecting the DAG ordering. Rust's ownership system guarantees at compile time that our parallel build runner can't have data races. The Send and Sync traits ensure that shared state is correctly synchronized. We've never had a concurrency bug in the build runner — the compiler won't let us.
Single binary distribution
Rust compiles to a single static binary with no runtime dependencies. This is crucial for a developer tool that needs to work across different systems. No Python version conflicts, no shared library issues, no JVM to install. Just download the binary and run it.
The Workspace Architecture
cmod is organized as a Cargo workspace with 8 crates, mirroring the layered architecture:
cmod-cli → CLI frontend, subcommand dispatch
cmod-resolver → Git operations, semver solving, registry, features
cmod-build → DAG construction, Clang invocation, distributed builds
cmod-cache → Content-addressed artifact cache, remote cache, BMI distribution
cmod-workspace → Monorepo management
cmod-security → Verification, trust model, cryptographic signing
cmod-lsp → Language Server Protocol server
cmod-core → Core types, config parsing, error model
Each crate has a single responsibility and a narrow public API. Dependencies flow strictly downward — the CLI depends on everything, core depends on nothing. This isn't accidental; it's enforced by Cargo's dependency rules.
Key Libraries We Rely On
- clap — CLI argument parsing with derive macros. Our 30+ commands are defined declaratively.
- serde + toml — Zero-effort TOML serialization/deserialization for manifests and lockfiles.
- semver — Semantic version parsing and constraint matching.
- sha2 — SHA-256 hashing for cache keys and verification.
- git2 — libgit2 bindings for Git operations.
- petgraph — Graph data structures for the module DAG.
- tempfile — Safe temporary file handling for atomic cache writes.
The Rust ecosystem gave us battle-tested implementations for every component we needed.
Lessons Learned
1. The type system catches design errors
Early in development, we modeled module identifiers as plain strings. Rust's type system made it painfully obvious when we were comparing a module name with a file path or a Git URL. We introduced the ModuleId newtype, and an entire class of bugs disappeared.
2. Test infrastructure is excellent
Rust's built-in test framework, combined with Cargo's test runner, makes it trivial to write and run tests. We have 780+ tests covering everything from TOML parsing edge cases to full resolution workflows. Running cargo test takes seconds.
3. Cross-compilation works
Because cmod compiles to native code, we can cross-compile for any target Rust supports. Our CI builds binaries for Linux (x86_64, aarch64), macOS (x86_64, aarch64), and Windows — from a single workflow.
4. Interacting with C++ tooling requires care
cmod invokes Clang as a subprocess for actual compilation. Parsing Clang's output, handling its error messages, and managing the clang-scan-deps JSON output required careful string handling. Rust's strong typing prevented several potential parsing bugs that would have been silent errors in a dynamically-typed language.
5. The Rust community is welcoming
Building a C++ tool in Rust means our contributor base spans both communities. We've had contributions from C++ developers who learned Rust through the project, and from Rust developers curious about C++ modules. The cross-pollination has been valuable.
Would We Choose Rust Again?
Absolutely. The initial learning curve is real, but the payoff is substantial:
- Zero runtime crashes in production use
- No concurrency bugs despite heavy parallelism
- Single-binary distribution across all major platforms
- Excellent test infrastructure
- A thriving ecosystem of libraries
- Compiler-enforced correctness that lets us refactor fearlessly
cmod proves that the best language for building a tool isn't always the language the tool targets. What matters is choosing the right tool for the job — and for building a reliable, concurrent, cross-platform developer tool, Rust is hard to beat.
Explore the cmod source code and see the architecture for yourself.