Why We Built a Package Manager in Rust
The Rust ecosystem for CLI tools is mature and fast. Here's why we chose Rust over Go, Python, and C++ for stout — and what the ecosystem looks like in 2026.
When we started building stout, the first decision was the language. A package manager touches the filesystem, the network, compression codecs, cryptographic verification, and the terminal — all in a single binary that users expect to start in milliseconds. We evaluated Go, Python, C++, and Rust. We chose Rust. Here is a concrete account of why, and what the Rust CLI ecosystem looks like in practice.
The requirements
A Homebrew replacement has a specific set of constraints:
- Startup time under 10ms. Users type
stout install ripgrepand expect it to feel instant. Any interpreted language is out. - Single static binary. No runtime dependencies, no version conflicts, no “please install Python 3.11 first.”
- Safe concurrency. Downloading 25 bottles in parallel while writing to SQLite and printing progress bars must not corrupt data or crash.
- Cross-platform. macOS ARM64, macOS Intel, Linux x86_64, Linux ARM64 — all from a single codebase.
- Small binary size. Users download stout over the network. Every megabyte matters.
Why not Python
Python was never a serious candidate, but it is worth explaining why — because Homebrew itself is a cautionary tale of what happens when you build a package manager on an interpreted runtime.
Python startup time is 30-80ms for a bare interpreter, and 200-500ms once you import common libraries like argparse, json, urllib3, and sqlite3. That puts you in the same ballpark as Homebrew’s Ruby overhead before you write a single line of application code.
Distribution is the deeper problem. PyInstaller and Nuitka can bundle Python into a standalone executable, but the results are 30-80MB, slow to start, and fragile across OS versions. A package manager that itself needs a package manager to install is a dependency cycle we wanted no part of.
Why not C++
C++ produces fast binaries with fine-grained memory control. It was a real option. But two factors ruled it out.
First, safety. A package manager runs as the current user and writes to /usr/local, /opt/homebrew, and ~/.stout. A use-after-free or buffer overflow in a tool that modifies the filesystem is a serious risk. Rust’s ownership model eliminates these classes of bugs at compile time. In a year of development, we have had zero memory safety bugs in production. That is not because we are better C++ programmers than average — it is because the compiler catches what code review misses.
Second, the package ecosystem. C++ dependency management in 2026 is better than it was in 2020 (vcpkg, Conan, and CMake presets have improved), but it still does not match cargo add tokio and having a working async runtime in 30 seconds. The Rust crate ecosystem gave us production-quality libraries for every component of stout without writing glue code.
Why not Go
Go was the closest competitor. It produces static binaries, compiles fast, has a strong standard library, and handles concurrency well with goroutines. Many excellent CLI tools are written in Go — kubectl, Docker CLI, Terraform, and gh.
Three things tipped the decision toward Rust:
Memory control. A package manager operating on large dependency graphs and multi-gigabyte bottle files needs predictable memory usage. Go’s garbage collector is good, but it introduces latency spikes and makes memory usage harder to reason about. Rust’s ownership model gives us deterministic deallocation without a GC pause. When stout extracts a 500MB bottle, memory usage tracks the buffer size, not the GC’s heuristics.
Error handling. Rust’s Result<T, E> type and the ? operator force every error path to be handled explicitly. Go’s if err != nil pattern achieves the same goal in theory, but in practice it is easy to forget a check or shadow an error variable. In stout, the compiler rejects code that ignores an error. This matters for a tool that interacts with the network, the filesystem, and SQLite in every operation.
// Every fallible operation is explicit in the type signature
pub async fn download_bottle(url: &str, dest: &Path) -> Result<VerifiedBottle> {
let response = reqwest::get(url).await?.error_for_status()?;
let bytes = response.bytes().await?;
let hash = sha256(&bytes);
verify_checksum(&hash, dest)?;
let bottle = extract_tar_zstd(&bytes, dest)?;
Ok(bottle)
}
Zero-cost abstractions. Rust’s trait system, iterators, and generics compile down to the same machine code you would write by hand. stout uses iterators extensively for dependency resolution and formula filtering, and the optimizer collapses them into tight loops with no heap allocation.
The Rust CLI ecosystem in 2026
The crate ecosystem is what makes Rust practical for CLI tools, not just possible. Here are the libraries stout depends on and what they replaced:
Clap for argument parsing. Clap’s derive API lets us define the entire CLI interface as Rust structs with attributes:
#[derive(Parser)]
#[command(name = "stout", version, about)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Install { formulae: Vec<String> },
Search { query: String },
Update,
Info { formula: String },
}
Clap generates shell completions, help text, and error messages automatically. We get bash, zsh, and fish completions for free.
Tokio for async I/O. Tokio powers all of stout’s network operations — parallel bottle downloads, index fetching, and HTTP HEAD staleness checks. The runtime adds roughly 1MB to the binary but gives us a battle-tested async executor with timers, cancellation, and backpressure.
reqwest for HTTP. Built on Tokio and hyper, reqwest handles TLS, connection pooling, redirects, and streaming responses. We use it for every network request in stout.
serde for serialization. Formulae metadata, configuration files, and the JSON API all go through serde. The derive macros generate zero-allocation deserialization for the common path:
#[derive(Deserialize)]
struct Formula {
name: String,
version: String,
dependencies: Vec<String>,
bottle: BottleSpec,
}
rusqlite for the package index. rusqlite provides a safe Rust wrapper around SQLite’s C library. We use it with the bundled feature to statically link SQLite, so there is no system dependency.
zstd for compression. The zstd crate wraps Facebook’s zstd library and gives us 3:1 compression on the package index with decompression speeds above 1 GB/s.
Ed25519-dalek for signature verification. Every package index update is signed with Ed25519. The dalek crate provides a pure-Rust implementation that is fast and auditable.
What we learned
Building stout in Rust took longer than it would have in Go for the first prototype. The borrow checker has a learning curve, and async Rust has sharp edges around lifetimes in futures. But the payoff came in the second half of development: refactoring is fearless, performance is predictable, and the class of bugs we debug in production is limited to logic errors — not memory corruption, not data races, not null pointer dereferences.
The Rust CLI ecosystem in 2026 is mature. Clap, Tokio, serde, and reqwest are not experimental — they are production infrastructure used by thousands of projects. If you are building a CLI tool that needs to be fast, safe, and distributable as a single binary, Rust is the strongest choice available today.
Need Rust performance engineering or AI agent expertise?
Neul Labs — the team behind stout — consults on Rust development, performance optimization, CLI tool design, and AI agent infrastructure. We build fast, reliable systems that ship.