homebrew-dev
Homebrew Packaging Skill
You are an expert at packaging software with Homebrew — both formulas (source builds and pre-built binaries) and casks (macOS apps, fonts, arbitrary files). You help users create, test, and distribute packages through personal taps on GitHub.
Deciding: Formula vs. Cask
| Scenario | Use |
|---|---|
| CLI tool with source code (C, Go, Rust, etc.) | Formula |
| Pre-built binary tarball for a CLI tool | Formula (no build step) |
macOS .app bundle |
Cask |
Font files (.ttf, .otf) |
Cask |
| PKG installer | Cask |
| Arbitrary file placement (e.g. completions, configs) | Cask with artifact |
| App with auto-updater | Cask with auto_updates true |
| Both a CLI and a GUI component | Both (linked from cask with binary) |
Creating a Formula
Scaffold
brew create https://example.com/mytool-1.0.tar.gz
This auto-populates url, sha256, version, and opens the editor.
Key fields
class Mytool < Formula
desc "One-line description (imperative, no trailing period, no 'A' prefix)"
homepage "https://example.com/mytool"
url "https://example.com/mytool-1.0.tar.gz"
sha256 "abc123..." # from: shasum -a 256 <file>
license "MIT"
version "1.0" # omit if parseable from url
depends_on "cmake" => :build # build-time only
depends_on "openssl@3" # runtime
depends_on "python@3.12" => :optional # optional runtime
depends_on :macos => :monterey # minimum macOS
def install
system "./configure", *std_configure_args
system "make", "install"
end
test do
system "#{bin}/mytool", "--version"
assert_match "1.0", shell_output("#{bin}/mytool --version")
end
end
Install directory variables
| Variable | Expands to |
|---|---|
bin |
$(brew --prefix)/bin |
lib |
$(brew --prefix)/lib |
include |
$(brew --prefix)/include |
share |
$(brew --prefix)/share |
etc |
$(brew --prefix)/etc |
var |
$(brew --prefix)/var |
libexec |
$(brew --prefix)/opt/<name>/libexec |
Build systems
# Autoconf/Make
system "./configure", *std_configure_args
system "make", "install"
# CMake
system "cmake", "-S", ".", "-B", "build", *std_cmake_args
system "cmake", "--build", "build"
system "cmake", "--install", "build"
# Cargo (Rust)
system "cargo", "install", *std_cargo_args
# Go
system "go", "build", *std_go_args, "-o", bin/"mytool", "./cmd/mytool"
# Pre-built binary (no build)
def install
bin.install "mytool"
man1.install "mytool.1"
end
Architecture conditionals
on_arm do
url "https://example.com/mytool-1.0-arm64.tar.gz"
sha256 "arm_sha..."
end
on_intel do
url "https://example.com/mytool-1.0-x86_64.tar.gz"
sha256 "intel_sha..."
end
Creating a Cask
Scaffold
brew create --cask https://example.com/MyApp-1.0.dmg
Artifact types and their uses
| Artifact | Use case | Installs to |
|---|---|---|
app "MyApp.app" |
macOS .app bundles |
/Applications |
font "MyFont.ttf" |
Font files | ~/Library/Fonts/ |
binary "mytool" |
Pre-built CLI tool from archive | $(brew --prefix)/bin/ |
artifact "myfile", target: "~/.config/myfile" |
Arbitrary file placement | Custom path |
pkg "MyApp.pkg" |
PKG installer | System (requires uninstall) |
suite "MyApp.app" |
Multi-app suites | /Applications |
Minimal cask (DMG with app)
cask "myapp" do
version "1.0"
sha256 "abc123..."
url "https://example.com/MyApp-#{version}.dmg"
name "MyApp"
desc "Does something useful"
homepage "https://example.com/myapp"
app "MyApp.app"
zap trash: [
"~/Library/Application Support/MyApp",
"~/Library/Preferences/com.example.myapp.plist",
]
end
Font cask
cask "font-my-font" do
version "2.0"
sha256 "abc123..."
url "https://example.com/MyFont-#{version}.zip"
name "My Font"
desc "A beautiful typeface"
homepage "https://example.com/myfont"
font "MyFont-Regular.ttf"
font "MyFont-Bold.ttf"
font "MyFont-Italic.ttf"
end
Arbitrary file placement
cask "mycli" do
version "1.0"
sha256 "abc123..."
url "https://example.com/mycli-#{version}.zip"
name "MyCLI"
desc "A command-line tool"
homepage "https://example.com/mycli"
binary "mycli"
artifact "mycli.zsh-completion", target: "#{HOMEBREW_PREFIX}/share/zsh/site-functions/_mycli"
artifact "mycli.fish", target: "#{HOMEBREW_PREFIX}/share/fish/vendor_completions.d/mycli.fish"
end
PKG installer
cask "myapp" do
version "1.0"
sha256 "abc123..."
url "https://example.com/MyApp-#{version}.pkg"
name "MyApp"
desc "Does something useful"
homepage "https://example.com/myapp"
pkg "MyApp.pkg"
uninstall pkgutil: "com.example.myapp"
zap trash: [
"~/Library/Application Support/MyApp",
"~/Library/Preferences/com.example.myapp.plist",
]
end
Architecture-conditional cask
cask "myapp" do
version "1.0"
on_arm do
url "https://example.com/MyApp-#{version}-arm64.dmg"
sha256 "arm_sha..."
end
on_intel do
url "https://example.com/MyApp-#{version}-x86_64.dmg"
sha256 "intel_sha..."
end
name "MyApp"
desc "Does something useful"
homepage "https://example.com/myapp"
app "MyApp.app"
end
Rolling release (no stable version)
cask "myapp" do
version :latest
sha256 :no_check
url "https://example.com/MyApp-latest.dmg"
name "MyApp"
desc "Does something useful"
homepage "https://example.com/myapp"
auto_updates true # app has built-in updater
app "MyApp.app"
end
Bumping a Version
When a new release is out, update three things:
Formula:
url "https://example.com/mytool-1.1.tar.gz" # new URL
sha256 "newsha256..." # recompute (see below)
version "1.1" # if not parseable from url
Cask: same fields, but also verify #{version} interpolation in url still produces the correct download URL with the new version string.
Get the new checksum:
# Option A: download and compute locally
shasum -a 256 mytool-1.1.tar.gz
# Option B: let brew fetch it
brew fetch --force --build-from-source ./Formula/mytool.rb
Then verify:
brew audit mytool
brew test mytool
Faster alternative — let brew do it all:
brew bump-formula-pr mytool --version 1.1 --url https://example.com/mytool-1.1.tar.gz
brew bump-cask-pr myapp --version 1.1
These commands fetch the new tarball, compute the sha256, update the formula/cask, and open a pull request — all in one step. Recommended when submitting bumps to homebrew-core or homebrew-cask.
Submitting to Homebrew Core / Cask
To get your package into the official homebrew/core or homebrew/cask taps (so users can brew install without adding your tap), you need to open a PR against the upstream repo.
Eligibility requirements, the fork/PR workflow, audit flags, commit conventions, and common rejection reasons are covered in full at:
references/homebrew-core-submission.md
Setting up a Personal Tap
1. Create the GitHub repo
Name it homebrew-<tapname> (e.g. homebrew-tools).
2. Directory structure
homebrew-tools/
├── Formula/
│ └── mytool.rb
└── Casks/
└── myapp.rb
3. Tap and install
brew tap username/tools # taps github.com/username/homebrew-tools
brew install username/tools/mytool
brew install --cask username/tools/myapp
4. Test before publishing
brew install --build-from-source ./Formula/mytool.rb
brew install --cask ./Casks/myapp.rb
Development Commands
# Create scaffolds
brew create https://example.com/tool-1.0.tar.gz
brew create --cask https://example.com/App-1.0.dmg
# Edit formula/cask
brew edit mytool
brew edit --cask myapp
# Install for testing
brew install --build-from-source ./Formula/mytool.rb
brew install --cask ./Casks/myapp.rb
# Run tests
brew test mytool
brew test --cask myapp
# Audit (validate before publishing)
brew audit --new mytool
brew audit --new --cask myapp
# Uninstall / reinstall cycle
brew uninstall mytool && brew install --build-from-source ./Formula/mytool.rb
# Style check
brew style ./Formula/mytool.rb
brew style --cask ./Casks/myapp.rb
# Fetch without installing (verify download)
brew fetch mytool
brew fetch --cask myapp
Checksum Workflow
# Compute SHA-256 for a local file
shasum -a 256 myapp-1.0.dmg
# Or let brew fetch and show it
brew fetch --force --build-from-source ./Formula/mytool.rb
brew create auto-populates sha256 if it can download the file during scaffold creation.
Validation
Run before publishing to a tap:
brew audit --new mytool # stricter checks for new packages
brew audit --new --cask myapp
brew style ./Formula/mytool.rb # RuboCop style check
Common audit issues:
descmust not start with "A" or "The", must not end with a periodhomepagemust use HTTPSlicenserequired for formulas (use SPDX identifiers:"MIT","Apache-2.0", etc.)urlmust be stable, versioned, and use HTTPSsha256must match the download exactly- Test block required for formulas
References
- Formula patterns & annotated examples:
references/formula-cookbook.md - Cask patterns & annotated examples:
references/cask-cookbook.md - Submitting to homebrew-core / homebrew-cask:
references/homebrew-core-submission.md - Official docs: https://docs.brew.sh/Formula-Cookbook
- Cask docs: https://docs.brew.sh/Cask-Cookbook