Binaries to OSX App Bundle

Reading time ~6 minutes

Unfortunately I tend to find myself compiling Linux apps for OSX more often then I’d like 😔

Anyway, say you’ve got a C++ binary compiled successfully on OSX, but now you want to distribute that binary as an OSX app. That’s easy, just create the .app folder structure, as documented all over the web. But what if it calls on some libraries and you want to package them with your app bundle so it works across any system without any external dependencies? There’s a few steps that you need to do and it’s not that difficult - but I’ll admit, it took me a while to figure it out (in my Hyne post, I mentioned I’d attempted to use install_name_tools but couldn’t get it to work… until now!). I’ll speak about dynamic libraries only here, although I think the same applies to Frameworks too. NOTE: It really helps if you’ve got as many of your library dependencies compiled statically, as you’ll see later.

Most people simply rely on XCode to do all this work for them (and there’s some wisdom in that as you’re about to discover), but in my case as I was using portable code, it wasn’t configured to use XCode, only standard configure/make, so I decided to do it manually steps. Anyway I prefer the CLI 😏

Step 0: how to reference another executable within your app bundle from code

The title of this post says “Binaries” - thats plural. If you just want to package a single executable into an app file you can skip this step; this is for those instances where you have multiple executables and you’d like to reference them within your main executable.

This commit shows how to do it. Few points:

  1. Notice the guard macro - if you want your code to be portable, make sure you include a guard macro so that all of this only compiles on OSX, as App bundles aren’t available on other systems!
  2. As of OSX 10.11, you need to add #include <CoreFoundation/CFBundle.h> for bundle related code to work.
  3. Use CFBundleCopyAuxiliaryExecutableURL to get a CFURLRef pointing to the binary (in this case, named “abgx360”). Don’t worry about how this is figured out automatically, OSX magic!
  4. Declare a char array - either calculate the size it should be somehow or make it real big in case the user places your app in a folder with a large path. Why char? Because we want the data usable in C++ code, assuming the rest of the code was written in standard C++, not Objective-C/C++.
  5. CFURLGetFileSystemRepresentation will convert your CFURLRef to string, storing result in the char array you declared previously. Notice reinterpret_cast<UInt8*>, because the function expects a UInt8 buffer, but thats Objective-C and we want to get to standard C++ to use with the rest of the program.
  6. The result will be a char array/buffer with the value of the absolute path to the other binary specified in CFBundleCopyAuxiliaryExecutableURL. But notice I did a if statement to check for the existence of '\0', the NULL terminator, in case there was an error in finding the path. Unfortunately checking for “NULL” return value from the OSX CoreFoundation calls won’t cut it.

Step 1: create app bundle folder structure and place binaries as appropriate

e.g.

MyApp.app
    --Contents              <-- REQUIRED
        --MacOS             <-- REQUIRED
            -MyApp          <-- REQUIRED - must be same name as the .app filename, unless using a Info.plist file
            -other_binary
        --Libraries
        --Frameworks
        --Resources
        ...

Step 2: figure out which libraries are being dynamically loaded, and are not part of the default system

cd MyApp.app/Contents/MacOS
otool -L MyApp

You’ll get an output like below:

/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1226.10.1)
/System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL (compatibility version 1.0.0, current version 1.0.0)
/usr/local/opt/libpng/lib/libpng16.16.dylib (compatibility version 41.0.0, current version 41.0.0)
/usr/local/opt/jpeg/lib/libjpeg.8.dylib (compatibility version 13.0.0, current version 13.0.0)
/usr/local/opt/libtiff/lib/libtiff.5.dylib (compatibility version 8.0.0, current version 8.4.0)
/System/Library/Frameworks/WebKit.framework/Versions/A/WebKit (compatibility version 1.0.0, current version 601.7.7)
/usr/lib/libexpat.1.dylib (compatibility version 7.0.0, current version 8.0.0)

I can tell which are system provided libraries (anything in /System is a good bet, and /usr/lib too). As I use Homebrew, I know that the /usr/local/paths are all non-default, meaning I’ll have to distribute them with my app.

Step 3: copy non-default libraries to my “Libraries” folder in my app bundle

cp -R /usr/local/opt/libpng/lib/libpng16.16.dylib ../Libraries/
cp -R /usr/local/opt/jpeg/lib/libjpeg.8.dylib ../Libraries/
cp -R /usr/local/opt/libtiff/lib/libtiff.5.dylib ../Libraries/

Using the -R flag to copy recursively, including any symbolic links (doesn’t apply in my case as these are just single dylib files).

Step 4: tell my main binary to use the locally copied dylib files instead of the ones installed on my system

install_name_tool -change /usr/local/opt/libpng/lib/libpng16.16.dylib @executable_path/../Libraries/libpng16.16.dylib MyApp
install_name_tool -change /usr/local/opt/jpeg/lib/libjpeg.8.dylib @executable_path/../Libraries/libjpeg.8.dylib MyApp
install_name_tool -change /usr/local/opt/libtiff/lib/libtiff.5.dylib @executable_path/../Libraries/libtiff.5.dylib MyApp

-change flag, changes the paths that my binary will use for the 3 libraries. It’s important that the first parameter (/usr/local paths) are exactly the same as what was listed by otool previously. Otherwise it won’t find the reference.

Step 5: change my locally copied dylib files ids so that they’re “aware” that they are local copies

Yeah OK, I don’t understand exactly what this step does 😂, but its needed.

cd ../Libraries
sudo install_name_tool -id @executable_path/../Libraries/libpng16.16.dylib libpng16.16.dylib
sudo install_name_tool -id @executable_path/../Libraries/libjpeg.8.dylib libjpeg.8.dylib
sudo install_name_tool -id @executable_path/../Libraries/libtiff.5.dylib libtiff.5.dylib

Notice sudo this time, and the -id flag. You may not need sudo, but I did. I guess it’s because I’m changing a library that I don’t technically own (didn’t build myself)?

Step 6: now check my locally copied dylib files to see if THEY are referencing any non-default libraries, and copy those libraries and change references accordingly

Yes now you see why this is a long winded process. Basically recursively repeating the above steps for each library file that references yet another non-default library. Each time one is found (using otool -L) the referenced file needs to be copied locally, changed in the calling library (using -change flag), then updating the id (using -id flag) of the called library. Finally the newly copied library needs to be checked (using otool -L again) to see if it uses another library. And so on.

This is also why I mentioned it’s super handy to have built as many non-default libraries as possible statically - doing so prevents this at the cost of larger storage and memory space.

Anyway, luckily in my example I have only 1 local dylib file that refers to another library - which just so happens to be a library I already have locally!

otool -L libtiff.5.dylib

resulted in:

@executable_path/../Libraries/libtiff.5.dylib (compatibility version 8.0.0, current version 8.4.0)
/usr/local/opt/jpeg/lib/libjpeg.8.dylib (compatibility version 13.0.0, current version 13.0.0)
/usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.5)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1226.10.1)

The first line, with @executable_path is the id of the current dylib file that I changed previously. Something similar will be there for all the other dylib files changed. It’s the second line that needs to be changed; as it refers to libjpeg.8.dylib which I have locally already, I just need to update the reference in this file to use the local copy too.

sudo install_name_tool -change /usr/local/opt/jpeg/lib/libjpeg.8.dylib @executable_path/../Libraries/libjpeg.8.dylib libtiff.5.dylib

So I’m (or rather, sudo is) telling libtiff.5.dylib to -change it’s reference of /usr/local/opt/jpeg/lib/libjpeg.8.dylib to @executable_path/../Libraries/libjpeg.8.dylib.

And fini! The app bundle should now work on a standard OSX install with all dependencies contained locally! 😁

The Story of Signature Extensions Limited

Information on a questionable construction company Continue reading

First Post - Creating a Website

Published on March 31, 2016