Upload a CLI Written in Swift to Homebrew

Writing a command tool with Swift

I read a blog about the spell checking in Xcode. Spell-checking is very useful for me but it has some problems when learning or forgetting words in Xcode. Actually, the spelling checking in Xcode bases on the local dictionary of Mac system. Unfortunately, Mac system doesn’t provide a convenience way for us to manage the local dictionary. The only way to modify the local dictionary is to edit the ~/Library/Spelling/LocalDictionary file, then you must restart the AppleSpelling service with Activity Monitor app by killing the service. The AppleSpell service will restart automatically after being killed, applying modifications to all editors.

localdic

A CLI for managing the local dictionary on Mac.

So I want to make a CLI tool to list, learn and forget words in the local dictionary. I have some Ruby experience but I want to write it with Swift this time for measuring efficiency of developing scripts with Swift.

Swift team has already provided a package swift-argument-parser to help us customize CLIs easily and efficiently.

Tool

The best web for finding Swift packages you want.

Bootstrapping

Using SPM to quickly create an executable package.

1
2
3
mkdir localdic
cd localdic
swift package init --name LocalDic --type executable

There are two generated files, Package.swift is the SPM configuration file, main.swift is the application entrypoint.

1
2
3
4
5
.
├── Package.swift
└── Sources
└── main.swift
2 directories, 2 files
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Package.swift
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "localdic",
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.executableTarget(
name: "localdic"),
]
)
1
2
3
4
5
6
// main.swift
// The Swift Programming Language
// https://docs.swift.org/swift-book

print("Hello, world!")

As you can see, SPM created a HelloWorld example for us. Using swift run to build and run the package.

1
2
3
4
swift run
Building for debugging...
Build complete! (0.36s)
Hello, world!

Now we add dependencies swift-argument-parse and RainBow (help output colorful string) to the Package.swift file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import PackageDescription

let package = Package(
name: "localdic",
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/onevcat/Rainbow.git", from: "4.0.1"),
],
targets: [
.executableTarget(
name: "localdic",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Rainbow", package: "rainbow"),
]),
.testTarget(
name: "localdicTests",
dependencies: ["localdic"]),
]
)

Want to use the swift-argument-parser to create a CLI, we need to renamed the file main.swift to LocalDic.swift. In the LocalDic.swift, we defined a struct LocalDicconforming to the protocol ParsableCommand and mark it with @main. The @main indicates the struct is the program entrypoint.

A main.file or another file defined a type marked with @main can be as the entrypoint of executable, but you can not do these simultaneously.

To see the full version code, check out localdic on github. Detail about using ArgumentParser, you can see the official document

Create a Makefile for your CLI

Finished coding, you need to build it as a executable file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
swift build -c release

Fetching https://github.com/onevcat/Rainbow.git from cache
Fetching https://github.com/apple/swift-argument-parser.git from cache
Fetched https://github.com/onevcat/Rainbow.git (1.87s)
Fetched https://github.com/apple/swift-argument-parser.git (1.87s)
Computing version for https://github.com/onevcat/Rainbow.git
Computed https://github.com/onevcat/Rainbow.git at 4.0.1 (0.60s)
Computing version for https://github.com/apple/swift-argument-parser.git
Computed https://github.com/apple/swift-argument-parser.git at 1.2.3 (0.64s)
Creating working copy for https://github.com/onevcat/Rainbow.git
Working copy of https://github.com/onevcat/Rainbow.git resolved at 4.0.1
Creating working copy for https://github.com/apple/swift-argument-parser.git
Working copy of https://github.com/apple/swift-argument-parser.git resolved at 1.2.3
Building for production...
remark: Incremental compilation has been disabled: it is not compatible with whole module optimization
remark: Incremental compilation has been disabled: it is not compatible with whole module optimization
remark: Incremental compilation has been disabled: it is not compatible with whole module optimization
remark: Incremental compilation has been disabled: it is not compatible with whole module optimization
[6/6] Linking localdic
Build complete! (27.85s)

After the build finished, you can find a executable file localdic at the path .build/release/localdic. For easily using, you can copy it to the folder /usr/local/bin and then you can directly print localdic in you terminal to invoke the command.

A better way to build or manage our application is to create a Makefile, a tool that defines a graph of rules for creating targets. A quick tutorial to learn Makefile.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Define variables.
prefix ?= /usr/local
bindir = $(prefix)/bin

# Command building targets.
build:
swift build -c release --disable-sandbox

install: build
install -d "$(bindir)"
install ".build/release/localdic" "$(bindir)"

uninstall:
rm -rf "$(bindir)/localdic"

clean:
rm -rf .build

.PHONY: build install uninstall clean

In the Makefile, I defined four commands. Other users can directly invoke make install to build and install the CLI.

Your own homebrew formulae and taps

The best way to share your applications to other Mac users is uploading them to Homebrew. Homebrew is a package manager for macOS, you can use it to install CLI or GUI applications. Of course, every developer can upload his applications to homebrew for sharing them with the world.

The precondition of uploading our applications to homebrew is a formulae file that is a ruby-based description file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// localdic.rb
class Localdic < Formula
desc "Manage the local dictionary on Mac."
homepage "https://github.com/SSBun/localdic"
url "https://github.com/SSBun/localdic.git", tag: "0.1.2"
version "0.1.2"

depends_on "xcode": [:build]

def install
system "make", "install", "prefix=#{prefix}"
end

test do
system "#{bin}localdic", "list"
end
end

The formula file configures basic information of your program, description, download url, version and installation way.

But uploading our formulae to the homebrew/core repository has strict requirements, the homebrew provides another way "tap", a personal repository to collect your own formulae.

To create a formula repository, you should initialize a git repository containing a folder Formula and copy the formula file localdic.rb into the repository.

1
2
3
4
5
6
.
├── Formula
   └── localdic.rb
└── README.md

2 directories, 2 files

Uploading you repository to github with name homebrew-formulae, then you can invoke the command brew tap <github-name>/homebrew-formulae to link your remote formula repository to local.

1
2
3
4
5
6
7
8
9
10
brew tap ssbun/formulae
==> Tapping ssbun/formulae
Cloning into '/usr/local/Homebrew/Library/Taps/ssbun/homebrew-formulae'...
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 15 (delta 2), reused 12 (delta 2), pack-reused 0
Receiving objects: 100% (15/15), done.
Resolving deltas: 100% (2/2), done.
Tapped 1 formula (13 files, 7.0KB).

Installed your own tap repository, you can directly invoke the brew install localdic to install the formula into your local environment.

References

Homebrew Document
A tool explaining shell commands for you
Swift Program Distribution with Homebrew
Build a Command-line Tool