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. - Use
CFBundleCopyAuxiliaryExecutableURL
to get aCFURLRef
pointing to the binary (in this case, named āabgx360ā). Donāt worry about how this is figured out automatically, OSX magic! - 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. Whychar
? Because we want the data usable in C++ code, assuming the rest of the code was written in standard C++, not Objective-C/C++. CFURLGetFileSystemRepresentation
will convert yourCFURLRef
to string, storing result in thechar
array you declared previously. Noticereinterpret_cast<UInt8*>
, because the function expects aUInt8
buffer, 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
char
array/buffer with the value of the absolute path to the other binary specified inCFBundleCopyAuxiliaryExecutableURL
. But notice I did a if statement to check for the existence of'\0'
, theNULL 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! š