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.
- 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 find interesting information their, in addition to this script, which is a life saver.