Updated: 2024-03-07
What is a Universal Mac Build?
A Universal Mac build refers to a software package or executable that is designed to run on both Intel-based Macs and Apple Silicon (ARM-based) Macs. This concept was introduced by Apple as they transitioned from Intel processors to their custom-designed Apple Silicon processors. It ensures that applications can work efficiently on both architectures without the need for separate versions or emulation.
To create a Universal Mac build
developers typically use Apple’s development tools, like Xcode, and adopt specific practices to ensure their software is compatible with both Intel and Apple Silicon Macs. This often involves compiling code for both architectures and bundling them together in a single executable or package.
developers generally need a macOS version that supports Apple Silicon, which is macOS 11.0 (Big Sur) or later. macOS Big Sur introduced native support for Apple Silicon, allowing developers to create Universal builds that work on both Intel and Apple Silicon Macs.
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_64 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.
Ref: Official Documentation | Building a Universal MacOS Binary
Creating Universal builds is essential for software developers to provide a seamless experience to users on different Mac hardware. It allows applications to take full advantage of the performance improvements offered by Apple Silicon while maintaining compatibility with older Intel-based Macs.
Alright, but how to create Universal Mac Build with Rust?
- Install Rust using the
rustuptool following the instructions from the official website: https://www.rust-lang.org/tools/install
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | bash -s -- -y
- Install Mac Command Line Tools using the following command:
xcode-select --install
- Check the installed targets for the active rustup toolchain version using the following command:
rustup show
- Make sure that:
aarch64-apple-darwin
x86_64-apple-darwin
targets are installed. If not, install them using the following commands:
rustup target add aarch64-apple-darwin
rustup target add x86_64-apple-darwin
PS: Rust will already atleast one of the targets installed. So, you might not need to install only one of them.
- Build your Rust project twice (one for each target architecture) for
releaseusing the following command:
cargo build --release --target x86_64-apple-darwin
cargo build --release --target aarch64-apple-darwin
- Create a fat Universal binary using the following command:
lipo -create -output "<output_path>" "target/x86_64-apple-darwin/release/<binary_name>" "target/aarch64-apple-darwin/release/<binary_name>"
In case, the target x86_64-apple-darwin is default target in the rustup toolchain, the command can be simplified as:
lipo -create -output "<output_path>" "target/release/<binary_name>" "target/aarch64-apple-darwin/release/<binary_name>"
How to verify the Universal Mac Build?
Since, the Universal Mac Build should support both the architectures, it is important to verify the same. This can be done using the following steps:
Verify the architecture of the generated output using the following command:
lipo -detailed_info "<output_path>"
or
lipo -archs "<output_path>"
or
file "<output_path>"
All the above command should tell that the generated output supports both the architectures. Something like:
x86_64 arm64
Size of the Universal Mac Build
Since the Universal Mac Build comprise of both the architectures, it is expected to be larger in size than the individual builds and almost twice of any single architecture. This can be verified using the following command:
ls -lh "<output_path>"
Consuming the Universal Mac Build
The Universal Mac build created above doesn’t work directly when consumed on the other Mac machine. This is because the Universal Mac build contains the paths to the libraries and frameworks that are specific to the machine on which it was built. This can be verified using the following command:
otool -L "<output_path>"
This will show the paths to the libraries and frameworks that are specific to the machine on which it was built.
To consume the Universal Mac build on other Mac machine, the paths to the libraries and frameworks need to be made relative. This can be done using the following set of commands:
- Extract the architecture specific libraries from the Universal Mac build using the following command:
lipo -extract aarch64 <universal mac library path> -o <output_path_of_arm64_library>
lipo -extract x86_64 <universal mac library path> -o <output_path_of_amd64_library>
- Change the paths to the libraries and frameworks to be relative using the following command:
install_name_tool -id <output_path_of_arm64_library> <output_path_of_arm64_library>
install_name_tool -id <output_path_of_amd64_library> <output_path_of_amd64_library>
- Aggregate the architecture specific libraries to create the Universal Mac library using the following command:
rm -f <universal mac library path> && lipo -create -o <universal mac library path> <output_path_of_arm64_library> <output_path_of_amd64_library>
- Verify the paths to the libraries and frameworks using the following command:
otool -L "<output_path>"
Ref: Understanding dyld @executable_path, @loader_path and @rpath
Tribulations
Using the @rpath, @loader_path, @executable_path, etc. in the Universal Mac Build with the
install_name_toolcommand didn’t worked for me.The
-changeoption of theinstall_name_toolcommand can be used to change the paths to the libraries and frameworks to be relative. But, it didn’t worked for me.