Homebrew Formulae Explained: What the Ruby DSL Runs at Install Time
A formula is a Ruby class that only runs when no bottle matches. Here is a worked example, what each part controls, and the mistakes that break installs.
24 Jan 2026, 04:45 UTC

A Homebrew formula is a Ruby class, and brew install is the program that evaluates it. The useful consequence is that the Ruby install method often never runs: if a pre-built "bottle" exists for your operating system and CPU, Homebrew downloads that binary archive, unpacks it into a versioned directory, and links it. The DSL only executes when Homebrew falls back to building from source — which is exactly when you need to read it.
This article walks through one small formula, explains what each part controls, and covers the failure modes that show up when you write or debug one. Homebrew's DSL changes between releases, so treat the helper names below as the shape of the thing and confirm details against brew edit on a formula you already have installed.
The one thing to know before writing a formula
Homebrew installs into a versioned prefix rather than dropping files straight into /usr/local/bin. The prefix helper inside a formula resolves to something like $(brew --cellar)/fast-tool/1.2.0, and bin, lib, libexec and share are subdirectories of it. Everything your build produces should land under prefix; Homebrew then symlinks selected files into the global bin. Hardcoding /usr/local breaks that contract and is a common reason a locally written formula appears to succeed but leaves no trace of itself.
A worked example
Save this as fast-tool.rb in a scratch directory. The digest is a placeholder — a real formula needs the actual SHA-256 of the tarball, and Homebrew will refuse to proceed without a matching one.
class FastTool < Formula
desc "Example data processor used to illustrate the Formula DSL"
homepage "https://example.com/fast-tool"
url "https://example.com/fast-tool-1.2.0.tar.gz"
sha256 "0000000000000000000000000000000000000000000000000000000000000000"
license "MIT"
depends_on "cmake" => :build
depends_on "openssl@3"
def install
system "cmake", "-S", ".", "-B", "build", *std_cmake_args
system "cmake", "--build", "build"
system "cmake", "--install", "build"
end
test do
assert_match "fast-tool", shell_output("#{bin}/fast-tool --version")
end
end
Reading it in order:
urlandsha256define the source artifact. Homebrew downloads it, verifies the digest, and unpacks it into a temporary build directory that becomes the working directory forinstall.depends_on "cmake" => :builddeclares a build-time-only dependency. It is present while compiling and is not recorded as a runtime requirement of the installed package.depends_on "openssl@3"is a runtime dependency. Homebrew resolves the whole set as a directed acyclic graph before any build starts, so dependencies are installed first.systemruns an external command and raises if it exits non-zero, which aborts the install. The array form passes arguments directly without shell interpretation; the single-string form goes through a shell, so quoting and globbing behave differently.*std_cmake_argsis Homebrew's canned CMake argument list. It already sets the install prefix and build type, which is why the example does not pass-DCMAKE_INSTALL_PREFIXitself.test dois not run during install. It is the smoke test executed bybrew test, and it is what lets a maintainer confirm the installed binary actually works.
To try it, from the directory containing the file:
brew install --build-from-source ./fast-tool.rb
brew test fast-tool
brew uninstall fast-tool
Run these as your normal user; Homebrew refuses to run under sudo. The install writes into the Homebrew prefix, so it is a state-changing operation, and brew uninstall is the way back. If you only want to inspect dependency resolution without installing anything, brew deps --tree fast-tool prints the graph.
Bottles versus source builds
A bottle do block in a formula lists pre-compiled archives keyed by platform tags. When one matches your system, Homebrew pours it and skips install entirely. When none matches — an unusual macOS version, a Linux distribution without a published bottle, or an explicit --build-from-source — the Ruby path runs.
| Path | What runs | Typical failure |
|---|---|---|
| Bottle pour | Download, digest check, unpack, link | No bottle matching your platform tag |
| Source build | Your install method and its system calls | Missing compiler or headers, or a system call returning non-zero |
Limits and common mistakes
- Editing core formulae in place. Files under the Homebrew installation's formula directory are managed by
brew updateand will be overwritten. Put custom formulae in a tap or install them from a local path. - Circular dependencies. If formula A depends on B and B depends on A, resolution cannot terminate and the install fails before anything is built.
- Assuming a source build is self-contained. On macOS, compiling requires the Xcode Command Line Tools; a formula that calls CMake, Autoconf or a compiler will fail without them. On Linux, the equivalent toolchain packages must be present.
- Putting user instructions in
install. Anything a human needs to read after installation belongs in acaveatsblock, which Homebrew prints once the install finishes. Output frominstallscrolls past during the build. - Hardcoding paths. Use the
prefix,bin,libexecandetchelpers rather than absolute paths; the Homebrew prefix differs between Intel and Apple Silicon Macs and between Linux distributions. - Treating the DSL as stable. Helper methods and audit rules change between Homebrew releases. A formula that was clean when written may produce warnings or errors later.
One caveat before relying on any specific flag: Homebrew's subcommands and audit rules change between releases. Run brew audit --help and brew install --help on your own machine to confirm which options exist in your version rather than copying flags from an older guide.
How to check your result
- Confirm where files landed:
brew --cellar fast-toolprints the versioned directory, andbrew --prefixprints the Homebrew root. - Confirm the dependency record:
brew info --json=v2 fast-toolemits formula metadata, including declared dependencies, as JSON you can pipe to a parser. - Confirm the binary runs:
brew test fast-toolexecutes thetest doblock. - Confirm the link:
which fast-toolshould resolve to a symlink under the Homebrewbindirectory.
If any of those disagree with what you intended, the formula is the place to look — not the build output.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.