Skip to content

Homebrew: Formula Development Guide

Guide to creating, testing, and maintaining Homebrew formulas for macOS package management.

Terminal window
brew create https://example.com/foo-1.0.tar.gz

This creates a formula template in:

  • /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/
class Foo < Formula
desc "Description of foo"
homepage "https://example.com/foo"
url "https://example.com/foo-1.0.tar.gz"
sha256 "abc123..."
license "MIT"
depends_on "cmake" => :build
depends_on "openssl@3"
def install
system "./configure", "--prefix=#{prefix}"
system "make", "install"
end
test do
system "#{bin}/foo", "--version"
end
end
Terminal window
HOMEBREW_NO_INSTALL_FROM_API=1 brew install --build-from-source ./iproute2.rb

Flags:

  • HOMEBREW_NO_INSTALL_FROM_API=1 - Disable bottle API, force local formula
  • --build-from-source - Compile from source instead of using prebuilt bottles

Check formula for common issues:

Terminal window
brew audit --strict iproute2

Options:

  • --strict - Stricter checks for official submission
  • --online - Check URLs are reachable
  • --new-formula - Checks specific to new formulas
Terminal window
brew test iproute2

This runs the test do block defined in the formula.

Terminal window
brew style iproute2

Checks Ruby code style compliance using RuboCop.

Fix style issues automatically:

Terminal window
brew style --fix iproute2
Terminal window
brew edit iproute2

Opens formula in $EDITOR.

Terminal window
brew install --debug --verbose iproute2
  • --debug - Drop into debugger on error
  • --verbose - Show compilation output
Terminal window
brew reinstall iproute2
Terminal window
brew uninstall iproute2
class FooAT2 < Formula
desc "Foo version 2.x"
url "https://example.com/foo-2.0.tar.gz"
# ...
keg_only :versioned_formula
end
class MyPythonApp < Formula
include Language::Python::Virtualenv
depends_on "python@3.11"
resource "requests" do
url "https://files.pythonhosted.org/..."
sha256 "abc123..."
end
def install
virtualenv_install_with_resources
end
end
class MyNodeApp < Formula
depends_on "node"
def install
system "npm", "install", *Language::Node.std_npm_install_args(libexec)
bin.install_symlink Dir["#{libexec}/bin/*"]
end
end
class MyGoApp < Formula
depends_on "go" => :build
def install
system "go", "build", *std_go_args(ldflags: "-s -w")
end
end
depends_on "cmake" => :build # Build-time only
depends_on "pkg-config" => :build
depends_on "openssl@3" # Runtime dependency
depends_on "python@3.11" => [:build, :test]
depends_on :macos => :big_sur # Minimum macOS version
depends_on :xcode => "12.0" # Xcode requirement
depends_on "readline" => :optional
def install
system "./configure", "--prefix=#{prefix}"
system "make", "install"
end
def install
system "cmake", "-S", ".", "-B", "build", *std_cmake_args
system "cmake", "--build", "build"
system "cmake", "--install", "build"
end
def install
system "meson", "setup", "build", *std_meson_args
system "meson", "compile", "-C", "build", "--verbose"
system "meson", "install", "-C", "build"
end
def install
bin.install "myapp"
lib.install Dir["lib/*"]
(etc/"myapp").install "config.yml"
doc.install "README.md"
end
test do
# Test version output
assert_match version.to_s, shell_output("#{bin}/foo --version")
# Test functionality
(testpath/"test.txt").write("content")
assert_equal "content", shell_output("#{bin}/foo read test.txt").strip
# Test compilation
system ENV.cc, "test.c", "-o", "test", "-L#{lib}", "-lfoo"
system "./test"
end
assert_match /pattern/, string
assert_equal expected, actual
assert_predicate path, :exist?
refute_predicate path, :exist?
Terminal window
brew audit --strict --online ./formula.rb
Terminal window
brew install --debug --verbose --build-from-source ./formula.rb

When build fails, you’re dropped into shell in build directory.

SHA256 mismatch:

Terminal window
# Calculate correct SHA256
shasum -a 256 downloaded_file.tar.gz
# Update formula with correct hash

Missing dependencies:

Terminal window
# Check what libraries the binary needs
otool -L /usr/local/bin/myapp
# Add missing dependencies to formula

Build errors:

Terminal window
# Check config.log for autotools projects
cat config.log
# Enable verbose make output
system "make", "V=1", "install"

Create a custom tap:

Terminal window
brew tap-new username/repo

Directory structure:

homebrew-repo/
└── Formula/
├── foo.rb
└── bar.rb
Terminal window
brew tap username/repo
brew install foo
  1. Fork homebrew-core repository
  2. Create formula in Formula/ directory
  3. Test thoroughly:
    Terminal window
    brew audit --strict --online foo
    brew test foo
    brew style foo
  4. Submit pull request
Terminal window
brew install --build-bottle foo
brew bottle foo

This creates a .tar.gz bottle file and JSON with bottle DSL.

Add bottle block to formula:

bottle do
sha256 cellar: :any, arm64_ventura: "abc123..."
sha256 cellar: :any, ventura: "def456..."
end
Terminal window
brew bump-formula-pr foo \
--url=https://example.com/foo-2.0.tar.gz \
--sha256=abc123...
Terminal window
brew outdated
brew upgrade foo
  1. Use standard helpers:

    • std_cmake_args
    • std_meson_args
    • std_configure_args
  2. Test thoroughly:

    • Run audit before submitting
    • Test on multiple macOS versions
    • Include meaningful test block
  3. Handle edge cases:

    • Different architectures (ARM vs Intel)
    • macOS version differences
    • Optional features
  4. Documentation:

    • Clear desc and homepage
    • Document caveats in caveats block
    • Add deprecation notices if needed
  5. Respect conventions:

    • Follow naming conventions
    • Use proper dependency types
    • Include license information
Terminal window
# Formula information
brew info foo
brew desc foo
# Dependencies
brew deps foo
brew uses --installed foo
# Installation locations
brew --prefix foo
brew --cellar foo
# Cleanup
brew cleanup foo
brew unlink foo
brew link foo
# Formula management
brew pin foo # Prevent updates
brew unpin foo
brew switch foo 1.0 # Switch versions