This is part 1 of the deployment process overview. Part 2 will cover code signing and notarization.
Level 0: warming up
Deploying anything anywhere has never been easy business. Deploying applications to desktop computers is no different. OS X (now macOS) had its own solution of “dll hell”: each application should be absolutely self contained. All the dependencies and dependencies of dependencies should be distributed within the “bundle” so apps won’t conflict with other apps because of missing or outdated libraries.
Sounds good in theory, but what does it mean in practice? You have to collect all the dependencies somehow and fit them into your “bundle”. Bundle structure usually looks like this:
HelloWorld.app/
- Contents/
- Frameworks/
- MacOS/
- HelloWorld (executable)
- Resources/
- PlugIns/
Bundle is just a directory with suffix “.app” which magically converts it to the “bundle” where real executable is inside Contents/MacOS/
directory, its library dependencies live in Frameworks/
directory and other resources in other appropriate places (Resources/
or PlugIns
).
To understand which dependencies does your HelloWorld
executable have, you can use otool
command line utility (part of XCode) with parameter -L
:
> otool -L HelloWorld.app/Contents/MacOS/HelloWorld
@rpath/QtNetwork.framework/Versions/5/QtNetwork
@rpath/QtCore.framework/Versions/5/QtCore
/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit
@rpath/QtGui.framework/Versions/5/QtGui
/usr/lib/libc++.1.dylib
/usr/lib/libSystem.B.dylib
.....
It lists all dependencies of your app and their “install names”. You may ask what is @rpath
? It’s a special variable (a friend of @executable_path
and @loader_path
) that means a list of locations where the dynamic linker can find dependent dylib at runtime. Items in list usually are set by a linker and can overriden in a number of ways (e.g. by install_name_tool
or with environmental DYLD_LIBRARY_PATH
variable). For native applications it could be /usr/lib
. For Qt applications it could be /path/to/Qt5.6.2/5.6/clang_64/lib
or anything alike.
You can learn this path also using otool
but this time with parameter -l
and checking for LC_RPATH
block:
> otool -l HelloWorld.app/Contents/MacOS/HelloWorld
......
Load command 24
cmd LC_RPATH
cmdsize 56
path /Users/user/Qt5.6.2/5.6/clang_64/lib (offset 12)
......
Also otool -l
is pretty useful command to learn how does your application start anyway. How does it know it’s depedencies and so on (yes, they are all just listed in your app).
So your user’s computer probably will not have same path as the one on your computer so you have to change it. Remember bundle structure couple of passages above? All the libraries should live in Frameworks/
directory so your application @rpath
will point to Frameworks/
and will be able to start using the libraries that come along.
In order to change @rpath
(and other sections) you need install_name_tool
utility (also comes with XCode) so in the end your app’s @rpath
will have at least one entry of @executable_path/../Frameworks/
(where @executable_path
is another magic variable with self-descriptive name).
Qt world
If you’re bundling a Qt app you can make use of macdeployqt
utility which comes with Qt distribution and it can pretty much pack all the frameworks and libraries required for your app and missing in /usr/lib/
.
macdeployqt HelloWorld.app -executable="HelloWorld.app/Contents/MacOS/HelloWorld" -qmldir=../src/
This command will bundle all the dependencies of the executable passed to it via command line. In addition this app will change @rpath
using install_name_tool
and will produce ready to deploy bundle. Although this will only work if your application depends on some system and Qt libraries and nothing else.
Level 1: additional files
If your application depends on other resources (icon, videos, dictionaries etc.) that are not compiled into the executable, you also have to ship them using Resources/
directory. I recommend creating a deployment script that will do that for you of course because doing it manually every time is very error-prone.
Deployment as easy as:
cp /path/to/your/file HelloWorld.app/Contents/Resources/
Level 2: custom libraries
This is harder. Simple copying library or framework to Frameworks/
in order to deploy it is not enough. Your executable will look for the library in the place that was set by the linker and chances are it’s not @rpath
, but something like /usr/local/lib/mycustomname.dylib
. Things get more complicated if the library requires other dynamic libraries so you will need to deploy transitive dependencies as well.
Bottom line is: in order to deploy custom libraries you have to first copy them to Frameworks/
, fix library load path in your app and fix the librarie’s LC_LOAD_DYLIB
sections (like LC_RPATH
but for dynamically linked libraries) where its own dependencies are listed. Rough example:
# copy library to Frameworks
cp /path/to/library.dylib HelloWorld.app/Contents/Frameworks/
# fix dependency of the main app if needed
install_name_tool -change "/previous/link/path/to/library.dylib" "@rpath/library.dylib" HelloWorld.app/Contents/MacOS/HelloWorld
# and dependencies of dependencies of the library (repeat for every $depend_lib)
install_name_tool -change "/usr/local/lib/$depend_lib" "@loader_path/$depend_lib" "$lib"
Previous link path to the dependent library can be learned using otool -L
command against your executable as well as library dependencies. If you put all the dependencies on the same level together then they can be referred as @loader_path/dependentlibrary.dylib
where @loader_path
is magical variable expanded to the path of the library which caused current library to be loaded.
Level 3: additional applications
Say you need some additional application like a crash handler to be deployed alongside with your main one. If you were skimming this post thoroughly then you know the answer: you copy its dependencies to Frameworks/
and tweak @rpath
using install_name_tool
.
To be more precise first you need to choose where exactly your app will be with regards to the main app. Usually it’s put alongside with it in the Contents/MacOS
directory like this:
HelloWorld.app/
- Contents/
- Frameworks/
- Resources/
- MacOS/
- HelloWorld (main exe)
- MyHelper.app/
- Contents
- MacOS/
- MyHelper (additional exe)
- Frameworks/
- Resources/
Of course you can first create a fully self-contained bundle of your other app, but this will only increase total size of the main bundle. If you want your other app to have an icon you will anyway need to create a bundle, but no need to copy dependencies in there.
What makes more sense is to reuse dependencies of the main app as much as possible (usually they cover smaller one). In order to do so you will need to tweak @rpath
of the smaller executable to point to Frameworks/
directory of the parent. Also sounds like a job for install_name_tool
and a fresh couple of lines in your deployment script like this:
install_name_tool -add_rpath "@executable_path/../../../../Frameworks" "${ADDITIONAL_APP}/Contents/MacOS/OtherAppName"
Qt world
If your small additional application is also a Qt application (which is likely if the main one is) then above is not enough. Problem is that Qt apps need also platform-specific plugins and resources shipped alongside with them. If your main app already has them deployed using macdeployqt
then you can reuse them using qt.conf
file. This is a magical file that QApplication
-descendent classes look for in order to learn about the environment, parameters, paths to plugins and resources and what not.
So to reuse resources and platform-specific plugins of the main app you would need to create a qt.conf
in Resources/
directory of the dependent app with relative paths like this:
> cat << EOF > "${ADDITIONAL_APP}/Contents/Resources/qt.conf"
[Paths]
Plugins = ../../../PlugIns
Imports = ../../../Resources/qml
Qml2Imports = ../../../Resources/qml
EOF
Where relative paths point to parent’s appropriate directories.
Bonus level: DMG creation
Of course it would be too easy if bundling an .app
was everything that you needed to ship your application. In Apple world applications are usually shipped in .dmg
files which are Apple Disk Image files. Good news is that if you only want to compress .app
to .dmg
you need pretty much one command:
> hdiutil create -srcfolder "/path/to/HelloWorld.app" -volname "HelloWorld" -fs HFS+ \
-fsargs "-c c=64,a=16,e=16" -format UDRW -size ${SIZE}m "HelloWorld.dmg"
Bad news is that it will lead to bad user experience. Imagine you open an installer and what you see is just ugly white background and some HelloWorld.app
in top left corner left totally at your disposal. You should guess yourself what you want to do with it. Probably just close or trash immediately.
Pretty much RAW bundle created with a command above
What people usually do is they set a pretty background image with some instructions and place a link to Applications/
directory. Instructions usually visually explain to drag the HelloWorld.app
to Applications
in order to install it.
Bundle with set background and link to Applications
In order to do so you have to copy background file to .background
directory of the DMG, then select it in Finder as background image, resize the window and icons inside to fit it. Mount your first dmg, do these manipulations and then reexport it from Disk Utility
to read-only dmg. You can also automate these actions with a Apple Script but that’s a topic for another post.
The end
As you can see deploying desktop apps on macOS is a total hassle as soon as you have something more complex than HelloWorld.app
. XCode/QtCreator can rule many things out to an extent but custom libraries, additional applications and others may require writing custom deployment scripts.
Make sure to read part 2 about signing and notarizing for macOS.
References
- Xpiks deployment script
- There’s an awesome article about dynamic loading in Linux. It explains about
rpath
,runpath
and other quirks. - Linking and install names on OS X
- Deploying Qt applications.