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:
- 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!
- As of OSX 10.11, you need to add
#include <CoreFoundation/CFBundle.h>for bundle related code to work.
CFBundleCopyAuxiliaryExecutableURLto get a
CFURLRefpointing to the binary (in this case, named “abgx360”). Don’t worry about how this is figured out automatically, OSX magic!
- Declare a
chararray - 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++.
CFURLGetFileSystemRepresentationwill convert your
CFURLRefto string, storing result in the
chararray you declared previously. Notice
reinterpret_cast<UInt8*>, because the function expects a
UInt8buffer, but thats Objective-C and we want to get to standard C++ to use with the rest of the program.
- The result will be a
chararray/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
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
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
You’ll get an output like below:
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
-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
-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.
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!
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.
So I’m (or rather,
sudo is) telling
-change it’s reference of
And fini! The app bundle should now work on a standard OSX install with all dependencies contained locally! 😁