Making fat static Libraries (Simulator + Device), and applying to Three20

I am starting to get involved with the Three20 project. This project contains valuable classes and UI elements that I need. However, this big library is notorious for its difficulty to include inside an XCode 4 project. The included install script does not work, and the manual install instructions are a miss, and I ended up with Xcode complaining that it can’t find header files, even if I had set up the header search paths correctly.

So, I decided to pre-build the static libraries and include them to my project, and I was successful… to an extent. You see, I couldn’t use the same static libraries for the Simulator and the device, because the libraries built are built each time for the device you specify, and that device only. For example, if you build the libraries for the simulator, the produced libraries will work for the simulator. For the device, you need a different library package.

That led me to the long trip of finding a way to compile a static library for iOS that works for different architectures: armv6, armv7, and i386. Read on to find out how you can manage to make a static library that will work on all platforms.

The first thing we are going to do is to learn to make static libraries that work on both the simulator, iPhone and iPad. Then, we will apply this knowledge to a project of our choice in order to be able to successfully include the Three20 framework to our project.

Step 1: How to build your library for all platforms

  • Open your existing static library project. Select your project file on the top, and then select your target. 
  • Go into the Build Phases
  • Add a new “Run Script” build phase.
  • In this phase, paste the following script:
# Version 2.0 (updated for Xcode 4, with some fixes)
# Changes:
#    - Works with xcode 4, even when running xcode 3 projects (Workarounds for apple bugs)
#    - Faster / better: only runs lipo once, instead of once per recursion
#    - Added some debugging statemetns that can be switched on/off by changing the DEBUG_THIS_SCRIPT variable to "true"
#    - Fixed some typos
# 
# Purpose:
#   Create a static library for iPhone from within XCode
#   Because Apple staff DELIBERATELY broke Xcode to make this impossible from the GUI (Xcode 3.2.3 specifically states this in the Release notes!)
#   ...no, I don't understand why they did this!
#
# Author: Adam Martin - http://twitter.com/redglassesapps
# Based on: original script from Eonil (main changes: Eonil's script WILL NOT WORK in Xcode GUI - it WILL CRASH YOUR COMPUTER)
#
# More info: see this Stack Overflow question: http://stackoverflow.com/questions/3520977/build-fat-static-library-device-simulator-using-xcode-and-sdk-4
#################[ Tests: helps workaround any future bugs in Xcode ]########
#
DEBUG_THIS_SCRIPT="false"
if [ $DEBUG_THIS_SCRIPT = "true" ]
then
echo "########### TESTS #############"
echo "Use the following variables when debugging this script; note that they may change on recursions"
echo "BUILD_DIR = $BUILD_DIR"
echo "BUILD_ROOT = $BUILD_ROOT"
echo "CONFIGURATION_BUILD_DIR = $CONFIGURATION_BUILD_DIR"
echo "BUILT_PRODUCTS_DIR = $BUILT_PRODUCTS_DIR"
echo "CONFIGURATION_TEMP_DIR = $CONFIGURATION_TEMP_DIR"
echo "TARGET_BUILD_DIR = $TARGET_BUILD_DIR"
fi
#####################[ part 1 ]##################
# First, work out the BASESDK version number (NB: Apple ought to report this, but they hide it)
#    (incidental: searching for substrings in sh is a nightmare! Sob)
SDK_VERSION=$(echo ${SDK_NAME} | grep -o '.{3}$')
# Next, work out if we're in SIM or DEVICE
if [ ${PLATFORM_NAME} = "iphonesimulator" ]
then
OTHER_SDK_TO_BUILD=iphoneos${SDK_VERSION}
else
OTHER_SDK_TO_BUILD=iphonesimulator${SDK_VERSION}
fi
echo "XCode has selected SDK: ${PLATFORM_NAME} with version: ${SDK_VERSION} (although back-targetting: ${IPHONEOS_DEPLOYMENT_TARGET})"
echo "...therefore, OTHER_SDK_TO_BUILD = ${OTHER_SDK_TO_BUILD}"
#
#####################[ end of part 1 ]##################
#####################[ part 2 ]##################
#
# IF this is the original invocation, invoke WHATEVER other builds are required
#
# Xcode is already building ONE target...
#
# ...but this is a LIBRARY, so Apple is wrong to set it to build just one.
# ...we need to build ALL targets
# ...we MUST NOT re-build the target that is ALREADY being built: Xcode WILL CRASH YOUR COMPUTER if you try this (infinite recursion!)
#
#
# So: build ONLY the missing platforms/configurations.
if [ "true" == ${ALREADYINVOKED:-false} ]
then
echo "RECURSION: I am NOT the root invocation, so I'm NOT going to recurse"
else
# CRITICAL:
# Prevent infinite recursion (Xcode sucks)
export ALREADYINVOKED="true"
echo "RECURSION: I am the root ... recursing all missing build targets NOW..."
echo "RECURSION: ...about to invoke: xcodebuild -configuration "${CONFIGURATION}" -target "${TARGET_NAME}" -sdk "${OTHER_SDK_TO_BUILD}" ${ACTION} RUN_CLANG_STATIC_ANALYZER=NO"
xcodebuild -configuration "${CONFIGURATION}" -target "${TARGET_NAME}" -sdk "${OTHER_SDK_TO_BUILD}" ${ACTION} RUN_CLANG_STATIC_ANALYZER=NO BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}"
ACTION="build"
#Merge all platform binaries as a fat binary for each configurations.
# Calculate where the (multiple) built files are coming from:
CURRENTCONFIG_DEVICE_DIR=${SYMROOT}/${CONFIGURATION}-iphoneos
CURRENTCONFIG_SIMULATOR_DIR=${SYMROOT}/${CONFIGURATION}-iphonesimulator
echo "Taking device build from: ${CURRENTCONFIG_DEVICE_DIR}"
echo "Taking simulator build from: ${CURRENTCONFIG_SIMULATOR_DIR}"
CREATING_UNIVERSAL_DIR=${SYMROOT}/${CONFIGURATION}-universal
echo "...I will output a universal build to: ${CREATING_UNIVERSAL_DIR}"
# ... remove the products of previous runs of this script
#      NB: this directory is ONLY created by this script - it should be safe to delete!
#rm -rf "${CREATING_UNIVERSAL_DIR}"
#mkdir "${CREATING_UNIVERSAL_DIR}"
#
echo "lipo: for current configuration (${CONFIGURATION}) creating output file: ${CREATING_UNIVERSAL_DIR}/${EXECUTABLE_NAME}"
lipo -create -output "${CREATING_UNIVERSAL_DIR}/${EXECUTABLE_NAME}" "${CURRENTCONFIG_DEVICE_DIR}/${EXECUTABLE_NAME}" "${CURRENTCONFIG_SIMULATOR_DIR}/${EXECUTABLE_NAME}"
#########
#
# Added: StackOverflow suggestion to also copy "include" files
#    (untested, but should work OK)
#
if [ -d "${CURRENTCONFIG_DEVICE_DIR}/usr/local/include" ]
then
mkdir -p "${CREATING_UNIVERSAL_DIR}/usr/local/include"
# * needs to be outside the double quotes?
cp "${CURRENTCONFIG_DEVICE_DIR}/usr/local/include/"* "${CREATING_UNIVERSAL_DIR}/usr/local/include"
fi
fi

This script tells Xcode to compile the library for all targets, both for the device and the iOS simulator. Then, it uses lipo() (a very handy terminal command) to take the resulting .a files, merge their architectures, and make a single .a file that will include code for every device.

You can check the resulting file with the following lipo command, which will output the kind of the library and the architectures which is build for:

lipo -info /path/to/library.a

And… that’s it! We have reached the end of the first part of this tutorial. For those of you who are interested in using this technique in order to be able to implement Three20 in their framework, read on.

Applying this technique to Three20

First of all, you will need to create the libraries and build them for all devices. Follow these steps:

  • Go into Three20 Root Dir-> src -> Three20 and open Three20.xcodeproj
  • Select your project On the top, select the target named “Three20” and add the above script into the appropriate build phase, as you did before.
  • Note that you don’t need to alter the “Three20UnitTests” target. This target is for testing only. It’s not used in any other occasion at all.
  • Now, in your files list, on the left, inside the Three20 project, go into Dependencies. See all the dependencies projects?
  • We need to add the same script phase we did before in all of these projects. Do this for every dependency project. If you don’t do this, Xcode will produce all .a libraries, but only the main Three20 library will be created for all devices. The rest will be device dependent, and will not work for all devices. DO NOT FORGET THIS STEP.
  • Once you have added the svript build phase to all projects and dependencies, build the project.
  • Building will take considerably longer this time. Be patient.
After building completes, Xcode will have produced following for the Three20 project:
  • Libraries for each configuration. They are inside the three20 root->Build->Products
  • Libraries for all devices will be put into three20 root->Build->Products into a folder named ***-universal where *** will be one of “Debug”, “Release” or anything that you had selected in Xcode at the time you pressed build. Note that whatever configuration you had selected at the time does not matter. Inside the universal folder there exist fat libraries, each built for ALL configurations.
  • Necessary headers. Put into three20 root->Build->Products->three20 . This folder is as much important as the universal folder. It contains necessary headers for inclusion in your project.
You can toss every folder except for the universal libraries folder and the headers folder. Now, you will only need these two. All you now have to do, is to go to your project, drag and drop the universal libraries into your project, and include the necessary headers. How you include these headers it’s up to you. You can import them, or you can go into your project settings and set your header include path to be “path-to-three20-root/Build/Products/three20”. I strongly recommend the second method. Note that when using the second method, make sure you DO NOT select the “recursive” button when specifying the path to the headers. Three20’s header files specify relative paths to required headers. By specifying recusrsive header search into Xcode, you basically tell Xcode to ignore relative paths, and that will create conflicts with all headers of the Three20 project. Be very careful with that option in general.

Want the libraries directly?
Here are the universal libraries compiled for the device and simulator. They are for version 1.0.6.2 of Three20, which is the current version at the time these lines are written. I hope this saves people from the frustration of having to do everything themselves. Note that when a new version comes out, you will need to follow the instructions to produce a new version of the libraries. I will not share the new versions of the libraries every time.
Included with the .zip file, there are also the produced headers. You must leave the header directory structure exactly as you see it. Each header contains relative paths to other headers, you don’t want that to be messed up. On the contrary, you can do the actual libraries whatever you want.
Credits:
I didn’t make the script we use here. I merely modified it, by saying not delete the universal folder each time the script encounters a new target. I found the script into a thread in stack overflow here: 

http://stackoverflow.com/questions/3520977/build-fat-static-library-device-simulator-using-xcode-and-sdk-4

You can find interesting information their, in addition to this script, which is a life saver.