Recently working on an opensource project I came across an iOS warning, fixing which consumed my whole week.

ld: warning: building for iOS Simulator, but linking in object file (…) built for macOS

I tried searching answer on StackOverflow but I couldn’t find any right answers so I thought of writing a blog myself to help anyone else facing same problem. Before diving in here’s a preview of how I solved the above warning:

How I solved this problem:

With Xcode 7 the linker enforces that all files linked together were built for same platform. Xcode 7+ makes sure that every single object file is marked with extra flag of the paltform we are building for, default is set to MacOS. I realised the error was because the binaries were not made targeting x86_64-Apple-iOS-simulator but with macOS-x86_64. Because stimulator works inside macOS therefore it comes off as only a warning.

I performed following steps:

  1. 1: Check and copy Your iOS Simulator SDK version(Here iOS Simulator version is 13.2)
    $ xcodebuild -showsdks
    
  2. 2: Configure the sysdir gcc should use for building the object files. A sysdir is basically a copy of part of the file system of the target architecture that will include the libraries and header files available in that system. We have two options for specifying the sysdir directory to the compiler: exporting the path to the sysdir export SDKROOT path-to-sysdir, or using the gcc flag -isysdir path-to-sysdir.
    $ export SDKROOT=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iphonesimulator13.2.sdk
    
  3. 3: Using gcc compile all c++ files into object file with correct compiler flags. Having -std and -stlib flags are optional but in some cases using them might be useful.
    $ gcc -c fileA.c -o fileA.o  -std=c++11 -stdlib=libc++ -target x86_64-apple-ios-simulator
    
  4. 4: Finally using libtool I created universal static library (.a file)
    $ libtool -static fileA.o fileB.o -o library.a
    

    There is no need of combining original macOS and iOS stimulator binary into a single fat file because both had same architecture and will result in an error.

Universal Binaries

In macOS, there are two categories of binary files:

  1. 1: MACH-O: binary files that contain code for a single architecture.
  2. 2: Universal (or Fat): non-executable binary files that contain code for multiple architectures.

When we run the app in Xcode to a device or simulator, Xcode compiles our app for that architecture and uses lipo to extract the code for that particular architecture from all the universal binaries we have as dependencies.

There is also a utility called “file” that helps in Inspecting the binaries.

$ file fileA.o 
fileA.o: Mach-O 64-bit object x86_64
$ file library.a
library.a: current ar archive random library

Creating Universal Static Library

It requires following steps:

  1. 1: Generating multiple object files for each source file, one for each architecture.
  2. 2: Merging the content from the object files into a universal object file.
  3. 3: Creating a static library using the universal object files as input.
$ gcc -c fileA.c -o fileA-x86_64.o -target x86_64-apple-ios-simulator
$ gcc -c fileB.c -o fileB-x86_64.o -target x86_64-apple-ios-simulator
creating object files for x86_64 architecture for use in the simulator
$ gcc -c fileA.c -o fileA-arm64.o -target arm64-apple-ios
$ gcc -c fileB.c -o fileB-arm64.o -target arm64-apple-ios
creating object files for arm64 architecture for use in real devices
$ lipo -create fileA-x86_64.o fileA-arm64.o -output fileA.o
$ lipo -create fileB-x86_64.o fileB-arm64.o -output fileB.o
creating universal object files
$ libtool -static fileA.o fileB.o -o library.a
creating universal static library

On inspecting the new files, we can see that they are multi-architecture:

$ file fileA.o 
fileA.o: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit object x86_64] [arm64:Mach-O 64-bit object arm64]
$ file library.a
library.a: Mach-O universal binary with 2 architectures: [x86_64:current ar archive random library] [arm64:current ar archive random library]

App works on the simulator as expected BUT if you run it on a real device, you will get an error:

ld: ‘~/MyApp/library.a(fileA.o)’ does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. for architecture arm64

Building library with bitcode enabled

Apple builds iOS apps using the LLVM compiler infrastructure. In this setup, we have what is called front-end compilers, which receive as input files source code written in a high-level programming language and output machine code for an imaginary architecture. That machine code is called bitcode. At this point, LLVM performs optimizations on top of the bitcode and pass it to what is know as back-end compilers, which generate machine code for specific architectures.

When we enable bitcode in an iOS project, the package we submit to the App Store is compiled only until the bitcode stage. This allows Apple to compile the app in their servers, opening the doors for applying future optimizations to our code. Enabling bitcode in an iOS app requires all its dependencies to have bitcode support too, so we need to support it in our static library. We can do this by using the gcc flag -fembed-bitcode.

$ gcc -c fileA.c -o fileA-arm64.o -target arm64-apple-ios -fembed-bitcode
$ gcc -c fileB.c -o fileB-arm64.o -target arm64-apple-ios -fembed-bitcode
re-creating object files *ONLY* for arm64 with bitcode enabled
$ lipo -create fileA-x86_64.o fileA-arm64.o -output fileA.o
$ lipo -create fileB-x86_64.o fileB-arm64.o -output fileB.o
creating universal object files
$ libtool -static fileA.o fileB.o -o library.a
creating universal static library

Automating Build Process

Imagine there are hundreds of files, doing same thing over again is not efficient. You can automate the whole build process using makefile and run the command make. It will do everything for you. I’ll write probably another article about makefiles and stuff.

Having a Prebuilt C library?

lipo can be used to combine multiple library binaries into a single file.

$ lipo -create library-x86_64.a library-arm64.a -output library.a

If the library binary for arm64 doesn’t support bitcode, you won’t be able to enable it because you don’t have access to the source files, so you should disable it for your Xcode project. This can be done by setting the flag Build Settings -> Enable Bitcode to No.