Demystifying Homebrew: A Developer's Guide to Distributing CLI Tools
Share this article
For developers building command-line tools, Homebrew offers a streamlined installation experience users love. Yet many creators default to language-specific package managers like npm or RubyGems, intimidated by Homebrew's perceived complexity. As Justin Searls candidly admits: "I had no fucking clue how Homebrew works... I'm not enough of a neckbeard to peer behind the curtain."
But distributing via Homebrew doesn't require deep systems expertise—just clear guidance. Here's how to package your CLI tool for Homebrew's ecosystem, complete with automation for painless updates.
Decoding Homebrew's Terminology
Before diving in, translate these essential terms:
- Formula: Package definition (your CLI's blueprint)
- Tap: Git repository hosting formulae
- Bottle: Pre-built binary package
- Cellar: Installation directory (/opt/homebrew/Cellar)
- Keg: Version-specific installation directory
The Publishing Workflow: 3 Critical Steps
1. Create Your Tap
Homebrew prefers community tools live in personal taps rather than core. Initialize your repository:
brew tap-new your_github_handle/homebrew-tap
git -C $(brew --repo your_github_handle/tap) remote add origin [email protected]:your_github_handle/homebrew-tap.git
Push the scaffolded repository to GitHub. Users can now install tools via:
brew tap your_github_handle/tap
2. Build Your Formula
Reference versioned tarballs for reproducibility. After tagging a release in your CLI's repo:
git tag v1.0.0
git push --tags
Generate the formula:
brew create https://github.com/you/your_cli/archive/refs/tags/v1.0.0.tar.gz \
--tap your_github_handle/homebrew-tap \
--set-name your_cli \
--ruby # Or --python, --node, etc.
Critical Formula Tweaks:
- Replace outdated macOS Ruby with modern version:
depends_on "ruby@3" # Instead of uses_from_macos "ruby"
- Add livecheck for version tracking
- Implement basic install test:
test do
assert_match "Usage: ", shell_output("#{bin}/your_cli --help")
end
3. Automate Release Updates
Manually updating SHAs and versions is unsustainable. Implement this GitHub Actions workflow in your tap repository:
name: Update Formula
on:
release:
types: [published]
jobs:
update:
runs-on: ubuntu-latest
steps:
- name: Checkout tap
uses: actions/checkout@v4
with:
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
- name: Update formula
run: |
brew update
brew bump-formula-pr \
--url=https://github.com/$GITHUB_REPOSITORY_OWNER/your_cli/archive/${{ github.ref }}.tar.gz \
--sha256=$(curl -sL https://github.com/$GITHUB_REPOSITORY_OWNER/your_cli/archive/${{ github.ref }}.tar.gz | shasum -a 256 | cut -d' ' -f1) \
--no-browse \
--write \
--verbose \
your_cli
- name: Commit changes
run: |
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config user.name "github-actions[bot]"
git commit -am "Update your_cli to ${{ github.ref_name }}"
git push
Prerequisites:
- Create HOMEBREW_TAP_TOKEN secret with repo write permissions
- Replace your_cli with actual formula name
Why This Matters
Packaging for Homebrew future-proofs your tool. As Searls notes: "I could theoretically swap out the implementation for some other language entirely without disrupting users' ability to upgrade." The initial setup delivers compounding returns—subsequent tools become trivial to publish.
While the process feels arcane initially, mastering it unlocks professional-grade distribution. Developers installing your tool now enjoy the same seamless experience they get from household-name CLIs:
brew tap your_github_handle/tap
brew install your_cli
And that’s worth raising a glass to. 🍻
Source: Justin Searls