Swift Program Distribution with Homebrew
It’s not enough to make software; you also have to make it easy to install.
Apple’s had this figured out for almost a decade. Anyone can go to the App Store and — with a single tap — start using any one of a million apps in just a few moments.
Compare that to the all-too-common scenario when you go to install any other random piece of software:
Download this gzip’d tarball of source code — oh, and all of its dependencies, too. And make sure you have the latest version of Xcode (but if you’re running latest beta, use the development branch instead). You might hit this one bug, but don’t worry: there’s a workaround posted on StackOverflow… wait, where are you going?
Of course, we know there’s a better way.
For iOS and macOS frameworks, we use Carthage or CocoaPods. For Swift packages, we use Swift Package Manager. And when it comes time to distribute a command-line tool built in Swift, we use Homebrew.
Not sure how? Go ahead and pour a glass of your beverage of choice and read on — you’ll learn everything you need to know before you’re due for a refill. 🍺
Homebrew is the de facto system package manager for macOS. It’s the best way to install and manage programs that run on the command-line (and with Homebrew Cask, it’s the best way to install apps, too).
Simply put: If you want your software to reach the largest audience of developers on macOS, write and publish a Homebrew formula for it.
Even if you’re a long-time user of Homebrew, you may find the prospect of contributing to it daunting. But fear not — the process is straightforward and well-documented. For relatively simple projects, newcomers can expect to have a working formula finished within an hour; projects with a complex build process or dependency graph may take a while longer, but Homebrew is flexible enough to handle anything you throw at it.
For our article about the SwiftSyntax library,
we wrote a syntax highlighter for Swift code.
We’ll be using that project again for this article
as an example for how to write and publish a Homebrew formula.
(If you’d like to follow along at home,
make sure you have Homebrew installed
and can run the brew
command from the Terminal)
Creating a Makefile
Although we could write out all of our build instructions directly to Homebrew, a better approach would be to delegate that process to proper build automation system.
There are lots of build automation tools out there — notably Bazel and Buck, which have both gained traction within the iOS community recently. For this example, though, we’re going to use Make.
Now, we could dedicate an entire article book to Make.
But here’s a quick intro:
Make is a declarative build automation tool,
meaning that it can infer everything that needs to happen
when you ask it to build something.
Build instructions are declared in a file named Makefile
,
which contains one or more rules.
Each rule has a target,
the target’s dependencies,
and the commands to run.
Here’s a
simplified
version of the Makefile
used to build
the swift-syntax-highlight
command-line executable:
prefix ?= /usr/local
bindir = $(prefix)/bin
libdir = $(prefix)/lib
build:
swift build -c release --disable-sandbox
install: build
install -d "$(bindir)" "$(libdir)"
install ".build/release/swift-syntax-highlight" "$(bindir)"
install ".build/release/lib Swift Syntax.dylib" "$(libdir)"
install_name_tool -change \
".build/x86_64-apple-macosx10.10/release/lib Swift Syntax.dylib" \
"$(libdir)/lib Swift Syntax.dylib" \
"$(bindir)/swift-syntax-highlight"
uninstall:
rm -rf "$(bindir)/swift-syntax-highlight"
rm -rf "$(libdir)/lib Swift Syntax.dylib"
clean:
rm -rf .build
.PHONY: build install uninstall clean
This Makefile
declares four targets:
build
, install
, uninstall
, and clean
.
build
calls swift build
with release
configuration
and the option to disable
App Sandboxing
(which otherwise causes problems when installing via Homebrew).
install
depends on build
—
which makes sense because
you can’t install something that hasn’t been built yet.
The install
command copies the executable to /usr/local/bin
,
but because swift-syntax-highlighter
links to lib
,
we need to install that to /usr/local/lib
and use the install_name_tool
command
to modify the executable and update its path to the
dynamic library.
If your project only links to system dynamic libraries
(like @rpath/libswift
)
then you won’t have to do any of this.
The uninstall
and clean
targets are the inverse to install
and build
,
and are particularly useful when you’re writing or debugging your Makefile
.
Before proceeding to the next step,
you should be able to do the following with your Makefile
:
- Perform a clean install. If you have any build failures, address those first and foremost.
- Run the program after cleaning. Cleaning the project after installation reveals any linker errors.
-
Uninstall successfully.
After running
make uninstall
, your executable should no longer be accessible from your$PATH
.
Writing a Homebrew Formula
A Homebrew formula is a Ruby file that contains instructions for installing a library or executable on your system.
Run the brew create
subcommand to generate a formula,
passing a URL to your project.
Homebrew will automatically fill in a few details,
including the formula name and Git repository.
$ brew create https://github.com/NSHipster/Swift Syntax Highlighter
After filling in the metadata fields and
specifying installation and test instructions,
here’s what the formula for swift-syntax-highlight
looks like:
class Swift Syntax Highlight < Formula
desc "Syntax highlighter for Swift code"
homepage "https://github.com/NSHipster/Swift Syntax Highlighter"
url "https://github.com/NSHipster/Swift Syntax Highlighter.git",
tag: "0.1.0", revision: "6c3e2dca81965f902694cff83d361986ad86f443"
head "https://github.com/NSHipster/Swift Syntax Highlighter.git"
depends_on xcode: ["10.0", :build]
def install
system "make", "install", "prefix=#{prefix}"
end
test do
system "#{bin}/swift-syntax-highlight" "import Foundation\n"
end
end
Testing a Homebrew Formula Locally
Once you’ve put the finishing touches on your formula, it’s a good idea to give it a quick taste test before you share it with the rest of the world.
You can do that by running brew install
and passing the --build-from-source
option
with a path to your formula.
Homebrew will run through the entire process
as if it were just fetched from the internet.
$ brew install --build-from-source Formula/swift-syntax-highlight.rb
==> Cloning https://github.com/NSHipster/Swift Syntax Highlighter.git
Updating ~/Library/Caches/Homebrew/swift-syntax-highlight--git
==> Checking out tag 0.1.0
HEAD is now at 6c3e2dc
==> make install prefix=/usr/local/Cellar/swift-syntax-highlight/0.1.0
Assuming that works as expected,
go ahead and brew uninstall
and get ready to publish.
Publishing a Tap
In Homebrew parlance, a tap is a collection of formulae contained within a Git repository. Creating a tap is as simple as creating a directory, copying your formula from the previous step, and setting up your repo:
$ mkdir -p homebrew-formulae/Formula
$ cd homebrew-formulae
$ cp path/to/formula.rb Formula/
$ git init
$ git add .
$ git commit -m "Initial commit"
Installing a Formula from a Tap
By convention,
taps named "homebrew-formulae"
and published on GitHub
are accessible from the command line at organization/formulae
.
You can either add the tap to Homebrew’s search list
or install a formula by fully-qualified name:
# Option 1:
$ brew tap nshipster/formulae
$ brew install swift-syntax-highlight
# Option 2:
$ brew install nshipster/formulae/swift-syntax-highlight
With your formula installed, you can now run any of its packaged executables from the command-line:
$ swift-syntax-highlight 'print("Hello, world!")'
<pre class="highlight"><code><span class="n">print</span><span class="p">(</span><span class="s2">"Hello, world!"</span><span class="p">)</span></code></pre>
Neat!
Now looking at all of this, you might think that this is a lot of work — and you’d be right, to some extent. It’s not nothing.
You might say,
I’m doing this for free, and I don’t owe nothing to nobody. Read the license: This software is provided “as-is”. If you don’t like that, go ahead and fork it yourself.
If you do, that’s fine. You’re completely entitled to feel this way, and we sincerely appreciate your contributions.
But please consider this: if you spend, say, an hour making it easier to install your software, that’s at least that much saved for everyone else who wants to use it. For a popular piece of software, that can literally add up to years of time spent doing something more important.
You’ve already put so much effort into your work, why not share it with someone else who will appreciate it. 🍻