Homebrew Tap
Distributing a Rust CLI tool to macOS users with a custom Homebrew tap, GitHub Releases, and a 12-line formula.
- DATE:
- FEB.06.2026
- READ:
- 10 MIN
The distribution problem
You’ve built a CLI tool. It compiles, it works, it solves a real problem. Now someone asks “how do I install it?” and you say “clone the repo, install the Rust toolchain, run cargo build --release, copy the binary to your PATH.” They don’t install it.
The gap between “working binary” and “installable tool” is the gap between a project and a product. On macOS, that gap is bridged by Homebrew — and specifically, by taps: third-party formula repositories that let anyone distribute software through brew install.
This post walks through setting up a Homebrew tap for coderev, a Rust-based semantic code intelligence engine. The same pattern works for any compiled binary.
What a tap actually is
A Homebrew tap is a Git repository with a specific naming convention: <github-user>/homebrew-<name>. When a user runs brew tap <user>/<name>, Homebrew clones github.com/<user>/homebrew-<name> and registers its formulas. That’s it — no registry, no approval process, no package signing ceremony.
The repository structure is minimal:
homebrew-tap/
├── Formula/
│ └── coderev.rb # one formula per tool
└── README.md The Formula/ directory contains Ruby files — one per installable package. Each formula tells Homebrew where to download the binary, how to verify it, and how to install it.
The formula
Here’s the complete formula for coderev:
class Coderev < Formula
desc "A powerful AI-driven grep tool"
homepage "https://github.com/been-there-done-that/coderev"
url "https://github.com/been-there-done-that/coderev/releases/download/v0.0.3/coderev-x86_64-apple-darwin.tar.gz"
sha256 "f9e4bd293100655fed09edbfbdfd989d73cbaba8135a62319499f45942570d88"
version "0.0.3"
def install
bin.install "coderev"
end
test do
system "#{bin}/coderev", "--version"
end
end Twelve lines. Let’s break down what each part does.
Metadata
desc "A powerful AI-driven grep tool"
homepage "https://github.com/been-there-done-that/coderev"
version "0.0.3" desc appears in brew info and brew search results. homepage is where Homebrew links users for documentation. version is explicitly declared rather than inferred from the URL — useful when your URL doesn’t follow Homebrew’s version-detection patterns.
Source
url "https://github.com/been-there-done-that/coderev/releases/download/v0.0.3/coderev-x86_64-apple-darwin.tar.gz"
sha256 "f9e4bd293100655fed09edbfbdfd989d73cbaba8135a62319499f45942570d88" The url points to a pre-compiled binary archive hosted on GitHub Releases. Homebrew downloads and extracts it. The sha256 hash verifies integrity — if the downloaded file doesn’t match, installation fails. This prevents tampered binaries from being installed silently.
Install
def install
bin.install "coderev"
end bin.install copies the named binary into Homebrew’s bin directory (typically /usr/local/bin or /opt/homebrew/bin). For a single-binary tool, this is the entire installation process. No compilation, no dependency resolution, no post-install hooks.
Test
test do
system "#{bin}/coderev", "--version"
end The test block runs during brew test coderev. It’s a smoke test — does the binary execute at all? For a CLI tool, checking --version is the standard minimum. If this fails, the formula is broken.
The release pipeline
The formula downloads pre-built binaries, which means you need a release pipeline that produces them. For a Rust project, the flow is:
- Tag a release:
git tag v0.0.3 && git push --tags - CI builds binaries: GitHub Actions compiles for each target architecture
- Upload to Releases: attach the
.tar.gzarchives to the GitHub release - Update the formula: bump
version,url, andsha256in the.rbfile
The build matrix for coderev targets four platforms:
+--------------------+--------------------+--------+ | Target | Archive | Size | +--------------------+--------------------+--------+ | aarch64-apple-darwin | coderev-aarch64-apple-darwin.tar.gz | ~11 MB | +--------------------+--------------------+--------+ | x86_64-apple-darwin | coderev-x86_64-apple-darwin.tar.gz | ~13 MB | +--------------------+--------------------+--------+ | x86_64-unknown-linux-gnu | coderev-x86_64-linux.tar.gz | ~14 MB | +--------------------+--------------------+--------+ | x86_64-pc-windows-msvc | coderev-x86_64-windows.tar.gz | ~33 MB | +--------------------+--------------------+--------+
Each archive contains a single binary named coderev — no directory nesting, no extra files. This keeps the formula’s bin.install one line.
Installing from the tap
From the user’s perspective, installation is two commands:
# add the tap (once)
brew tap been-there-done-that/tap
# install the tool
brew install coderev brew tap clones the repository. brew install finds the formula in Formula/coderev.rb, downloads the archive, verifies the hash, extracts the binary, and symlinks it into PATH. Subsequent updates are brew upgrade coderev.
Under the hood, brew tap been-there-done-that/tap resolves to github.com/been-there-done-that/homebrew-tap. The homebrew- prefix is implicit — you never type it in the brew tap command, but the repository must have it.
Architecture handling
The current formula hardcodes the x86_64 binary. On Apple Silicon Macs, it still works because macOS runs x86_64 binaries through Rosetta 2 — but with a performance penalty and an unnecessary translation layer.
A better approach uses Homebrew’s on_macos and Hardware::CPU APIs:
if Hardware::CPU.arm?
url "https://github.com/been-there-done-that/coderev/releases/download/v0.0.3/coderev-aarch64-apple-darwin.tar.gz"
sha256 "abc123..."
else
url "https://github.com/been-there-done-that/coderev/releases/download/v0.0.3/coderev-x86_64-apple-darwin.tar.gz"
sha256 "def456..."
end This serves the native binary for each architecture. The tradeoff is maintaining two SHA256 hashes per release instead of one — worth it for a tool that users will run frequently.
Automating formula updates
The manual workflow — tag, build, upload, edit formula, push — is error-prone. A GitHub Actions workflow can automate the last step:
# after uploading release artifacts, compute the new SHA256
SHA=$(curl -sL "$RELEASE_URL" | shasum -a 256 | awk '{print $1}')
# update the formula in the tap repo
sed -i '' "s/sha256 ".*"/sha256 "$SHA"/" Formula/coderev.rb
sed -i '' "s/version ".*"/version "$NEW_VERSION"/" Formula/coderev.rb This runs in the coderev repo’s release workflow and pushes a commit to the homebrew-tap repo. The tap always stays in sync with the latest release.
What you get
For twelve lines of Ruby and a standard GitHub Releases setup, users get:
- One-command install:
brew install coderevafter tapping - Automatic updates:
brew upgradepicks up new versions - Integrity verification: SHA256 hash checked on every install
- Clean uninstall:
brew uninstall coderevremoves everything - Discoverability:
brew search coderevfinds it once tapped
Compare this to the alternatives — curl scripts that pipe to bash, manual binary downloads, or asking users to install a build toolchain. The Homebrew tap is more work to set up (once), but the installation experience is what users expect from production tools.
The pattern generalizes
This isn’t specific to Rust or to coderev. Any compiled binary can use the same pattern:
- Build for target platforms in CI
- Upload archives to GitHub Releases
- Write a formula that downloads and installs the binary
- Host the formula in a
homebrew-<name>repository
Go binaries, C++ tools, Zig projects — if it compiles to a standalone executable, it can be a Homebrew formula. The formula is just a download URL, a hash, and an install path. The complexity is in building the binary, not in distributing it.
The gap between a project and a product is often just a package manager entry.