Brew what? Link to heading

Being a MacOS user for the last 6 years, one of the things I install almost immediately is Homebrew. Homebrew is the de facto package manager for Mac and provides a nifty and intuitive way to install binaries through what they call formulae. Besides installing binaries Homebrew also supports installing Mac applications with the built-in cask functionality.

A collection of formulae is distributed trough a tap (hence the ๐Ÿบ gimmick). There is a tap containing the core formulae, one with drivers, one with fonts and even one with solely eid-software. More information on hosting your own tap can be found here.

Brew your own formula Link to heading

Although being a heavy user of the ecosystem it wasn’t until this week I actually had the need to create my own formula. Finding out about this amazing open source project awsweeper (a tool written in go to clean up resources in your AWS account) I felt it would be convenient to have it it installable as a Homebrew formula. Being flabbergasted by the ease of it I wanted to share the process of creating a formula and submitting a pull request to the Homebrew core tap.

Create a local version of the formula Link to heading

As with all great software Homebrew uses some sane default convenient to the majority of users. If the source code you want to wrap in a formula is hosted on github (or any other publicly accessible source) it is as simple as running brew create https://github.com/cloudetc/awsweeper/archive/v0.2.0.tar.gz which point to a tarball containing the release you want to package. Releases of a github project can be found on the releases page of a project available at the /releases url of the project. In my case this is: https://github.com/cloudetc/awsweeper/releases.

This results in the creation a local version of the formula based on the homebrew template:

 1# Documentation: https://docs.brew.sh/Formula-Cookbook
 2#                https://www.ruby"oc.info/github/Homebrew/brew/master/Formula
 3# PLEASE REMOVE ALL GENERATED COMMENTS BEFORE SUBMITTING YOUR PULL REQUEST!
 4class Awsweeper < Formula
 5  desc "A tool to clean out your AWS account"
 6  homepage ""
 7  url "https://github.com/cloudetc/awsweeper/archive/v0.2.0.tar.gz"
 8  sha256 "e867ecc3df01fb799fe900bc587676460ade1752f63c9176542bb66d27a1833a"
 9  # depends_on "cmake" => :build
10
11  def install
12    # ENV.deparallelize  # if your formula fails when building in parallel
13    # Remove unrecognized options if warned by configure
14    system "./configure", "--disable-debug",
15                          "--disable-dependency-tracking",
16                          "--disable-silent-rules",
17                          "--prefix=#{prefix}"
18    # system "cmake", ".", *std_cmake_args
19    system "make", "install" # if this fails, try separate make/make install steps
20  end
21
22  test do
23    # `test do` will create, run in and delete a temporary directory.
24    #
25    # This test will fail and we won't accept that! For Homebrew/homebrew-core
26    # this will need to be a test that verifies the functionality of the
27    # software. Run the test with `brew test awsweeper`. Options passed
28    # to `brew install` such as `--HEAD` also need to be provided to `brew test`.
29    #
30    # The installed folder is not in the path, so use the entire path to any
31    # executables being tested: `system "#{bin}/program", "do", "something"`.
32    system "false"
33  end
34end

The creation of this newly created file will be /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/awsweeper.rb. As you see it parses the name of the package from the tar file you provided when running brew create. Homebrew formulae are basically simple ruby classes which inherit from a base class Formula. Don’t panic, the DSL is pretty simple and you won’t need any prior Ruby knowledge to get going.

Dissection of a formula Link to heading

There are 4 important parts in a Homebrew formula:

1. The metadata: Link to heading

desc "A tool to clean out your AWS account"
homepage ""
url "https://github.com/cloudetc/awsweeper/archive/v0.2.0.tar.gz"
sha256 "e867ecc3df01fb799fe900bc587676460ade1752f63c9176542bb66d27a1833a"
This section contains all the necessary information about the package you want to wrap in the formula:

  • desc: a one-liner describing the package that will be installed by the formula. It will fetch the description from the related Github project by default. Be aware that these are not always compliant with the naming conventions of Homebrew itself. For example the desc above will be rejected because it starts with the article a. Also there is a maximum length of 80 characters for this field. The style guide is enforced by Rubocop a static code linting tool for Ruby. You can find the configuration files for all these rules here.
  • homepage: The URL of the location where people can find more about the project. By default it will use the Website it found on the Github page of the project.

    An elephant at sunset

  • url: URL of the source code tarball.
  • sha256: The SHA-256 encrypted hash of the source code tar.

In the case of my awsweeper formula this becomes:

desc "Clear out your AWS account with this convenient tool"
homepage "https://github.com/cloudetc"
url "https://github.com/cloudetc/awsweeper/archive/v0.2.0.tar.gz"
sha256 "e867ecc3df01fb799fe900bc587676460ade1752f63c9176542bb66d27a1833a
As you will notice the fields name and version are not present. This is the case when both those fields can be parsed from the tarball’s name.

2. External dependencies: Link to heading

# depends_on "cmake" => :build
This part allows you to specify dependencies the package needs either at build time, during test or at runtime. You can depend on other formulae which can be defined with the name of the formula(depends_on "pkg-config") or when using a symbol specifying a dependency which can be fulfilled by one or more formulae, casks or other system-wide installed software e.g. depends_on :xcode => "9.3".

Given the fact we are trying to build a Go package we will need to define Go as a build dependency.

So our dependency config will become:

  depends_on "go" => :build

3. The install block: Link to heading

def install
  # ENV.deparallelize  # if your formula fails when building in parallel
  # Remove unrecognized options if warned by configure
  system "./configure", "--disable-debug",
                        "--disable-dependency-tracking",
                        "--disable-silent-rules",
                        "--prefix=#{prefix}"
  # system "cmake", ".", *std_cmake_args
  system "make", "install" # if this fails, try separate make/make install steps
end
This part of the formula describes how Homebrew should proceed with installing the formula. Information on this can mostly be found in the README.md of the Github repo of the package you are wrapping. In our case it’s as straightforward as running go build. Notice we will have to set the environment variable GOPATH to point to the location where you are building the binary for the package. GOPATH is the workspace directory where go looks for dependencies. It will also contain your own go source code.

This is how our install command ends up looking:

def install
  ENV["GOPATH"] = buildpath
  awsweeper_path = buildpath/"src/github.com/cloudetc/awsweeper"
  awsweeper_path.install buildpath.children

  cd awsweeper_path do
    system "go", "build"
    bin.install "awsweeper"
  end
end
The variable awsweeper_path refers to the go compliant path we set up for the build to be able to proceed. awsweeper_path.install buildpath.children copies all the files extracted from the tarball to the given awsweeper_path. The cd awsweeper_path changes the current path to the given directory. system "go", "build" makes a system call running the default build commando for go binaries. bin.install marks which file should be copied to the bin folder of our formula, in this case the name of the binary is awsweeper. After installing this formula Homebrew will make sure the binary becomes available on our path

4. The test block: Link to heading

test do
  # `test do` will create, run in and delete a temporary directory.
  #
  # This test will fail and we won't accept that! For Homebrew/homebrew-core
  # this will need to be a test that verifies the functionality of the
  # software. Run the test with `brew test awsweeper`. Options passed
  # to `brew install` such as `--HEAD` also need to be provided to `brew test`.
  #
  # The installed folder is not in the path, so use the entire path to any
  # executables being tested: `system "#{bin}/program", "do", "something"`.
  system "false"
end
This block is expected to provide a test that demonstrates the functionality of the package we are wrapping. In the case of awsweeper this could look something like this:
test do
  assert_match "0.1.1", shell_output("#{bin}/awsweeper --version ")
end
It’s not exactly a tests that demonstrate the functionality but it’s out of the scope of a Hombrew test to set up actual AWS resources to be able to query and delete them.

What’s brewing next? Link to heading

So I hope this post helped in understanding the anatomy of Homebrew formulas and can motivate you in writing your own. In case you wonder how we can get this formula into homebrew-core you can find the explanation here. Oh, and if you want to you know how my awsweeper pull request is doing, you can check the status of my pull request here.