Developing a Swift command-line tool that runs on both Intel and M1 Macs can be a challenging task, especially when you consider the differences in architectures between these two types of Macs. Thankfully, Apple’s Universal Binary technology can help you achieve just that. In this blog post, we’ll walk through the process of creating a Universal Binary for a Swift Package Manager (SPM) command-line tool, which can run on both Intel (x86_64) and M1 (arm64) Macs.

Step 1: Building for Different Architectures

Make sure you have Xcode and its command-line tools installed on your machine. Open Terminal and navigate to your Swift project directory.

To create a Universal Binary, we need to build our Swift tool for both the arm64 and x86_64 architectures. Thankfully, with the latest versions of Swift, we can achieve this with a single swift build command:

arm64:

swift build --configuration release --arch arm64

x86_64:

swift build --configuration release --arch x86_64

Step 2: Creating a Universal Binary

Having built the tool for both architectures, the next step is to use the lipo command to combine the two into a Universal Binary. You would do this by running the following command in your terminal:

lipo -create -output "YourTool" ".build/arm64-apple-macosx/release/YourTool" ".build/x86_64-apple-macosx/release/YourTool"

Replace "YourTool" with the name of your Swift command-line tool. This command combines the arm64 and x86_64 versions of your tool into a single Universal Binary that can run on both architectures.

You can confirm that the Universal Binary was created successfully by running:

lipo -info YourTool

If successful, this command will output something like:

Architectures in the fat file: YourTool are: x86_64 arm64

Step 3: Distributing Your Tool

Now that you’ve created a Universal Binary, the next step is distribution. One of the most popular methods of distributing command-line tools for macOS is through Homebrew. To distribute your tool through Homebrew, you’d need to create a Homebrew formula.

Before you can create a formula, however, you’ll need to host your binary somewhere. A good place to host your binary is on GitHub as part of a release. After uploading your binary to a new release on GitHub, you can create a Homebrew formula that references the binary directly.

Here’s a sample Homebrew formula:

class YourTool < Formula
  desc "Your tool description"
  homepage "https://github.com/yourusername/YourTool"
  url "https://github.com/yourusername/YourTool/releases/download/v0.2.3/YourTool"
  sha256 "sha256-of-the-binary"

  def install
    bin.install "YourTool"
  end

  test do
    system "#{bin}/YourTool", "--version"
  end
end

In this formula, replace the desc, homepage, url, and sha256 with the appropriate values for your tool.

Remember to update the sha256 field in the formula with the new SHA-256 hash of your binary file. This can be calculated by running

shasum -a 256 /path/to/your/YourTool

in the terminal. This hash ensures the integrity of the downloaded file during the Homebrew installation process.

Once you’ve created your formula, users can install your tool with a simple brew install YourTool command, regardless of whether they're on an Intel or M1 Mac.

Conclusion

Creating a Swift command-line tool that supports both Intel and M1 Macs might seem daunting at first, but with Apple’s Universal Binary technology, it’s relatively straightforward. By building your tool for each architecture and combining them with the lipo command, you can ensure compatibility across different Mac models.

In this guide, we’ve walked you through creating a Universal Binary for a Swift command-line tool and distributing it via Homebrew. Whether you're building a brand-new tool or enhancing an existing one, this tutorial should assist you in ensuring your tool reaches a wider audience of Mac users.