Apple Silicon and the library incompatibility problem for iOS development
You work your ass off day and night to save money so that you can get a shiny new Macbook with M1 processor. You get your Macbook, you do the setup and try to get some work done. You open your iOS project and click the play button. And you are greeted with this.
building for iOS Simulator, but linking in object file built for iOS, file '/Users/user/ios-project/Pods/GoogleMaps/Base/Frameworks/GoogleMapsBase.framework/GoogleMapsBase' for architecture arm64
What the hell! Being an experienced developer you search StackOverflow and you meet your savior who tells you to slap EXCLUDED_ARCHS[sdk=iphonesimulator*]=arm64
wherever you can in the project. And voila, your project builds and runs. Then you move on with your life and forget that you had EXCLUDED_ARCHS[sdk=iphonesimulator*]=arm64
all over the place that will come to hunt you and your colleagues in the future.
Have you ever wondered why that single line worked? What was causing the problem in the first place? If you know the answer then I am sorry to say you will have to end your journey here. If you don’t know the answer
Basics
Before we dive right into the problem, let’s learn some basics.
CPU Architectures
Every CPU comes with a set of instructions that it can execute. These instructions can be complex(Complex Instruction Set Computing, CISC) or simple(Reduced Instruction Set Computing, RISC). These instructions roughly make up the CPU Architecture. The most common architectures are x86(CISC) and ARM(RISC). These are 32-bit architectures and they have 64-bit counterparts namely x86_64 and ARM64. Just to make the story concise, I will be using x86 to refer to x86_64 and ARM to refer to ARM64. Intel and AMD are some of the big players in manufacturing x86 processors. Qualcomm and MediaTek are some of the manufacturers of ARM processors. And Apple has joined the race of ARM processors with the M1.
M1 and Rosetta
Since M1 processors are ARM based, this is quite a huge transition for Apple and the entire Apple ecosystem. Existing software won’t be able to run on M1 as these were built for x86 architectures. So, Apple came up with Rosetta for the smooth transition from x86 to ARM. Rosetta is a compatibility layer that makes it possible to run software built for x86 architectures on M1 processors.
Xcode, Simulators, and its versions
After the launch of M1 Macbooks, Xcode started using Rosetta as it was originally written for x86. This was fine and most of the developers didn’t notice any difference. The apps were building and running on the Simulator as normal. But Apple can’t rely on Rosetta forever as it is just for the transition period and not a long-term solution. So, with Xcode 12 Apple switched Xcode from x86 to ARM so that it can run natively on M1 Macbook. Simulators before iOS 14 only supported x86 (by running the Simulator using Rosetta) but the Simulators running iOS 14 and above support both ARM and x86(using Rosetta), Simulator in this case actually run natively on M1 but any apps that don’t support ARM runs through Rosetta.
Here is a test app(TestAlertIOS) that is compiled to ARM and runs on a Simulator. As you can see both the Simulator and the app is using the CPU of type Apple.
Here is the same app but compiled to x86. You can see that the simulator is using the CPU of type Apple but the app is using the CPU of type Intel. So, the app is running through Rosetta.
The Problem
3rd party dependencies
We all use 3rd party dependencies in various forms like libraries and frameworks to add functionality to our app. More often than not most of these dependencies are distributed as a compiled binary and very few dependencies are directly distributed as a source code. In the case of iOS, the dependency will contain compiled code for the iPhone and the Simulator. Here are the binaries included in a GoogleMaps framework.
As you can see, the framework includes two binaries one for ARM64 and one for x86_64. The x86_64 is for the simulator and the ARM64 is for the real iPhone. This version of the framework doesn’t support ARM simulators so if we use this framework then we have to make sure that our app runs through Rosetta.
Here are the binaries of the same framework but with support for ARM simulators. This is an xcframework
so the folder structure is a little bit different. But as you can see under the ios-arm64_x86_64-simulator
folder we have binaries for both ARM and x86 architecture. So, this will run both natively and through Rosetta on the simulator.
With this information, I think we are ready to dive into the mess that M1 has created for developers. Starting with Xcode 12, whenever you build your app for the Simulator it will build for the ARM architecture. You might be wondering, my app already works on iPhone which uses ARM so it should also work on Simulator running on M1, right? Well, theoretically it should be possible but simulators use a different identifier for the compiled binary. You can use tutorials like this to repackage the framework to add support for the M1 Simulator but I am not so sure if it will work on all frameworks.
The Solution
The easiest and most maintainable way is for the framework and library developers to provide the binaries for both x86 and ARM-based simulators. Back to the problem mentioned at the start of the story.
building for iOS Simulator, but linking in object file built for iOS, file '/Users/user/ios-project/Pods/GoogleMaps/Base/Frameworks/GoogleMapsBase.framework/GoogleMapsBase' for architecture arm64
As you can see this is a linker error which means the code has compiled successfully. But the linker is looking for the binary specifically built for an ARM Simulator. It has found the binary that matches the architecture of the Simulator(ARM) but that binary is supposed to be used for the iPhone. To solve this problem, you have to do something like this in the Podfile.
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64'
end
end
end
This works because you are telling Xcode to not build for the ARM architecture if the target device is a Simulator. For the Simulator, Xcode will build for x86 and the simulator will run your app using Rosetta. This is just a workaround and you should actively look into the libraries that don’t have support for M1 and try to either change the library or contact the developer to include support for it.
That is it for the story. I hope you learn something new from this story. If you have any questions or suggestion don’t forget to comment. If you like this story don’t forget to click the clap button. See you in the next one.