We at Octiplex often need to share code across projects. To do so, we use two solutions: CocoaPods and static libraries. But today, I will only deal with static libraries.
These libraries are generated automatically by a shell script that basically executes
xcodebuild several times to build the libraries for each architecture and then
lipo in order to create a single file from several architectures. Once that’s done, the result is exported using the best tool for sharing files: Git (whoever said “Dropbox” may leave now).
We’ve got a problem
Unfortunately, every time the script was executed, we ended up with a new version of all our libraries even if the input source files hadn’t changed, which caused Git to generate unnecessarily long diffs.
The reason is that the script deletes the destination directory before building the libraries in order to clean the files that might have been removed. However, the intermediate build files aren’t touched and xcodebuild doesn’t modify them unless their input source files changed. So we should be able to get the exact same library: basically a static library is just an archive of object files. Let’s compare two versions of the library:
$ cmp -l libMathHelper-1.a libMathHelper-2.a 32 60 65 33 71 70 34 63 65
So the two versions differ only by a few bytes in the beginning of the file. Now let’s see what
$ ar -tv libMathHelper-1.a rw-r--r-- 502/20 1560 Dec 12 16:28 2012 __.SYMDEF rw-r--r-- 502/20 12736 Dec 12 12:12 2012 RationalizingPi.o rw-r--r-- 502/20 14344 Dec 12 12:12 2012 AngleTrisection.o rw-r--r-- 502/20 54160 Dec 12 12:12 2012 SquaringTheCircle.o rw-r--r-- 502/20 45936 Dec 12 12:12 2012 DoublingTheCube.o rw-r--r-- 502/20 3184 Dec 12 12:12 2012 DivisionByZero.o $ ar -tv libMathHelper-2.a rw-r--r-- 502/20 1560 Dec 12 16:36 2012 __.SYMDEF rw-r--r-- 502/20 12736 Dec 12 12:12 2012 RationalizingPi.o rw-r--r-- 502/20 14344 Dec 12 12:12 2012 AngleTrisection.o rw-r--r-- 502/20 54160 Dec 12 12:12 2012 SquaringTheCircle.o rw-r--r-- 502/20 45936 Dec 12 12:12 2012 DoublingTheCube.o rw-r--r-- 502/20 3184 Dec 12 12:12 2012 DivisionByZero.o
You got it? The difference comes from the modification date of the
__.SYMDEF entry. This entry lists the external symbol names defined by the library and is created at the same time as the archive.
We just need to find a way to change that modification date to be, let’s say, the modification date of the archive’s most recent object file. That’s not very difficult: as explained here, an
ar archive begins with the header string
"!<arch>\n", then come the files, each prefixed by a 60 bytes-long header with its name, modification date, owner ID, group ID, file mode and size. And that’s it! Almost…
On Apple operating systems, static libraries may sometimes take the form of Universal binaries, as known as Mach-O Fat binaries. We need to handle that case too.
We could have used
lipo to extract the different architectures, replaced the faulty bytes (they’re always located in the same range), then used
lipo again. But we preferred a tool that could modify the file in place.
We developped a simple command-line utility that would do the job: Artichoke. Artichoke reads a static library and replace the modification date of its first entry by the one of its most recent object file. It supports single-architecture libraries and Universal binary libraries and is available on GitHub.