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:
- It finds the formula (
hello--0.1.0.rb). - It downloads the
url(our localhellofile). - It verifies the
sha256hash. - It "unpacks" the downloaded artifact (in our case, it’s just the single file).
- It executes the
installmethod within a temporary directory. - It moves the installed files to their final locations (e.g.,
bin.installputs 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.