Details on our Universal Binary port

Thursday, March 30, 2006

This is for the Cocoa developers out there, or anyone who might be interested in what I had to change to make Curio a Universal Binary.

First, some background: Curio is about 250,000 lines of code. It's broken into about 13 separate Xcode projects. For example, our global app-independent framework, our Curio core framework, a project for all the Inspector plugins, a project for all the Preference plugins, etc. We end up creating or bundling 9 frameworks, 21 plugins, a Spotlight plugin, and the Curio executable itself.

Some key points before I get started:
  1. We wanted to maintain compatibility with 10.3.9 Panther customers. We couldn't just be Tiger and above.

  2. That said, we do make some Tiger-only calls but we check the OS version before making those calls so they are done on-the-fly. The important thing here is that we can't build against the 10.3.9 SDK since that would result in compile errors for those Tiger-only calls we make.
For all of our projects, I had to make some changes to each project's build properties. To access these properties simply open the project in Xcode and double-click on the icon on the very top of the Groups and Files list. The advantage here is that all the changes you make will be inherited by all the Targets of this project. For example, all of our Inspectors are in one project but there is a Target for each Inspector. So I can change just the project's build settings and all 13 targets for each Inspector automatically inherit those changes.

So, in the project's build properties:
  1. Click the General tab, choose "Mac OS X 10.4 (Universal)" for the Cross Develop Using Target SDK item. This will set the SDK Path for all configurations. It will also set the Architectures to $(NATIVE_BUILD) so it only builds for your machine's architecture.

  2. Click the Build tab, choose " for the Configuration item and "Customized Settings" for the Collection item. Now add two custom items by clicking the + button. The first is MACOSX_DEPLOYMENT_TARGET_i386 and set it to 10.4, then the second is MACOSX_DEPLOYMENT_TARGET_ppc and set it to 10.3. This tells the application that it can be run on PowerPC Macs on Panther or above, but on Intel it requires Tiger and above.

  3. Next change the Configuration dropdown to "Deployment/Release". Change the Architectures setting to "ppc i386" (without the quotes). If you switch Configuration to "Development/Debug" you'll see that Architectures is still $(NATIVE_BUILD). This means when doing debug builds you'll only compile for the machine you're using (and not spend time building for the other architecture). But when you do your release build then you'll build for both architectures.
That's it. Do those same 3 steps for all the projects you have and you're basically done. For most everyone out there, that'll all you have to do to become Universal and still work on Panther.

Except, in the case of Curio, we have an additional wrinkle. One of our frameworks uses the system BZip2 library for compressing a number of things. We also use the system Crypto library for encrypting some things.

So, I had to edit the build settings for that particular target and add some custom link flags.
OTHER_LDFLAGS_i386 -lcrypto.0.9 -lbz2
OTHER_LDFLAGS_ppc -lcrypto.0.9 -L$(HOME)/Library/lib -lggbz2
Ew, what's all of that?

-lcrypto.0.9 means to link against libcrypto.0.9.dylib. So that's the same for both Intel and PowerPC builds.

But the BZip2 was tricky. Apple changed that library from a static library (a .a file) to a dynamic library (a .dylib file) when they released Tiger. So, when compiling against the 10.4 SDK we can't simply link against "-lbz2" because that'll find the dylib and thus not embed the static library in Curio, and, therefore, running on Panther will die since it won't find the dylib out there.

Here's how we fix it: on Intel we're safe just linking against the dylib since Intel is obviously Tiger and above only, so the dylib will definitely exist.

But on PowerPC, since we support Panther, we link against a copy of the static library and embed that in Curio. So, even though, technically, the dylib does exist in Tiger, we're ignoring it since we will just use the same static library found in Panther (which is just fine with us).

Where do we get the static library? From the 10.3.9 SDK!

We have a build script that builds all of Curio's projects for all 4 editions of Curio (Pro, K12, Home, and Basic). Just building Curio Pro takes 13 minutes on a dual 1GHz G4, or an amazing 4 and a half minutes on our new MacBook Pro.

That script does some "prep work" for us to get the build environment ready. One of the additional items it now handles is putting a copy of the static library in a special spot:
cp /Developer/SDKs/MacOSX10.3.9.sdk/usr/lib/libbz2.a ~/Library/lib/libggbz2.a
ranlib ~/Library/lib/libggbz2.a
Now our link will find that static library for our PowerPC builds and all is happy. (I was unable to have the linker link directly against the static library in the 10.3.9 SDK directory unfortunately, so this copying trick is a workaround.)

Lastly, Curio had a handful of places that needed some special code changes:
  1. GetKeys. We use the Carbon GetKeys call to find out the state of the keyboard on the fly. We had to make a tweak to use a KeyMapByteArray unioned with a KeyMap to get past a compiler error. Pretty straightforward code change:
     union {
    KeyMap keymap;
    KeyMapByteArray bytes;
    } keymap;

    GetKeys(keymap.keymap);

    // Now use keymap.bytes to look at the 16 array values
  2. Byte swapping. We use memmove to pack 4-byte and 2-byte integers into an array of bytes. Because of the Endian change between Intel and PowerPC, I had to make use of CFSwapInt32HostToBig and CFSwapInt16HostToBig before packing, and CFSwapInt32BigToHost and CFSwapInt16BigToHost after unpacking. This was very simple, just 6 places in the code had to change.
So that's it. The magic of Xcode's support of building universal binaries took care of the rest. Two days after I unpacked our MacBook Pro I had Curio running as a Universal Binary!