Homebrew Formulae Are Ruby Scripts: How to Read Them and What Can Go Wrong
Homebrew defines packages as Ruby scripts. Learn how to read a formula, inspect what will be installed, and understand the trade-offs of a dynamic DSL.
28 Jul 2025, 09:27 UTC

The Hidden Code Behind brew install
When you type brew install wget, Homebrew downloads a pre-compiled binary and drops it into a versioned directory. What you probably don't see is that the instructions for that install came from a Ruby script. Homebrew's decision to define packages as Ruby code—rather than JSON, YAML, or a custom config format—is one of the most consequential engineering choices in the project. It makes formulae readable and adaptable, but it also means a formula is executable code with all the fragility that implies.
This post looks at how Homebrew's Ruby DSL works, why it pairs with bottles and the Cellar, and what you should check before trusting a formula from a third-party tap.
A Formula Is a Ruby Class, Not a Config File
Every package in Homebrew is defined by a formula: a Ruby file, usually named after the software, that declares a class inheriting from Formula. The class body is not inert data; it is Ruby code that Homebrew evaluates at runtime.
class Example < Formula
desc "Illustrative formula"
homepage "https://example.com"
url "https://example.com/example-1.0.tar.gz"
sha256 "abc...def"
license "MIT"
depends_on "cmake" => :build
depends_on "openssl@3"
on_macos do
depends_on "llvm"
end
def install
system "make", "install", "PREFIX=#{prefix}"
end
test do
system "#{bin}/example", "--version"
end
end
Methods like depends_on and on_macos are ordinary Ruby method calls. The install block is a method definition, and system shells out to run build commands. This lets maintainers express conditional logic—such as pulling in llvm on macOS but gcc on Linux—in a way a static config file cannot.
Bottles and the Cellar: Why the DSL Needs Both
The Ruby DSL describes how to build software, but most users never compile anything. Homebrew distributes bottles—pre-compiled tarballs tagged by operating system version and CPU architecture. When you install a formula, Homebrew first looks for a matching bottle. If one exists, it pours the binary into the Cellar, a directory tree where each formula version receives its own prefix (for example, /usr/local/Cellar/<formula>/<version> on Intel Macs, or /opt/homebrew/Cellar/<formula>/<version> on Apple Silicon).
This architecture is why brew list --versions can show multiple versions of the same tool side by side: each version lives in its own Cellar directory, and symlinks in bin point to the active one. The formula's Ruby code is what tells Homebrew how to build from source when no bottle matches your system.
Taps Keep Core Small
Homebrew separates the brew command and the core formulae from user-contributed taps. A tap is simply a Git repository containing Ruby formula files. This decentralization means you can install software that has not been merged into homebrew-core, but it also means the Ruby code in a tap has not been reviewed by core maintainers.
Worked Example: Inspecting a Formula Before You Install
Before installing an unfamiliar formula, read its source. Run the following in your terminal (macOS or Linux with Homebrew installed; no sudo required):
brew cat <formula>
Replace <formula> with the package name. This prints the Ruby DSL. Look for the url and sha256 to see where the source comes from, the depends_on lines to see what else will be installed, and the install block to see what commands will run on your machine.
Next, check what Homebrew plans to do:
brew info <formula>
Expected check: the output lists dependencies and indicates whether a bottle is available for your system. If no bottle is listed, expect a source build, which takes longer and requires build tools such as the Xcode Command Line Tools on macOS.
To watch the decision happen in real time, run:
brew install --verbose <formula>
Expected check: the log will show either a bottle download and pour, or a sequence of compilation commands. Relevant risk: source builds execute arbitrary Ruby and shell commands from the formula; if the tap is untrusted, treat it with the same caution as any third-party script. If the install fails or misbehaves, brew uninstall <formula> removes the installed version from the Cellar, though downloaded bottles may remain in the cache.
Trade-Offs and Limitations
The Ruby DSL is expressive, but Ruby is dynamically typed and interpreted. A typo in a formula—say, a misspelled method or a changed API in Homebrew's core—does not surface until the formula is evaluated during install. This means formulae can break when Homebrew updates its Ruby version or internal API, even if the underlying software has not changed.
Dependency resolution is also more linear than in system package managers such as apt or dnf. Homebrew generally resolves dependencies depth-first and does not always handle complex version conflicts as gracefully. If two formulae require incompatible versions of the same library, you may need to build one from source or use a versioned formula such as openssl@3.
An Actionable Habit
Treat every formula as code. For anything outside homebrew/core, run brew cat before brew install, skim the depends_on and install blocks, and verify the url points where you expect. Use brew list --versions to audit what is actually sitting in your Cellar. The Ruby DSL gives maintainers and tap authors enormous flexibility; reading the formula is the simplest way to understand what that flexibility is doing to your machine.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.