Bart Kessels
Bart Kessels
Passionate open source software engineer who loves to go backpacking

Building a Qt MacOS bundle using CMake

Building a Qt MacOS bundle using CMake
This image is generated using Dall-E
  • Prompt: Generate an image of an iMac displaying a spinning gear which is connected to an application icon in minimalistic flat style
  • Introduction

    The past couple of weeks I’ve been working on a new project, a Qt Widgets desktop application called It Depends.

    As with all software projects, the question arises “how to properly build and release it?”. For API’s and mobile applications this is easily answered because there’s an API to either release to the cloud, the Apple App Store and the Google Play Store. But for generic desktop applications this is a bit harder.

    But tracing back this question, the first that needs to happen is it should be built automatically in a pipeline. After that we can start worrying about publishing it to some sort of store. Because the project I’m working on is built using C++ with CMake I’d like to use CMake for the entire lifecycle management of the application, from compiling to publishing.

    Setting up the folder structure

    For my application I have the following folder structure

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    - src/
      - main.cpp
      - CMakeLists.txt
    - packaging/
      - macos/
        - icons/
          - icon.icns
          - CMakeLists.txt
        - CMakeLists.txt
    - CMakeLists.txt
    

    Where in the source folder the all the source code files are linked to the dependencies and are being compiled into the specified target. In the packaging folder I have all the logic for each platform to publish for that specific platform, in the packaging/CMakeLists.txt file is just a simple if-statement to include the correct platform, for this tutorial that is going to be macOS.

    For this tutorial we’re going to use the setup from the use CMake target in other folder post from earlier.

    CMakeLists.txt

    1
    2
    3
    
    cmake_minimum_required(VERSION 3.15 FATAL_ERROR)
    
    add_subdirectory(./src)
    

    src/main.cpp

    1
    2
    3
    4
    
    int main(int argc, char** args)
    {
      return 0;
    }
    

    src/CMakeLists.txt

    1
    2
    3
    4
    5
    6
    7
    8
    
    cmake_minimum_required(VERSION 3.15 FATAL_ERROR)
    
    add_executable(
          test_target
          main.cpp
    )
    
    include(../packaging/CMakeLists.txt)
    

    packaging/CMakeLists.txt

    1
    2
    3
    4
    5
    6
    7
    
    cmake_minimum_required(VERSION 3.15 FATAL_ERROR)
    
    set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR})
    
    if(APPLE)
        include(${CMAKE_SOURCE_DIR}/packaging/macos/CMakeLists.txt)
    endif()
    

    Why this works, I suggest you read the use CMake target in another folder post from before.

    Now we’re going to implement the CMakeLists.txt file for the packaging/macos target with the following contents

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    cmake_minimum_required(VERSION 3.15 FATAL_ERROR)
    
    set(CPACK_BUNDLE_NAME ${CMAKE_PROJECT_NAME})
    set(CPACK_GENERATOR "DragNDrop")
    
    get_target_property(qmake_executable Qt5::qmake IMPORTED_LOCATION)
    get_filename_component(_qt_bin_dir "${qmake_executable}" DIRECTORY)
    find_program(MACDEPLOYQT_BIN macdeployqt HINT "${_qt_bin_dir}")
    
    # Link Qt dependencies to TestTarget.app bundle
    add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
            COMMAND ${MACDEPLOYQT_BIN} ${BUNDLE_NAME} -always-overwrite
    )
    

    What this file does, it tries to locate QMake executable using the get_target_property(qmake_executable Qt5::qmake IMPORTED_LOCATION) function. This still uses Qt5, so for Qt6 you might need to update it to Qt or Qt6. And puts it into the qmake_executable variable.

    Since we don’t need the QMake executable but just the installation directory of Qt, we’ll be using the get_filename_component function with the DIRECTORY argument to just get the directory where the QMake executable is located. This will be stored inside the _qt_bin_dir variable.

    Next we’ll try to locate the macdeployqt executable which will copy the Qt dependencies to our own executable using the find_program function. To help CMake find the executable we give the _qt_bin_dir as the HINT argument. This output will be stored in the MACDEPLOYQT_BIN command.

    After all these steps we have the executable we need to execute AFTER our build has finished. This is thus dependent on our target, which is the reason why we’re using the include(../packaging) command after we declare our target using add_executable(test_target main.cpp) in the src/CMakeLists.txt file.

    Therefore, we make use of the add_custom_command function with the POST_BUILD argument. Using this function we can execute any system command we’d like, and thus fire the macdeployqt command which is stored in our variable with the executable name as an argument to the macdeployqt command. To make sure our application is re-linked every time we build again, we add the -always-overwrite flag to the macdeployqt call. This is

    Add a custom icon for your application

    When using the CMakeLists.txt file from above it will give you an app bundle without an icon. To fix this we need to copy an icon into the app bundle itself. This can be done using the cp command because an app bundle is basically a folder.

    1
    2
    3
    
    add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
            COMMAND cp ${CMAKE_BINARY_DIR}/packaging/macos/icons/icon.icns ${BUNDLE_NAME}/Contents/Resources
    )
    

    Build the macOS app bundle

    To build our executable we can simply use our default generator with CMake and build it.

    1
    2
    
    $ cmake . -G Ninja
    $ ninja test_target
    

    This will generate a stand-alone app bundle of your Qt application which is ready to be deployed.

    Categories

    Related articles

    Releasing It Depends

    Gat a grip on the versions of the dependencies your using by visualizing a generated CycloneDX SBOM file.

    Building a Qt Windows Executable using CMake

    Build a stand-alone Windows executable of your Qt application using CMake.