Building Mach-O bundles which work on both macOS and iOS

This post contains notes on how I automated my CI to generate a Mach-O bundle containing code targetting macOS and x86_64, and iOS and ARMv7.

Setting up a working cross toolchain on macOS

The only way to get the iphoneOS SDK seems to be to install XCode, so a simple xcode-select --install won’t be enough. We can install XCode the regular way via the App Store or, if you only have an SSH access, by downloading the installer on Apple’s developer portal.

The good news is that’s all you need to do. XCode ships with a full cross toolchain working for every one of Apple’s OS, and multiple architectures, including ARM and x86_64.. No need to build the cross toolchain by yourself.

Setting up the correct sysroot

You need to tell the toolchain where are the headers and libraries for your target system. It’s the sysroot in GCC and Clang parlance.

$ clang ... -sysroot <path> ...

You can use the command xcode-select -p to get the path’s prefix. Since we’re interested in macOS and iphoneOS, we would have:

  • <base>/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
  • <base>/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.1.sdk

You shouldn’t need to manually set any sysroot when building for macOS on your personal macOS machine. It’s useful when you want to build against a specific version of the MacOSX SDK.

Specifying what architecture to build for

When generating binaries for the Mach-O ABI, you can add architectures by chaining calls to the -arch switch. In my case I was interested in ARMv7 and x86_64.

In projects where you have the same sysroot for both architectures, you can just do the following:

$ clang ... -arch armv7 -arch x86_64 ...

As I said earlier, you have two distinct sysroots when targetting iphoneOS and macOS. So you can’t build the final bundle in a single command.

Merging Mach-O containers

What I did was generating both executables separately by reusing a common Makefile, and then merging them together with the command lipo.

$ lipo -create -output <output-path> <input-path>...

In my case I had something like that:

$ lipo -create -output MyMiddleware.bundle \
  MyMiddleware.macOS-x86_64.bundle \
  MyMiddleware.iphoneOS-ARMv7.bundle

Previous post