Building a universal macOS binary
Overview
Native apps run more efficiently than translated apps because the compiler is able to optimize your code for the target architecture. An app that supports only the x86 architecture must run under Rosetta translation on Apple silicon. A universal binary runs natively on both Apple silicon and Intel-based Mac computers, because it contains executable code for both architectures.
Turn all of your compiled code into universal binaries, not just apps. The following list includes the most common types of executables to turn into universal binaries. This list is not exhaustive, but you can use it as a starting point to assess your projects.
Apps
App extensions
Plug-ins
Custom frameworks
Static libraries
Dynamic libraries
Build tools
Command-line tools
Daemons and launch agents
DriverKit extensions
Kernel extensions
Note
You can build a universal binary on either an Apple silicon or Intel-based Mac computer, but you cannot debug the arm64 slice of your binary on an Intel-based Mac computer. It’s possible to debug both slices of a universal binary on Apple silicon, but you must run the x86 slice under Rosetta translation.
Download and Install Xcode on Your Mac Computer
Xcode 12.2 and later is a requirement for building universal binaries. Earlier versions of Xcode don’t contain the support needed to build and test universal versions of your macOS code.
Download the latest public version of Xcode from the App Store. Download prerelease versions of Xcode from developer.apple.com.
Update the Architecture List in Your Xcode Projects
Xcode 12.2 and later automatically adds the arm64 architecture to the list of standard architectures for all macOS binaries, including apps and libraries. During the debugging and testing process, Xcode builds only for the current system architecture by default. However, it automatically builds a universal binary for the release version of your code.
If you customized the Architectures build setting in your Xcode project, remove your customizations and use the Standard Architectures setting instead.
For additional information about Xcode build settings and how to configure them, see Configure build settings.
Update the Architecture List of Custom Makefiles
If you build your project using custom scripts or makefiles, add the arm64 architecture to the appropriate environment variables. Xcode uses the ARCHS environment variable to define the current build architectures. Other build systems may use different environment variables, but with similar purposes. After adding the variable to the appropriate environment variable, build your code and verify that the compiler creates an arm64 variant of your code. To create a universal binary for your project, merge the resulting executable files into a single executable binary using the lipo tool.
For makefiles you create outside of Xcode, pass the appropriate architecture values to the compiler using the -target option. The following example shows a makefile that compiles a single-source file twice—once for each architecture. It then creates a universal binary by merging the resulting executable files together with the lipo tool.
x86_app: main.c
$(CC) main.c -o x86_app -target x86_64-apple-macos10.12
arm_app: main.c
$(CC) main.c -o arm_app -target arm64-apple-macos11
universal_app: x86_app arm_app
lipo -create -output universal_app x86_app arm_app
Wrap Platform-Specific Code with Conditional Compilation Macros
When writing code for a specific platform or processor type, isolate that code using the appropriate conditional compilation statements. For C-based code, the system defines a set of macros for you to use in /usr/include/Target. The Swift language also supports conditional compilation using conditional compilation blocks.
Note
If you share code across multiple platforms, you can also use compiler-specific macros such as _ or _ in conditional compilation statements. These macros don’t require you to include the Target header file. For a list of compiler macros, see the compiler documentation.
To distinguish code meant for a specific type of processor, add conditional compilation statements that target the appropriate architecture. Universal macOS apps support the arm64 and x86 architectures, and the following example shows how to write conditional code for those architectures in Swift:
// Swift example
#if arch(arm64)
// Code meant for the arm64 architecture here.
#elseif arch(x86_64)
// Code meant for the x86_64 architecture here.
#endif
For Objective-C, use the following code:
// Objective-C example
// Code meant for the arm64 architecture here.
// Code meant for the x86_64 architecture here.
If you share code between an iOS and macOS app, don’t assume that code intended for the arm64 architecture runs only on an iOS device. That code also runs in your macOS app on an Apple silicon. To isolate code specifically for macOS or iOS, use the conditional compilation statements shown in the following example. You can also combine both platform- and architecture-specific conditional compilation to further refine your code.
//Swift example
#if os(macOS)
// Put CPU-independent macOS code here.
#if arch(arm64)
// Put 64-bit arm64 Mac code here.
#elseif arch(x86_64)
// Put 64-bit x86_64 Mac code here.
#endif
#elseif targetEnvironment(macCatalyst)
// Put Mac Catalyst-specific code here.
#elseif os(iOS)
// Put iOS-specific code here.
#endif
In Objective-C, implement the same code as shown below.
// Objective-C example
// Put CPU-independent macOS code here.
// Put 64-bit Apple silicon macOS code here.
// Put 64-bit Intel macOS code here.
// Put Mac Catalyst-specific code here.
// Put iOS-specific code here.
For the complete list of conditional compilation macros, see the /usr/include/Target header file in the appropriate SDK. For a list of Swift compilation conditions and arguments, see Conditional Compilation Block in The Swift Programming Language.
Build Your Target
When you build a debuggable version of your code, Xcode builds only for the current architecture by default. Building for one architecture saves time when you are trying to debug your code and fix problems quickly.
You can create a universal binary with debug symbols on any Mac computer by changing the Build Active Architecture Only build setting of your project. Although you can create this binary on an Intel-based Mac computer, you can’t run or debug the arm64 slice of it. Only a Mac with Apple silicon is capable of running and debugging both slices of your binary; use Rosetta translation to run and debug the x86 slice.
Determine Whether Your Binary Is Universal
To users, a universal binary looks no different than a binary built for a single architecture. When you build a universal binary, Xcode compiles your source files twice—once for each architecture. After linking the binaries for each architecture, Xcode then merges the architecture-specific binaries into a single executable file using the lipo tool. If you build the source files yourself, you must call lipo as part of your build scripts to merge your architecture-specific binaries into a single universal binary.
To see the architectures present in a built executable file, run the lipo or file command-line tools. When running either tool, specify the path to the actual executable file, not to any intermediate directories such as the app bundle. For example, the executable file of a macOS app is in the Contents/Mac directory of its bundle. When running the lipo tool, include the -archs parameter to see the architectures. The following example shows how to use lipo to view the list of architectures for the Mail app in macOS, and the results when Mail is a universal binary.
% lipo -archs /System/Applications/Mail.app/Contents/MacOS/Mail
x86_64 arm64
To obtain more information about each architecture, pass the -detailed argument to lipo.
For information about how to determine whether your app is running as a translated binary, see Determine Whether Your App Is Running as a Translated Binary.
Specify the Launch Behavior of Your App
For universal binaries, the system prefers to execute the slice that is native to the current platform. On an Intel-based Mac computer, the system always executes the x86 slice of the binary. On Apple silicon, the system prefers to execute the arm64 slice when one is present. Users can force the system to run the app under Rosetta translation by enabling the appropriate option from the app’s Get Info window in the Finder.
If you never want users to run your app under Rosetta translation, add the LSRequires key to your app’s Info file. When that key is present and set to YES, the system prevents your app from running under translation. In addition, the system removes the Rosetta translation option from your app’s Get Info window. Don’t include this key until you verify that your app runs correctly on both Apple silicon and Intel-based Mac computers.
If you want to prioritize one architecture, without preventing users from running your app under translation, add the LSArchitecture key to your app’s Info file. The value of this key is an ordered array of strings, which define the priority order for selecting an architecture.
Note
If an app doesn’t contain an executable binary, the system may run it under Rosetta translation as a precautionary measure to prevent potential runtime issues. For example, the system runs script-only apps under Rosetta translation. If you verified that your app runs correctly on both Apple silicon and Intel-based Mac computers, add the LSArchitecture key to your app’s Info file and list the arm64 architecture first.