Thursday, May 5, 2016

Qt Mac Application Failed to Create Self-contained App Bundle (Qt Creator Build)

Recently, I encountered a problem in creating an app bundle using Qt Creator with Qt 5.6, so I posted my question with detail on StackOverflow here.

In this post, I am going to point out the places I got wrong, and some studies.

Scott

Scott is a friend of mine for years, and he is best programmer I’ve ever met in Taiwan. He helped me on this question, and I would like to quote his words here:

Do try to figure out what you did wrong before. Look at the RPATH, install names etc in your executable and update your StackOverflow question with those findings. Finding out what you did wrong is an important step in understanding a system. This makes your exercise of publishing apps on multiple platforms more meaningful.

@executable_path, @loader_path, @rpath

The first reason I couldn’t build the app build is that I didn’t fully understand the path names used on Mac, and here is my study of @executable_path, @loader_path, and @rpath.

  • @executable_path: the folder path of application’s executable
    • ex. /Applications/Foo.app/Contents/MacOS
    • useful for frameworks embedded inside the applications
  • @loader_path: the folder path of the related plug-in’s code
    • ex. /Library/Application Support/Foo/Plug-Ins/Bar.bundle/Contents/MacOS
    • useful for frameworks embedded inside plug-ins
    • availabe from Mac OS X 10.4
  • @rpath: instructs the dynamic linker to search a list of paths in order to locate the framework
    • no need to specify the “install path” using either @executable_path or @loader_path, but pass additional flags when building the host application (ex. -rpath @excutable/…/Frameworks or /Library/Frameworks)
    • availabe from Mac OS X 10.5

otool

The second reason I was stuck is that otool didn’t resolve @rpath names, so I was confused when it always returned me the same thing.

However, Scott wrote another version of otool that resolves the rpaths here. Here are the steps that demostrate the difference:

> otool -L bibi.app/Contents/MacOS/bibi
bibi.app/Contents/MacOS/bibi:
        @rpath/QtWidgets.framework/Versions/5/QtWidgets (compatibility version 5.6.0, current version 5.6.0)
        @rpath/QtGui.framework/Versions/5/QtGui (compatibility version 5.6.0, current version 5.6.0)
        @rpath/QtCore.framework/Versions/5/QtCore (compatibility version 5.6.0, current version 5.6.0)
        /System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL (compatibility version 1.0.0, current version 1.0.0)
        /System/Library/Frameworks/AGL.framework/Versions/A/AGL (compatibility version 1.0.0, current version 1.0.0)
        /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 120.1.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1226.10.1)

> otool-rpath bibi.app/Contents/MacOS/bibi
/Users/heron/Qt/5.6/clang_64/lib

> macdeployqt ./*.app -verbose=3 -always-overwrite -appstore-compliant

> otool -L bibi.app/Contents/MacOS/bibi
bibi.app/Contents/MacOS/bibi:
        @rpath/QtWidgets.framework/Versions/5/QtWidgets (compatibility version 5.6.0, current version 5.6.0)
        @rpath/QtGui.framework/Versions/5/QtGui (compatibility version 5.6.0, current version 5.6.0)
        @rpath/QtCore.framework/Versions/5/QtCore (compatibility version 5.6.0, current version 5.6.0)
        /System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL (compatibility version 1.0.0, current version 1.0.0)
        /System/Library/Frameworks/AGL.framework/Versions/A/AGL (compatibility version 1.0.0, current version 1.0.0)
        /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 120.1.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1226.10.1)

> otool-rpath bibi.app/Contents/MacOS/bibi
@executable_path/../Frameworks

macdeployqt

The last reason I failed to understand what’s going on is the output of macdeployqt, which confused me.

> macdeployqt bibi.app
File exists, skip copy: "bibi.app/Contents/PlugIns/platforms/libqcocoa.dylib"
File exists, skip copy: "bibi.app/Contents/PlugIns/printsupport/libcocoaprintersupport.dylib"
File exists, skip copy: "bibi.app/Contents/PlugIns/imageformats/libqdds.dylib"
File exists, skip copy: "bibi.app/Contents/PlugIns/imageformats/libqgif.dylib"
File exists, skip copy: "bibi.app/Contents/PlugIns/imageformats/libqicns.dylib"
File exists, skip copy: "bibi.app/Contents/PlugIns/imageformats/libqico.dylib"
File exists, skip copy: "bibi.app/Contents/PlugIns/imageformats/libqjpeg.dylib"
File exists, skip copy: "bibi.app/Contents/PlugIns/imageformats/libqtga.dylib"
File exists, skip copy: "bibi.app/Contents/PlugIns/imageformats/libqtiff.dylib"
File exists, skip copy: "bibi.app/Contents/PlugIns/imageformats/libqwbmp.dylib"
File exists, skip copy: "bibi.app/Contents/PlugIns/imageformats/libqwebp.dylib"
WARNING:
WARNING: "bibi.app/Contents/Resources/qt.conf" already exists, will not overwrite.
WARNING: To make sure the plugins are loaded from the correct location,
WARNING: please make sure qt.conf contains the following lines:
WARNING: [Paths]
WARNING:   Plugins = PlugIns

However, in Scott’s solution, he gave following additional arguments:

  • -verbose=3: see how the rpaths are updated in details (Scott’s log)
  • always-overwrite: copy files even if the target file exists, so the first (Scott: I used “always-overwrite” to get predictable results after repeated testing, since the Qt frameworks would be copied into the app bundle.)
  • appstore-compliant: skip deployment of components that use private API (Scott: appstore-compliant was just for your convenience)

Test

Testing is one additional thing the made the original question harder to be solved: there’s no easy way to see if my app bundle works on the other machine without Qt installed.

Instead of asking friends to run the app, Scott mentioned that we can use `lsof at run-time.

> ps aux|grep bibi
heron           21610   0.0  0.5  2632680  40272   ??  S    Tue09PM   5:32.80 /Users/heron/Project/bibi/bibi/build-bibi-Desktop_Qt_5_6_0_clang_64bit-Release/bibi.app/Contents/MacOS/bibi
heron           39245   0.0  0.0  2434840    664 s003  R+    9:31AM   0:00.00 grep --color=auto bibi

> lsof -p 39183 | grep QtCore
bibi    21610 heron  txt      REG                1,4   6441676 168354669 /Users/heron/Qt-free/5.6/clang_64/lib/QtCore.framework/Versions/5/QtCore

After macdeployqt, the app bundle no longer needs to link to frameworks outside the bundle:

> ps aux|grep bibi
heron           39352   0.0  0.0  2435864    788 s003  S+    9:32AM   0:00.00 grep --color=auto bibi
heron           39315   0.0  0.8  2611176  63000   ??  S     9:32AM   0:00.68 /Users/heron/Project/bibi/bibi/bibi/bibi.app/Contents/MacOS/bibi

> lsof -p 39315 | grep QtCore
bibi    39315 heron  txt      REG    1,4   6017532 171823963 /Users/heron/Project/bibi/bibi/bibi/bibi.app/Contents/Frameworks/QtCore.framework/Versions/5/QtCore

Summary

I would say the biggest problem is that I didn’t know how to read @rpath, so Scott’s otool-rpath or lsof helps eventually.

Reference