macOS Universal Apps with Xcode 12 and CMake

Apple has just announced on WWDC 2020 on the move from Intel platform to its own ARM64 based Apple silicon. The new Mac devices (e.g. iMac / MacBook Pro / Mac Book Air) with the new Apple SOC and new macOS Big Sur will come for the holiday season 2020 and 2021. This business move allows Apple to have closely integrated eco-system both in hardware and software. What will that mean for software developers on the Mac platform? As a software developer on cross-platform applications, I write this article to answer the first question: source code build configuration software. We will only touch on Xcode 12 and CMake with Objective-C. I would also assume you already have an apple developer account and have Xcode 12 for macOS Universal Apps Beta installed on your macOS Catalina or Sur. You will need latest CMake 18 for macOS Sur.

Xcode 12

The Universal App is enabled default. The build settings already have the build architectures set to Standard Architecture with 64-bit Intel and ARM.

Those iOS developers would find these settings familiar to them. When your project are built for both iOS simulator and iPhone, you have the same settings. The difference is the Support Platform; iOS vs macOS.

Xcode Under the Hood

Xcode 12 command line tools setup

Before we start, make sure we have proper Xcode 12 setup with:

xcode-select -p

When the default xcode command line tools is not Xcode 12, we can switch to the Xcode 12 command line tools with:

sudo xcode-select -s /Applications/Xcode12-beta.app/

xcodebuild

Now, it is time to build the new example project with:

xcodebuild -target example build -project example.xcodeproj/ -arch x86_64 -arch arm64 ONLY_ACTIVE_ARCH=NO > build.log

-arch x86_64 -arch arm64” sets the build architecture. “ONLY_ACTIVE_ARCH=NO” enforces the build architecture.

Without “ONLY_ACTIVE_ARCH=NO”, the current active architecture (i.e. x86_64) will be used.

Universal App Binary

The release binary is placed into a folder :

build/Release/example.app

At this point, we will confirm our app is really Universal App with:

lipo -info build/Release/example.app/Contents/MacOS/example

Architectures in the fat file: build/Release/example.app/Contents/MacOS/example are: x86_64 arm64

Yes. It is Fat!

Build.log

Open the Build.log file with a text editor, we can find build rules for compilations and linkings that are working hard both in x86_64 and ARM64:

clang -x objective-c -target arm64-apple-macos10.15 …

clang -x objective-c -target x86_64-apple-macos10.15 …

lipo -create …x86_64/Binary/example …arm64/Binary/example -output …MacOS/example

The last line produces a Fat binary from both x86_64 and arm64 binaries. Each binary is about 50~Kb, with the fat binary at 130Kb with another layer of Fat at 20Kb.

CMake

We will put in a CMakeLists.txt in the example folder for our CMake build journey.

cmake_minimum_required(VERSION 3.18)
project(example C CXX OBJC)

set(APP example)

set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED “NO”)
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY “”)

# Xcode XIB/Storyboard
set(APP_XIB ${CMAKE_CURRENT_SOURCE_DIR}/example/Base.lproj/MainMenu.xib)

add_executable(${APP} MACOSX_BUNDLE
example/main.m
example/AppDelegate.h
example/AppDelegate.m
${APP_XIB}
)

# Ninja: handle the XIB ourselves
if (NOT ${CMAKE_GENERATOR} MATCHES “^Xcode.*”)
# Compile the xib/storyboard file with the ibtool.
find_program(IBTOOL NAMES ibtool)
add_custom_command(TARGET ${APP} POST_BUILD
COMMAND ${IBTOOL} — errors — warnings — notices — output-format human-readable-text
— compile ${CMAKE_CURRENT_BINARY_DIR}/${APP}.app/Contents/Resources/MainMenu.nib
${APP_XIB}
COMMENT “Compiling xib”
)
endif()

set_target_properties(${APP} PROPERTIES
MACOSX_BUNDLE YES
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/example/Info.plist
RESOURCE “${APP_XIB}”
)

target_link_libraries(${APP}
“-lobjc”
“-framework Cocoa”
“-framework Metal”
“-framework MetalKit”
“-framework QuartzCore”
)

Xcode generator

Let’s generate the xcode project and build the project:

cmake -GXcode -B build_xcode “-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64”

xcodebuild -target example build -project build_xcode/example.xcodeproj/ -arch x86_64 -arch arm64 ONLY_ACTIVE_ARCH=NO

lipo -info build_xcode/Debug/example.app/Contents/MacOS/example

Yes. It is Fat!

Ninja generator

Let’s continue on the Ninja generator:

cmake -GNinja -B build_ninja “-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64”

ninja -C build_ninja/

lipo -info build_ninja/example.app/Contents/MacOS/example

Architectures in the fat file: build_ninja/example.app/Contents/MacOS/example are: x86_64 arm64

Yes. It is Fat!

Conclusion

Here, I would like to express my gratitudes to Brad King of Kitware and Co. to make CMake a wonderful build tool. And to other helpful developers at Medium and stackoverflow for their great writings and tips. Your work helped me to finish this investigation in one single Saturday. For building yet another Universal App.

Disclaimer

My idea of working Universal App is that the built example App can run on Intel Mac. And both the lipo tool and the build log show the final App binary are produced from both x86_64 and ARM64 binaries.

The original idea of this investigation on yesterday was purely my own and sole efforts and do not have any other single input other than public information from Google Search. The foundation work are already there!

Coder at daily work.