Creating Homebrew Formulae

This is the second part of the "missing guide" to porting software to Homebrew. The first part prepares our project, pixelserv-tls ready for packaging. In the second part, we will go through the packaging process aka creating a forumula in Homebrew speak.

Resources on this topic are abundant, for example, this official formula cookbook and this example formula with detailed annotations. I found both are great references but not the right places to start perhaps. I took a slightly different approach by picking an existing formula of a project with similar nature. Also the new formula is initially created in a user directory instead of Homebrew's cellar as most other guides do.

Creating the Formula

I picked HAProxy's formula as a starting point. To begin, create an empty file named "pixelserv-tls.rb" in your home directory. The extension indicates it is a Ruby script. A Homebrew formula is technically a class written in Ruby. The first few lines look like these:

class PixelservTls < Formula
  desc "A tiny bespoke HTTP/1.1 server for adblock and accelerating web browsing."
  homepage "https://kazoo.ga/pixelserv-tls/"
  url "https://github.com/kvic-z/pixelserv-tls/archive/0fdf1f529c733b7dd0b2bef4a023335497397492.zip"
  sha256 ""
  
  depends_on "autoconf" => :build
  depends_on "automake" => :build
  depends_on "[email protected]"

  $cert_path = '/var/cache/pixelserv'

Line 1 defines the new class "PixelservTls" which is inherited from class "Formula". The next two lines are of informational purpose only.

Line 4 defines the URL to the source code of your project. Line 5 is a sha256 hash of the code tarball. It serves as data integrity check. We can leave it empty for now. Line 7-9 define the build dependencies. We know pixelserv-tls need "automake" and "autoconf" build tools. We also know it requires OpenSSL libraries at run-time. Note that ":build" indicates a dependency is build-time requirement only.

Line 11 defines a global variable that stores CERT_PATH. This makes configuration change easier in the rest of the formula.

  def install
    system "autoreconf", "-i"
    system "./configure", "--prefix=#{prefix}"
    system "make", "install"
    mkdir $cert_path
  end

Now we could contruct the build instructions. Line 13 defines a function "install." The lines inside the function body follow closely Build from Source guide. We want to customize and fit into MacOS/Homebrew. In particular, "#{prefix}" is an environment variable to the cellar's location where pixelserv-tls files are centrally stored. Homebrew has the variable defined. Its value works out to be  "/usr/local/cellar/pixelserv-tls/2.2.0/" in our example.

  def caveats
    s = <<~EOS
    Set directory permission of CERT_PATH to 'nobody' by running:
      sudo chown nobody #{$cert_path}

    To serve HTTPS requests, copy your ca.crt & ca.key into:
      #{$cert_path}

    Instructions to generate ca.crt & ca.key:
      https://github.com/kvic-z/pixelserv-tls/wiki/Create-and-Import-the-CA-Certificate
    EOS
    s
  end

Next define a function "caveats." It contains text to be displayed at the end of the installation. To remind users of optional and/or mandatory actions that aren't automatically taken care of. Note that any system "caveats" will be appended at the end during installation.

  plist_options :startup => true
  def plist; <<~EOS

  EOS
  end

If your project is a service, an Apple "plist" definition in XML is required. Homebrew will automatically generate the plist file at installation. Place and enable the plist file in MacOS system directory (/Library/LaunchDaemons) when started through "sudo brew services start <service>". Disable and remove the plist file when stopped by "sudo brew services stop <service>". This is a very nice feature from Homebrew to manage its services and interact with MacOS launchd. "plist_options :startup" at line 34 indicates the service should run on startup.

test do
    assert_match "pixelserv-tls 2", `#{bin}/pixelserv-tls -v`
  end
end

Line 60-62 defines a simple test. The complete formula is available HERE.

Test and Distribute the Formula

To test the new formula, I prefer to run it locally from a user directory.

$ brew install --build-from-source ./pixelserv-tls.rb
######################################################################## 100.0%
==> Downloading https://github.com/kvic-z/pixelserv-tls/archive/0fdf1f529c733b7dd0b2bef4a023335497397492.zip
==> Downloading from https://codeload.github.com/kvic-z/pixelserv-tls/zip/0fdf1f529c733b7dd0b2bef4a023335497397492
######################################################################## 100.0%
==> autoreconf -i
==> ./configure --prefix=/usr/local/Cellar/pixelserv-tls/2.2.0
==> make install
==> Caveats
Set directory permission of CERT_PATH to 'nobody' by running:
  sudo chown nobody /var/cache/pixelserv

To serve HTTPS requests, copy your ca.crt & ca.key into:
  /var/cache/pixelserv

Instructions to generate ca.crt & ca.key:
  https://github.com/kvic-z/pixelserv-tls/wiki/Create-and-Import-the-CA-Certificate

To have launchd start pixelserv-tls now and restart at startup:
  sudo brew services start pixelserv-tls
==> Summary
🍺  /usr/local/Cellar/pixelserv-tls/2.2.0: 8 files, 101.8KB, built in 19 seconds

Note that the first three "caveats" are what we include in the formula. The last "caveat" is added by Homebrew automatically. Unlike above output, you'll receive a warning about missing sha256 hash, together with the actual hash calculated by Homebrew. Now you could copy and paste the hash value back to variable "sha256" at Line 5, and run the installatioin again.

$ sudo chown nobody /var/cache/pixelserv
$ sudo brew services start pixelserv-tls
==> Successfully started `pixelserv-tls` (label: homebrew.mxcl.pixelserv-tls)

We follow the "caveats" reminders and have successfully installed and started pixelserv-tls.

To distribute your formula, submit it to Homebrew and be part of its core repository. It will be available to everyone who could install through a familiar command line "brew install pixelserv-tls". Many good tutorials on this topic, for example, the official guide from Homebrew.

It takes time and perhaps an uphill struggle to get a new formula reviewed and accepted. You also might not have the appetite for worldwide distribution. Homebrew has a facility known as Tap. A "tap" is a user repository of packages and houses one or more formula on a git server. After a tap is added, any formula done through "brew install" will be automatically checked for update just like formula from the core repository. The official how-to from Homebrew explains its usage in detail.

What if you only have one package to share?

$ brew install https://kazoo.ga/pixelserv-tls/pixelserv-tls.rb

With a tap respository setup, adding new formula to distribution is trivial and quick. If you only have one formula to share, the simplest way is perhaps putting up your formula for download. "brew install" takes any URL formats that are recognizable by git. For example, pixelserv-tls could be installed like in the above command line. The formula is placed on my webserver, kazoo.ga. In fact, the file could be anywhere that takes and gives text content in raw e.g. Pastebin, Google and other cloud drives.

This concludes my initial deep-dive in developing for Homebrew. The two-part guide is more like notes to remind myself. Hopefully it also helps a few more people and do let me hear you if it does.

comments powered by Disqus