Homebrew formulas aren’t just for installed software; they’re the blueprints for how Homebrew builds and manages any software on your Mac.

Let’s build a simple "hello world" program and package it as a Homebrew formula.

First, create a hello.c file:

#include <stdio.h>

int main() {
    printf("Hello, Homebrew!\n");
    return 0;
}

Compile it:

gcc hello.c -o hello

Now, we need to turn this into a Homebrew formula. Homebrew formulas are Ruby files. Create a file named hello--0.1.0.rb (the version number is important) in a directory you’ll use for local formulas.

class Hello < Formula
  desc "A simple hello world program"
  homepage "https://example.com/hello" # Replace with a real URL if you have one
  url "file://#{Dir.pwd}/hello"
  sha256 "YOUR_SHA256_HASH_HERE" # We'll generate this next

  def install
    bin.install "hello"
  end
end

To get the SHA256 hash, run:

shasum -a 256 hello

This will output a long string. Copy that and paste it into the sha256 line in your Ruby file.

Now, tell Homebrew about your local formula. Navigate to the directory containing your hello--0.1.0.rb file.

brew tap --local ./

This tells Homebrew to look for formulas in the current directory. You should see output like:

==> Tapping local formula directory: /path/to/your/formula/directory

Now you can install your formula:

brew install hello

And run it:

hello

Output:

Hello, Homebrew!

The install method in the formula is where the magic happens. bin.install "hello" tells Homebrew to take the compiled hello executable from the unpacked source (in this case, directly from the url) and place it into Homebrew’s bin directory for your system. Homebrew automatically manages this directory, ensuring your executable is in your $PATH.

The url pointing to a local file is a bit of a trick. In a real-world scenario, you’d typically point to a .tar.gz or .zip archive hosted online. Homebrew downloads this archive, unpacks it, and then runs the install method within the context of that unpacked directory. For our simple case, file://#{Dir.pwd}/hello makes Homebrew treat the single executable as the "source" to be installed.

The desc and homepage are metadata. desc is what you see when you brew search hello, and homepage is a link for more information.

When you run brew install hello, Homebrew performs several steps:

  1. It finds the formula (hello--0.1.0.rb).
  2. It downloads the url (our local hello file).
  3. It verifies the sha256 hash.
  4. It "unpacks" the downloaded artifact (in our case, it’s just the single file).
  5. It executes the install method within a temporary directory.
  6. It moves the installed files to their final locations (e.g., bin.install puts it in $(brew --prefix)/bin).

If you were building from source code, your install method might look more like this:

  def install
    system "make", "install" # Assumes a Makefile with an install target
  end

Or, if you’re just copying a pre-compiled binary:

  def install
    bin.install "my_precompiled_binary" => "mycommand" # Installs 'my_precompiled_binary' and renames it to 'mycommand'
  end

The keg_only option is crucial for avoiding conflicts. If your formula installs a binary with the same name as a system binary (like curl or git), you don’t want it to overwrite the system version or be used by default. You’d add keg_only true to your class definition. This means you’d have to explicitly link it using brew link hello if you wanted it in your $PATH, or call it using its full Homebrew prefix path like $(brew --prefix)/opt/hello/bin/hello.

The next step is usually to package a more complex application with dependencies.

Want structured learning?

Take the full Homebrew course →