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
It is quite easy to create an macOS Universal Apps using Xcode 12. With new project popup, choose the App template for macOS and create an AppKit app in Objective-C. With the application built, you have an macOS Universal App now and run.
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
It can be as easy as it is to create the example app with just a few mouse clicks. Can life be a bit more challenging? Let’s look at what the Xcode build log can help us understand what is under the hood of Xcode build process. Let’s get into the project folder from Finder and Terminal.
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
As a cross-platform application developer, we have to use cross-platform build configuration software like CMake / Bazel / Meson / GN / premake/ etc. Days are gone with unproductive build changes across Windows Visual Studio project / Mac Xcode project / Make files for Linux. Here we use CMake 18 Rc1 for the test setup using both Xcode generator and Ninja generator. Repeated here: you will need latest CMake 18 for macOS Sur as mentioned in Xcode 12 Universal Apps Beta release notes.
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
In years of building iOS simulators, Apple has already made the foundation work to make the software switch. The build tool community have also followed and build up the relevant work. This article uses a simple example project to illustrate the steps and build tool settings to make the transition work as smoothly as possible for fellow developers. Coding is to be enjoyed!
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
At the point of writing this article on June 28, I do not own an Apple Mac mini with Apple’s A12Z Bionic SoC (IPad Pro Soc) to validate the example App.
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!