Now that Macs are finally available with Intel chips, it occurred to me to give serious consideration to Universal Binaries. I never considered it a big deal, since XCode handles that for you. But all my recent binary code (non-Java) has been built with Makefiles so it can be compiled for multiple platforms. So I thought I'd better learn how to do it from the command line.
My first thought was that gcc might have some sort of "Mac-universal" option for the -arch flag. The man page says:
I naively thought that -arch i386 -arch ppc would work. However, the result was just a PPC file. The man page for ld had the answer for this:
Compile for the specified target architecture arch. The allowable
values are i386, ppc and ppc64. Multiple options work, and direct
the compiler to produce ``universal'' binaries including object
code for each architecture specified with -arch. This option only
works if assembler and libraries are available for each architecture
specified. (APPLE ONLY)
So how were universal binaries made if the linker can't do it? Again, the man page for ld provided the answer:
The link editor accepts ``universal'' (multiple-architecture) input
files, but always creates a ``thin'' (single-architecture), standard
Mach-O output file.
Only one -arch arch_type can be specified.
So it looks like the build process starts in the same way that it always did, only now it repeats the process for the other architecture, and merges the final executables. This means that intermediate files either need to be in separate directories, or else they need to have different names (which doesn't sound like a good idea to me). It also includes the new lipo tool for merging. (I can't think of what this might be short for. I keep thinking "liposuction".)
The compiler driver cc(1) handles creating universal executables by
calling ld(1) multiple times and using lipo(1) to create a ``univer-
sal'' file from the results of the ld(1) executions.
I tried using this technique to build a universal binary for a "Hello World" program, but kept getting missing symbols for the linker stage on the i386 architecture. A look at the compiler log for an XCode project showed an option of -isysroot /Developer/SDKs/MacOSX10.4u.sdk. This isn't mentioned in the manual for gcc or ld, but it does the trick.
To illustrate the process, a simple program might be built from two files: first.c and second.c
(I've used gcc to handle linker arguments automatically for me.)
gcc src/first.c -c -o obj/first.o
gcc src/second.c -c -o obj/second.o
gcc obj/first.o obj/second.o -o bin/program
The same build for a universal binary would look like:
lipo can also be used with the -info flag to check a file type:
gcc src/first.c -c -arch ppc -o obj/ppc/first.o
gcc src/second.c -c -arch ppc -o obj/ppc/second.o
gcc obj/ppc/first.o obj/ppc/second.o -o bin/ppc/program
gcc src/first.c -c -arch i386 -o obj/i386/first.o
gcc src/second.c -c -arch i386 -o obj/i386/second.o
gcc obj/i386/first.o obj/i386/second.o -arch i386 -isysroot /Developer/SDKs/MacOSX10.4u.sdk -o bin/i386/program
lipo -create bin/ppc/program bin/i386/program -output bin/program
This is all fine for Apples, but how should I write a portable Makefile that does all of this? Looking at a current Makefile, I really want to define the architecture in variables, and call into the same Makefile twice, followed by the lipo command to merge the results. However, Makefiles are not as easy to program as most languages. The biggest problem for me is how to call into another Makefile regardless of which target you are currently making. A test for "Mac OS X" at the top of the Makefile will not work, as commands can't be placed outside of a "target" block.
$ lipo -info bin/ppc/program
Non-fat file: bin/ppc/program is architecture: ppc
$ lipo -info bin/i386/program
Non-fat file: bin/i386/program is architecture: i386
$ lipo -info bin/program
Architectures in the fat file: bin/program are: ppc i386
The only way I know to do this is to test for "Mac OS X" in every target, and duplicate the compilations. This would be tedious to write, be prone to errors, and be awkward to update (particularly for non-Mac users). I suppose I need to learn a lot more about Makefiles, but I'd rather not have to.
This could be easier in Ant. After all, the code I'm writing is actually a JNI library. Maybe I should write a gcc module?