Search code examples
c++macosqtcmakebundle

CMake MacOS X bundle with BundleUtiliies for Qt application


I am a CMake beginner and have an issue with creation of an Qt application bundle for MacOS X. Let's consider a simple widget "helloworld" app in only one main.cpp file.

// main.cpp
#include <QApplication>
#include <QLabel>

int main(int argc, char** argv)
{
    QApplication app(argc,argv);
    QLabel lbl("Hello");
    lbl.show();
    return app.exec();
}

The CMakeLists.txt file is also simple.

# CMakeLists.txt
cmake_minimum_required( VERSION 3.0 )
project( QtBundle )    
set( CMAKE_INCLUDE_CURRENT_DIR ON )
set( CMAKE_AUTOMOC ON )

set( SOURCES main.cpp )    
find_package( Qt5Widgets REQUIRED )

add_executable( ${PROJECT_NAME} MACOSX_BUNDLE ${SOURCES} )    
qt5_use_modules( ${PROJECT_NAME} Widgets )

I run cmake .. -DCMAKE_PREFIX_PATH=/path/to/Qt5.5.1/ and it generates Makefile in the build directory.

Then I run make and have QtBundle.app directory as I wanted and QtBundle.app/Contents/MacOS/QtBundle executable, OK.

But when I launch it I get:

This application failed to start because it could not find or load the Qt platform plugin "cocoa".

Reinstalling the application may fix this problem.
Abort trap: 6 

As far as I understand that error is occurred because application bundle doesn't have any Qt stuffs (Framework libs and plugins), so I run macdeployqt and it populates bundles directory with a lot of files in Framework and PlugIns folders and application is able to run and relocate to another system.

It partially solves the problem but I want to populate bundle with CMake and BundleUtilities and without macdeployqt tool.

Unfortunately I didn't find any good and simple example for Qt5 deployment with BundleUtilities.

Could someone help me to modify my 'helloworld' example in such way that CMake automatically creates ready-to-deploy bundle?

Thanks in advance.

Main question: how to use CMake BundleUtilities to get a relocatable application?


Solution

  • Add the code below to CMakeLists.txt. The most challenging thing is to figure out, what plugins do you need, find their names and then properly specify paths for BundleUtilities' fixup_bundle().

    install_qt5_plugin() macro locates plugin by name. It will only find plugin for Qt module already found. In this case Qt5::QCocoaIntegrationPlugin is plugin in Qt5Gui module, which is found as dependency for Qt5Widgets by find_package(Qt5 COMPONENTS Widgets REQUIRED). Macro generates install() command for plugin and calculates full path to installed plugin. The latter we'll pass (see QT_PLUGIN variable) to fixup_bundle().

    Notes:

    1. We create and install qt.conf file, so plugin can be found, when application starts.
    2. APPS variable specifies path to bundle, not to executable inside it.
    3. Filling DIRS is very important. Note, how it uses CMAKE_PREFIX_PATH.
    4. Printing APPS, QT_PLUGINS and DIRS is optional yet very useful.
    5. One should manually copy/install only those dynamic libraries (including plugins), that aren't referenced from app. Qt platform plugin is such dynamic library.

    Dependencies lookup and fixing happens on installation. To get relocatable bundle in necessary location one may configure with CMAKE_INSTALL_PREFIX pointing to that location and then build install target.

    I prefer creating .dmg file with

    mkdir build
    cd build
    cmake ..
    cpack -G DragNDrop
    

    Contents to add to CMakeLists.txt is from here:

    set(prefix "${PROJECT_NAME}.app/Contents")
    set(INSTALL_RUNTIME_DIR "${prefix}/MacOS")
    set(INSTALL_CMAKE_DIR "${prefix}/Resources")
    
    # based on code from CMakes QtDialog/CMakeLists.txt
    macro(install_qt5_plugin _qt_plugin_name _qt_plugins_var _prefix)
        get_target_property(_qt_plugin_path "${_qt_plugin_name}" LOCATION)
        if(EXISTS "${_qt_plugin_path}")
            get_filename_component(_qt_plugin_file "${_qt_plugin_path}" NAME)
            get_filename_component(_qt_plugin_type "${_qt_plugin_path}" PATH)
            get_filename_component(_qt_plugin_type "${_qt_plugin_type}" NAME)
            set(_qt_plugin_dest "${_prefix}/PlugIns/${_qt_plugin_type}")
            install(FILES "${_qt_plugin_path}"
                DESTINATION "${_qt_plugin_dest}")
            set(${_qt_plugins_var}
                "${${_qt_plugins_var}};\$ENV{DEST_DIR}\${CMAKE_INSTALL_PREFIX}/${_qt_plugin_dest}/${_qt_plugin_file}")
        else()
            message(FATAL_ERROR "QT plugin ${_qt_plugin_name} not found")
        endif()
    endmacro()
    
    install_qt5_plugin("Qt5::QCocoaIntegrationPlugin" QT_PLUGINS ${prefix})
    file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/qt.conf"
        "[Paths]\nPlugins = ${_qt_plugin_dir}\n")
    install(FILES "${CMAKE_CURRENT_BINARY_DIR}/qt.conf"
        DESTINATION "${INSTALL_CMAKE_DIR}")
    
    # Destination paths below are relative to ${CMAKE_INSTALL_PREFIX}
    install(TARGETS ${PROJECT_NAME}
        BUNDLE DESTINATION . COMPONENT Runtime
        RUNTIME DESTINATION ${INSTALL_RUNTIME_DIR} COMPONENT Runtime
        )
    
    # Note Mac specific extension .app
    set(APPS "\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${PROJECT_NAME}.app")
    
    # Directories to look for dependencies
    set(DIRS "${CMAKE_BINARY_DIR}")
    
    # Path used for searching by FIND_XXX(), with appropriate suffixes added
    if(CMAKE_PREFIX_PATH)
        foreach(dir ${CMAKE_PREFIX_PATH})
            list(APPEND DIRS "${dir}/bin" "${dir}/lib")
        endforeach()
    endif()
    
    # Append Qt's lib folder which is two levels above Qt5Widgets_DIR
    list(APPEND DIRS "${Qt5Widgets_DIR}/../..")
    
    include(InstallRequiredSystemLibraries)
    
    message(STATUS "APPS: ${APPS}")
    message(STATUS "QT_PLUGINS: ${QT_PLUGINS}")
    message(STATUS "DIRS: ${DIRS}")
    
    install(CODE "include(BundleUtilities)
        fixup_bundle(\"${APPS}\" \"${QT_PLUGINS}\" \"${DIRS}\")")
    
    set(CPACK_GENERATOR "DRAGNDROP")
    include(CPack)