Automatic build of static library for iOS and many architectures

author: Kamil Burczyk

Last time I showed you how to create a static library using CocoaPods. Now it's time to build a distribute package.

First of all you probably wonder where files built by Xcode are placed? In Java world you may have src and bin directories in your project but these are not present in Obj-C world. Xcode by default places all built files and whole composed libraries in Derived Data directory. The easiest way to jump there is to open Organizer by pressing ⌘⇧2, select your project and click right grey arrow next to the Derived Data path, like on the screen below:
Derived Data directory

When you build your library by pressing ⌘B (be sure you opened NetworkLib instead of ReposBrowser) built library will be placed in directory Build/Products/Debug-iphonesimulator
Built static library

If you wonder why you have so many files here's what they are:

libPods-NetworkLib-AFNetworking.a is built by CocoaPods AFNetworking static library.

libPods-NetworkLib.a is a big static library that contains all libraries built before. If we had also library ABC, it would be compiled as libPods-NetworkLib-ABC.a and together with AFNetworking would be combined into fat libPods-NetworkLib.a library. This fat one is actually linked by CocoaPods to our project as a dependency.

libNetworkLib.a is actually our product.

You might have noticed that we only have a library for iphonesimulator, which is x86 architecture.
If you select iOS device instead of iOS Simulator as build target you will get Debug-iphoneos directory with library built for ARM.

But we don't want to have 2 separate libraries built for uncompatible architectures. lipo tool will help us create one combined library for all architectures supported by Xcode.

We can actually try it right away!
Move to Products directory in Terminal and type:
lipo -create Debug-iphoneos/libNetworkLib.a Debug-iphonesimulator/libNetworkLib.a -output libNetworkLib-universal.a

When command succeeds you should see libNetworkLib-universal.a file created. Mine weights 5.7MB, so it's a lot considering it's simplicity.

Above command succeeded only because we built library twice: for x86 and ARM architectures. Then we had to call lipo command to combine both products. Why not to automate it?

The best build system is the one that requires only one command to finish whole process.

You know what is even better? When this command is bind to your IDE and you can run it with keyboard shortcut!
Let's make it happen :)

We will add Aggregate Lib target to our project.
In Xcode click on project file and select Add Target.

Add target

In modal panel select group Other from OS X templates and then Aggregate. Give it a name and finish this step.


When we have a new target we should add a Run Script phase to it. Select EditorAdd Build PhaseAdd Run Script Phase. You can add Add Copy Files Build Phase as well because we will use it right after running script.
Add build phase

In Run Script view add code:

xcodebuild -workspace NetworkLib.xcworkspace -scheme NetworkLib -sdk iphonesimulator -configuration Debug  
xcodebuild -workspace NetworkLib.xcworkspace -scheme NetworkLib -sdk iphoneos -configuration Debug

# make a new output folder
mkdir -p ${TARGET_BUILD_DIR}/../NetworkLib

# combine lib files for various platforms into one
lipo -create "${TARGET_BUILD_DIR}/../Debug-iphoneos/libNetworkLib.a" "${TARGET_BUILD_DIR}/../Debug-iphonesimulator/libNetworkLib.a" -output "${TARGET_BUILD_DIR}/../NetworkLib/libNetworkLib-debug-${CURRENT_PROJECT_VERSION}.a"  

xcodebuild is a command that performs build just like ⌘B in Xcode. We pass workspace and scheme, select sdk and configuration Debug. Please notice we call this command twice, once for iphonesimulator (x86) and once for iphoneos (ARM).

TARGET_BUILD_DIR and CURRENT_PROJECT_VERSION are environment variables available during build. Those symbols places inside ${} brackets will be automatically replaced with proper values.

Next we create directory NetworkLib for our combined library and call lipo command with some dynamically resolved paths.
CURRENT_PROJECT_VERSION is a variable that can be set in Build Settings tab and is my way to keep tracking of static library version because at this point we don't have access to Project Version property of Xcode project (it's just a library). Current project version

Whole configuration should look like this:
Aggregate Lib Run Phase configuration

When you press ⌘B NetworkLib directory should be created in Derived Data corresponding directory and libNetworkLib-debug-0.1.a should appear.
The only thing we still need is a header file (or files if we had more of them). They are placed in include directories in built architectures so we can easily export them using Copy files phase.
Let's configure it to use Absolute Path with value of ${TARGET_BUILD_DIR}/../NetworkLib so it looks similar to this:
Copy files phase

You should add header files by pressing + button.

Now after build we should have a static library for both x86 and ARM architectures with all needed .h files!
Built lib

All you have to do now is to drag&drop whole directory to new Xcode project.
Xcode project with static library


If you try to build it you will get error AFNetworking.h file not found:
AFNetworking file not found

I actually did it on purpose and I imported library in header file:

#import "AFNetworking.h"

In our example it is not actually needed and we could move this import to .m file, but you can easily imagine situation where you have to import something in header file because your methods return some objects or accept them as arguments.

We can use forward class declaration to have symbols available in .h file and actually import them in .m which is not visible publicly.

So we can modify our NetworkLib.h file to contain something like

@class AFHTTPRequestOperationManager;

(of course if we actually needed it) and NetworkLib.m to truly import it

#import "AFNetworking.h"

After build everything passes and we are ready to take off with our new app! :)

comments powered by Disqus